import { useMutation, useQuery } from '@tanstack/react-query'; import { toast } from 'sonner'; import { api, ApiError, type ApiResponse } from '../lib/api'; /** * جستجوی وقت، رزرو موقت و ثبت نهایی در حالت منبع‌محور. * * سه عمل جدا هستند و باید جدا بمانند: بین «دیدن وقت» و «گرفتنش» صندلی هنوز آزاد است، * و بین «گرفتن» و «ثبت» یک مهلت وجود دارد که اگر نگذرد، ظرفیت برای همیشه قفل می‌ماند. */ export interface ResourceRef { uuid: string; name: string; } export type SlotAssignment = Record; export interface AvailableSlot { start: number; end: number; assignment: SlotAssignment; } export interface PlanSegment { sequence: number; name: string; offset_minutes: number; duration_minutes: number; patient_present: boolean; requirements: { role: string; role_name: string; count: number }[]; } export interface AvailabilityResult { plan: { total_minutes: number; segments: PlanSegment[] }; slots: AvailableSlot[]; /** خالی بودن فهرست خطا نیست؛ این می‌گوید چرا خالی است. */ reason: string | null; } export interface HoldResult { hold_uuid: string; starts_at: number; ends_at: number; expires_at: number; confirmed: boolean; assignment: SlotAssignment; } export const REASON_LABELS: Record = { no_capacity_in_range: 'در این بازه هیچ ظرفیتی نیست — بازه را بزرگ‌تر کنید یا شعبهٔ دیگری را امتحان کنید.', no_working_hours: 'شعبه در این بازه ساعت کاری ندارد.', no_eligible_resource: 'هیچ منبعی شرایط بخش‌های این خدمت را ندارد.', }; function fail(e: unknown, fallback: string) { toast.error(e instanceof ApiError ? e.message : fallback); } export function useAvailabilitySearch( params: { serviceUuid: string; branchUuid: string; from: number; to: number; stepMinutes?: number }, enabled: boolean, ) { const query = useQuery({ queryKey: ['resource-availability', params], queryFn: () => api.post>('/api/v1/appointment-availability', { service_uuid: params.serviceUuid, branch_uuid: params.branchUuid, from: params.from, to: params.to, ...(params.stepMinutes ? { step_minutes: params.stepMinutes } : {}), }), enabled: enabled && !!params.serviceUuid && !!params.branchUuid, retry: false, }); return { result: query.data?.data, loading: query.isFetching, error: query.error, refetch: query.refetch, }; } export function useHold() { const create = useMutation({ mutationFn: (body: { service_uuid: string; branch_uuid: string; start: number; assignment: Record; item_uuids?: string[]; patient_gender?: string; }) => api.post>('/api/v1/appointment-hold', body), // ۴۰۹ یعنی همین لحظه کس دیگری گرفت — پیام سرور دقیقاً همین را می‌گوید. onError: (e) => fail(e, 'گرفتن این زمان ناموفق بود'), }); const release = useMutation({ mutationFn: (uuid: string) => api.delete>(`/api/v1/appointment-hold/${uuid}`), onSuccess: () => toast.success('رزرو موقت آزاد شد'), onError: (e) => fail(e, 'آزادسازی ناموفق بود'), }); const confirm = useMutation({ mutationFn: (body: { hold_uuid: string; doctor_uuid: string; patient_uuid?: string }) => api.post>('/api/v1/appointment-confirm', body), onSuccess: () => toast.success('نوبت ثبت شد'), onError: (e) => fail(e, 'ثبت نهایی ناموفق بود'), }); /** * جابه‌جایی: رزرو موقتِ زمان تازه از قبل گرفته شده و اینجا فقط تأیید می‌شود. * * عمداً از `confirm` جداست — سرور در همین یک درخواست زمان قدیم را هم آزاد می‌کند، پس * لغو دستی پیش از رزرو یعنی پنجره‌ای که در آن بیمار اصلاً نوبت ندارد. */ const rebook = useMutation({ mutationFn: ({ appointmentUuid, holdUuid }: { appointmentUuid: string; holdUuid: string }) => api.post>( `/api/v1/appointment/${appointmentUuid}/rebook`, { hold_uuid: holdUuid }, ), onSuccess: () => toast.success('نوبت جابه‌جا شد'), onError: (e) => fail(e, 'جابه‌جایی نوبت ناموفق بود'), }); return { create, release, confirm, rebook }; } export interface AppointmentSegmentRow { sequence: number; name: string; starts_at: number; ends_at: number; duration_minutes: number; patient_present: boolean; } /** فهرست خالی یعنی نوبت اسلاتی است، نه اینکه چیزی خراب باشد. */ export function useAppointmentSegments(uuid: string | undefined) { const query = useQuery({ queryKey: ['appointment-segments', uuid], queryFn: () => api.get>(`/api/v1/appointment/${uuid}/segments`), enabled: !!uuid, }); const rows = query.data?.data; return { segments: Array.isArray(rows) ? rows : [], loading: query.isLoading }; }