update
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user