The invoice PDF and the invoice page now share one branded layout: logo (uploaded brand logo, otherwise the drawn honeycomb mark) and wordmark in the header, honey accents, a forest-green table header, a meta box with invoice number, date, order number and VAT ID, and a footer with address, contact, VAT ID and page numbers on every page. A mixed cart (e.g. honey at 7 % and a beeswax candle at 19 %) lists its items in one block per VAT rate with a subtotal each. Next to the totals, a VAT table shows net, VAT and gross per rate plus a sum row. Long invoices continue on further pages with the table header repeated. The VAT identification number is a new company field (company.vatId), editable in Admin > Inhalte, defaulting to the number already stated in the imprint. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
109 lines
3.4 KiB
TypeScript
109 lines
3.4 KiB
TypeScript
import { readFile } from "node:fs/promises";
|
|
import sharp from "sharp";
|
|
import { getBrandAssets } from "@/lib/brand";
|
|
import { resolveUploadPath } from "@/lib/uploads";
|
|
|
|
/**
|
|
* Markenfarben fuer das Rechnungs-PDF — dieselben Werte wie die Tokens in
|
|
* app/globals.css (helles Farbschema). pdfkit kennt keine CSS-Variablen,
|
|
* deshalb hier als feste Hexwerte; bei einer Aenderung der Markenfarben
|
|
* beide Stellen anpassen.
|
|
*/
|
|
export const INVOICE_COLORS = {
|
|
forest: "#24352B",
|
|
honey: "#B88A44",
|
|
honeyDark: "#8A6531",
|
|
honeyPale: "#E4CFA4",
|
|
cream: "#F4EFE5",
|
|
ink: "#242424",
|
|
sage: "#788774",
|
|
hairline: "#E6DDCB",
|
|
} as const;
|
|
|
|
export interface InvoiceLogo {
|
|
png: Buffer;
|
|
width: number;
|
|
height: number;
|
|
}
|
|
|
|
/**
|
|
* Laedt das im Admin hochgeladene Logo (Inhalte > Logo und Favicon) als PNG.
|
|
*
|
|
* pdfkit bettet nur PNG und JPEG ein, Uploads duerfen aber auch WebP/AVIF
|
|
* sein — sharp wandelt deshalb immer nach PNG (behaelt Transparenz).
|
|
* Der Pfad ist doppelt abgesichert: getBrandAssets laesst nur das feste
|
|
* Upload-Muster durch, resolveUploadPath verhindert das Verlassen von
|
|
* UPLOAD_DIR.
|
|
*
|
|
* Jeder Fehler (Datei fehlt, kaputtes Bild) fuehrt zu null und damit zum
|
|
* gezeichneten Waben-Zeichen: eine Rechnung darf nie an einem Logo scheitern.
|
|
*/
|
|
export async function loadInvoiceLogo(): Promise<InvoiceLogo | null> {
|
|
const { logo } = await getBrandAssets();
|
|
if (!logo) return null;
|
|
|
|
const filePath = resolveUploadPath([logo.replace(/^\/uploads\//, "")]);
|
|
if (!filePath) return null;
|
|
|
|
try {
|
|
const { data, info } = await sharp(await readFile(filePath)).png().toBuffer({ resolveWithObject: true });
|
|
return { png: data, width: info.width, height: info.height };
|
|
} catch (error) {
|
|
console.error(`[invoice-pdf] Logo ${logo} konnte nicht geladen werden — es wird das Waben-Zeichen verwendet.`, error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Zeichnet das Waben-Zeichen (components/logomark.tsx) als Vektor: Hexagon in
|
|
* Honiggold, darin drei Tannen in Waldgruen. Gleiche Geometrie wie im
|
|
* Web-Logo (viewBox 100), skaliert auf `size` Punkt.
|
|
*/
|
|
export function drawLogomark(doc: PDFKit.PDFDocument, x: number, y: number, size: number): void {
|
|
doc.save();
|
|
doc.translate(x, y).scale(size / 100);
|
|
doc
|
|
.polygon([50, 5], [88.97, 27.5], [88.97, 72.5], [50, 95], [11.03, 72.5], [11.03, 27.5])
|
|
.lineWidth(2.5)
|
|
.strokeColor(INVOICE_COLORS.honey)
|
|
.stroke();
|
|
|
|
for (const tree of [
|
|
{ x: 20, y: 45, scale: 0.7 },
|
|
{ x: 38, y: 32, scale: 0.95 },
|
|
{ x: 56, y: 45, scale: 0.7 },
|
|
]) {
|
|
doc.save();
|
|
doc.translate(tree.x, tree.y).scale(tree.scale);
|
|
doc.lineWidth(1.4).lineJoin("round").strokeColor(INVOICE_COLORS.forest);
|
|
doc.polygon([10, 2], [5, 13], [15, 13]).stroke();
|
|
doc.polygon([10, 10], [2, 22], [18, 22]).stroke();
|
|
doc.rect(8, 22, 4, 4).stroke();
|
|
doc.restore();
|
|
}
|
|
doc.restore();
|
|
}
|
|
|
|
/**
|
|
* Setzt das Logo in eine Box der Hoehe `height` und gibt die tatsaechlich
|
|
* belegte Breite zurueck — breite Wortmarken werden bis `maxWidth` breit,
|
|
* quadratische Zeichen bleiben quadratisch.
|
|
*/
|
|
export function drawInvoiceLogo(
|
|
doc: PDFKit.PDFDocument,
|
|
logo: InvoiceLogo | null,
|
|
x: number,
|
|
y: number,
|
|
height: number,
|
|
maxWidth: number
|
|
): number {
|
|
if (!logo) {
|
|
drawLogomark(doc, x, y, height);
|
|
return height;
|
|
}
|
|
const scale = Math.min(height / logo.height, maxWidth / logo.width);
|
|
const width = logo.width * scale;
|
|
doc.image(logo.png, x, y + (height - logo.height * scale) / 2, { width });
|
|
return width;
|
|
}
|