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:
@@ -11,6 +11,8 @@ import {
|
||||
escapeHtml,
|
||||
cleanQuillHtml,
|
||||
buildQuillIndentCss,
|
||||
logoDataUriFromSettings,
|
||||
buildPdfHeaderTemplate,
|
||||
} from "../../utils/pdf-shared";
|
||||
import createDOMPurify from "dompurify";
|
||||
import { JSDOM } from "jsdom";
|
||||
@@ -226,13 +228,28 @@ export function buildIssuedOrderPdfFooter(
|
||||
const t = translations[lang];
|
||||
const pageWord = lang === "cs" ? "Strana" : "Page";
|
||||
const ofWord = lang === "cs" ? "z" : "of";
|
||||
return `<div style="width:100%; font-size:8pt; font-family:Tahoma, sans-serif; color:#646464; padding:0 10mm; box-sizing:border-box; display:flex; align-items:center;">
|
||||
return `<div style="width:100%; font-size:8pt; font-family:Tahoma, sans-serif; color:#646464; padding:0 12mm; box-sizing:border-box; display:flex; align-items:center;">
|
||||
<div style="flex:1; text-align:left;"><span style="font-weight:600;">${escapeHtml(t.issued_by)}</span> ${escapeHtml(issuerName)}</div>
|
||||
<div style="flex:1; text-align:center;">${pageWord} <span class="pageNumber"></span> ${ofWord} <span class="totalPages"></span></div>
|
||||
<div style="flex:1;"></div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Repeating per-page header for the issued-order PDF — the unified
|
||||
* red-accent family header (shared with invoices): logo left, red
|
||||
* "OBJEDNÁVKA č. X" right, red rule underneath, on EVERY page. The body's
|
||||
* own header is print-hidden so page 1 doesn't show it twice.
|
||||
*/
|
||||
export function buildIssuedOrderPdfHeader(
|
||||
lang: Lang,
|
||||
logoDataUri: string,
|
||||
poNumber: string,
|
||||
): string {
|
||||
const t = translations[lang];
|
||||
return buildPdfHeaderTemplate(logoDataUri, `${t.heading} ${poNumber}`);
|
||||
}
|
||||
|
||||
export function renderIssuedOrderHtml(
|
||||
order: IssuedOrderPdfData,
|
||||
items: IssuedOrderPdfItem[],
|
||||
@@ -249,17 +266,11 @@ export function renderIssuedOrderHtml(
|
||||
const currency = order.currency || "CZK";
|
||||
const poNumber = escapeHtml(order.po_number || "");
|
||||
|
||||
// Logo embedding (same logic as the confirmation template).
|
||||
let logoImg = "";
|
||||
if (settings?.logo_data) {
|
||||
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";
|
||||
const b64 = buf.toString("base64");
|
||||
logoImg = `<img src="data:${escapeHtml(mime)};base64,${b64}" class="logo" />`;
|
||||
}
|
||||
// Logo embedding — shared helper (also feeds the Puppeteer headerTemplate).
|
||||
const logoDataUri = logoDataUriFromSettings(settings);
|
||||
const logoImg = logoDataUri
|
||||
? `<img src="${logoDataUri}" class="logo" />`
|
||||
: "";
|
||||
|
||||
// PO direction: our company (settings) = Odběratel (buyer);
|
||||
// the sklad_suppliers record = Dodavatel (supplier).
|
||||
@@ -350,7 +361,11 @@ export function renderIssuedOrderHtml(
|
||||
<style>
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 8mm 12mm 10mm 12mm;
|
||||
/* Chromium uses THESE margins for layout (they override Puppeteer's
|
||||
margin option) — they must match the header/footer template space:
|
||||
32mm top (22mm logo + red heading + rule), 18mm bottom (Vystavil +
|
||||
Strana X z Y). Same geometry as the invoice PDF. */
|
||||
margin: 32mm 12mm 18mm 12mm;
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html, body {
|
||||
@@ -621,6 +636,10 @@ ${indentCSS}
|
||||
|
||||
@media print {
|
||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
/* The logo + red heading print via Puppeteer's headerTemplate on EVERY
|
||||
page — hide the body copy so page 1 doesn't show it twice. The screen
|
||||
(HTML preview) keeps the body header. */
|
||||
.invoice-header { display: none; }
|
||||
}
|
||||
@media screen {
|
||||
html { background: #525659; }
|
||||
@@ -795,6 +814,11 @@ export default async function issuedOrdersPdfRoutes(fastify: FastifyInstance) {
|
||||
: new Date();
|
||||
nasOrdersManager.cleanIssued(order.po_number);
|
||||
const pdfBuffer = await htmlToPdf(html, {
|
||||
headerTemplate: buildIssuedOrderPdfHeader(
|
||||
lang,
|
||||
logoDataUriFromSettings(settings),
|
||||
order.po_number || "",
|
||||
),
|
||||
footerTemplate: buildIssuedOrderPdfFooter(lang, issuer.name),
|
||||
});
|
||||
nasOrdersManager.saveIssuedPdf(
|
||||
@@ -807,6 +831,11 @@ export default async function issuedOrdersPdfRoutes(fastify: FastifyInstance) {
|
||||
}
|
||||
|
||||
const pdf = await htmlToPdf(html, {
|
||||
headerTemplate: buildIssuedOrderPdfHeader(
|
||||
lang,
|
||||
logoDataUriFromSettings(settings),
|
||||
order.po_number || "",
|
||||
),
|
||||
footerTemplate: buildIssuedOrderPdfFooter(lang, issuer.name),
|
||||
});
|
||||
const filename = `Objednavka-${order.po_number || String(id)}.pdf`;
|
||||
|
||||
Reference in New Issue
Block a user