This commit is contained in:
Matthias Hochmeister
2026-03-26 10:56:39 +01:00
parent d5e5f2d44e
commit ca12a23a30
4 changed files with 76 additions and 49 deletions

View File

@@ -361,7 +361,13 @@ export default function BestellungDetail() {
: String(bestellung.id); : String(bestellung.id);
const title = `Bestellung #${kennung}`; const title = `Bestellung #${kennung}`;
let curY = addPdfHeader(doc, title, settings, 210); let curY = await addPdfHeader(doc, settings, 210);
// Document title below header
doc.setFontSize(14);
doc.setFont('helvetica', 'bold');
doc.text(title, 10, curY);
curY += 8;
// Metadata block // Metadata block
doc.setFontSize(10); doc.setFontSize(10);

View File

@@ -192,7 +192,13 @@ export default function Bestellungen() {
let settings; let settings;
try { settings = await configApi.getPdfSettings(); } catch { settings = { pdf_header: '', pdf_footer: '', pdf_logo: '', pdf_org_name: '' }; } try { settings = await configApi.getPdfSettings(); } catch { settings = { pdf_header: '', pdf_footer: '', pdf_logo: '', pdf_org_name: '' }; }
const startY = addPdfHeader(doc, 'Bestellungen — Übersicht', settings, 210); let startY = await addPdfHeader(doc, settings, 210);
// Document title below header
doc.setFontSize(14);
doc.setFont('helvetica', 'bold');
doc.text('Bestellungen — Übersicht', 10, startY);
startY += 8;
const rows = filteredOrders.map((o) => { const rows = filteredOrders.map((o) => {
const brutto = calcBrutto(o); const brutto = calcBrutto(o);

View File

@@ -642,7 +642,14 @@ async function generatePdf(
const pdfSettings = await fetchPdfSettings(); const pdfSettings = await fetchPdfSettings();
// Header // Header
const tableStartY = addPdfHeader(doc, `Kalender — ${monthLabel} ${year}`, pdfSettings, 297); let tableStartY = await addPdfHeader(doc, pdfSettings, 297);
// Document title below header
const titleText = `Kalender — ${monthLabel} ${year}`;
doc.setFontSize(14);
doc.setFont('helvetica', 'bold');
doc.text(titleText, 10, tableStartY);
tableStartY += 8;
// Build combined list (same logic as CombinedListView) // Build combined list (same logic as CombinedListView)
type ListEntry = type ListEntry =

View File

@@ -38,86 +38,94 @@ export function renderMarkdownText(
return curY; return curY;
} }
/** Load an image from a data URL and return its natural pixel dimensions. */
function getImageDimensions(src: string): Promise<{ w: number; h: number }> {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => resolve({ w: img.naturalWidth, h: img.naturalHeight });
img.onerror = () => resolve({ w: 1, h: 1 });
img.src = src;
});
}
/** /**
* Add a PDF header with white background: * Add a white-background PDF header matching the official template:
* - Left: pdf_header text (bold italic, multi-line) * - Left: pdf_header text (bold italic, multi-line org hierarchy)
* - Right: org name + logo side-by-side * - Right: org name (bold) + logo (aspect-ratio correct)
* - Thin dark separator line below * - Thin dark separator line below
* Returns Y position where content should start. *
* Does NOT render the document title — callers do that below the returned Y.
* Returns Y position where document content should start.
*/ */
export function addPdfHeader( export async function addPdfHeader(
doc: jsPDF, doc: jsPDF,
title: string,
settings: PdfSettings, settings: PdfSettings,
pageWidth: number, pageWidth: number,
): number { ): Promise<number> {
const logoSize = 14;
const margin = 6; const margin = 6;
const rightEdge = pageWidth - margin; const rightEdge = pageWidth - margin;
const logoMaxH = 16;
// ── Left side: title (bold) ── // ── Right: logo (aspect-ratio-correct) ──
doc.setFontSize(14);
doc.setFont('helvetica', 'bold');
doc.setTextColor(0, 0, 0);
doc.text(title, 10, 12);
// ── Left side: pdf_header text below title (bold italic, smaller) ──
let headerEndY = 16;
if (settings.pdf_header) {
doc.setFontSize(8);
doc.setTextColor(100, 100, 100);
const headerLines = settings.pdf_header.split('\n');
let hy = 18;
for (const line of headerLines) {
const segments = line.split('**');
let hx = 10;
for (let i = 0; i < segments.length; i++) {
const seg = segments[i];
if (!seg) continue;
const isBold = i % 2 === 1;
doc.setFont('helvetica', isBold ? 'bolditalic' : 'italic');
doc.text(seg, hx, hy);
hx += doc.getTextWidth(seg);
}
hy += 3.5;
}
headerEndY = Math.max(headerEndY, hy);
}
// ── Right side: logo + org name ──
const logoSrc = settings.app_logo || settings.pdf_logo; const logoSrc = settings.app_logo || settings.pdf_logo;
let logoLeftEdge = rightEdge; let logoLeftEdge = rightEdge;
if (logoSrc) { if (logoSrc) {
try { try {
const { w, h } = await getImageDimensions(logoSrc);
const ratio = w / h;
const logoH = logoMaxH;
const logoW = logoH * ratio;
const fmt = logoSrc.startsWith('data:image/png') ? 'PNG' : 'JPEG'; const fmt = logoSrc.startsWith('data:image/png') ? 'PNG' : 'JPEG';
const logoX = rightEdge - logoSize; const logoX = rightEdge - logoW;
doc.addImage(logoSrc, fmt, logoX, 3, logoSize, logoSize); doc.addImage(logoSrc, fmt, logoX, 2, logoW, logoH);
logoLeftEdge = logoX - 3; logoLeftEdge = logoX - 3;
} catch { /* ignore invalid image */ } } catch { /* ignore invalid image */ }
} }
// ── Right: org name (to the left of logo, vertically centred) ──
if (settings.pdf_org_name) { if (settings.pdf_org_name) {
doc.setFontSize(10); doc.setFontSize(10);
doc.setFont('helvetica', 'bold'); doc.setFont('helvetica', 'bold');
doc.setTextColor(0, 0, 0); doc.setTextColor(0, 0, 0);
const nameW = doc.getTextWidth(settings.pdf_org_name); const nameW = doc.getTextWidth(settings.pdf_org_name);
const nameX = logoLeftEdge - nameW; doc.text(settings.pdf_org_name, logoLeftEdge - nameW, 11);
// Vertically centered with logo area (~y=10) }
doc.text(settings.pdf_org_name, nameX, 11);
// ── Left: pdf_header text (bold italic, multi-line) ──
let headerEndY = 10;
if (settings.pdf_header) {
doc.setFontSize(8);
doc.setTextColor(60, 60, 60);
const headerLines = settings.pdf_header.split('\n');
let hy = 8;
for (const line of headerLines) {
const segments = line.split('**');
let hx = margin;
for (let i = 0; i < segments.length; i++) {
const seg = segments[i];
if (!seg) continue;
doc.setFont('helvetica', i % 2 === 1 ? 'bolditalic' : 'italic');
doc.text(seg, hx, hy);
hx += doc.getTextWidth(seg);
}
hy += 4;
}
headerEndY = hy;
} }
// ── Separator line ── // ── Separator line ──
const lineY = Math.max(headerEndY, 20) + 2; const lineY = Math.max(headerEndY, logoMaxH + 4) + 2;
doc.setDrawColor(60, 60, 60); doc.setDrawColor(60, 60, 60);
doc.setLineWidth(0.5); doc.setLineWidth(0.5);
doc.line(margin, lineY, pageWidth - margin, lineY); doc.line(margin, lineY, pageWidth - margin, lineY);
// Reset // Reset styles
doc.setFont('helvetica', 'normal'); doc.setFont('helvetica', 'normal');
doc.setTextColor(0, 0, 0); doc.setTextColor(0, 0, 0);
doc.setFontSize(10);
return lineY + 4; return lineY + 5;
} }
/** /**