Files
FEDEO/backend/src/utils/pdf.ts

1141 lines
42 KiB
TypeScript

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
}
}