import React, { useMemo, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import Modal from '../ui/Modal'; import SearchableSelect from '../ui/SearchableSelect'; import { api, type ApiResponse } from '../../lib/api'; import { formatNumber } from '../../lib/utils'; import { REASON_LABELS, useAvailabilitySearch, useHold, type AvailableSlot, } from '../../hooks/useResourceBooking'; import type { ClinicResource, ResourceServiceOffering } from '../../types'; const DAY = 86400; function hhmm(ts: number): string { const d = new Date(ts * 1000); return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; } /** نقشی از تخصیص که این منبع در آن پیشنهاد شده — `null` یعنی این وقت مالِ منبع دیگری است. */ function roleHolding(slot: AvailableSlot, resourceUuid: string): string | null { for (const [role, resources] of Object.entries(slot.assignment)) { if (resources.some((r) => r.uuid === resourceUuid)) return role; } return null; } /** * ثبت نوبت برای یک منبع مشخص — سرویس‌های خودِ منبع، بعد زمان، بعد پزشک مسئول. * * موتور همان `appointment-availability → hold → confirm` است که از قبل هست؛ تنها * تفاوت این است که جستجو خدمت‌محور جواب می‌دهد و ما نتیجه را به وقت‌هایی تنگ می‌کنیم * که موتور در آن‌ها **همین منبع** را پیشنهاد داده. وقتی که منبع در آن آزاد نبوده اصلاً * نشان داده نمی‌شود، وگرنه اپراتور چیزی را انتخاب می‌کند که در `hold` خطای ۴۰۹ می‌گیرد. * * پزشک مسئول اجباری است: هر نوبت در این سیستم پزشک دارد و `confirm` بدونش کار نمی‌کند. */ export default function ResourceBookingModal({ resource, onClose, onBooked }: { resource: ClinicResource; onClose: () => void; onBooked: () => void; }) { const { create, confirm } = useHold(); const [serviceUuid, setServiceUuid] = useState(''); const [days, setDays] = useState('7'); const [pickedSlot, setPickedSlot] = useState(null); const [submitting, setSubmitting] = useState(false); // پزشکِ نوبت از ناظرِ خودِ منبع می‌آید، نه از یک انتخابگرِ دیگر: ارتباط پزشک و منبع // یک جا تعریف شده (فرم منبع) و تکرارش اینجا یعنی دو منبعِ حقیقت. const doctorUuid = resource.supervisor?.uuid ?? ''; // سرویس‌های همین منبع، نه کل کاتالوگ: منبعی که سرویسی را ارائه نمی‌دهد نباید // در فهرست بیاید — سرور هم همان را با ۴۲۲ رد می‌کند. const offeringsQuery = useQuery>({ queryKey: ['resource-services', resource.uuid], queryFn: () => api.get(`/api/v1/resource/${resource.uuid}/services`), }); const offerings = (offeringsQuery.data?.data ?? []).filter((o) => o.active); const range = useMemo(() => { const from = Math.floor(Date.now() / 1000); return { from, to: from + Number(days) * DAY }; }, [days]); const { result, loading } = useAvailabilitySearch( { serviceUuid, branchUuid: resource.address_uuid, from: range.from, to: range.to }, !!serviceUuid, ); const slots = useMemo( () => (result?.slots ?? []).filter((s) => roleHolding(s, resource.uuid) !== null), [result, resource.uuid], ); const reasonText = result?.reason ? REASON_LABELS[result.reason] ?? result.reason : null; const canSubmit = !!serviceUuid && !!pickedSlot && !!doctorUuid && !submitting; // ناظر در API الزامی است؛ ردیف‌های قدیمی‌ای که پزشکشان حذف شده `supervisor: null` دارند. const missingSupervisor = !resource.supervisor; const submit = async () => { if (!pickedSlot) return; setSubmitting(true); try { // تخصیص همان چیزی است که موتور برای این وقت داده، با یک قید: نقشی که این منبع // در آن آمده به خودش قفل می‌شود تا نوبت واقعاً روی همین دستگاه بنشیند. const role = roleHolding(pickedSlot, resource.uuid); const assignment = Object.fromEntries( Object.entries(pickedSlot.assignment).map(([r, list]) => [ r, r === role ? [resource.uuid] : list.map((x) => x.uuid), ]), ); const held = await create.mutateAsync({ service_uuid: serviceUuid, branch_uuid: resource.address_uuid, start: pickedSlot.start, assignment, }); await confirm.mutateAsync({ hold_uuid: held.data.hold_uuid, doctor_uuid: doctorUuid }); onBooked(); onClose(); } finally { setSubmitting(false); } }; return ( } > {missingSupervisor && (

این منبع پزشک ناظر ندارد و نوبتی برایش ثبت نمی‌شود — از «منابع» ناظرش را تعیین کنید.

)}
{offeringsQuery.isLoading ? (
) : offerings.length === 0 ? (

برای این منبع سرویسی تعریف نشده است — از تب «سرویس‌ها»ی همین منبع اضافه کنید.

) : ( ({ value: o.service_uuid, label: o.effective_duration_minutes ? `${o.service_name} · ${formatNumber(o.effective_duration_minutes)} دقیقه` : o.service_name, }))} value={serviceUuid || null} onChange={(v) => { setServiceUuid(v ? String(v) : ''); setPickedSlot(null); }} placeholder="سرویس را انتخاب کنید" height={40} /> )}
{serviceUuid && (
{ setDays(v ? String(v) : '7'); setPickedSlot(null); }} height={40} />
)} {serviceUuid && (
زمان‌های خالی این منبع {loading ? (
در حال محاسبه…
) : slots.length === 0 ? (

{reasonText ?? 'برای این منبع در این بازه وقت آزادی نیست — بازه را بزرگ‌تر کنید.'}

) : (
{slots.slice(0, 40).map((s) => { const active = pickedSlot?.start === s.start; return ( ); })}
)}
)}
پزشک ناظر: {resource.supervisor?.name ?? '—'}
نوبت به نام همین پزشک ثبت می‌شود. برای تغییرش، ناظرِ منبع را در «منابع» ویرایش کنید.
); }