Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
176 lines
4.8 KiB
TypeScript
176 lines
4.8 KiB
TypeScript
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);
|
|
}
|