87 lines
2.9 KiB
TypeScript
87 lines
2.9 KiB
TypeScript
|
|
import type { ExportResult, ReportPayload } from '@/lib/reports/types';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* PDF export. Unlike CSV + Excel (which can serialise in the browser),
|
||
|
|
* PDF generation runs server-side via `@react-pdf/renderer` so the
|
||
|
|
* client posts the payload to `/api/v1/reports/export-pdf` and receives
|
||
|
|
* the rendered bytes back.
|
||
|
|
*
|
||
|
|
* The server resolves the active port's branding (logo + primary
|
||
|
|
* color + name) so per-port theming flows through automatically — the
|
||
|
|
* client doesn't need to send branding fields.
|
||
|
|
*/
|
||
|
|
|
||
|
|
interface PdfExportOptions {
|
||
|
|
/** Filename override mirroring the CSV / Excel exporters. */
|
||
|
|
filenameOverride?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function exportReportAsPdf(
|
||
|
|
payload: ReportPayload,
|
||
|
|
options: PdfExportOptions = {},
|
||
|
|
): Promise<ExportResult> {
|
||
|
|
// Serialise dates to ISO so they survive the JSON trip.
|
||
|
|
const wireBody = {
|
||
|
|
title: payload.title,
|
||
|
|
description: payload.description,
|
||
|
|
filenameSlug: payload.filenameSlug,
|
||
|
|
range: {
|
||
|
|
from: payload.range.from.toISOString(),
|
||
|
|
to: payload.range.to.toISOString(),
|
||
|
|
},
|
||
|
|
kpis: payload.kpis,
|
||
|
|
sections: payload.sections.map((s) => ({
|
||
|
|
title: s.title,
|
||
|
|
columns: s.columns.map((c) => ({
|
||
|
|
key: c.key,
|
||
|
|
label: c.label,
|
||
|
|
align: c.align,
|
||
|
|
// `format` is a function and isn't serialisable; the server
|
||
|
|
// falls back to plain stringification, which matches the CSV
|
||
|
|
// exporter's default behaviour when no format is set.
|
||
|
|
})),
|
||
|
|
// Apply client-side format functions BEFORE serialising so the
|
||
|
|
// server sees pre-formatted strings. This preserves money /
|
||
|
|
// percentage / date formatting that the original ReportPayload
|
||
|
|
// declared.
|
||
|
|
rows: s.rows.map((row) => {
|
||
|
|
const out: Record<string, unknown> = {};
|
||
|
|
for (const col of s.columns) {
|
||
|
|
out[col.key] = col.format ? col.format(row[col.key]) : row[col.key];
|
||
|
|
}
|
||
|
|
return out;
|
||
|
|
}),
|
||
|
|
})),
|
||
|
|
filenameOverride: options.filenameOverride,
|
||
|
|
};
|
||
|
|
|
||
|
|
const res = await fetch('/api/v1/reports/export-pdf', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify(wireBody),
|
||
|
|
credentials: 'include',
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!res.ok) {
|
||
|
|
const text = await res.text().catch(() => '');
|
||
|
|
throw new Error(text || `PDF generation failed (${res.status})`);
|
||
|
|
}
|
||
|
|
|
||
|
|
const blob = await res.blob();
|
||
|
|
const cdHeader = res.headers.get('content-disposition') ?? '';
|
||
|
|
const match = cdHeader.match(/filename="([^"]+)"/);
|
||
|
|
const filename = match?.[1] ?? options.filenameOverride ?? defaultPdfFilename(payload);
|
||
|
|
|
||
|
|
return {
|
||
|
|
filename,
|
||
|
|
mimeType: 'application/pdf',
|
||
|
|
body: blob,
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
export function defaultPdfFilename(payload: ReportPayload): string {
|
||
|
|
const fromIso = payload.range.from.toISOString().slice(0, 10);
|
||
|
|
const toIso = payload.range.to.toISOString().slice(0, 10);
|
||
|
|
return `${payload.filenameSlug}-${fromIso}_${toIso}.pdf`;
|
||
|
|
}
|