Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-19 18:23:30 +00:00
co-authored by renee-png
parent 498816dddb
commit e1ef9495cb
+112
View File
@@ -0,0 +1,112 @@
import jsPDF from "jspdf";
import autoTable from "jspdf-autotable";
export interface TimeReportRow {
work_date: string;
case_label: string;
user_name: string;
description: string;
hours: number;
rate: number;
amount: number;
billable: boolean;
}
export interface ExpenseReportRow {
expense_date: string;
case_label: string;
user_name: string;
description: string;
amount: number;
billable: boolean;
}
interface BaseOpts {
clientName: string;
fromDate?: string;
toDate?: string;
}
function fmtDate(iso?: string | null) {
if (!iso) return "—";
const m = iso.match(/^(\d{4})-(\d{2})-(\d{2})/);
if (m) {
const d = new Date(Date.UTC(+m[1], +m[2] - 1, +m[3]));
return d.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric", timeZone: "UTC" });
}
return new Date(iso).toLocaleDateString("en-US");
}
function fmtCurrency(n: number) {
return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" }).format(n || 0);
}
function header(doc: jsPDF, title: string, opts: BaseOpts) {
const margin = 54;
doc.setFont("times", "bold");
doc.setFontSize(18);
doc.text(title, margin, margin);
doc.setFont("times", "normal");
doc.setFontSize(11);
doc.setTextColor(90);
doc.text(opts.clientName, margin, margin + 22);
if (opts.fromDate || opts.toDate) {
const range = `${opts.fromDate ? fmtDate(opts.fromDate) : "—"} to ${opts.toDate ? fmtDate(opts.toDate) : "—"}`;
doc.text(range, margin, margin + 38);
}
doc.setTextColor(0);
}
export function generateTimeReportPdf(opts: BaseOpts & { rows: TimeReportRow[] }): jsPDF {
const doc = new jsPDF({ unit: "pt", format: "letter" });
header(doc, "Time Report", opts);
const totalHours = opts.rows.reduce((a, r) => a + (r.hours || 0), 0);
const totalAmt = opts.rows.reduce((a, r) => a + (r.amount || 0), 0);
autoTable(doc, {
startY: 110,
head: [["Date", "Case", "User", "Description", "Hours", "Rate", "Amount"]],
body: opts.rows.map((r) => [
fmtDate(r.work_date),
r.case_label,
r.user_name,
r.description,
r.hours.toFixed(2),
fmtCurrency(r.rate),
fmtCurrency(r.amount),
]),
foot: [["", "", "", "Totals", totalHours.toFixed(2), "", fmtCurrency(totalAmt)]],
styles: { font: "times", fontSize: 9, cellPadding: 4 },
headStyles: { fillColor: [240, 240, 240], textColor: 20 },
footStyles: { fillColor: [240, 240, 240], textColor: 20, fontStyle: "bold" },
columnStyles: {
4: { halign: "right" },
5: { halign: "right" },
6: { halign: "right" },
},
});
return doc;
}
export function generateExpenseReportPdf(opts: BaseOpts & { rows: ExpenseReportRow[] }): jsPDF {
const doc = new jsPDF({ unit: "pt", format: "letter" });
header(doc, "Expense Report", opts);
const totalAmt = opts.rows.reduce((a, r) => a + (r.amount || 0), 0);
autoTable(doc, {
startY: 110,
head: [["Date", "Case", "User", "Description", "Billable", "Amount"]],
body: opts.rows.map((r) => [
fmtDate(r.expense_date),
r.case_label,
r.user_name,
r.description,
r.billable ? "Yes" : "No",
fmtCurrency(r.amount),
]),
foot: [["", "", "", "", "Total", fmtCurrency(totalAmt)]],
styles: { font: "times", fontSize: 9, cellPadding: 4 },
headStyles: { fillColor: [240, 240, 240], textColor: 20 },
footStyles: { fillColor: [240, 240, 240], textColor: 20, fontStyle: "bold" },
columnStyles: { 5: { halign: "right" } },
});
return doc;
}