import { useEffect, useMemo, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { PlusIcon } from '@heroicons/react/24/outline'; import { toast } from 'sonner'; import { api } from '../lib/api'; import type { ApiResponse } from '../lib/api'; import Modal from './ui/Modal'; import PersianDateInput from './ui/PersianDateInput'; import PriceInput from './ui/PriceInput'; import { WalletChargeLink } from './AppointmentActions'; interface Option { uuid: string; name?: string; full_name?: string } interface PatientRow { uuid: string; user_name?: string; user_mobile?: string; user_national_code?: string } const toEpoch = (isoDate: string, time: string) => Math.floor(new Date(`${isoDate}T${time || '00:00'}`).getTime() / 1000); const addMinutes = (time: string, min: number) => { const [h, m] = time.split(':').map(Number); const t = h * 60 + m + min; return `${String(Math.floor(t / 60) % 24).padStart(2, '0')}:${String(t % 60).padStart(2, '0')}`; }; /** * اضافه کردن نوبت جدید (Figma add.pdf) — rich create form: patient * search-or-new, بخش/سرویس/پرسنل, date + default-duration + start/end time, * deposit toggle, status and notes. POSTs the extended /my/appointment. */ export default function NewAppointmentDrawer({ doctorUuid, defaultDate, queryKey, onClose, isReserve = false }: { doctorUuid: string; /** ISO Y-m-d — the currently viewed day. */ defaultDate: string; queryKey: unknown[]; onClose: () => void; /** true → «اضافه کردن نوبت رزرو» (day-level entry, no time fields). */ isReserve?: boolean; }) { const qc = useQueryClient(); // ── patient: pick an existing record or enter a new person ──────────────── const [patientSearch, setPatientSearch] = useState(''); const [pickedPatient, setPickedPatient] = useState(null); const [name, setName] = useState(''); const [mobile, setMobile] = useState(''); const [nationalCode, setNationalCode] = useState(''); const patientsQ = useQuery>({ queryKey: ['drawer-patients', patientSearch], queryFn: () => api.get(`/api/v1/patients?search=${encodeURIComponent(patientSearch)}&limit=10`), enabled: patientSearch.trim().length >= 2, }); // ── service specs ────────────────────────────────────────────────────────── const [sectionUuid, setSectionUuid] = useState(''); const [itemUuid, setItemUuid] = useState(''); const [staffUuid, setStaffUuid] = useState(''); // روش نوبت‌دهی پزشک: در حالت «سرویس» زمان از مدت سرویس محاسبه و پیشنهاد می‌شود. const scheduleQ = useQuery>({ queryKey: ['drawer-schedule', doctorUuid], queryFn: () => api.get(`/api/v1/appointment-settings/weekly-schedule/${doctorUuid}`), enabled: !!doctorUuid, }); const bookingMode: 'slot' | 'service' = ((scheduleQ.data?.data as any)?.data?.meta ?? (scheduleQ.data?.data as any)?.meta)?.booking_mode === 'service' ? 'service' : 'slot'; const serviceMode = bookingMode === 'service' && !isReserve; const sectionsQ = useQuery>({ queryKey: ['service-sections'], queryFn: () => api.get('/api/v1/service-sections'), }); const itemsQ = useQuery>({ queryKey: ['service-items', sectionUuid], queryFn: () => api.get(`/api/v1/service-items/${sectionUuid}`), enabled: !!sectionUuid, }); const staffQ = useQuery>({ queryKey: ['staff-list'], queryFn: () => api.get('/api/v1/staff'), }); // ── timing ───────────────────────────────────────────────────────────────── const [date, setDate] = useState(defaultDate); const [duration, setDuration] = useState(40); const [start, setStart] = useState('15:00'); const [end, setEnd] = useState(addMinutes('15:00', 40)); useEffect(() => { setEnd(addMinutes(start, duration)); }, [start, duration]); // ── service-mode: چند سرویس + زمان‌های خالیِ پیشنهادی ───────────────────────── const [serviceUuids, setServiceUuids] = useState([]); const [svcNames, setSvcNames] = useState>({}); const [pickedSlot, setPickedSlot] = useState<{ start: number; end: number } | null>(null); useEffect(() => { setPickedSlot(null); }, [serviceUuids, date]); const svcSlotsQ = useQuery>({ queryKey: ['drawer-service-slots', doctorUuid, date, serviceUuids], queryFn: () => api.get( `/api/v1/appointment-service-slots?doctor_uuid=${doctorUuid}&date=${date}` + serviceUuids.map(u => `&service_item_uuids[]=${encodeURIComponent(u)}`).join('') ), enabled: serviceMode && !!date && serviceUuids.length > 0, }); const svcSlots = ((svcSlotsQ.data?.data as any)?.start_times ?? []) as Array<{ start: number; end: number; start_time: string }>; const totalMinutes = (svcSlotsQ.data?.data as any)?.total_duration_minutes as number | undefined; // ── deposit / status / notes ─────────────────────────────────────────────── const [depositRequired, setDepositRequired] = useState(false); const [depositRials, setDepositRials] = useState(0); const [status, setStatus] = useState('pending'); const [note, setNote] = useState(''); const effectiveName = pickedPatient?.user_name || name.trim(); const effectiveMobile = pickedPatient?.user_mobile || mobile.trim(); const effectiveNationalCode = (pickedPatient?.user_national_code || nationalCode).replace(/\D/g, ''); const timingValid = isReserve ? true : serviceMode ? (serviceUuids.length > 0 && !!pickedSlot) : (!!start && !!end); const valid = !!doctorUuid && !!date && effectiveName.length >= 2 && effectiveMobile.length >= 10 && effectiveNationalCode.length === 10 && timingValid; const create = useMutation({ mutationFn: async () => { const slotStart = isReserve ? toEpoch(date, '00:00') : serviceMode ? pickedSlot!.start : toEpoch(date, start); const slotEnd = isReserve ? toEpoch(date, '00:00') : serviceMode ? pickedSlot!.end : toEpoch(date, end); const payload: Record = { doctor_uuid: doctorUuid, slot_start: slotStart, slot_end: slotEnd, patient_name: effectiveName, patient_mobile: effectiveMobile, patient_national_code: effectiveNationalCode, is_reserve: isReserve, ...(sectionUuid ? { service_section_uuid: sectionUuid } : {}), // حالت سرویس: چند سرویس؛ حالت اسلاتی: تک سرویسِ workflow (اختیاری). ...(serviceMode ? { service_item_uuids: serviceUuids } : itemUuid ? { service_item_uuid: itemUuid } : {}), ...(staffUuid ? { staff_uuid: staffUuid } : {}), ...(depositRequired ? { deposit_required: true, deposit_amount_rials: depositRials } : {}), ...(note.trim() ? { note: note.trim() } : {}), }; const res: any = await api.post('/api/v1/my/appointment', payload); // POST creates a pending booking; apply the picked status afterwards. if (status !== 'pending' && res?.data?.uuid) { await api.patch(`/api/v1/appointment/${res.data.uuid}/status`, { status, version: 1 }); } return res; }, onSuccess: () => { qc.invalidateQueries({ queryKey }); toast.success(isReserve ? 'نوبت رزرو ثبت شد' : 'نوبت با موفقیت ثبت شد'); onClose(); }, onError: (e: any) => toast.error(e.message || 'خطا در ثبت نوبت'), }); const label = { fontSize: 12.5, color: 'var(--text-3)' } as const; const sel = { width: '100%', height: 38, borderRadius: 'var(--r-sm)', border: '1px solid var(--border)', background: 'var(--surface)', fontSize: 13, fontFamily: 'inherit', padding: '0 10px' } as const; const patients = useMemo(() => patientsQ.data?.data ?? [], [patientsQ.data]); return (
اطلاعات مراجعه کننده:
{ setPickedPatient(null); setPatientSearch(e.target.value); }} placeholder="جستجوی نام، شماره تماس، شماره پرونده..." />
{!pickedPatient && patients.length > 0 && (
{patients.map(p => ( ))}
)} {pickedPatient === null && ( <>
مراجعه کننده جدید
setName(e.target.value)} placeholder="نام و نام خانوادگی مراجعه کننده را وارد نمایید" />
setMobile(e.target.value)} placeholder="شماره تماس مراجعه کننده را وارد نمایید" dir="ltr" />
setNationalCode(e.target.value.replace(/\D/g, '').slice(0, 10))} placeholder="کد ملی مراجعه کننده را وارد نمایید" dir="ltr" inputMode="numeric" maxLength={10} />
)}
مشخصات سرویس:
{serviceMode && serviceUuids.length > 0 && (
{serviceUuids.map(uuid => ( {svcNames[uuid] ?? uuid} ))} {totalMinutes != null && مدت کل: {totalMinutes} دقیقه}
)}
زمان نوبت:
{serviceMode ? (
{serviceUuids.length === 0 ? (
ابتدا سرویس را انتخاب کنید.
) : svcSlotsQ.isLoading ? (
در حال محاسبه...
) : svcSlots.length === 0 ? (
برای این سرویس در این روز زمان خالی کافی نیست؛ روز دیگری انتخاب کنید.
) : (
{svcSlots.map(s => { const active = pickedSlot?.start === s.start; return ( ); })}
)}
) : ( <>
setDuration(Math.max(5, Number(e.target.value) || 0))} dir="ltr" />
{!isReserve && (
setStart(e.target.value)} dir="ltr" />
setEnd(e.target.value)} dir="ltr" />
)} )} {!isReserve && ( <>
بیعانه:
{depositRequired && (
)} )}