/** Self-contained printable maintenance report for one object. * * Produces a full HTML document (inline print CSS) that the panel opens in a new * tab via a Blob URL — the user then prints or "Save as PDF" from the browser. * No PDF dependency, works offline. All user content is HTML-escaped. */ import type { MaintenanceObject, MaintenanceTask } from "../types"; export interface ReportLabels { title: string; generated: string; manufacturer: string; model: string; serial: string; installed: string; warranty: string; area: string; notes: string; tasksHeading: string; colTask: string; colType: string; colStatus: string; colSchedule: string; colLastDone: string; colNextDue: string; colCost: string; colTimes: string; totalCost: string; /** Human schedule label for a task — pass formatRecurrence so calendar * kinds (weekdays/nth_weekday/…) render properly, not just intervals. */ scheduleLabel: (t: MaintenanceTask) => string; statusLabel: (s: string) => string; typeLabel: (t: string) => string; none: string; } function esc(v: unknown): string { return String(v ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c] as string)); } export function buildObjectReportHtml( obj: MaintenanceObject, tasks: MaintenanceTask[], labels: ReportLabels, fmtDate: (iso: string | null | undefined) => string, currencySymbol: string, nowIso: string, ): string { const meta = ([ [labels.manufacturer, obj.manufacturer], [labels.model, obj.model], [labels.serial, obj.serial_number], [labels.installed, obj.installation_date ? fmtDate(obj.installation_date) : null], [labels.warranty, obj.warranty_expiry ? fmtDate(obj.warranty_expiry) : null], ] as Array<[string, string | null | undefined]>).filter(([, v]) => !!v); const rows = tasks.map((t) => { const schedule = labels.scheduleLabel(t); return ` ${esc(t.name)} ${esc(labels.typeLabel(t.type))} ${esc(labels.statusLabel(t.status))} ${esc(schedule)} ${esc(t.last_performed ? fmtDate(t.last_performed) : labels.none)} ${esc(t.next_due ? fmtDate(t.next_due) : labels.none)} ${t.times_performed ?? 0} ${(t.total_cost ?? 0).toFixed(2)} ${esc(currencySymbol)} `; }).join(""); const totalCost = tasks.reduce((n, t) => n + (t.total_cost ?? 0), 0); return ` ${esc(labels.title)} — ${esc(obj.name)}

${esc(obj.name)}

${esc(labels.title)} · ${esc(labels.generated)}: ${esc(fmtDate(nowIso))}

${meta.length ? `
${meta.map(([k, v]) => `
${esc(k)}
${esc(v)}
`).join("")}
` : ""}

${esc(labels.tasksHeading)} (${tasks.length})

${rows || ``}
${esc(labels.colTask)}${esc(labels.colType)}${esc(labels.colStatus)} ${esc(labels.colSchedule)}${esc(labels.colLastDone)}${esc(labels.colNextDue)} ${esc(labels.colTimes)}${esc(labels.colCost)}
${esc(labels.none)}
${esc(labels.totalCost)}${totalCost.toFixed(2)} ${esc(currencySymbol)}
${obj.notes ? `
${esc(labels.notes)}:\n${esc(obj.notes)}
` : ""} `; }