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:
1 parent
f263ec79f2
commit
0d23963f07
24 files changed
+941
-113
No files matched your search
@@ -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);
|
||||
}
|
||||
Reference in new issue
Block a user