Changes
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
co-authored by
renee-png
parent
9ee8cc4859
commit
e158862ca5
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user