From e158862ca59c48f727ef2eea0bb9f1e6abfd487f Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 02:07:04 +0000 Subject: [PATCH] Changes Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com> --- src/lib/status-pdf.ts | 175 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 src/lib/status-pdf.ts diff --git a/src/lib/status-pdf.ts b/src/lib/status-pdf.ts new file mode 100644 index 0000000..24c6e50 --- /dev/null +++ b/src/lib/status-pdf.ts @@ -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(); + 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); +}