Files
clinicpro/assets/admin/components/InvoiceSummaryModal.tsx
T
hamedandClaude Opus 4.8 0e0e4ebe71 feat: enrich invoice summary with session payments, consumables and discount
Port the remaining gap of tauri /files/invoice-summary into the admin
invoice summary: GET /api/v1/billing/invoices/{uuid} now includes a
'session' key (full PatientSession payload) so InvoiceSummaryModal can
render the consumables table, the itemized payments table (method,
amount, date-time, recorder) and real discount/paid/remaining figures
instead of heuristics. Invoices without a source session keep the
previous behavior (session: null).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 14:38:38 +03:30

151 lines
7.1 KiB
TypeScript

import { useQuery } from '@tanstack/react-query';
import { api } from '../lib/api';
import type { ApiResponse } from '../lib/api';
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 SessionPayment { uuid: string; method: string; amount_rials: number; paid_at: number; created_by_name: string | 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;
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;
items: InvoiceItem[];
session?: SessionData | null;
}
const STATUS_LABEL: Record<string, string> = { 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 (
<div style={{ marginBottom: 24 }}>
<div style={{ fontSize: 16, fontWeight: 600, color: 'var(--text)', marginBottom: 12 }}>{title}</div>
<div style={{ border: '1px solid var(--border)', borderRadius: 8, overflow: 'hidden' }}>
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: 13 }}>
<thead>
<tr>
{cols.map((c, i) => (
<th key={i} style={{ textAlign: 'center', fontWeight: 600, padding: '12px 16px', background: 'var(--info-bg, #ebf5ff)', borderBottom: '1px solid var(--border)', color: 'var(--text-2)' }}>{c}</th>
))}
</tr>
</thead>
<tbody>
{rows.map((row, i) => (
<tr key={i} style={{ background: i % 2 === 1 ? 'var(--surface-2)' : 'transparent' }}>
{row.map((cell, j) => (
<td key={j} style={{ textAlign: 'center', padding: '12px 16px', borderBottom: i === rows.length - 1 ? 'none' : '1px solid var(--border)', color: 'var(--text)' }}>{cell}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
/** خلاصه فاکتور — invoice summary, ported pixel-for-pixel from tauri InvoiceSummary. */
export default function InvoiceSummaryModal({ invoiceUuid, onClose }: { invoiceUuid: string | null; onClose: () => void }) {
const { data, isLoading } = useQuery<ApiResponse<any>>({
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: مبالغ واقعی پرداخت؛ بدون آن (فاکتور قدیمی): heuristic قبلی.
const remaining = session
? session.final_price_rials - session.paid_total_rials
: inv ? (paid ? 0 : inv.patient_rials) : 0;
const paidAmount = session ? session.paid_total_rials : inv ? inv.total_rials - remaining : 0;
return (
<Modal open={!!invoiceUuid} onClose={onClose} title="خلاصه فاکتور" size="xl">
{isLoading || !inv ? (
<div style={{ padding: 24, textAlign: 'center', color: 'var(--text-3)' }}>در حال بارگذاری…</div>
) : (
<div dir="rtl">
<SectionTable
title="اطلاعات فاکتور"
cols={['تاریخ سرویس', 'تاریخ پرداخت', 'وضعیت پرداخت']}
rows={[[
formatDate(session?.session_at ?? inv.issued_at),
session?.paid_at ? formatDate(session.paid_at) : paid ? formatDate(inv.issued_at) : '—',
STATUS_LABEL[inv.status] ?? inv.status,
]]}
/>
<SectionTable
title="اطلاعات سرویس"
cols={['سرویس', 'تعداد', 'مبلغ']}
rows={inv.items.length ? inv.items.map((it) => [it.title, it.quantity, formatRial(it.total_rials)]) : [['—', '—', '—']]}
/>
{session && (
<SectionTable
title="اطلاعات کالای مصرفی"
cols={['کالای مصرفی', 'تعداد', 'مبلغ']}
rows={session.consumables.length
? session.consumables.map((c) => [c.item_name, c.quantity, formatRial(c.line_total_rials)])
: [['-', '-', '-']]}
/>
)}
{session ? (
<SectionTable
title="خلاصه مالی"
cols={['جمع مبلغ سرویس', 'جمع مبلغ کالا', 'تخفیف', 'سهم بیمه پایه', 'سهم بیمه تکمیلی', 'مبلغ کل']}
rows={[[
formatRial(session.services_total_rials), formatRial(session.consumables_total_rials),
session.discount_rials > 0 ? formatRial(session.discount_rials) : '-',
formatRial(inv.base_insurance_rials), formatRial(inv.supplementary_rials),
formatRial(session.final_price_rials),
]]}
/>
) : (
<SectionTable
title="خلاصه مالی"
cols={['جمع مبلغ سرویس', 'سهم بیمه پایه', 'سهم بیمه تکمیلی', 'سهم بیمار', 'مبلغ کل']}
rows={[[
formatRial(inv.total_rials), formatRial(inv.base_insurance_rials),
formatRial(inv.supplementary_rials), formatRial(inv.patient_rials), formatRial(inv.total_rials),
]]}
/>
)}
{session && (
<SectionTable
title="پرداختی ها"
cols={['ردیف', 'شیوه پرداخت', 'مبلغ', 'تاریخ و ساعت', 'ثبت‌کننده']}
rows={session.payments.length
? session.payments.map((p, i) => [
i + 1,
METHOD_LABELS[p.method] ?? p.method,
formatRial(p.amount_rials),
p.paid_at ? formatDateTime(p.paid_at) : '-',
p.created_by_name ?? '-',
])
: [['-', '-', '-', '-', '-']]}
/>
)}
<SectionTable
title="وضعیت"
cols={['مبلغ کل پرداخت شده', 'مبلغ باقی مانده']}
rows={[[
formatRial(paidAmount),
<span style={{ color: remaining > 0 ? '#d32f2f' : '#388e3c', fontWeight: 600 }}>{formatRial(remaining)}</span>,
]]}
/>
</div>
)}
</Modal>
);
}