// src/pages/DashboardPage/BillingPage/PrintableInvoice.tsx
// The formal, print-only rendering of one invoice (letterhead + meta + bill-to + line table + totals),
// assembled by invoicePrintModel and revealed only under @media print (see index.css.printable-invoice).
// Kept plain semantic HTML (a real
for the lines) so the browser's print output is clean and
// black-on-white. The org logo resolves through usePhotoUrl (org thumbnails are base64, so synchronous).
import { createPortal } from 'react-dom';
import type { Invoice } from 'fhir/r4';
import { useAuthServiceContext } from '../../../services';
import { usePhotoUrl } from '../../../hooks/usePhotoUrl';
import { buildInvoicePrintModel, type InvoiceLedger } from './invoicePrintModel';
/** `ledger` is the payment ledger read for THIS invoice; without one the bill says payments are not shown. */
const PrintableInvoice = ({ invoice, ledger }: { invoice: Invoice; ledger?: InvoiceLedger }) => {
const { currentOrganization } = useAuthServiceContext();
const model = buildInvoicePrintModel(invoice, currentOrganization, ledger);
const { url: logoUrl } = usePhotoUrl(model.logo, 'organization');
const showLogo = Boolean(model.logo);
// Portal to so the absolute-positioned print block isn't anchored inside the Radix dialog's
// transformed container (which would offset it on the printed page). The print CSS is intentionally
// unscoped (.printable-invoice), so it applies here just the same.
return createPortal(
{showLogo &&
}
{model.orgName &&
{model.orgName}
}
{model.orgAddress &&
{model.orgAddress}
}
{model.orgPhone &&
{model.orgPhone}
}
Invoice {model.invoiceNumber}
Invoice date: {model.dateText}
Status: {model.statusLabel}
Bill to
{model.patientName}
| Description | Qty | Unit | Amount |
{model.lines.map((line, i) => (
| {line.description} |
{line.quantity} |
{line.unitPrice} |
{line.amount} |
))}
{model.tax !== undefined && (
<>
Subtotal{model.subtotal}
Tax{model.tax}
>
)}
Total{model.total}
{/* What was paid, from the ledger, never from the status; an unread ledger says so. */}
Payments
{model.ledger.kind === 'unread' ? (
{model.ledger.note}
) : (
<>
{model.ledger.rows.length ? (
| Paid on | Amount |
{model.ledger.rows.map((row, i) => (
| {row.date} | {row.amount} |
))}
) : (
No payments recorded.
)}
Total paid{model.ledger.paid}
Balance due{model.ledger.balance}
>
)}
,
document.body,
);
};
export default PrintableInvoice;