diff --git a/src/routes/invoices.$invoiceId.tsx b/src/routes/invoices.$invoiceId.tsx index 258fc1a..72ead90 100644 --- a/src/routes/invoices.$invoiceId.tsx +++ b/src/routes/invoices.$invoiceId.tsx @@ -70,7 +70,12 @@ function InvoiceDetail() { if (!map.has(key)) map.set(key, { caseRow: it.case, items: [], subtotal: 0 }); const g = map.get(key)!; g.items.push(it); - g.subtotal += Number(it.amount); + // Only billable rows contribute to the case subtotal. A line is + // non-billable when it references a source time/expense entry but its + // amount is zero (the generator stores it that way for transparency). + const isNonBillable = + Number(it.amount) === 0 && (it.time_entry_id || it.expense_id); + if (!isNonBillable) g.subtotal += Number(it.amount); } return Array.from(map.values()); }, [items]); @@ -205,15 +210,31 @@ function InvoiceDetail() { caseTitle: g.caseRow?.title ?? "(unassigned)", practiceArea: g.caseRow?.practice_area, subtotal: g.subtotal, - items: g.items.map((it) => ({ - kind: it.kind, - description: it.description, - work_date: it.work_date, - quantity: Number(it.quantity), - rate: Number(it.rate), - amount: Number(it.amount), - user_name: it.user?.full_name || it.user?.email, - })), + items: g.items.map((it) => { + const name: string = + it.user?.full_name || it.user?.email || ""; + const initials = name + .trim() + .split(/\s+/) + .map((p: string) => p[0]) + .filter(Boolean) + .slice(0, 3) + .join("") + .toUpperCase(); + const isNonBillable = + Number(it.amount) === 0 && (it.time_entry_id || it.expense_id); + return { + kind: it.kind, + description: it.description, + work_date: it.work_date, + quantity: Number(it.quantity), + rate: Number(it.rate), + amount: Number(it.amount), + user_name: name || null, + user_initials: initials || null, + billable: !isNonBillable, + }; + }), })), totals: { subtotal: Number(invoice.subtotal),