feat(invoices)!: Obsah sections, internal-only notes, unified per-page PDF header+footer
Invoices now mirror the issued-orders document model: - New invoice_sections table (CZ/EN rich-text "Obsah") edited via the shared SectionsEditor, printed inline right after the items on the PDF. Full-replace on update, same transaction as items. - Printed notes dropped: the notes column is removed (migration merges existing content into internal_notes first); the form field is now "Interni poznamky", never printed. Legacy payloads sending notes are silently stripped. - Form cleanup: Cislo faktury and Vystavil fields removed (number lives in the header, issued_by auto-fills); page header title restyled to the orders/offers pattern (number span + status chip). - Unified per-page PDF header for the red-accent family: shared buildPdfHeaderTemplate in pdf-shared (22mm logo, red heading, red rule) rendered by a Puppeteer headerTemplate on EVERY page of both invoices and issued orders (incl. the /file fallback render); body headers are print-hidden. htmlToPdf gained the headerTemplate option. - Footer parity: invoices get the per-page "Vystavil + Strana X z Y" footer; the invoice bottom block (notice + QR/VAT recap + Prevzal) is break-inside: avoid so a page break can never split it. - @page margins now match the template space (32mm top, 18mm bottom) - Chromium lays out by CSS @page margins, which also fixes issued orders' content running into the 18mm footer zone on full pages. BREAKING CHANGE: invoices.notes column dropped (data merged into internal_notes); deploy must run prisma migrate deploy + generate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -74,6 +74,15 @@ export interface HtmlToPdfOptions {
|
||||
* Chromium's default date/title header.
|
||||
*/
|
||||
footerTemplate?: string;
|
||||
/**
|
||||
* Chromium headerTemplate HTML rendered in the top margin of EVERY page
|
||||
* (same rules as footerTemplate: inline styles, explicit font-size; images
|
||||
* must be data: URLs). When set, the top margin grows to 32mm to make room
|
||||
* — the document body must NOT render its own header then, or page 1 shows
|
||||
* it twice. NOTE: Chromium lays pages out by the document's CSS @page
|
||||
* margins when present (they override this option) — keep them in sync.
|
||||
*/
|
||||
headerTemplate?: string;
|
||||
}
|
||||
|
||||
export async function htmlToPdf(
|
||||
@@ -95,16 +104,16 @@ export async function htmlToPdf(
|
||||
format: "A4",
|
||||
printBackground: true,
|
||||
margin: {
|
||||
top: "10mm",
|
||||
top: options.headerTemplate ? "32mm" : "10mm",
|
||||
bottom: options.footerTemplate ? "18mm" : "10mm",
|
||||
left: "10mm",
|
||||
right: "10mm",
|
||||
},
|
||||
...(options.footerTemplate
|
||||
...(options.footerTemplate || options.headerTemplate
|
||||
? {
|
||||
displayHeaderFooter: true,
|
||||
headerTemplate: "<span></span>",
|
||||
footerTemplate: options.footerTemplate,
|
||||
headerTemplate: options.headerTemplate || "<span></span>",
|
||||
footerTemplate: options.footerTemplate || "<span></span>",
|
||||
}
|
||||
: {}),
|
||||
timeout: 15_000,
|
||||
|
||||
@@ -61,6 +61,47 @@ export function escapeHtml(str: string | null | undefined): string {
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
/**
|
||||
* Company logo from company_settings.logo_data as a data: URI (mime sniffed
|
||||
* from magic bytes). Data URIs are the ONLY image source Chromium loads in
|
||||
* header/footer templates; the body templates reuse the same URI.
|
||||
*/
|
||||
export function logoDataUriFromSettings(
|
||||
settings: Record<string, unknown> | null,
|
||||
): string {
|
||||
if (!settings?.logo_data) return "";
|
||||
const buf = Buffer.from(settings.logo_data as Buffer);
|
||||
let mime = "image/png";
|
||||
if (buf[0] === 0xff && buf[1] === 0xd8) mime = "image/jpeg";
|
||||
else if (buf[0] === 0x47 && buf[1] === 0x49) mime = "image/gif";
|
||||
else if (buf[0] === 0x52 && buf[1] === 0x49) mime = "image/webp";
|
||||
return `data:${escapeHtml(mime)};base64,${buf.toString("base64")}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified repeating per-page header for the red-accent PDF family (invoices +
|
||||
* issued orders), rendered by Puppeteer in the top page margin: company logo
|
||||
* left (max 22mm tall — same size as the documents' original body header),
|
||||
* red bold heading right, 2pt #de3a3a rule underneath.
|
||||
*
|
||||
* Contract shared with the footers: inline styles only, explicit font-size
|
||||
* (Chromium defaults template text to 0), images as data: URIs only, padding
|
||||
* matched to the documents' 12mm @page side margins. The @page top margin
|
||||
* must be 32mm to make room (htmlToPdf grows it when headerTemplate is set,
|
||||
* but Chromium lays out by the CSS @page margins — keep them in sync).
|
||||
*/
|
||||
export function buildPdfHeaderTemplate(
|
||||
logoDataUri: string,
|
||||
heading: string,
|
||||
): string {
|
||||
return `<div style="width:100%; font-family:Tahoma, sans-serif; padding:0 12mm; box-sizing:border-box;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; padding-bottom:1mm; border-bottom:2pt solid #de3a3a;">
|
||||
<div style="flex:0 0 auto;">${logoDataUri ? `<img src="${logoDataUri}" style="max-width:42mm; max-height:22mm; object-fit:contain;" />` : ""}</div>
|
||||
<div style="font-size:13pt; font-weight:700; color:#de3a3a; text-align:right; letter-spacing:0.03em;">${escapeHtml(heading)}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize Quill HTML for the PDF templates: remove dangerous tags, event
|
||||
* handlers, javascript:/data:/vbscript: URLs (href AND src), inline styles,
|
||||
|
||||
Reference in New Issue
Block a user