import React, { useState, useMemo } from 'react'; import { useQuery, useMutation } from '@tanstack/react-query'; import { useNavigate } from 'react-router-dom'; import { MagnifyingGlassIcon, EyeIcon, TableCellsIcon, CalendarDaysIcon as CalendarViewIcon, CalendarIcon, ClockIcon, CheckCircleIcon, XCircleIcon, PlusIcon, XMarkIcon, ChevronRightIcon, ChevronLeftIcon, UserCircleIcon, } from '@heroicons/react/24/outline'; import { toast } from 'sonner'; import { api } from '../lib/api'; import type { PaginatedResponse, ApiResponse } from '../lib/api'; import type { Appointment } from '../types'; import { formatDate, maskMobile, toGregorianDate } from '../lib/utils'; import DataTable, { Column } from '../components/ui/DataTable'; import Pagination from '../components/ui/Pagination'; import PersianDateInput from '../components/ui/PersianDateInput'; import { useAuthStore } from '../stores/authStore'; // ── Status config ────────────────────────────────────────────────────────── const STATUS_META: Record = { waiting_for_payment: { label: 'انتظار پرداخت', cls: 'status-amber' }, reserved: { label: 'رزرو شده', cls: 'status-blue' }, checked_in: { label: 'ورود به مطب', cls: 'status-violet' }, waiting: { label: 'صف انتظار', cls: 'status-amber' }, in_progress: { label: 'در حال ویزیت', cls: 'status-violet' }, visited: { label: 'ویزیت شده', cls: 'status-green' }, completed: { label: 'تکمیل شده', cls: 'status-green' }, cancelled_by_user: { label: 'لغو توسط بیمار', cls: 'status-red' }, cancelled_by_doctor: { label: 'لغو توسط پزشک', cls: 'status-red' }, cancelled_by_admin: { label: 'لغو توسط ادمین', cls: 'status-red' }, auto_cancel_unpaid: { label: 'لغو خودکار', cls: 'status-gray' }, no_show: { label: 'غیبت', cls: 'status-gray' }, }; const STATUS_FILTERS = [ { value: '', label: 'همه وضعیت‌ها' }, { value: 'reserved', label: 'رزرو شده' }, { value: 'waiting_for_payment', label: 'در انتظار پرداخت' }, { value: 'checked_in', label: 'ورود به مطب' }, { value: 'waiting', label: 'صف انتظار' }, { value: 'in_progress', label: 'در حال ویزیت' }, { value: 'visited', label: 'ویزیت شده' }, { value: 'completed', label: 'تکمیل شده' }, { value: 'cancelled_by_doctor', label: 'لغو پزشک' }, { value: 'cancelled_by_user', label: 'لغو بیمار' }, { value: 'no_show', label: 'غیبت' }, ]; function ApptStatus({ status }: { status: string }) { const m = STATUS_META[status] ?? { label: status, cls: 'status-gray' }; return ( {m.label} ); } // ── Timeline View ───────────────────────────────────────────────────────── interface TimelineProps { items: Appointment[]; loading: boolean; onView: (uuid: string) => void; groupByDoctor?: boolean; } function DayGroup({ date, appts, onView }: { date: string; appts: Appointment[]; onView: (u: string) => void }) { return (
{formatDate(date)}
{appts.map((a) => (
onView(a.uuid)} onMouseEnter={e => (e.currentTarget.style.boxShadow = '0 2px 8px rgba(0,0,0,.07)')} onMouseLeave={e => (e.currentTarget.style.boxShadow = 'none')} >
{a.appointment_time}
{a.patient_name || maskMobile(a.patient_mobile)}
دکتر {a.doctor_name}{a.clinic_name ? ` · ${a.clinic_name}` : ''}
))}
); } function TimelineView({ items, loading, onView, groupByDoctor }: TimelineProps) { if (loading) { return (
{Array.from({ length: 5 }).map((_, i) => (
))}
); } if (!items.length) { return

هیچ نوبتی یافت نشد

; } if (groupByDoctor) { // group by doctor → date const byDoctor = items.reduce>((acc, a) => { (acc[a.doctor_name] ??= []).push(a); return acc; }, {}); return (
{Object.entries(byDoctor).map(([docName, docAppts]) => { const byDate = docAppts.reduce>((acc, a) => { (acc[a.appointment_date] ??= []).push(a); return acc; }, {}); return (
دکتر {docName} {docAppts.length} نوبت
{Object.entries(byDate).map(([date, appts]) => ( ))}
); })}
); } // default: group by date only const byDate = items.reduce>((acc, a) => { (acc[a.appointment_date] ??= []).push(a); return acc; }, {}); return (
{Object.entries(byDate).map(([date, appts]) => ( ))}
); } // ── Doctor-grouped table ────────────────────────────────────────────────── interface DoctorGroupedTableProps { items: Appointment[]; loading: boolean; onView: (uuid: string) => void; columns: Column[]; } function DoctorGroupedTable({ items, loading, onView, columns }: DoctorGroupedTableProps) { if (loading) { return (
{Array.from({ length: 5 }).map((_, i) => (
))}
); } if (!items.length) { return

هیچ نوبتی یافت نشد

; } const byDoctor = items.reduce>((acc, a) => { (acc[a.doctor_name] ??= []).push(a); return acc; }, {}); return (
{Object.entries(byDoctor).map(([docName, docAppts]) => (
{/* Doctor header row */}
دکتر {docName} {docAppts.length} نوبت
{/* Appointments table for this doctor */} columns={columns} data={docAppts} loading={false} emptyMessage="" actions={(appt) => ( )} />
))}
); } // ── New Appointment Modal ───────────────────────────────────────────────── interface NewApptModalProps { onClose: () => void; onCreated: () => void; defaultDoctorUuid?: string; } function NewAppointmentModal({ onClose, onCreated, defaultDoctorUuid }: NewApptModalProps) { const { primaryRole } = useAuthStore(); const isAdmin = primaryRole === 'admin'; const [step, setStep] = useState<1 | 2 | 3>(1); const [doctorUuid, setDoctorUuid] = useState(defaultDoctorUuid ?? ''); const [patientMobile, setPatientMobile] = useState(''); const [dateStr, setDateStr] = useState(toGregorianDate(new Date())); const [slots, setSlots] = useState>([]); const [selectedSlot, setSelectedSlot] = useState<{ start: number; end: number; label: string } | null>(null); const [note, setNote] = useState(''); const [loadingSlots, setLoadingSlots] = useState(false); const fetchSlots = async () => { if (!doctorUuid.trim() || !dateStr) { toast.error('UUID پزشک و تاریخ را وارد کنید'); return; } setLoadingSlots(true); try { const res = await api.get }>>( `/api/v1/appointment-slots?doctor_uuid=${doctorUuid.trim()}&date=${dateStr}` ); const raw = (res as any)?.data?.slots ?? []; setSlots(raw); setSelectedSlot(null); setStep(2); if (!raw.length) toast.info('هیچ نوبت خالی در این تاریخ وجود ندارد'); } catch (e: any) { toast.error(e.message ?? 'خطا در دریافت نوبت‌ها'); } finally { setLoadingSlots(false); } }; const createMut = useMutation({ mutationFn: () => { if (!selectedSlot) throw new Error('نوبت را انتخاب کنید'); const body: Record = { doctor_uuid: doctorUuid.trim(), slot_start: selectedSlot.start, slot_end: selectedSlot.end, note: note || undefined, }; if (isAdmin) { body.patient_mobile = patientMobile.trim(); return api.post('/api/v1/admin/appointment', body); } return api.post('/api/v1/appointment', body); }, onSuccess: () => { toast.success('نوبت با موفقیت ثبت شد'); onCreated(); onClose(); }, onError: (e: Error) => toast.error(e.message), }); const handleDateChange = (v: string) => { setDateStr(v); setSlots([]); setSelectedSlot(null); setStep(1); }; const changeDate = (delta: number) => { const d = new Date(dateStr + 'T12:00:00'); d.setDate(d.getDate() + delta); handleDateChange(toGregorianDate(d)); }; return (
e.stopPropagation()}>
ثبت نوبت جدید
{ setDoctorUuid(e.target.value); setStep(1); setSlots([]); setSelectedSlot(null); }} />
{isAdmin && (
setPatientMobile(e.target.value)} />
)} {step >= 2 && slots.length > 0 && (
{slots.map((s) => ( ))}
)} {step >= 2 && slots.length === 0 && (

نوبت خالی در این تاریخ وجود ندارد

)} {step >= 3 && (