/** * Small pdfkit helpers shared by the printable records (Stockkarte, * Bestandsbuch): dates, empty values and a table that repeats its header on * every page. */ export const PDF_MARGIN = 36; export interface PdfColumn { header: string; width: number; /** Numbers read better right-aligned; the default is left. */ align?: 'left' | 'right'; } export interface DrawTableOptions { /** Draws the last row in bold, e.g. a totals row. */ boldLastRow?: boolean; } export function formatDay(date: Date): string { return date.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', timeZone: 'UTC', }); } export function orDash(value: string | number | null | undefined): string { return value === null || value === undefined || value === '' ? '–' : String(value); } /** Widens the last column so the table fills the page width. */ export function fillWidth( doc: PDFKit.PDFDocument, columns: PdfColumn[], ): PdfColumn[] { const fixed = columns.slice(0, -1).reduce((sum, c) => sum + c.width, 0); const last = columns[columns.length - 1]; return [ ...columns.slice(0, -1), { ...last, width: doc.page.width - 2 * PDF_MARGIN - fixed }, ]; } export function drawSection(doc: PDFKit.PDFDocument, title: string): void { if (doc.y > doc.page.height - PDF_MARGIN - 80) doc.addPage(); doc.moveDown(0.6); doc .font('Helvetica-Bold') .fontSize(11) .fillColor('#000000') .text(title, PDF_MARGIN); doc.moveDown(0.2); } /** Draws a simple table, repeating the header on every new page. */ export function drawTable( doc: PDFKit.PDFDocument, columns: PdfColumn[], rows: string[][], options: DrawTableOptions = {}, ): void { const left = PDF_MARGIN; const bottomLimit = doc.page.height - PDF_MARGIN - 20; const drawRow = (cells: string[], isHeader: boolean, bold = isHeader) => { doc.font(bold ? 'Helvetica-Bold' : 'Helvetica').fontSize(8); const heights = cells.map((cell, index) => doc.heightOfString(cell, { width: columns[index].width - 4 }), ); const rowHeight = Math.max(...heights, 10) + 4; if (doc.y + rowHeight > bottomLimit) { doc.addPage(); if (!isHeader) { drawRow( columns.map((c) => c.header), true, ); doc.font(bold ? 'Helvetica-Bold' : 'Helvetica').fontSize(8); } } const top = doc.y; let x = left; cells.forEach((cell, index) => { doc.text(cell, x + 2, top + 2, { width: columns[index].width - 4, align: columns[index].align ?? 'left', }); x += columns[index].width; }); doc .moveTo(left, top + rowHeight) .lineTo(x, top + rowHeight) .lineWidth(0.3) .strokeColor('#999999') .stroke(); doc.y = top + rowHeight; }; drawRow( columns.map((c) => c.header), true, ); if (rows.length === 0) { doc .font('Helvetica-Oblique') .fontSize(8) .text('Keine Einträge.', left + 2, doc.y + 2); doc.moveDown(0.5); return; } rows.forEach((row, index) => drawRow( row, false, options.boldLastRow === true && index === rows.length - 1, ), ); doc.moveDown(0.5); }