Files
ApiCore/ApiCore-Backend/src/export/pdf-table.ts
T
Nick SzaunigandClaude Opus 5.5 b912d29166
Backend Image / build-and-push (push) Successful in 4m58s
Frontend Image / build-and-push (push) Successful in 7m57s
feat: T6 wave 4 – notice PDFs, compliance notifications, readiness core
- Authority notice PDFs: pure model plus renderer, TSK count in A4
  landscape, FEHLT for missing particulars, stored totals with correction
  note; GET /authority-notices/:id/pdf with compliance:read.
- Compliance notifications in category COMPLIANCE: herd change and TSK
  reference day, at most one unread per topic and user (advisory lock);
  daily rule TskReferenceDayReached with Berlin day and catch-up; handlers
  in API and worker.
- Readiness core: live evaluation of seven pure rules, fingerprinted
  acknowledgements that reopen when revoked or stale, subject masking,
  read-only GET paths; READINESS_RULES injectable; kept out of the worker.
- e2e hygiene: no unscoped deletes, seeded settings rows saved/restored.
- ARCHITECTURE.md §12.8–12.10, §10 handler rows, Annahmen 78–85.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-15 14:48:57 +02:00

125 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
}