import { useQuery } from '@tanstack/react-query'; import { Link } from 'react-router-dom'; import { api } from '../lib/api'; import type { ApiResponse } from '../lib/api'; import { useAuthStore } from '../stores/authStore'; import Modal from './ui/Modal'; import { formatDate, formatDateTime, formatRial } from '../lib/utils'; import { METHOD_LABELS } from './session/PaymentStep'; interface InvoiceItem { uuid: string; title: string; quantity: number; total_rials: number; patient_rials: number } /** ثبت‌کنندهٔ پرداخت — نامش زنده از خودِ کاربر حل می‌شود، نه از عکسِ لحظهٔ ثبت. */ interface PaymentRecorder { user_uuid: string; name: string | null; role: 'admin' | 'clinic' | 'doctor' | 'secretary' | 'staff' | 'representation' | 'user'; doctor_uuid: string | null; } interface SessionPayment { uuid: string; method: string; amount_rials: number; paid_at: number; created_by_name: string | null; created_by?: PaymentRecorder | null; } interface SessionConsumable { uuid: string; item_name: string; quantity: number; line_total_rials: number } interface SessionData { session_at: number | null; paid_at: number | null; services_total_rials: number; consumables_total_rials: number; discount_rials: number; final_price_rials: number; paid_total_rials: number; gross_total_rials?: number; base_insurance_rials?: number; supplementary_insurance_rials?: number; patient_share_rials?: number; remaining_rials?: number; payments: SessionPayment[]; consumables: SessionConsumable[]; } interface Invoice { uuid: string; status: string; issued_at: number; total_rials: number; base_insurance_rials: number; supplementary_rials: number; patient_rials: number; /** نوع خدمتِ بیمه‌ای که فاکتور با آن محاسبه شده + نام بیمه‌ها. */ service_category?: string | null; service_category_label?: string | null; base_insurance_name?: string | null; supplementary_insurance_name?: string | null; items: InvoiceItem[]; session?: SessionData | null; } const STATUS_LABEL: Record = { paid: 'پرداخت شده', finalized: 'بدهکار', draft: 'پیش‌نویس', void: 'باطل' }; /** A titled table block — mirrors tauri InvoiceSummary `SectionTable`. */ function SectionTable({ title, cols, rows }: { title: string; cols: string[]; rows: React.ReactNode[][] }) { return (
{title}
{cols.map((c, i) => ( ))} {rows.map((row, i) => ( {row.map((cell, j) => ( ))} ))}
{c}
{cell}
); } /** * پروفایلِ ثبت‌کننده در پنلِ همین بیننده — یا `null` وقتی صفحه‌ای برایش وجود ندارد. * * فقط پزشک صفحهٔ پروفایلِ مستقل دارد؛ منشی و پرسنل صفحهٔ فهرستِ مدیریتشان را دارند * و ادمین فهرستِ خودش را. مسیری که نقشِ بیننده اجازه‌اش را ندارد لینک نمی‌شود، وگرنه * کلیک به داشبورد پرت می‌کرد. */ function recorderProfilePath(recorder: PaymentRecorder, viewerRole: string | null): string | null { if (recorder.role === 'doctor' && recorder.doctor_uuid) { return ['admin', 'doctor', 'clinic', 'representation'].includes(viewerRole ?? '') ? `/admin/doctors/${recorder.doctor_uuid}` : null; } if (recorder.role === 'secretary') { if (viewerRole === 'admin') return '/admin/secretaries'; return viewerRole === 'clinic' || viewerRole === 'doctor' ? '/admin/my-secretaries' : null; } if (recorder.role === 'staff') { return ['clinic', 'doctor', 'secretary'].includes(viewerRole ?? '') ? '/admin/staff' : null; } if (viewerRole === 'admin') return `/admin/users/${recorder.user_uuid}`; return null; } /** سلولِ «ثبت‌کننده»: نام، و اگر پروفایلی در دسترسِ بیننده باشد، لینکش. */ function RecorderCell({ payment, viewerRole }: { payment: SessionPayment; viewerRole: string | null }) { const recorder = payment.created_by ?? null; const name = recorder?.name ?? payment.created_by_name; if (!name) return <>-; const path = recorder ? recorderProfilePath(recorder, viewerRole) : null; return path ? {name} : <>{name}; } /** خلاصه فاکتور — invoice summary, ported pixel-for-pixel from tauri InvoiceSummary. */ export default function InvoiceSummaryModal({ invoiceUuid, onClose }: { invoiceUuid: string | null; onClose: () => void }) { const viewerRole = useAuthStore(s => s.primaryRole); const { data, isLoading } = useQuery>({ queryKey: ['invoice', invoiceUuid], queryFn: () => api.get(`/api/v1/billing/invoices/${invoiceUuid}`), enabled: !!invoiceUuid, }); // billing show wraps as { data: { data: invoice } } const inv = ((data?.data as any)?.data ?? data?.data ?? null) as Invoice | null; const session = inv?.session ?? null; const paid = inv?.status === 'paid'; // مانده را سرور می‌دهد (session.remaining_rials)؛ محاسبه‌ی محلی با صفحه‌ی پرداخت واگرا می‌شد. const remaining = session ? session.remaining_rials ?? Math.max(0, session.final_price_rials - (session.discount_rials ?? 0) - session.paid_total_rials) : inv ? (paid ? 0 : inv.patient_rials) : 0; const paidAmount = session ? session.paid_total_rials : inv ? inv.total_rials - remaining : 0; // یک شکل واحد برای خلاصه‌ی مالی؛ با session از خود مراجعه، بدون آن از فاکتور. const summary = { services: session ? session.services_total_rials : inv?.total_rials ?? 0, consumables: session?.consumables_total_rials ?? 0, discount: session?.discount_rials ?? 0, baseInsurance: session?.base_insurance_rials ?? inv?.base_insurance_rials ?? 0, suppInsurance: session?.supplementary_insurance_rials ?? inv?.supplementary_rials ?? 0, patientShare: session?.patient_share_rials ?? session?.final_price_rials ?? inv?.patient_rials ?? 0, gross: session?.gross_total_rials ?? inv?.total_rials ?? 0, }; // وضعیت واقعی پرداخت (مستقل از وضعیت فریزشده‌ی فاکتور): تسویه‌شده اگر مانده صفر. const statusLabel = session ? (remaining <= 0 ? 'تسویه شده' : 'بدهکار') : (inv ? (STATUS_LABEL[inv.status] ?? inv.status) : ''); return ( {isLoading || !inv ? (
در حال بارگذاری…
) : (
0 ? 'var(--danger)' : 'var(--success)', fontWeight: 600 }}>{statusLabel}, ]]} /> {(inv.base_insurance_name || inv.service_category_label) && ( )} [it.title, it.quantity, formatRial(it.total_rials)]) : [['—', '—', '—']]} /> {session && ( [c.item_name, c.quantity, formatRial(c.line_total_rials)]) : [['-', '-', '-']]} /> )} 0 ? formatRial(summary.consumables) : '-', formatRial(summary.baseInsurance), formatRial(summary.suppInsurance), formatRial(summary.patientShare), summary.discount > 0 ? formatRial(summary.discount) : '-', formatRial(Math.max(0, summary.patientShare - summary.discount)), ]]} /> {session && ( [ i + 1, METHOD_LABELS[p.method] ?? p.method, formatRial(p.amount_rials), p.paid_at ? formatDateTime(p.paid_at) : '-', , ]), ['', مجموع پرداختی‌ها, {formatRial(session.paid_total_rials)}, '', ''], ] : [['-', '-', '-', '-', '-']]} /> )} 0 ? 'var(--danger)' : 'var(--success)', fontWeight: 600 }}>{formatRial(remaining)}, ]]} />
)}
); }