feat: invoices — patient billing with installments + PDF export

Backend (new `invoice` RBAC resource, granted to clinicians + reception):
- invoices table (line items + installments as JSONB), types, zod validation,
  service (CRUD + splitIntoInstallments + auto invoice numbers), org-scoped
  REST routes mounted at /api/invoices, activity logging (migration 0015)

Frontend:
- lib/invoices.ts API client + money/date helpers
- /invoices page: list with KPIs and search, create/edit dialog (searchable
  patient combobox, inline line-item editor, live total), detail sheet to split
  a bill into equal monthly installments, delete, and Download PDF
- dependency-free PDF via a print-styled window (browser "Save as PDF")
- sidebar "Invoices" entry under the Patients group; "Added by AI" badge honored

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Khalid Abdi
2026-06-14 19:58:08 +03:00
parent 9eb6353e00
commit d096c4fe9d
21 changed files with 4734 additions and 0 deletions
+1
View File
@@ -6,6 +6,7 @@ export type ActivityEntityType =
| "note"
| "appointment"
| "prescription"
| "invoice"
| "inventory"
| "task";
+36
View File
@@ -0,0 +1,36 @@
// The canonical Invoice shape returned by the API. Mirrors the frontend
// `lib/invoices.ts` Invoice type. Scoped to the active clinic; patient fields
// are denormalized for display and `fileNumber` links to a patient record.
export type InvoiceStatus = "draft" | "sent" | "paid" | "void";
export type InvoiceLineItem = {
description: string;
quantity: number;
unitPrice: number;
};
// One slice of a split bill. `amount` is the installment total; `dueAt` is an
// optional ISO date; `paid` tracks settlement.
export type InvoiceInstallment = {
label: string;
amount: number;
dueAt: string | null;
paid: boolean;
};
export type Invoice = {
id: string;
fileNumber: string;
name: string;
initials: string;
number: string; // human invoice number, e.g. "INV-1001"
issuedAt: string; // YYYY-MM-DD
dueAt: string | null; // YYYY-MM-DD
status: InvoiceStatus;
lineItems: InvoiceLineItem[];
installments: InvoiceInstallment[];
notes: string | null;
source: "manual" | "ai";
createdAt: string;
updatedAt: string;
};