Changes
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
co-authored by
renee-png
parent
baadbbfa26
commit
1393c7e50e
+80
-63
@@ -380,73 +380,90 @@ export async function downloadInvoicePdf(input: InvoicePdfInput, filename: strin
|
||||
const taxRate = (input.totals.taxRatePct ?? 0) / 100;
|
||||
const multipleCases = input.groups.length > 1;
|
||||
|
||||
const drawSubsectionHeader = (label: string, subtotal: number, billable: boolean) => {
|
||||
ensure(22);
|
||||
pdf.setFillColor(250, 250, 252);
|
||||
pdf.rect(MARGIN, y, CONTENT_W, 16, "F");
|
||||
setFont(pdf, { bold: true, size: 8, color: MUTED });
|
||||
pdf.text(label.toUpperCase(), MARGIN + 8, y + 11);
|
||||
if (billable) {
|
||||
const sub = fmtCurrency(subtotal);
|
||||
const sw = pdf.getTextWidth(sub);
|
||||
setFont(pdf, { bold: true, size: 8, color: TEXT });
|
||||
pdf.text(sub, colAmtX - sw, y + 11);
|
||||
}
|
||||
y += 16;
|
||||
pdf.setDrawColor(...RULE);
|
||||
pdf.setLineWidth(0.4);
|
||||
pdf.line(MARGIN, y, PAGE_W - MARGIN, y);
|
||||
};
|
||||
|
||||
const drawItem = (item: InvoiceLineItem) => {
|
||||
const isNonBillable = item.billable === false;
|
||||
const descMaxW = colQtyR - colDescX - 14;
|
||||
const descLines = wrap(pdf, item.description || (item.kind === "expense" ? "Expense" : ""), descMaxW);
|
||||
const lineHeight = 12;
|
||||
const blockH = Math.max(lineHeight * descLines.length + 14, item.user_name ? 32 : 26);
|
||||
ensure(blockH + 4);
|
||||
|
||||
setFont(pdf, { size: 9, color: TEXT });
|
||||
pdf.text(fmtDateShort(item.work_date), colDateX, y + 12);
|
||||
if (item.user_name) {
|
||||
setFont(pdf, { size: 7.5, color: MUTED });
|
||||
const nameLines = wrap(pdf, item.user_name, colKindX - colDateX - 4);
|
||||
pdf.text(nameLines[0], colDateX, y + 23);
|
||||
}
|
||||
|
||||
setFont(pdf, { size: 9, color: MUTED });
|
||||
const kindLabel =
|
||||
item.kind === "expense" ? "Expense" : item.kind === "time" ? "Fee" : item.kind === "manual" ? "Fee" : item.kind;
|
||||
pdf.text(kindLabel, colKindX, y + 12);
|
||||
|
||||
setFont(pdf, { size: 9, color: isNonBillable ? MUTED : TEXT, italic: isNonBillable });
|
||||
let dy = y + 12;
|
||||
for (const dl of descLines) {
|
||||
pdf.text(dl, colDescX, dy);
|
||||
dy += lineHeight;
|
||||
}
|
||||
|
||||
setFont(pdf, { size: 9, color: TEXT });
|
||||
const qtyText = item.kind === "expense" ? "—" : Number(item.quantity).toFixed(2);
|
||||
const qW = pdf.getTextWidth(qtyText);
|
||||
pdf.text(qtyText, colQtyR - qW, y + 12);
|
||||
|
||||
const rateText = fmtCurrency(item.rate);
|
||||
const rW = pdf.getTextWidth(rateText);
|
||||
pdf.text(rateText, colRateR - rW, y + 12);
|
||||
|
||||
const lineTax = isNonBillable ? 0 : item.amount * taxRate;
|
||||
const taxText = fmtCurrency(lineTax);
|
||||
const taxW = pdf.getTextWidth(taxText);
|
||||
pdf.text(taxText, colTaxR - taxW, y + 12);
|
||||
|
||||
const lineTotal = isNonBillable ? 0 : item.amount + lineTax;
|
||||
const amt = isNonBillable ? "—" : fmtCurrency(lineTotal);
|
||||
setFont(pdf, { size: 9, bold: true, color: TEXT });
|
||||
const aw = pdf.getTextWidth(amt);
|
||||
pdf.text(amt, colAmtX - aw, y + 12);
|
||||
|
||||
y += blockH;
|
||||
pdf.setDrawColor(...RULE);
|
||||
pdf.setLineWidth(0.4);
|
||||
pdf.line(MARGIN, y, PAGE_W - MARGIN, y);
|
||||
};
|
||||
|
||||
for (const g of input.groups) {
|
||||
// Always show the case name above its charges (per-case subtotal only when >1 case).
|
||||
drawCaseHeader(g);
|
||||
|
||||
for (const item of g.items) {
|
||||
const isNonBillable = item.billable === false;
|
||||
const descMaxW = colQtyR - colDescX - 14;
|
||||
const descLines = wrap(pdf, item.description || (item.kind === "expense" ? "Expense" : ""), descMaxW);
|
||||
const lineHeight = 12;
|
||||
// Reserve enough vertical space: description lines + room for staff name under date.
|
||||
const blockH = Math.max(lineHeight * descLines.length + 14, item.user_name ? 32 : 26);
|
||||
ensure(blockH + 4);
|
||||
// Build subsections: use provided list if any, else a single all-items section.
|
||||
const subs: InvoiceSubsection[] =
|
||||
g.subsections && g.subsections.length > 0
|
||||
? g.subsections
|
||||
: [{ key: "_all", label: "", billable: true, items: g.items, subtotal: g.subtotal }];
|
||||
|
||||
// Date + staff name
|
||||
setFont(pdf, { size: 9, color: TEXT });
|
||||
pdf.text(fmtDateShort(item.work_date), colDateX, y + 12);
|
||||
if (item.user_name) {
|
||||
setFont(pdf, { size: 7.5, color: MUTED });
|
||||
const nameLines = wrap(pdf, item.user_name, colKindX - colDateX - 4);
|
||||
pdf.text(nameLines[0], colDateX, y + 23);
|
||||
}
|
||||
|
||||
// Type label (Fee / Expense)
|
||||
setFont(pdf, { size: 9, color: MUTED });
|
||||
const kindLabel =
|
||||
item.kind === "expense" ? "Expense" : item.kind === "time" ? "Fee" : item.kind === "manual" ? "Fee" : item.kind;
|
||||
pdf.text(kindLabel, colKindX, y + 12);
|
||||
|
||||
// Description
|
||||
setFont(pdf, { size: 9, color: isNonBillable ? MUTED : TEXT, italic: isNonBillable });
|
||||
let dy = y + 12;
|
||||
for (const dl of descLines) {
|
||||
pdf.text(dl, colDescX, dy);
|
||||
dy += lineHeight;
|
||||
}
|
||||
|
||||
// Qty (right-aligned). For expenses (qty=1, no hours concept) show "—".
|
||||
setFont(pdf, { size: 9, color: TEXT });
|
||||
const qtyText =
|
||||
item.kind === "expense"
|
||||
? "—"
|
||||
: Number(item.quantity).toFixed(2);
|
||||
const qW = pdf.getTextWidth(qtyText);
|
||||
pdf.text(qtyText, colQtyR - qW, y + 12);
|
||||
|
||||
// Rate (right-aligned)
|
||||
const rateText = fmtCurrency(item.rate);
|
||||
const rW = pdf.getTextWidth(rateText);
|
||||
pdf.text(rateText, colRateR - rW, y + 12);
|
||||
|
||||
// Tax per line
|
||||
const lineTax = isNonBillable ? 0 : item.amount * taxRate;
|
||||
const taxText = fmtCurrency(lineTax);
|
||||
const taxW = pdf.getTextWidth(taxText);
|
||||
pdf.text(taxText, colTaxR - taxW, y + 12);
|
||||
|
||||
// Total (amount + tax)
|
||||
const lineTotal = isNonBillable ? 0 : item.amount + lineTax;
|
||||
const amt = isNonBillable ? "—" : fmtCurrency(lineTotal);
|
||||
setFont(pdf, { size: 9, bold: true, color: TEXT });
|
||||
const aw = pdf.getTextWidth(amt);
|
||||
pdf.text(amt, colAmtX - aw, y + 12);
|
||||
|
||||
y += blockH;
|
||||
pdf.setDrawColor(...RULE);
|
||||
pdf.setLineWidth(0.4);
|
||||
pdf.line(MARGIN, y, PAGE_W - MARGIN, y);
|
||||
for (const sub of subs) {
|
||||
if (sub.label) drawSubsectionHeader(sub.label, sub.subtotal, sub.billable);
|
||||
for (const item of sub.items) drawItem(item);
|
||||
}
|
||||
|
||||
// Per-case subtotal when there are multiple cases
|
||||
|
||||
Reference in New Issue
Block a user