import {PDFDocument, StandardFonts, rgb} from "pdf-lib" import dayjs from "dayjs" import {renderAsCurrency, splitStringBySpace} from "./stringRendering"; import {FastifyInstance} from "fastify"; import { GetObjectCommand } from "@aws-sdk/client-s3"; import { s3 } from "./s3"; import { secrets } from "./secrets"; const getCoordinatesForPDFLib = (x:number ,y:number, page:any) => { /* * @param x the wanted X Parameter in Millimeters from Top Left * @param y the wanted Y Parameter in Millimeters from Top Left * @param page the page Object * * @returns x,y object * */ let retX = x * 2.83 let retY = page.getHeight()-(y*2.83) return { x: retX, y: retY } } const getBackgroundSourceBuffer = async (server:FastifyInstance, path:string) => { console.log(path) const { Body } = await s3.send( new GetObjectCommand({ Bucket: secrets.S3_BUCKET, Key: path }) ) const chunks: Buffer[] = [] for await (const chunk of Body as any) { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)) } return Buffer.concat(chunks) } const getDuration = (time) => { const minutes = Math.floor(dayjs(time.stopped_at).diff(dayjs(time.started_at),'minutes',true)) const hours = Math.floor(minutes/60) return { //dezimal: dez, hours: hours, minutes: minutes, composed: `${hours}:${String(minutes % 60).padStart(2,"0")} Std` } } export const createInvoicePDF = async (server:FastifyInstance, returnMode, invoiceData, backgroundPath:string) => { const genPDF = async (invoiceData, backgroundSourceBuffer) => { const pdfDoc = await PDFDocument.create() const font = await pdfDoc.embedFont(StandardFonts.Helvetica) const fontBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold) let pages = [] let pageCounter = 1 //const backgroundPdfSourceBuffer = await fetch("/Briefpapier.pdf").then((res) => res.arrayBuffer()) const backgroudPdf = await PDFDocument.load(backgroundSourceBuffer) const firstPageBackground = await pdfDoc.embedPage(backgroudPdf.getPages()[0]) const secondPageBackground = await pdfDoc.embedPage(backgroudPdf.getPages()[backgroudPdf.getPages().length > 1 ? 1 : 0]) // const page1 = pdfDoc.addPage() // page1.drawPage(firstPageBackground, { x: 0, y: 0, }) // pages.push(page1) // //Falzmarke 1 pages[pageCounter - 1].drawLine({ start: getCoordinatesForPDFLib(0, 105, page1), end: getCoordinatesForPDFLib(5, 105, page1), thickness: 0.2, color: rgb(0, 0, 0), opacity: 1 }) //Lochmarke pages[pageCounter - 1].drawLine({ start: getCoordinatesForPDFLib(0, 148.5, page1), end: getCoordinatesForPDFLib(5, 148.5, page1), thickness: 0.2, color: rgb(0, 0, 0), opacity: 1 }) //Falzmarke 2 pages[pageCounter - 1].drawLine({ start: getCoordinatesForPDFLib(0, 210, page1), end: getCoordinatesForPDFLib(5, 210, page1), thickness: 0.2, color: rgb(0, 0, 0), opacity: 1 }) /*page1.drawLine({ start: getCoordinatesForPDFLib(20,45,page1), end: getCoordinatesForPDFLib(105,45,page1), thickness: 0.5, color: rgb(0,0,0), opacity: 1 })*/ if (!invoiceData.addressLine) console.log("Missing Addressline") pages[pageCounter - 1].drawText(invoiceData.adressLine, { ...getCoordinatesForPDFLib(21, 48, page1), size: 6, color: rgb(0, 0, 0), lineHeight: 6, opacity: 1, maxWidth: 240 }) /*page1.drawLine({ start: getCoordinatesForPDFLib(20,50,page1), end: getCoordinatesForPDFLib(105,50,page1), thickness: 0.5, color: rgb(0,0,0), opacity: 1 })*/ let partLinesAdded = 0 invoiceData.recipient.forEach((info, index) => { let maxSplitLength = 35 let splittedContent = splitStringBySpace(info, maxSplitLength) splittedContent.forEach((part, partIndex) => { if (partIndex === 0) { pages[pageCounter - 1].drawText(part, { ...getCoordinatesForPDFLib(21, 55 + index * 5 + partLinesAdded * 5, page1), size: 10, color: rgb(0, 0, 0), lineHeight: 10, opacity: 1, maxWidth: 240 }) } else { partLinesAdded++ pages[pageCounter - 1].drawText(part, { ...getCoordinatesForPDFLib(21, 55 + index * 5 + partLinesAdded * 5, page1), size: 10, color: rgb(0, 0, 0), lineHeight: 10, opacity: 1, maxWidth: 240 }) } /*if(partIndex > 0) partLinesAdded++ pages[pageCounter - 1].drawText(part, { y: getCoordinatesForPDFLib(21,55+index*5+partLinesAdded*5, page1).y, x: getCoordinatesForPDFLib(21,55+index*5+partLinesAdded*5,page1).x + 230 - font.widthOfTextAtSize(part,10), size:10, color:rgb(0,0,0), lineHeight:10, opacity: 1, maxWidth: 240 })*/ }) }) //Rechts partLinesAdded = 0 invoiceData.info.forEach((info, index) => { let maxSplitLength = 34 let splittedContent = splitStringBySpace(info.content, maxSplitLength) splittedContent.forEach((part, partIndex) => { if (partIndex === 0) { pages[pageCounter - 1].drawText(info.label, { ...getCoordinatesForPDFLib(116, 55 + index * 5 + partLinesAdded * 5, page1), size: 10, color: rgb(0, 0, 0), lineHeight: 10, opacity: 1, maxWidth: 240 }) } if (partIndex > 0) partLinesAdded++ pages[pageCounter - 1].drawText(part, { y: getCoordinatesForPDFLib(116, 55 + index * 5 + partLinesAdded * 5, page1).y, x: getCoordinatesForPDFLib(116, 55 + index * 5 + partLinesAdded * 5, page1).x + 230 - font.widthOfTextAtSize(part, 10), size: 10, color: rgb(0, 0, 0), lineHeight: 10, opacity: 1, maxWidth: 240 }) }) }) /*page1.drawLine({ start: getCoordinatesForPDFLib(125,90,page1), end: getCoordinatesForPDFLib(200,90,page1), thickness: 0.5, color: rgb(0,0,0), opacity: 1 })*/ //Title /*page1.drawLine({ start: getCoordinatesForPDFLib(20,95,page1), end: getCoordinatesForPDFLib(200,95,page1), thickness: 0.5, color: rgb(0,0,0), opacity: 1 })*/ if (!invoiceData.title) console.log("Missing Title") pages[pageCounter - 1].drawText(invoiceData.title, { ...getCoordinatesForPDFLib(20, 100, page1), size: 13, color: rgb(0, 0, 0), lineHeight: 15, opacity: 1, maxWidth: 500 }) /*page1.drawLine({ start: getCoordinatesForPDFLib(20,105,page1), end: getCoordinatesForPDFLib(200,105,page1), thickness: 0.5, color: rgb(0,0,0), opacity: 1 })*/ if (!invoiceData.description) console.log("Missing Description") if (invoiceData.description) { pages[pageCounter - 1].drawText(invoiceData.description, { ...getCoordinatesForPDFLib(20, 112, page1), size: 13, color: rgb(0, 0, 0), lineHeight: 15, opacity: 1, maxWidth: 500 }) } if (!invoiceData.startText) console.log("Missing StartText") pages[pageCounter - 1].drawText(invoiceData.startText, { ...getCoordinatesForPDFLib(20, 119, page1), size: 10, color: rgb(0, 0, 0), lineHeight: 10, opacity: 1, maxWidth: 500 }) /*page1.drawLine({ start: getCoordinatesForPDFLib(20,115,page1), end: getCoordinatesForPDFLib(200,115,page1), thickness: 0.5, color: rgb(0,0,0), opacity: 1 })*/ pages[pageCounter - 1].drawLine({ start: getCoordinatesForPDFLib(20, 140, page1), end: getCoordinatesForPDFLib(199, 140, page1), thickness: 0.1, color: rgb(0, 0, 0), opacity: 1, }) /*pages[pageCounter - 1].drawRectangle({ ...getCoordinatesForPDFLib(20,140, page1), width: 180 * 2.83, height: 8 * 2.83, color: rgb(0,0,0), opacity: 0, borderWidth: 0.1 })*/ //Header pages[pageCounter - 1].drawText("Pos", { ...getCoordinatesForPDFLib(21, 137, page1), size: 12, color: rgb(0, 0, 0), lineHeight: 12, opacity: 1, maxWidth: 240, font: fontBold }) pages[pageCounter - 1].drawText("Menge", { ...getCoordinatesForPDFLib(35, 137, page1), size: 12, color: rgb(0, 0, 0), lineHeight: 12, opacity: 1, maxWidth: 240, font: fontBold }) pages[pageCounter - 1].drawText("Bezeichnung", { ...getCoordinatesForPDFLib(52, 137, page1), size: 12, color: rgb(0, 0, 0), lineHeight: 12, opacity: 1, maxWidth: 240, font: fontBold }) if (invoiceData.type !== "deliveryNotes") { pages[pageCounter - 1].drawText("Steuer", { ...getCoordinatesForPDFLib(135, 137, page1), size: 12, color: rgb(0, 0, 0), lineHeight: 12, opacity: 1, maxWidth: 240, font: fontBold }) pages[pageCounter - 1].drawText("Einheitspreis", { ...getCoordinatesForPDFLib(150, 137, page1), size: 12, color: rgb(0, 0, 0), lineHeight: 12, opacity: 1, maxWidth: 240, font: fontBold }) pages[pageCounter - 1].drawText("Gesamt", { y: getCoordinatesForPDFLib(25, 137, page1).y, x: getCoordinatesForPDFLib(25, 137, page1).x + 490 - fontBold.widthOfTextAtSize("Gesamt", 12), size: 12, color: rgb(0, 0, 0), lineHeight: 12, opacity: 1, maxWidth: 240, font: fontBold }) } let rowHeight = 145.5 let pageIndex = 0 invoiceData.rows.forEach((row, index) => { if (!["pagebreak", "title", "text"].includes(row.mode)) { if (!row.pos) console.log("Missing Row Pos") pages[pageCounter - 1].drawText(String(row.pos), { ...getCoordinatesForPDFLib(21, rowHeight, page1), size: 10, color: rgb(0, 0, 0), lineHeight: 10, opacity: 1, maxWidth: 240 }) if (!row.quantity) console.log("Missing Row Quantity") if (!row.unit) console.log("Missing Row Unit") pages[pageCounter - 1].drawText((row.optional || row.alternative) ? `(${row.quantity} ${row.unit})` : `${row.quantity} ${row.unit}`, { ...getCoordinatesForPDFLib(35, rowHeight, page1), size: 10, color: rgb(0, 0, 0), lineHeight: 10, opacity: 1, maxWidth: 240 }) let rowTextLines = 0 if (invoiceData.type !== "deliveryNotes") { pages[pageCounter - 1].drawText(splitStringBySpace(row.text, 35).join("\n"), { ...getCoordinatesForPDFLib(52, rowHeight, page1), size: 10, color: rgb(0, 0, 0), lineHeight: 10, opacity: 1, font: fontBold }) rowTextLines = splitStringBySpace(row.text, 35).length } else { pages[pageCounter - 1].drawText(splitStringBySpace(row.text, 80).join("\n"), { ...getCoordinatesForPDFLib(52, rowHeight, page1), size: 10, color: rgb(0, 0, 0), lineHeight: 10, opacity: 1, font: fontBold }) rowTextLines = splitStringBySpace(row.text, 80).length } let rowDescriptionLines = 0 if (row.descriptionText) { if (invoiceData.type !== "deliveryNotes") { rowDescriptionLines = splitStringBySpace(row.descriptionText, 60).length pages[pageCounter - 1].drawText(splitStringBySpace(row.descriptionText, 60).join("\n"), { ...getCoordinatesForPDFLib(52, rowHeight + (rowTextLines * 5), page1), size: 10, color: rgb(0, 0, 0), lineHeight: 10, opacity: 1, }) } else { rowDescriptionLines = splitStringBySpace(row.descriptionText, 80).length pages[pageCounter - 1].drawText(splitStringBySpace(row.descriptionText, 80).join("\n"), { ...getCoordinatesForPDFLib(52, rowHeight + (rowTextLines * 5), page1), size: 10, color: rgb(0, 0, 0), lineHeight: 10, opacity: 1, }) } } if (invoiceData.type !== "deliveryNotes") { pages[pageCounter - 1].drawText(`${row.taxPercent} %`, { ...getCoordinatesForPDFLib(135, rowHeight, page1), size: 10, color: rgb(0, 0, 0), lineHeight: 10, opacity: 1, maxWidth: 240 }) pages[pageCounter - 1].drawText(row.price, { ...getCoordinatesForPDFLib(150, rowHeight, page1), size: 10, color: rgb(0, 0, 0), lineHeight: 10, opacity: 1, maxWidth: 240 }) pages[pageCounter - 1].drawText((row.optional || row.alternative) ? `(${row.rowAmount})` : row.rowAmount, { y: getCoordinatesForPDFLib(25, rowHeight, page1).y, x: getCoordinatesForPDFLib(25, rowHeight, page1).x + 490 - font.widthOfTextAtSize((row.optional || row.alternative) ? `(${row.rowAmount})` : row.rowAmount, 10), size: 10, color: rgb(0, 0, 0), lineHeight: 10, opacity: 1, maxWidth: 240, }) if (row.discountPercent > 0) { let text = row.discountText if (row.optional) text = `Optional - ${text}` if (row.alternative) text = `Alternativ - ${text}` pages[pageCounter - 1].drawText(text, { y: getCoordinatesForPDFLib(25, rowHeight + 5, page1).y, x: getCoordinatesForPDFLib(25, rowHeight + 5, page1).x + 490 - font.widthOfTextAtSize(text, 8), size: 8, color: rgb(0, 0, 0), lineHeight: 10, opacity: 1, maxWidth: 240, }) } else if (row.optional) { pages[pageCounter - 1].drawText("Optional", { y: getCoordinatesForPDFLib(25, rowHeight + 5, page1).y, x: getCoordinatesForPDFLib(25, rowHeight + 5, page1).x + 490 - font.widthOfTextAtSize("Optional", 8), size: 8, color: rgb(0, 0, 0), lineHeight: 10, opacity: 1, maxWidth: 240, }) } else if (row.alternative) { pages[pageCounter - 1].drawText("Alternativ", { y: getCoordinatesForPDFLib(25, rowHeight + 5, page1).y, x: getCoordinatesForPDFLib(25, rowHeight + 5, page1).x + 490 - font.widthOfTextAtSize("Alternativ", 8), size: 8, color: rgb(0, 0, 0), lineHeight: 10, opacity: 1, maxWidth: 240, }) } } if (row.descriptionText) { rowHeight += rowDescriptionLines * 4.5 + rowTextLines * 5.5 } else if (row.discountPercent) { rowHeight += (rowTextLines + 1) * 5.5 } else if (row.optional || row.alternative) { rowHeight += (rowTextLines + 1) * 5.5 } else { rowHeight += rowTextLines * 5.5 } pageIndex += 1 } else if (row.mode === 'pagebreak') { console.log(invoiceData.rows[index + 1]) if (invoiceData.rows[index + 1].mode === 'title') { let transferSumText = `Übertrag: ${invoiceData.total.titleSumsTransfer[Object.keys(invoiceData.total.titleSums)[invoiceData.rows[index + 1].pos - 2]]}` pages[pageCounter - 1].drawText(transferSumText, { y: getCoordinatesForPDFLib(21, rowHeight - 2, page1).y, x: getCoordinatesForPDFLib(21, rowHeight - 2, page1).x + 500 - fontBold.widthOfTextAtSize(transferSumText, 10), size: 10, color: rgb(0, 0, 0), lineHeight: 10, opacity: 1, maxWidth: 240, font: fontBold }) } const page = pdfDoc.addPage() page.drawPage(secondPageBackground, { x: 0, y: 0, }) //Falzmarke 1 page.drawLine({ start: getCoordinatesForPDFLib(0, 105, page1), end: getCoordinatesForPDFLib(7, 105, page1), thickness: 0.25, color: rgb(0, 0, 0), opacity: 1 }) //Lochmarke page.drawLine({ start: getCoordinatesForPDFLib(0, 148.5, page1), end: getCoordinatesForPDFLib(7, 148.5, page1), thickness: 0.25, color: rgb(0, 0, 0), opacity: 1 }) //Falzmarke 2 page.drawLine({ start: getCoordinatesForPDFLib(0, 210, page1), end: getCoordinatesForPDFLib(7, 210, page1), thickness: 0.25, color: rgb(0, 0, 0), opacity: 1 }) page.drawText("Pos", { ...getCoordinatesForPDFLib(21, 22, page1), size: 12, color: rgb(0, 0, 0), lineHeight: 12, opacity: 1, maxWidth: 240, font: fontBold }) page.drawText("Menge", { ...getCoordinatesForPDFLib(35, 22, page1), size: 12, color: rgb(0, 0, 0), lineHeight: 12, opacity: 1, maxWidth: 240, font: fontBold }) page.drawText("Bezeichnung", { ...getCoordinatesForPDFLib(52, 22, page1), size: 12, color: rgb(0, 0, 0), lineHeight: 12, opacity: 1, maxWidth: 240, font: fontBold }) if (invoiceData.type !== "deliveryNotes") { page.drawText("Steuer", { ...getCoordinatesForPDFLib(135, 22, page1), size: 12, color: rgb(0, 0, 0), lineHeight: 12, opacity: 1, maxWidth: 240, font: fontBold }) page.drawText("Einheitspreis", { ...getCoordinatesForPDFLib(150, 22, page1), size: 12, color: rgb(0, 0, 0), lineHeight: 12, opacity: 1, maxWidth: 240, font: fontBold }) page.drawText("Gesamt", { y: getCoordinatesForPDFLib(25, 22, page1).y, x: getCoordinatesForPDFLib(25, 22, page1).x + 490 - fontBold.widthOfTextAtSize("Gesamt", 12), size: 12, color: rgb(0, 0, 0), lineHeight: 12, opacity: 1, maxWidth: 240, font: fontBold }) } pageCounter += 1; pageIndex = 0; rowHeight = 30; pages.push(page) } else if (row.mode === 'title') { if (index === 0 || pageIndex === 0) { rowHeight += 3 } else { let transferSumText = `Übertrag: ${invoiceData.total.titleSumsTransfer[Object.keys(invoiceData.total.titleSums)[row.pos - 2]]}` pages[pageCounter - 1].drawText(transferSumText, { y: getCoordinatesForPDFLib(21, rowHeight - 2, page1).y, x: getCoordinatesForPDFLib(21, rowHeight - 2, page1).x + 500 - fontBold.widthOfTextAtSize(transferSumText, 10), size: 10, color: rgb(0, 0, 0), lineHeight: 10, opacity: 1, maxWidth: 240, font: fontBold }) pages[pageCounter - 1].drawLine({ start: getCoordinatesForPDFLib(20, rowHeight, page1), end: getCoordinatesForPDFLib(199, rowHeight, page1), thickness: 0.2, color: rgb(0, 0, 0), opacity: 1, }) rowHeight += 5 } pages[pageCounter - 1].drawText(String(row.pos), { ...getCoordinatesForPDFLib(21, rowHeight, page1), size: 10, color: rgb(0, 0, 0), lineHeight: 10, opacity: 1, maxWidth: 240, font: fontBold }) pages[pageCounter - 1].drawText(splitStringBySpace(row.text, 60).join("\n"), { ...getCoordinatesForPDFLib(35, rowHeight, page1), size: 12, color: rgb(0, 0, 0), lineHeight: 12, opacity: 1, maxWidth: 500, font: fontBold }) rowHeight += splitStringBySpace(row.text, 60).length * 4.5 } else if (row.mode === 'text') { if (index === 0 || pageIndex === 0) { rowHeight += 3 } if (row.descriptionText) { pages[pageCounter - 1].drawText(splitStringBySpace(row.descriptionText, 70).join("\n"), { ...getCoordinatesForPDFLib(35, rowHeight, page1), size: 10, color: rgb(0, 0, 0), lineHeight: 10, opacity: 1, }) rowHeight += (splitStringBySpace(row.descriptionText, 70) || []).length * 4 rowHeight += 4 } } console.log(rowHeight) }) let endTextDiff = 35 if (invoiceData.type !== "deliveryNotes") { pages[pageCounter - 1].drawLine({ start: getCoordinatesForPDFLib(20, rowHeight, page1), end: getCoordinatesForPDFLib(198, rowHeight, page1), thickness: 0.2, color: rgb(0, 0, 0), opacity: 1, }) rowHeight += 6 if (Object.keys(invoiceData.total.titleSums).length > 0) { Object.keys(invoiceData.total.titleSums).forEach((key, index) => { pages[pageCounter - 1].drawText(splitStringBySpace(key, 60).join("\n"), { ...getCoordinatesForPDFLib(21, rowHeight, page1), size: 11, color: rgb(0, 0, 0), lineHeight: 11, opacity: 1, font: fontBold }) pages[pageCounter - 1].drawText(invoiceData.total.titleSums[key], { y: getCoordinatesForPDFLib(21, rowHeight, page1).y, x: getCoordinatesForPDFLib(21, rowHeight, page1).x + 500 - fontBold.widthOfTextAtSize(invoiceData.total.titleSums[key], 11), size: 11, color: rgb(0, 0, 0), lineHeight: 11, opacity: 1, maxWidth: 240, font: fontBold }) rowHeight += splitStringBySpace(key, 60).length * 5 }) /*let titleSumsArray = Object.keys(invoiceData.total.titleSums) titleSumsArray.forEach(sum => { let length = splitStringBySpace(sum,60).length rowHeight += length *6 })*/ //rowHeight += Object.keys(invoiceData.total.titleSums) pages[pageCounter - 1].drawLine({ start: getCoordinatesForPDFLib(20, rowHeight, page1), end: getCoordinatesForPDFLib(198, rowHeight, page1), thickness: 0.2, color: rgb(0, 0, 0), opacity: 1, }) rowHeight += 5 } invoiceData.totalArray.forEach((item, index) => { pages[pageCounter - 1].drawText(item.label, { ...getCoordinatesForPDFLib(21, rowHeight + 8 * index, page1), size: 11, color: rgb(0, 0, 0), lineHeight: 11, opacity: 1, maxWidth: 240, font: fontBold }) pages[pageCounter - 1].drawText(item.content, { y: getCoordinatesForPDFLib(21, rowHeight + 8 * index, page1).y, x: getCoordinatesForPDFLib(21, rowHeight + 8 * index, page1).x + 500 - fontBold.widthOfTextAtSize(item.content, 11), size: 11, color: rgb(0, 0, 0), lineHeight: 11, opacity: 1, maxWidth: 240, font: fontBold }) }) if (invoiceData.taxType !== "13b UStG" && invoiceData.taxType !== "19 UStG" && invoiceData.taxType !== "12.3 UStG") { } else { if (invoiceData.taxType === "13b UStG") { pages[pageCounter - 1].drawText("Die Umsatzsteuer für diese Leistung schuldet nach §13b UStG der Leistungsempfänger", { ...getCoordinatesForPDFLib(21, rowHeight + invoiceData.totalArray.length * 8, page1), size: 10, color: rgb(0, 0, 0), lineHeight: 10, opacity: 1, maxWidth: 500 }) } else if (invoiceData.taxType === "19 UStG") { pages[pageCounter - 1].drawText("Als Kleinunternehmer im Sinne von § 19 Abs. 1 UStG wird keine Umsatzsteuer berechnet.", { ...getCoordinatesForPDFLib(21, rowHeight + invoiceData.totalArray.length * 8, page1), size: 10, color: rgb(0, 0, 0), lineHeight: 10, opacity: 1, maxWidth: 500 }) } else if (invoiceData.taxType === "12.3 UStG") { pages[pageCounter - 1].drawText("Umsatzsteuer befreite Lieferung/Leistung für PV-Anlagen gemäß § 12 Absatz 3 UStG.", { ...getCoordinatesForPDFLib(21, rowHeight + invoiceData.totalArray.length * 8, page1), size: 10, color: rgb(0, 0, 0), lineHeight: 10, opacity: 1, maxWidth: 500 }) } } pages[pageCounter - 1].drawText(invoiceData.endText, { ...getCoordinatesForPDFLib(21, rowHeight + endTextDiff + (invoiceData.totalArray.length - 3) * 8, page1), size: 10, color: rgb(0, 0, 0), lineHeight: 10, opacity: 1, maxWidth: 500 }) return await pdfDoc.saveAsBase64() } } const pdfBytes = await genPDF(invoiceData, await getBackgroundSourceBuffer(server,backgroundPath)) if(returnMode === "base64"){ return { mimeType: 'application/pdf', base64: pdfBytes } } else { return null } } export const createTimeSheetPDF = async (server: FastifyInstance, returnMode, data, backgroundPath: string) => { const genPDF = async (input, backgroundSourceBuffer) => { const pdfDoc = await PDFDocument.create() const font = await pdfDoc.embedFont(StandardFonts.Helvetica) const fontBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold) let pages = [] let pageCounter = 1 const backgroudPdf = await PDFDocument.load(backgroundSourceBuffer) const firstPageBackground = await pdfDoc.embedPage(backgroudPdf.getPages()[0]) // Fallback für einseitige Hintergründe const secondPageBackground = await pdfDoc.embedPage(backgroudPdf.getPages()[backgroudPdf.getPages().length > 1 ? 1 : 0]) const page1 = pdfDoc.addPage() page1.drawPage(firstPageBackground, { x: 0, y: 0, }) pages.push(page1) console.log("PDF Input Data:", input) // --------------------------------------------------------- // DATEN-EXTRAKTION MIT FALLBACKS (Calculated vs. Standard) // --------------------------------------------------------- // Summen: Bevorzuge calculated..., falls vorhanden const sumSubmitted = input.calculatedSumWorkingMinutesSubmitted ?? input.sumWorkingMinutesSubmitted ?? 0; const sumApproved = input.calculatedSumWorkingMinutesApproved ?? input.sumWorkingMinutesApproved ?? 0; // Saldi: Bevorzuge calculated... const saldoSubmitted = input.calculatedSaldoSubmitted ?? input.saldoSubmitted ?? input.saldoInOfficial ?? 0; const saldoApproved = input.calculatedSaldoApproved ?? input.saldoApproved ?? input.saldo ?? 0; // Andere Summen (diese sind meist korrekt vom Backend) const sumRecreation = input.sumWorkingMinutesRecreationDays ?? 0; const sumVacation = input.sumWorkingMinutesVacationDays ?? 0; const sumSick = input.sumWorkingMinutesSickDays ?? 0; const sumTarget = input.timeSpanWorkingMinutes ?? 0; // Hilfsfunktion zur Formatierung von Minuten -> HH:MM const fmtTime = (mins) => { const m = Math.floor(Math.abs(mins)); return `${Math.floor(m / 60)}:${String(m % 60).padStart(2, "0")}`; }; const fmtSaldo = (mins) => { const sign = Math.sign(mins) >= 0 ? "+" : "-"; return `${sign} ${fmtTime(mins)}`; } // --------------------------------------------------------- // HEADER TEXTE ZEICHNEN // --------------------------------------------------------- pages[pageCounter - 1].drawText(`Anwesenheitsauswertung`,{ x: getCoordinatesForPDFLib(20,60,pages[pageCounter -1]).x, y: getCoordinatesForPDFLib(20,60,pages[pageCounter -1]).y, size: 15, font: fontBold }) pages[pageCounter - 1].drawText(`Mitarbeiter: ${input.full_name || ''}`,{ x: getCoordinatesForPDFLib(20,70,pages[pageCounter -1]).x, y: getCoordinatesForPDFLib(20,70,pages[pageCounter -1]).y, size: 10, }) pages[pageCounter - 1].drawText(`Nummer: ${input.employee_number || '-'}`,{ x: getCoordinatesForPDFLib(20,75,pages[pageCounter -1]).x, y: getCoordinatesForPDFLib(20,75,pages[pageCounter -1]).y, size: 10, }) // Zeile 1: Eingereicht & Genehmigt pages[pageCounter - 1].drawText(`Eingereicht: ${fmtTime(sumSubmitted)} Std`,{ x: getCoordinatesForPDFLib(20,80,pages[pageCounter -1]).x, y: getCoordinatesForPDFLib(20,80,pages[pageCounter -1]).y, size: 10, }) pages[pageCounter - 1].drawText(`Genehmigt: ${fmtTime(sumApproved)} Std`,{ x: getCoordinatesForPDFLib(20,85,pages[pageCounter -1]).x, y: getCoordinatesForPDFLib(20,85,pages[pageCounter -1]).y, size: 10, }) // Zeile 2: Ausgleichstage pages[pageCounter - 1].drawText(`Feiertagsausgleich: ${fmtTime(sumRecreation)} Std`,{ x: getCoordinatesForPDFLib(20,90,pages[pageCounter -1]).x, y: getCoordinatesForPDFLib(20,90,pages[pageCounter -1]).y, size: 10, }) pages[pageCounter - 1].drawText(`Urlaubsausgleich: ${fmtTime(sumVacation)} Std`,{ x: getCoordinatesForPDFLib(20,95,pages[pageCounter -1]).x, y: getCoordinatesForPDFLib(20,95,pages[pageCounter -1]).y, size: 10, }) pages[pageCounter - 1].drawText(`Krankheitsausgleich: ${fmtTime(sumSick)} Std`,{ x: getCoordinatesForPDFLib(20,100,pages[pageCounter -1]).x, y: getCoordinatesForPDFLib(20,100,pages[pageCounter -1]).y, size: 10, }) // Zeile 3: Soll & Saldo pages[pageCounter - 1].drawText(`Soll Stunden: ${fmtTime(sumTarget)} Std`,{ x: getCoordinatesForPDFLib(20,105,pages[pageCounter -1]).x, y: getCoordinatesForPDFLib(20,105,pages[pageCounter -1]).y, size: 10, }) // Wir nutzen hier die Begriffe "Inoffiziell" (Submitted Saldo) und "Saldo" (Approved Saldo) pages[pageCounter - 1].drawText(`Inoffizielles Saldo: ${fmtSaldo(saldoSubmitted)} Std`,{ x: getCoordinatesForPDFLib(20,110,pages[pageCounter -1]).x, y: getCoordinatesForPDFLib(20,110,pages[pageCounter -1]).y, size: 10, }) pages[pageCounter - 1].drawText(`Saldo: ${fmtSaldo(saldoApproved)} Std`,{ x: getCoordinatesForPDFLib(20,115,pages[pageCounter -1]).x, y: getCoordinatesForPDFLib(20,115,pages[pageCounter -1]).y, size: 10, }) // Tabellen-Header pages[pageCounter - 1].drawText(`Start:`,{ x: getCoordinatesForPDFLib(20,125,pages[pageCounter -1]).x, y: getCoordinatesForPDFLib(20,125,pages[pageCounter -1]).y, size: 10, }) pages[pageCounter - 1].drawText(`Ende:`,{ x: getCoordinatesForPDFLib(60,125,pages[pageCounter -1]).x, y: getCoordinatesForPDFLib(60,125,pages[pageCounter -1]).y, size: 10, }) pages[pageCounter - 1].drawText(`Dauer:`,{ x: getCoordinatesForPDFLib(100,125,pages[pageCounter -1]).x, y: getCoordinatesForPDFLib(100,125,pages[pageCounter -1]).y, size: 10, }) // --------------------------------------------------------- // TABELLE GENERIEREN (Spans verarbeiten) // --------------------------------------------------------- let rowHeight = 130 // WICHTIG: input.spans verwenden, fallback auf input.times (altes Format) // Wir filtern leere Einträge raus const rawItems = (input.spans || input.times || []).filter(t => t); // Sortierung umkehren (neueste zuletzt für den Druck? Oder wie gewünscht) // Im Original war es .reverse(). let reversedInput = rawItems.slice().reverse(); let splitted = [] const splittedLength = Math.floor((reversedInput.length - 25) / 40) // Erste Seite hat weniger Platz wegen Header (25 Zeilen) splitted.push(reversedInput.slice(0,25)) let lastIndex = 25 for (let i = 0; i < splittedLength; ++i ) { splitted.push(reversedInput.slice(lastIndex, lastIndex + (i + 1) * 40)) lastIndex = lastIndex + (i + 1) * 40 + 1 } if(reversedInput.slice(lastIndex, reversedInput.length).length > 0) { splitted.push(reversedInput.slice(lastIndex, reversedInput.length)) } console.log("PDF Pages Chunks:", splitted.length) splitted.forEach((chunk,index) => { if(index > 0) { const page = pdfDoc.addPage() page.drawPage(secondPageBackground, { x: 0, y: 0, }) pages.push(page) pageCounter++ rowHeight = 20 } chunk.forEach(span => { // Mapping für Felder: spans nutzen 'startedAt', times nutzten 'started_at' const startStr = span.startedAt || span.started_at; const endStr = span.endedAt || span.stopped_at; // endedAt oder stopped_at // Dauer berechnen (da Spans keine duration_minutes haben) let durationStr = ""; if (startStr && endStr) { const diffMins = dayjs(endStr).diff(dayjs(startStr), 'minute'); durationStr = fmtTime(diffMins); } else if (span.duration_minutes) { durationStr = fmtTime(span.duration_minutes); } else if (span.duration) { // Falls schon formatiert übergeben durationStr = span.duration; } pages[pageCounter - 1].drawText(`${dayjs(startStr).format("HH:mm DD.MM.YY")}`,{ x: getCoordinatesForPDFLib(20,rowHeight,pages[pageCounter -1]).x, y: getCoordinatesForPDFLib(20,rowHeight,pages[pageCounter -1]).y, size: 10, }) pages[pageCounter - 1].drawText(`${endStr ? dayjs(endStr).format("HH:mm DD.MM.YY") : 'läuft...'}`,{ x: getCoordinatesForPDFLib(60,rowHeight,pages[pageCounter -1]).x, y: getCoordinatesForPDFLib(60,rowHeight,pages[pageCounter -1]).y, size: 10, }) pages[pageCounter - 1].drawText(`${durationStr}`,{ x: getCoordinatesForPDFLib(100,rowHeight,pages[pageCounter -1]).x, y: getCoordinatesForPDFLib(100,rowHeight,pages[pageCounter -1]).y, size: 10, }) // Optional: Status anzeigen? /*pages[pageCounter - 1].drawText(`${span.status || span.state || ''}`,{ x: getCoordinatesForPDFLib(130,rowHeight,pages[pageCounter -1]).x, y: getCoordinatesForPDFLib(130,rowHeight,pages[pageCounter -1]).y, size: 8, })*/ rowHeight += 6 }) }) return await pdfDoc.saveAsBase64() } try { const pdfBytes = await genPDF(data, await getBackgroundSourceBuffer(server,backgroundPath)) if(returnMode === "base64"){ return { mimeType: 'application/pdf', base64: pdfBytes } } else { return "test" } } catch(error) { console.log(error) throw error; // Fehler weiterwerfen, damit er oben ankommt } }