Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-19 01:13:09 +00:00
co-authored by renee-png
parent c1b5a78ee8
commit 5a37fbbd54
+40 -8
View File
@@ -65,20 +65,52 @@ function InvoiceDetail() {
useEffect(() => { load(); }, [invoiceId]);
// Subsection keys & labels — order matters for display.
const SUBSECTIONS: { key: string; label: string; isExpense: boolean; billable: boolean }[] = [
{ key: "bill_fee", label: "Billable Fees", isExpense: false, billable: true },
{ key: "bill_exp", label: "Billable Expenses", isExpense: true, billable: true },
{ key: "nb_fee", label: "Non-Billable Fees", isExpense: false, billable: false },
{ key: "nb_exp", label: "Non-Billable Expenses", isExpense: true, billable: false },
];
const classifyItem = (it: any): string => {
const isExpense = it.kind === "expense";
const isNonBillable = Number(it.amount) === 0 && (it.time_entry_id || it.expense_id);
if (isExpense) return isNonBillable ? "nb_exp" : "bill_exp";
return isNonBillable ? "nb_fee" : "bill_fee";
};
const groups = useMemo(() => {
const map = new Map<string, { caseRow: any; items: any[]; subtotal: number }>();
const map = new Map<
string,
{
caseRow: any;
items: any[];
subtotal: number;
subsections: { key: string; label: string; billable: boolean; items: any[]; subtotal: number }[];
}
>();
for (const it of items) {
const key = it.case?.id ?? "_none";
if (!map.has(key)) map.set(key, { caseRow: it.case, items: [], subtotal: 0 });
if (!map.has(key)) {
map.set(key, {
caseRow: it.case,
items: [],
subtotal: 0,
subsections: SUBSECTIONS.map((s) => ({ key: s.key, label: s.label, billable: s.billable, items: [], subtotal: 0 })),
});
}
const g = map.get(key)!;
g.items.push(it);
// 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);
const subKey = classifyItem(it);
const sub = g.subsections.find((s) => s.key === subKey)!;
sub.items.push(it);
const amt = Number(it.amount);
sub.subtotal += amt;
if (sub.billable) g.subtotal += amt;
}
// Drop empty subsections
for (const g of map.values()) g.subsections = g.subsections.filter((s) => s.items.length > 0);
return Array.from(map.values());
}, [items]);