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) => [ `${initials(e.author_name, e.author_email)}\n${fmtDateTime(e.title)}`, e.body, ]); 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: [["Date", "Details"]], 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: 110 }, 1: { cellWidth: "auto" }, }, 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); } import { supabase } from "@/integrations/supabase/client"; /** * Upload a status-report PDF to the `generated-documents` bucket and create * a `generated_documents` row with kind='status_report'. Returns the new row id. */ export async function saveStatusReportToDb( opts: StatusReportOptions, filename: string, meta: { userId: string; payload?: Record }, ): Promise<{ id: string; storage_path: string }> { const doc = generateStatusReportPdf(opts); const blob = doc.output("blob"); const safe = filename.replace(/[^a-zA-Z0-9._-]/g, "_"); const storagePath = `${meta.userId}/${Date.now()}-${safe}`; const { error: upErr } = await supabase.storage .from("generated-documents") .upload(storagePath, blob, { contentType: "application/pdf" }); if (upErr) throw upErr; const { data, error } = await supabase .from("generated_documents") .insert({ kind: "status_report", name: filename.replace(/\.pdf$/i, ""), storage_path: storagePath, created_by: meta.userId, payload: (meta.payload ?? {}) as never, }) .select("id, storage_path") .single(); if (error) throw error; return data as { id: string; storage_path: string }; }