feat(export): Bestandsbuch für Tierarzneimittel nach Art. 108 VO (EU) 2019/6

- Treatment records supplier, purchase proof and prescribing vet, and
  keeps the apiary the colony stood at on the treatment day.
- Holder name and address in site settings (never in public output;
  regression test in public.service.spec).
- treatment-log.pdf: one sheet per apiary, identical treatments grouped,
  missing mandatory fields printed as FEHLT, uncertain locations marked.
- Shared PDF table helpers extracted from the record card.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-09-28 11:10:07 +02:00
1 parent f263ec79f2
commit 0d23963f07
24 files changed
+941 -113

No files matched your search

+107
View File
@@ -0,0 +1,107 @@
/**
* 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;
}
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[][],
): void {
const left = PDF_MARGIN;
const bottomLimit = doc.page.height - PDF_MARGIN - 20;
const drawRow = (cells: string[], bold: boolean) => {
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 (!bold) {
drawRow(
columns.map((c) => c.header),
true,
);
doc.font('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 });
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;
}
for (const row of rows) drawRow(row, false);
doc.moveDown(0.5);
}