/** * «لیست نوبت‌های جدید» داشبورد پزشک، با فیلتر و صفحه‌بندی سمت سرور. * * چرا endpoint داشبورد استفاده نمی‌شود: `/api/v1/dashboard/doctor` فقط ۱۰ نوبتِ * امروز را برمی‌گرداند و `version` ندارد، پس نه فیلتر معنا می‌دهد نه تغییر وضعیت. * به‌جای ساخت endpoint جدید، `/api/v1/appointments/doctor/{uuid}` توسعه داده شد. */ import { useMemo, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { api } from '../../lib/api'; import SearchableSelect from '../ui/SearchableSelect'; import PersianDateInput from '../ui/PersianDateInput'; import Pagination from '../ui/Pagination'; import { NewAppointmentsTable, type ApptRow } from './NewAppointmentsTable'; const PER_PAGE = 20; /** وضعیت‌هایی که «هنوز ویزیت نشده» محسوب می‌شوند — فیلتر پیش‌فرض. */ export const NOT_VISITED_STATUSES = ['pending', 'confirmed']; const STATUS_OPTIONS: { value: string; label: string }[] = [ { value: '', label: 'ویزیت‌نشده‌ها: ثبت شده + قطعی شده (پیش‌فرض)' }, { value: 'pending', label: 'ثبت شده' }, { value: 'confirmed', label: 'قطعی شده' }, { value: 'following_up', label: 'در حال پیگیری' }, { value: 'salon', label: 'سالن' }, { value: 'completed', label: 'ویزیت شده' }, { value: 'cancelled_by_doctor', label: 'لغو شده' }, { value: 'cancelled_by_user', label: 'لغو توسط بیمار' }, { value: 'no_show', label: 'غیبت' }, { value: 'expired', label: 'منقضی شده' }, ]; interface ServiceOption { uuid: string; name?: string } /** YYYY-MM-DD میلادی → تایم‌استمپ ثانیه‌ای در ابتدا/انتهای همان روز محلی. */ function dayBound(iso: string, edge: 'start' | 'end'): number | null { if (!iso) return null; const [y, m, d] = iso.split('-').map(Number); if (!y || !m || !d) return null; const dt = edge === 'start' ? new Date(y, m - 1, d, 0, 0, 0) : new Date(y, m - 1, d, 23, 59, 59); return Math.floor(dt.getTime() / 1000); } interface ApiAppointment { uuid: string; status: string; version: number; slot_start: number; slot_end?: number | null; patient_name?: string | null; patient_mobile?: string | null; doctor?: { name?: string | null } | null; user?: { mobile?: string | null } | null; service_item?: { name?: string | null } | null; } export default function DoctorAppointmentsPanel({ doctorUuid, clinicUuid }: { doctorUuid?: string | null; clinicUuid?: string | null; }) { const [status, setStatus] = useState(''); const [from, setFrom] = useState(''); const [to, setTo] = useState(''); const [q, setQ] = useState(''); const [service, setService] = useState(''); const [page, setPage] = useState(1); const servicesQ = useQuery<{ data?: ServiceOption[] }>({ queryKey: ['service-sections'], queryFn: () => api.get('/api/v1/service-sections'), }); const params = useMemo(() => { const p = new URLSearchParams(); // فیلتر پیش‌فرض: هر نوبتی که هنوز ویزیت یا لغو نشده. // براکت لازم است تا Symfony پارامتر را آرایه ببیند، نه رشته. (status ? [status] : NOT_VISITED_STATUSES).forEach(s => p.append('statuses[]', s)); const f = dayBound(from, 'start'); const t = dayBound(to, 'end'); if (f !== null) p.set('from', String(f)); if (t !== null) p.set('to', String(t)); if (q.trim()) p.set('q', q.trim()); if (service) p.set('service_uuid', service); if (clinicUuid) p.set('clinic_uuid', clinicUuid); p.set('page', String(page)); p.set('limit', String(PER_PAGE)); return p.toString(); }, [status, from, to, q, service, clinicUuid, page]); const queryKey = ['doctor-appointments', doctorUuid, params]; const listQ = useQuery<{ data?: ApiAppointment[]; meta?: { totalRecords?: number } }>({ queryKey, queryFn: () => api.get(`/api/v1/appointments/doctor/${doctorUuid}?${params}`), enabled: !!doctorUuid, staleTime: 30_000, }); const rows: ApptRow[] = useMemo( () => (listQ.data?.data ?? []).map(a => ({ uuid: a.uuid, patient_name: a.patient_name ?? null, patient_mobile: a.patient_mobile ?? a.user?.mobile ?? null, doctor_name: a.doctor?.name ?? null, service_name: a.service_item?.name ?? null, slot_start: a.slot_start, slot_end: a.slot_end ?? null, status: a.status, version: a.version, })), [listQ.data], ); const total = listQ.data?.meta?.totalRecords ?? 0; /** هر تغییر فیلتر صفحه را به اول برمی‌گرداند تا کاربر روی صفحهٔ خالی نماند. */ const onFilter = (setter: (v: T) => void) => (v: T) => { setter(v); setPage(1); }; return ( <>
onFilter(setStatus)(v == null ? '' : String(v))} options={STATUS_OPTIONS} placeholder="وضعیت" />
onFilter(setService)(v == null ? '' : String(v))} options={[ { value: '', label: 'همهٔ سرویس‌ها' }, ...(servicesQ.data?.data ?? []).map(s => ({ value: s.uuid, label: s.name ?? '—' })), ]} placeholder="سرویس" />
onFilter(setQ)(e.target.value)} placeholder="نام یا شماره تماس بیمار" />
{total > PER_PAGE && (
)} ); }