/** * «لیست نوبت‌های جدید» table — ported from clinic-pro-tauri * `src/components/dashboard/list/` (CustomTable + DetailT + Status). * * Source data was static mock; here it is fed by the real dashboard API * (`/api/v1/dashboard/{clinic,doctor}` → `today_appointments`) or, when a * `queryKey` is supplied, by the filtered doctor list endpoint. Status is * editable in place only when the row carries a `version` (optimistic lock) * and the parent passes the query key to invalidate. */ import React, { useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { toast } from 'sonner'; import AppointmentStatusDropdown, { STATUS_META } from '../ui/AppointmentStatusDropdown'; import { findRecordUuid } from '../AppointmentActions'; export interface ApptRow { uuid: string; patient_name: string | null; patient_mobile?: string | null; doctor_name?: string | null; service_name?: string | null; slot_start: number; slot_end?: number | null; status: string; /** لازم برای قفل خوش‌بینانهٔ PATCH وضعیت؛ نبودنش یعنی وضعیت فقط‌خواندنی است. */ version?: number; } /** * روزِ محلیِ نوبت (YYYY-MM-DD) برای دیپ‌لینک «مشاهده» به همان تاریخ در لیست نوبت‌ها. * همان قراردادی که AppointmentDetailPage/AppointmentsPage استفاده می‌کنند. */ const isoDay = (ts?: number | null): string => { if (!ts) return ''; const d = new Date(ts * 1000); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; }; function formatTime(ts?: number | null): string { if (!ts) return '—'; return new Intl.DateTimeFormat('fa-IR', { timeZone: 'Asia/Tehran', hour: '2-digit', minute: '2-digit' }).format(new Date(ts * 1000)); } const HEAD = ['ردیف', 'نام بیمار', 'شماره تماس', 'شروع', 'پایان', 'سرویس', 'پرسنل', 'وضعیت', 'عملیات']; interface Props { rows: ApptRow[]; loading?: boolean; /** کلید کوئری برای invalidate پس از تغییر وضعیت. بدون آن وضعیت فقط نمایش داده می‌شود. */ queryKey?: unknown[]; emptyText?: string; } export function NewAppointmentsTable({ rows, loading, queryKey, emptyText }: Props) { if (loading) { return
; } if (!rows.length) { return (

{emptyText ?? 'نوبتی برای امروز ثبت نشده'}

); } return (
{HEAD.map((h, i) => ( ))} {rows.map((r, idx) => ( {new Intl.NumberFormat('fa-IR').format(idx + 1)}{r.patient_name || '—'}{r.patient_mobile ? {r.patient_mobile} : '—'}{formatTime(r.slot_start)}{formatTime(r.slot_end)}{r.service_name || '—'}{r.doctor_name || '—'} {queryKey && r.version != null ? : } ))}
{h}
); } /** * وضعیت‌هایی که نوبت از مرحلهٔ «قطعی‌شدن» رد شده — در این‌ها پروندهٔ بیمار حتماً * ساخته شده است (ساخت پرونده اثر جانبیِ قطعی‌کردن است). */ const CONFIRMED_STATUSES = ['confirmed', 'following_up', 'salon', 'completed', 'no_show']; /** * «مشاهده» — نوبتِ قطعی‌شده به **پروندهٔ بیمار** می‌رود (چون از لحظهٔ قطعی‌شدن پرونده * دارد)، بقیه به لیست نوبت‌های همان روز. uuid پرونده روی ردیف نیست، پس با موبایلِ * بیمار resolve می‌شود — همان مسیری که منوی عملیات نوبت‌ها استفاده می‌کند. */ function ViewLink({ row }: { row: ApptRow }) { const navigate = useNavigate(); const [busy, setBusy] = useState(false); const dayHref = isoDay(row.slot_start) ? `/admin/appointments?date=${isoDay(row.slot_start)}` : '/admin/appointments'; const cls = 'text-[var(--primary)] text-[12px] font-medium hover:underline whitespace-nowrap'; if (!CONFIRMED_STATUSES.includes(row.status) || !row.patient_mobile) { return مشاهده; } const openRecord = async () => { setBusy(true); try { const recordUuid = await findRecordUuid(row.patient_mobile!); navigate(recordUuid ? `/admin/patients/${recordUuid}` : dayHref); } catch { toast.error('خطا در یافتن پرونده بیمار'); } finally { setBusy(false); } }; return ( ); } /** نمایش فقط‌خواندنی وضعیت — وقتی version یا queryKey در دسترس نیست. */ function StatusPill({ status }: { status: string }) { const meta = STATUS_META[status] ?? { label: status, color: 'var(--text-3)' }; return ( {meta.label} ); } /** همه‌ی سلول‌ها text-start هستند تا دقیقاً زیر هدر هم‌نامشان بنشینند. */ function Cell({ children, className = '' }: { children: React.ReactNode; className?: string }) { return ( {children} ); }