import { useEffect, useMemo, useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; import { UserCircleIcon } from '@heroicons/react/24/outline'; import { api } from '../../lib/api'; import type { ApiResponse } from '../../lib/api'; import { formatRial, rialToToman, tomanToRial } from '../../lib/utils'; import Modal from '../ui/Modal'; import PriceInput from '../ui/PriceInput'; import SearchableSelect from '../ui/SearchableSelect'; /** همان چهار روشِ SessionPayment::METHODS در بک‌اند. */ const METHOD_OPTIONS = [ { value: 'cash', label: 'پرداخت نقدی' }, { value: 'pos', label: 'پرداخت از طریق کارت خوان' }, { value: 'card', label: 'کارت به کارت' }, { value: 'wallet', label: 'پرداخت از طریق کیف پول' }, ]; interface ServiceItem { uuid: string; name: string; price_rials?: number | null; } interface AppointmentLike { uuid: string; version?: number; visit_price_rials?: number | null; service_items?: ServiceItem[] | null; patient_name?: string | null; } interface Props { open: boolean; appointmentUuid: string; /** اگر صفحه از قبل نوبت را دارد، پاس بده تا درخواست اضافه نرود. */ appointment?: AppointmentLike | null; onClose: () => void; /** کلید کوئریِ لیستی که بعد از قطعی‌شدن باید invalidate شود. */ queryKey?: unknown[]; } const rowStyle: React.CSSProperties = { display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12, padding: '9px 0', fontSize: 13.5, color: 'var(--text-2)', }; /** رنگ چیپِ «وضعیت پرداخت» بر اساس نسبت پرداخت به جمع کل. */ const STATE_TONE: Record = { 'بدون پرداخت': { fg: 'var(--text-2)', bg: 'var(--surface-3)' }, 'پرداخت جزئی': { fg: 'var(--warning)', bg: 'var(--warning-bg)' }, 'تسویه کامل': { fg: 'var(--success)', bg: 'var(--success-bg)' }, }; /** * «قطعی کردن نوبت» — هزینه‌های نوبت را نشان می‌دهد، پرداخت کامل یا جزئی می‌گیرد و * نوبت را از «ثبت شده» به «قطعی شده» می‌برد. * * سرور همین یک درخواست را اتمیک انجام می‌دهد: وضعیت + پرونده/مراجعه + پرداخت‌ها. */ export default function ConfirmAppointmentModal({ open, appointmentUuid, appointment, onClose, queryKey, }: Props) { const qc = useQueryClient(); const [method, setMethod] = useState('cash'); const [amountToman, setAmountToman] = useState(0); /** تا وقتی کاربر مبلغ را دست نزده، فیلد با باقی‌ماندهٔ نوبت پر می‌ماند. */ const [touched, setTouched] = useState(false); // وقتی صفحه‌ی میزبان نوبت را ندارد (مثل ردیف لیست) خودمان جزئیات را می‌گیریم: // مبلغ ویزیت و قیمت سرویس‌ها فقط در detail هستند. const detailQuery = useQuery({ queryKey: ['appointment', appointmentUuid], queryFn: () => api.get>(`/api/v1/appointment/${appointmentUuid}`), enabled: open && !appointment, }); const appt: AppointmentLike | null = appointment ?? ((detailQuery.data?.data as any)?.data ?? detailQuery.data?.data ?? null); const visitPrice = Number(appt?.visit_price_rials ?? 0); const services = appt?.service_items ?? []; const servicesTotal = useMemo( () => services.reduce((sum, s) => sum + Number(s.price_rials ?? 0), 0), [services], ); const total = visitPrice + servicesTotal; // جمع کل تا لحظه‌ای که کاربر مبلغ را دستی تغییر ندهد پیش‌فرضِ «پرداخت کامل» است؛ // نوبت هنوز session ندارد، پس باقی‌مانده‌اش برابر کل هزینه است. useEffect(() => { if (!open || touched || total <= 0) return; setAmountToman(rialToToman(total)); }, [open, touched, total]); const amountRials = tomanToRial(amountToman); const remaining = Math.max(0, total - amountRials); const overpaid = amountRials > total; const paymentState = amountRials === 0 ? 'بدون پرداخت' : remaining === 0 ? 'تسویه کامل' : 'پرداخت جزئی'; const confirmMut = useMutation({ mutationFn: () => api.post>(`/api/v1/appointment/${appointmentUuid}/confirm`, { version: appt?.version, payments: amountRials > 0 ? [{ method, amount_rials: amountRials }] : [], }), onSuccess: () => { toast.success('نوبت قطعی شد'); if (queryKey) qc.invalidateQueries({ queryKey }); qc.invalidateQueries({ queryKey: ['appointment', appointmentUuid] }); qc.invalidateQueries({ queryKey: ['appointment-events', appointmentUuid] }); reset(); onClose(); }, onError: (e: any) => toast.error(e?.message || 'قطعی کردن نوبت ناموفق بود'), }); function reset() { setAmountToman(0); setMethod('cash'); setTouched(false); } /** تغییر دستی مبلغ: از این به بعد پیش‌فرضِ خودکار دیگر روی فیلد ننشیند. */ function changeAmount(next: number) { setTouched(true); setAmountToman(next); } function handleClose() { reset(); onClose(); } const loading = detailQuery.isLoading && !appointment; return ( } > {loading ? (

در حال دریافت اطلاعات نوبت…

) : ( <> {appt?.patient_name && (
بیمار {appt.patient_name}
)} {/* هزینه‌ها */}
ویزیت {formatRial(visitPrice)}
{services.map((s) => (
{s.name} {formatRial(Number(s.price_rials ?? 0))}
))}
جمع کل {formatRial(total)}
{/* پرداخت */}
setMethod(String(v ?? 'cash'))} options={METHOD_OPTIONS} placeholder="روش پرداخت" height={40} />
باقی‌مانده پس از این پرداخت: {formatRial(remaining)}
{amountToman > 0 && ( )}
{overpaid && (

مبلغ پرداخت از جمع کل بیشتر است.

)} {/* خلاصه */}
پرداخت‌شده {formatRial(Math.min(amountRials, total))}
باقی‌مانده 0 ? 'var(--danger)' : 'var(--success)' }}> {formatRial(remaining)}
وضعیت پرداخت {paymentState}
)}
); }