import { useState, useMemo, useEffect } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { CheckIcon, XMarkIcon, PaperAirplaneIcon, BanknotesIcon, MagnifyingGlassIcon, ArrowPathIcon } from '@heroicons/react/24/outline'; import { toast } from 'sonner'; import { api } from '../lib/api'; import { formatRial, formatNumber, formatDate } from '../lib/utils'; import PageHeader from '../components/ui/PageHeader'; import Modal from '../components/ui/Modal'; import SearchableSelect from '../components/ui/SearchableSelect'; import PersianDatePicker from '../components/ui/PersianDatePicker'; import FeatureGate from '../components/ui/FeatureGate'; import Pagination from '../components/ui/Pagination'; const isoNDaysAgo = (days: number): string => { const d = new Date(); d.setDate(d.getDate() - days); return d.toISOString().slice(0, 10); }; const todayIso = (): string => new Date().toISOString().slice(0, 10); const toUnix = (iso: string): number | null => { if (!iso) return null; const t = new Date(iso + 'T00:00:00').getTime(); return Number.isNaN(t) ? null : Math.floor(t / 1000); }; const endOfDayUnix = (iso: string): number | null => { if (!iso) return null; const t = new Date(iso + 'T23:59:59').getTime(); return Number.isNaN(t) ? null : Math.floor(t / 1000); }; interface ClaimItem { invoice_item_id: number; claimed_rials: number; approved_rials: number | null; title: string | null; is_visit: boolean; quantity: number; total_rials: number | null; visit_date: number | null; } interface Claim { uuid: string; insurance_id: number; insurance_name: string | null; insurance_kind: string; total_claimed_rials: number; total_approved_rials: number | null; total_paid_rials: number | null; status: string; reject_reason: string | null; patient_name: string | null; patient_mobile: string | null; items: ClaimItem[]; } interface InsuranceOption { insurance_id: number; insurance_name: string | null } interface DebtRow { insurance_id: number; insurance_name: string | null; claimed: number; approved: number; paid: number; debt: number; } const STATUS_META: Record = { pending: { label: 'در انتظار', cls: 'gray' }, submitted: { label: 'ارسال‌شده', cls: 'blue' }, approved: { label: 'تأییدشده', cls: 'amber' }, rejected: { label: 'ردشده', cls: 'red' }, paid: { label: 'پرداخت‌شده', cls: 'green' }, }; const KIND_LABEL: Record = { base: 'پایه', supplementary: 'تکمیلی' }; const STATUS_FILTERS = ['', 'pending', 'submitted', 'approved', 'rejected', 'paid']; export default function ClaimsPage() { const qc = useQueryClient(); const [statusFilter, setStatusFilter] = useState(''); const [insuranceFilter, setInsuranceFilter] = useState(''); const [fromDate, setFromDate] = useState(''); const [toDate, setToDate] = useState(''); const [search, setSearch] = useState(''); const [searchInput, setSearchInput] = useState(''); const [rejectTarget, setRejectTarget] = useState(null); const [rejectReason, setRejectReason] = useState(''); const [page, setPage] = useState(1); const limit = 20; // back to page 1 whenever a filter changes useEffect(() => { setPage(1); }, [statusFilter, insuranceFilter, fromDate, toDate, search]); const queryString = useMemo(() => { const p = new URLSearchParams(); if (statusFilter) p.set('status', statusFilter); if (insuranceFilter) p.set('insurance_id', insuranceFilter); const from = toUnix(fromDate); if (from) p.set('from', String(from)); const to = endOfDayUnix(toDate); if (to) p.set('to', String(to)); if (search.trim()) p.set('q', search.trim()); p.set('page', String(page)); p.set('limit', String(limit)); const s = p.toString(); return s ? `?${s}` : ''; }, [statusFilter, insuranceFilter, fromDate, toDate, search, page]); const claimsQuery = useQuery<{ data: { data: Claim[] } }>({ queryKey: ['claims', queryString], queryFn: () => api.get(`/api/v1/billing/claims${queryString}`), }); const insuranceOptionsQuery = useQuery<{ data: { data: InsuranceOption[] } }>({ queryKey: ['claim-insurance-options'], queryFn: () => api.get('/api/v1/billing/reports/insurance-debt'), }); const insuranceOptions = ((insuranceOptionsQuery.data as any)?.data?.data ?? []) as InsuranceOption[]; const debtQuery = useQuery<{ data: { data: DebtRow[] } }>({ queryKey: ['insurance-debt'], queryFn: () => api.get('/api/v1/billing/reports/insurance-debt'), }); const claims = (claimsQuery.data as any)?.data?.data ?? []; const claimsTotal = (claimsQuery.data as any)?.data?.meta?.totalRecords ?? 0; const debt = (debtQuery.data as any)?.data?.data ?? []; const transitionMut = useMutation({ mutationFn: ({ uuid, action, body }: { uuid: string; action: string; body?: object }) => api.post(`/api/v1/billing/claims/${uuid}/${action}`, body ?? {}), onSuccess: () => { toast.success('وضعیت مطالبه به‌روزرسانی شد'); setRejectTarget(null); setRejectReason(''); qc.invalidateQueries({ queryKey: ['claims'] }); qc.invalidateQueries({ queryKey: ['insurance-debt'] }); }, onError: (e: Error) => toast.error(e.message), }); return (

بدهی بیمه‌ها

{debtQuery.isLoading ? (
در حال بارگذاری...
) : debt.length === 0 ? (
بدهی‌ای ثبت نشده است.
) : (
{debt.map((d: DebtRow) => (
{d.insurance_name ?? `بیمه #${formatNumber(d.insurance_id)}`} ادعا {formatRial(d.claimed)} · پرداخت {formatRial(d.paid)} 0 ? 'var(--danger)' : 'var(--success, #16a34a)' }}>بدهی {formatRial(d.debt)}
))}
)}

مطالبات

{(statusFilter || insuranceFilter || fromDate || toDate || search) && ( )}
{/* نوار فیلتر */}
({ value: String(o.insurance_id), label: o.insurance_name ?? `بیمه #${o.insurance_id}` }))]} value={insuranceFilter} onChange={(v) => setInsuranceFilter(v ? String(v) : '')} placeholder="همه بیمه‌ها" height={38} />
({ value: s, label: s === '' ? 'همه وضعیت‌ها' : STATUS_META[s]?.label ?? s }))} value={statusFilter} onChange={(v) => setStatusFilter(v ? String(v) : '')} placeholder="همه وضعیت‌ها" height={38} />
setSearchInput(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') setSearch(searchInput); }} placeholder="نام، موبایل یا کدملی بیمار" />
{claimsQuery.isLoading ? (
در حال بارگذاری...
) : claims.length === 0 ? (
مطالبه‌ای یافت نشد.
) : (
{claims.map((c: Claim) => { const meta = STATUS_META[c.status] ?? { label: c.status, cls: 'gray' }; return (
{c.insurance_name ?? `بیمه #${formatNumber(c.insurance_id)}`} {KIND_LABEL[c.insurance_kind] ?? c.insurance_kind} {meta.label}
{c.patient_name && (
بیمار: {c.patient_name} {c.patient_mobile && {' '}{c.patient_mobile}}
)}
ادعا {formatRial(c.total_claimed_rials)} {c.total_approved_rials != null && ` · تأیید ${formatRial(c.total_approved_rials)}`} {c.total_paid_rials != null && ` · پرداخت ${formatRial(c.total_paid_rials)}`} {c.reject_reason && ` · دلیل رد: ${c.reject_reason}`}
{c.status === 'pending' && ( )} {c.status === 'submitted' && ( <> )} {c.status === 'approved' && ( )}
{c.items.length > 0 && (
{c.items.map((it, idx) => ( ))}
شرح تاریخ مراجعه تعداد مبلغ کل سهم بیمه (ادعا) تأییدشده
{it.is_visit ? 'ویزیت' : 'خدمت'} {it.title ?? '—'} {it.visit_date ? formatDate(it.visit_date) : '—'} {formatNumber(it.quantity)} {it.total_rials != null ? formatRial(it.total_rials) : '—'} {formatRial(it.claimed_rials)} {it.approved_rials != null ? formatRial(it.approved_rials) : '—'}
)}
); })}
)}
setRejectTarget(null)} title="رد مطالبه" footer={ <> } >