1141 lines
42 KiB
TypeScript
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
|
|
}
|
|
} |