import { useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useParams, useSearchParams } from 'react-router-dom'; import { toast } from 'sonner'; import { api } from '../lib/api'; import { formatRial, formatNumber, formatDate, formatDateTime } from '../lib/utils'; import type { ClaimDetailRow, ClaimStatusLogEntry } from '../types'; import type { ApiResponse } from '../lib/api'; import PageHeader from '../components/ui/PageHeader'; import DataTable, { type Column } from '../components/ui/DataTable'; import StatusBadge from '../components/ui/StatusBadge'; import SearchableSelect from '../components/ui/SearchableSelect'; import Modal from '../components/ui/Modal'; import FeatureGate from '../components/ui/FeatureGate'; interface PatientHeader { uuid: string; record_uuid: string; full_name: string | null; mobile: string | null; national_code: string | null; } interface DetailPayload { patient: PatientHeader; claims: ClaimDetailRow[]; } const STATUS_OPTIONS = [ { value: '', label: 'همه وضعیت‌ها' }, { value: 'pending', label: 'در انتظار ارسال' }, { value: 'submitted', label: 'ارسال شده' }, { value: 'approved', label: 'تأیید شده' }, { value: 'rejected', label: 'رد شده' }, { value: 'paid', label: 'پرداخت شده' }, ]; const ACTION_LABEL: Record = { submitted: 'ارسال به بیمه', approved: 'تأیید', rejected: 'رد', paid: 'ثبت پرداخت', }; /** نام endpoint برای هر وضعیت مقصد — وضعیت‌ها اسم فعل ندارند. */ const ACTION_PATH: Record = { submitted: 'submit', approved: 'approve', rejected: 'reject', paid: 'pay', }; const KIND_LABEL: Record = { base: 'پایه', supplementary: 'تکمیلی' }; function Timeline({ logs }: { logs: ClaimStatusLogEntry[] }) { if (logs.length === 0) return
رویدادی ثبت نشده است.
; return (
{logs.map((log) => (
{formatDateTime(log.at)} {log.by ? ` · ${log.by}` : ''}
{log.note &&
{log.note}
}
))}
); } /** داشبورد پرونده‌های بیمه — سطح دوم: همه‌ی مطالبات یک بیمار با تاریخچه‌ی کامل. */ export default function ClaimPatientDetailPage() { const { patientUuid = '' } = useParams(); const [params, setParams] = useSearchParams(); const queryClient = useQueryClient(); const [selected, setSelected] = useState(null); const [pendingAction, setPendingAction] = useState<{ claim: ClaimDetailRow; target: string } | null>(null); const [reason, setReason] = useState(''); const [trackingNumber, setTrackingNumber] = useState(''); const status = params.get('status') ?? ''; const setParam = (patch: Record) => { const next = new URLSearchParams(params); Object.entries(patch).forEach(([k, v]) => (v ? next.set(k, v) : next.delete(k))); setParams(next, { replace: true }); }; const queryString = status ? `?status=${status}` : ''; const detailQuery = useQuery>({ queryKey: ['claims-patient-detail', patientUuid, status], queryFn: () => api.get(`/api/v1/billing/claims/by-patient/${patientUuid}${queryString}`), enabled: !!patientUuid, }); const payload = detailQuery.data?.data ?? null; const patient = payload?.patient ?? null; const claims = payload?.claims ?? []; const transitionMut = useMutation({ mutationFn: ({ uuid, target, body }: { uuid: string; target: string; body: Record }) => api.post(`/api/v1/billing/claims/${uuid}/${ACTION_PATH[target]}`, body), onSuccess: () => { toast.success('وضعیت مطالبه به‌روز شد'); queryClient.invalidateQueries({ queryKey: ['claims-patient-detail'] }); queryClient.invalidateQueries({ queryKey: ['claims-by-patient'] }); queryClient.invalidateQueries({ queryKey: ['insurance-debt'] }); closeAction(); }, onError: (e: unknown) => toast.error(e instanceof Error ? e.message : 'ثبت تغییر وضعیت ناموفق بود'), }); const closeAction = () => { setPendingAction(null); setReason(''); setTrackingNumber(''); }; const confirmAction = () => { if (!pendingAction) return; const { claim, target } = pendingAction; if (target === 'rejected' && reason.trim() === '') { toast.error('دلیل رد الزامی است'); return; } transitionMut.mutate({ uuid: claim.uuid, target, body: target === 'rejected' ? { reason: reason.trim() } : { tracking_number: trackingNumber.trim() || undefined }, }); }; const columns: Column[] = [ { key: 'visit_date', header: 'تاریخ مراجعه', render: (r) => (r.visit_date ? formatDate(r.visit_date) : '—') }, { key: 'doctor_name', header: 'پزشک', render: (r) => r.doctor_name ?? '—' }, { key: 'insurance_name', header: 'بیمه', render: (r) => `${r.insurance_name ?? '—'} (${KIND_LABEL[r.insurance_kind] ?? r.insurance_kind})` }, { key: 'service_base_rials', header: 'مبلغ اصلی', render: (r) => formatRial(r.service_base_rials) }, { key: 'coverage_percent', header: 'پوشش بیمه', render: (r) => `${formatNumber(r.coverage_percent)}٪` }, { key: 'insurance_share_rials', header: 'سهم بیمه', render: (r) => formatRial(r.insurance_share_rials) }, { key: 'patient_share_rials', header: 'سهم بیمار', render: (r) => formatRial(r.patient_share_rials) }, { key: 'status', header: 'وضعیت', render: (r) => }, { key: 'submitted_at', header: 'تاریخ ارسال', render: (r) => (r.submitted_at ? formatDate(r.submitted_at) : '—') }, { key: 'settled_at', header: 'تاریخ تسویه', render: (r) => (r.settled_at ? formatDate(r.settled_at) : '—') }, { key: 'tracking_number', header: 'شماره پیگیری', render: (r) => r.tracking_number ?? '—' }, ]; return (
setParam({ status: v ? String(v) : '' })} placeholder="همه وضعیت‌ها" height={38} />
(
{row.allowed_transitions.map((target) => ( ))}
)} />
setSelected(null)}> {selected && (
بیمه: {selected.insurance_name ?? '—'}
نوع: {KIND_LABEL[selected.insurance_kind] ?? selected.insurance_kind}
مبلغ اصلی: {formatRial(selected.service_base_rials)}
سهم بیمه: {formatRial(selected.insurance_share_rials)}
سهم بیمار: {formatRial(selected.patient_share_rials)}
تأییدشده: {selected.total_approved_rials !== null ? formatRial(selected.total_approved_rials) : '—'}
پرداخت‌شده: {selected.total_paid_rials !== null ? formatRial(selected.total_paid_rials) : '—'}
شماره پیگیری: {selected.tracking_number ?? '—'}
{selected.reject_reason && (
دلیل رد: {selected.reject_reason}
)}
تاریخچه تغییرات
)}
} > {pendingAction?.target === 'rejected' ? (