mirror of
https://github.com/temetro/temetro.git
synced 2026-08-07 01:13:12 +00:00
d096c4fe9d
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>
42 lines
1.6 KiB
TypeScript
42 lines
1.6 KiB
TypeScript
import { z } from "zod";
|
|
|
|
import { initialsFromName } from "./initials.js";
|
|
|
|
const isoDate = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Date must be YYYY-MM-DD.");
|
|
|
|
export const invoiceLineItemSchema = z.object({
|
|
description: z.string().trim().min(1, "Description is required.").max(300),
|
|
quantity: z.coerce.number().min(0).max(100_000).default(1),
|
|
unitPrice: z.coerce.number().min(0).max(10_000_000).default(0),
|
|
});
|
|
|
|
export const invoiceInstallmentSchema = z.object({
|
|
label: z.string().trim().max(120).default(""),
|
|
amount: z.coerce.number().min(0).max(10_000_000).default(0),
|
|
dueAt: isoDate.nullable().default(null),
|
|
paid: z.boolean().default(false),
|
|
});
|
|
|
|
// Payload accepted by POST/PUT /api/invoices. `number` and `issuedAt` are filled
|
|
// server-side on create when omitted; initials are derived from the name.
|
|
export const invoiceInputSchema = z
|
|
.object({
|
|
fileNumber: z.string().trim().default(""),
|
|
name: z.string().trim().min(1, "Patient name is required.").max(200),
|
|
initials: z.string().trim().max(4).default(""),
|
|
number: z.string().trim().max(60).optional(),
|
|
issuedAt: isoDate.optional(),
|
|
dueAt: isoDate.nullish(),
|
|
status: z.enum(["draft", "sent", "paid", "void"]).default("draft"),
|
|
lineItems: z.array(invoiceLineItemSchema).default([]),
|
|
installments: z.array(invoiceInstallmentSchema).default([]),
|
|
notes: z.string().max(5000).nullish(),
|
|
source: z.enum(["manual", "ai"]).default("manual"),
|
|
})
|
|
.transform((v) => ({
|
|
...v,
|
|
initials: v.initials || initialsFromName(v.name),
|
|
}));
|
|
|
|
export type InvoiceInput = z.infer<typeof invoiceInputSchema>;
|