Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-04-17 02:07:04 +00:00
co-authored by renee-png
parent 9ee8cc4859
commit e158862ca5
+175
View File
@@ -0,0 +1,175 @@
import jsPDF from "jspdf";
import autoTable from "jspdf-autotable";
export interface StatusEntry {
title: string; // event datetime (ISO)
body: string;
created_at: string;
author_name?: string | null;
author_email?: string | null;
case_number?: string | null;
case_title?: string | null;
}
export interface StatusReportOptions {
title: string;
subtitle?: string;
firmName?: string;
groupByCase?: boolean;
entries: StatusEntry[];
}
function initials(name?: string | null, email?: string | null): string {
const source = (name && name.trim()) || (email ? email.split("@")[0] : "");
if (!source) return "—";
const parts = source.split(/\s+|\./).filter(Boolean);
if (parts.length >= 2) return (parts[0][0] + parts[parts.length - 1][0]).toUpperCase();
return source.slice(0, 2).toUpperCase();
}
function fmtDateTime(iso?: string | null): string {
if (!iso) return "—";
const d = new Date(iso);
if (isNaN(d.getTime())) return iso;
return d.toLocaleString(undefined, {
year: "numeric",
month: "short",
day: "2-digit",
hour: "numeric",
minute: "2-digit",
});
}
export function generateStatusReportPdf(opts: StatusReportOptions): jsPDF {
const doc = new jsPDF({ unit: "pt", format: "letter" });
const pageWidth = doc.internal.pageSize.getWidth();
const pageHeight = doc.internal.pageSize.getHeight();
const margin = 54;
// Header
doc.setFont("times", "bold");
doc.setFontSize(18);
doc.text(opts.title, margin, margin);
let y = margin + 22;
if (opts.subtitle) {
doc.setFont("times", "normal");
doc.setFontSize(11);
doc.setTextColor(90);
doc.text(opts.subtitle, margin, y);
doc.setTextColor(0);
y += 14;
}
doc.setFont("helvetica", "normal");
doc.setFontSize(9);
doc.setTextColor(120);
doc.text(
`Generated ${new Date().toLocaleString()}${opts.firmName ? " · " + opts.firmName : ""}`,
margin,
y,
);
doc.setTextColor(0);
y += 10;
doc.setDrawColor(180);
doc.line(margin, y, pageWidth - margin, y);
y += 18;
// Group entries
const groups: { key: string; label: string; items: StatusEntry[] }[] = [];
if (opts.groupByCase) {
const map = new Map<string, StatusEntry[]>();
for (const e of opts.entries) {
const key = `${e.case_number ?? ""}::${e.case_title ?? ""}`;
if (!map.has(key)) map.set(key, []);
map.get(key)!.push(e);
}
for (const [key, items] of map) {
const [num, title] = key.split("::");
const label = [num, title].filter(Boolean).join(" — ") || "Case";
groups.push({ key, label, items });
}
} else {
groups.push({ key: "all", label: "", items: opts.entries });
}
const rowsForGroup = (items: StatusEntry[]) =>
items.map((e) => [
fmtDateTime(e.title),
e.body,
`${initials(e.author_name, e.author_email)}\n${fmtDateTime(e.created_at)}`,
]);
for (const g of groups) {
if (opts.groupByCase) {
if (y > pageHeight - 120) {
doc.addPage();
y = margin;
}
doc.setFont("times", "bold");
doc.setFontSize(13);
doc.text(g.label, margin, y);
y += 6;
}
if (g.items.length === 0) {
doc.setFont("helvetica", "italic");
doc.setFontSize(10);
doc.setTextColor(140);
doc.text("No status updates.", margin, y + 16);
doc.setTextColor(0);
y += 30;
continue;
}
autoTable(doc, {
startY: y + 6,
head: [["Event date", "Details", "Logged by"]],
body: rowsForGroup(g.items),
margin: { left: margin, right: margin },
styles: {
font: "helvetica",
fontSize: 9.5,
cellPadding: 6,
valign: "top",
textColor: 30,
lineColor: 220,
lineWidth: 0.5,
},
headStyles: {
fillColor: [40, 40, 40],
textColor: 255,
fontStyle: "bold",
fontSize: 9.5,
},
alternateRowStyles: { fillColor: [248, 248, 246] },
columnStyles: {
0: { cellWidth: 96 },
1: { cellWidth: "auto" },
2: { cellWidth: 96, fontSize: 8.5, textColor: 80 },
},
didDrawPage: () => {
// Footer with page number
const ph = doc.internal.pageSize.getHeight();
const pw = doc.internal.pageSize.getWidth();
doc.setFont("helvetica", "normal");
doc.setFontSize(8);
doc.setTextColor(140);
const pageNum = doc.getNumberOfPages();
doc.text(`Page ${pageNum}`, pw - margin, ph - 24, { align: "right" });
doc.text(opts.title, margin, ph - 24);
doc.setTextColor(0);
},
});
// @ts-expect-error lastAutoTable injected by plugin
y = (doc.lastAutoTable?.finalY ?? y) + 24;
}
return doc;
}
export function downloadStatusReport(opts: StatusReportOptions, filename: string) {
const doc = generateStatusReportPdf(opts);
doc.save(filename);
}