diff --git a/src/services/manifestPdf.js b/src/services/manifestPdf.js index 1225d6e..cf60318 100644 --- a/src/services/manifestPdf.js +++ b/src/services/manifestPdf.js @@ -52,11 +52,11 @@ export function formatQuantity(val) { const TYPE_CONFIG = { quote: { docTitle: 'ANGEBOT', - numberPrefix: 'Angebots-Nr.', + numberPrefix: 'Angebot-Nr.', showPrices: true, filenamePrefix: 'angebot', introText: 'Wir freuen uns über Ihr Interesse und bieten Ihnen freibleibend wie folgt an:', - closingText: 'Dieses Angebot ist freibleibend und 30 Tage ab Ausstellungsdatum gültig. Wir freuen uns auf Ihren Auftrag!', + closingText: '', defaultSubject: 'Angebot', }, order: { @@ -114,7 +114,12 @@ export function generateManifestPdf({ try { const doc = new PDFDocument({ size: 'A4', - margin: 45, + margins: { + top: 45, + bottom: 25, + left: 45, + right: 45, + }, bufferPages: true, info: { Title: `${config.docTitle} ${manifest.number || manifest.id}`, @@ -195,10 +200,16 @@ export function generateManifestPdf({ } // Document Number under Document Title - doc.font('Helvetica-Bold') - .fontSize(11) + const headerNumStr = `${config.numberPrefix} ${manifest.number || manifest.id}`; + let headerNumFontSize = 11; + const headerNumWidth = 250; + doc.font('Helvetica-Bold'); + while (headerNumFontSize > 8 && doc.fontSize(headerNumFontSize).widthOfString(headerNumStr) > headerNumWidth) { + headerNumFontSize -= 0.5; + } + doc.fontSize(headerNumFontSize) .fillColor(primaryColor) - .text(`${config.numberPrefix} ${manifest.number || manifest.id}`, 350, y, { align: 'right', width: 200 }); + .text(headerNumStr, 300, y, { align: 'right', width: headerNumWidth, lineBreak: false }); y += 18; @@ -261,12 +272,6 @@ export function generateManifestPdf({ doc.text(chosenAddress.county, 45, y); y += 13; } - } else { - doc.font('Helvetica') - .fontSize(9) - .fillColor(mutedColor) - .text(`Keine ${partnerLabel} hinterlegt`, 45, y); - y += 14; } // Right side Meta Box @@ -287,16 +292,24 @@ export function generateManifestPdf({ doc.font('Helvetica') .fontSize(8.5) .fillColor(mutedColor) - .text(label, metaBoxX + 10, metaY, { width: 95 }); - doc.font('Helvetica-Bold') - .fontSize(8.5) + .text(label, metaBoxX + 10, metaY, { width: 72, lineBreak: false }); + + const valText = val || '–'; + doc.font('Helvetica-Bold'); + let valFontSize = 8.5; + const maxValWidth = 128; + while (valFontSize > 6.5 && doc.fontSize(valFontSize).widthOfString(valText) > maxValWidth) { + valFontSize -= 0.5; + } + doc.fontSize(valFontSize) .fillColor(textColor) - .text(val || '–', metaBoxX + 105, metaY, { width: 105, align: 'right' }); + .text(valText, metaBoxX + 82, metaY, { width: maxValWidth, align: 'right', lineBreak: false }); metaY += 15; }; - drawMetaRow(`${config.docTitle.slice(0, 1) + config.docTitle.slice(1).toLowerCase()}-Nr.:`, String(manifest.number || manifest.id)); - const dateLabel = type === 'shipment' ? 'Versanddatum:' : (type === 'delivery' ? 'Eingangsdatum:' : 'Belegdatum:'); + const numLabel = config.numberPrefix ? (config.numberPrefix.endsWith(':') ? config.numberPrefix : `${config.numberPrefix}:`) : 'Beleg-Nr.:'; + drawMetaRow(numLabel, String(manifest.number || manifest.id)); + const dateLabel = type === 'shipment' ? 'Versanddatum:' : (type === 'delivery' ? 'Eingangsdatum:' : 'Datum:'); drawMetaRow(dateLabel, formatDate(manifest.date || manifest.createdAt)); if (manifest.externalNumber) { @@ -344,11 +357,11 @@ export function generateManifestPdf({ const showPrices = config.showPrices; // Column setups - const colXPrices = { pos: 45, sku: 75, name: 165, amount: 345, net: 405, tax: 465, total: 495 }; - const colWidthPrices = { pos: 25, sku: 85, name: 175, amount: 55, net: 55, tax: 25, total: 55 }; + const colXPrices = { pos: 45, sku: 70, name: 150, amount: 335, gross: 385, tax: 450, total: 482 }; + const colWidthPrices = { pos: 22, sku: 75, name: 180, amount: 45, gross: 60, tax: 30, total: 65 }; const colXNoPrices = { pos: 45, sku: 80, name: 185, amount: 455 }; - const colWidthNoPrices = { pos: 30, sku: 100, name: 265, amount: 95 }; + const colWidthNoPrices = { pos: 30, sku: 100, name: 265, amount: 92 }; const drawTableHeader = (atY) => { doc.rect(45, atY, 505, 20) @@ -364,9 +377,9 @@ export function generateManifestPdf({ doc.text('Art.-Nr.', colXPrices.sku, atY + 6, { width: colWidthPrices.sku }); doc.text('Bezeichnung', colXPrices.name, atY + 6, { width: colWidthPrices.name }); doc.text('Menge', colXPrices.amount, atY + 6, { width: colWidthPrices.amount, align: 'right' }); - doc.text('E-Preis Netto', colXPrices.net, atY + 6, { width: colWidthPrices.net, align: 'right' }); + doc.text('E-Preis Brutto', colXPrices.gross, atY + 6, { width: colWidthPrices.gross, align: 'right' }); doc.text('MwSt', colXPrices.tax, atY + 6, { width: colWidthPrices.tax, align: 'right' }); - doc.text('Gesamt Netto', colXPrices.total, atY + 6, { width: colWidthPrices.total, align: 'right' }); + doc.text('Gesamt Brutto', colXPrices.total, atY + 6, { width: colWidthPrices.total, align: 'right' }); } else { doc.text('Pos', colXNoPrices.pos + 3, atY + 6, { width: colWidthNoPrices.pos }); doc.text('Art.-Nr.', colXNoPrices.sku, atY + 6, { width: colWidthNoPrices.sku }); @@ -389,15 +402,21 @@ export function generateManifestPdf({ const net = Number(line.net || 0); const rebate = Number(line.rebate || 0); const taxrate = Number(line.taxrate || 0); + const taxFactor = 1 + taxrate / 100; totalQty += amount; let lineNet = 0; + let lineGross = 0; + let unitGross = net * taxFactor; let unitNetAfterRebate = net; + let unitGrossAfterRebate = unitGross; if (showPrices) { unitNetAfterRebate = net * (1 - rebate / 100); + unitGrossAfterRebate = unitNetAfterRebate * taxFactor; lineNet = amount * unitNetAfterRebate; const lineTax = lineNet * (taxrate / 100); + lineGross = lineNet + lineTax; totalNet += lineNet; @@ -407,13 +426,31 @@ export function generateManifestPdf({ taxBreakdown.set(taxrate, currTax); } - // Row height estimation - const hasComment = Boolean(line.comment); + const nameWidth = showPrices ? colWidthPrices.name : colWidthNoPrices.name; + const puSuffix = line.pu && line.pu > 1 ? ` (VPE ${line.pu})` : ''; + const nameText = (line.name || 'Position') + puSuffix; + + // Measure actual rendered text heights to prevent overlaps + doc.font('Helvetica').fontSize(8.5); + const nameHeight = doc.heightOfString(nameText, { width: nameWidth }); + const hasRebate = showPrices && rebate > 0; - let extraLines = 0; - if (hasComment) extraLines += 1; - if (hasRebate) extraLines += 1; - const rowHeight = 18 + extraLines * 11; + const rebateText = hasRebate ? `Rabatt: ${rebate}% (effektiver E-Preis: ${formatCurrency(unitGrossAfterRebate)})` : ''; + let rebateHeight = 0; + if (hasRebate) { + doc.font('Helvetica').fontSize(7.5); + rebateHeight = doc.heightOfString(rebateText, { width: nameWidth }) + 2; + } + + const hasComment = Boolean(line.comment); + let commentHeight = 0; + if (hasComment) { + doc.font('Helvetica-Oblique').fontSize(7.5); + commentHeight = doc.heightOfString(line.comment, { width: nameWidth }) + 2; + } + + const contentHeight = nameHeight + rebateHeight + commentHeight; + const rowHeight = Math.max(18, Math.ceil(contentHeight + 6)); // Check page overflow if (y + rowHeight > 730) { @@ -428,72 +465,70 @@ export function generateManifestPdf({ .fill(); } - doc.font('Helvetica') - .fontSize(8.5) - .fillColor(textColor); - - const puSuffix = line.pu && line.pu > 1 ? ` (VPE ${line.pu})` : ''; + const textY = y + 1; if (showPrices) { + doc.font('Helvetica') + .fontSize(8.5) + .fillColor(textColor); + // Pos - doc.text(String(index + 1), colXPrices.pos + 3, y, { width: colWidthPrices.pos }); + doc.text(String(index + 1), colXPrices.pos + 3, textY, { width: colWidthPrices.pos }); // SKU - doc.text(line.sku || '–', colXPrices.sku, y, { width: colWidthPrices.sku, ellipsis: true }); - // Name - doc.text((line.name || 'Position') + puSuffix, colXPrices.name, y, { - width: colWidthPrices.name, - ellipsis: true, - }); + doc.text(line.sku || '–', colXPrices.sku, textY, { width: colWidthPrices.sku, ellipsis: true }); + // Name (wraps naturally without clipping) + doc.text(nameText, colXPrices.name, textY, { width: nameWidth }); // Amount - doc.text(formatQuantity(amount), colXPrices.amount, y, { + doc.text(formatQuantity(amount), colXPrices.amount, textY, { width: colWidthPrices.amount, align: 'right', }); - // Unit Price Net - doc.text(formatCurrency(net), colXPrices.net, y, { - width: colWidthPrices.net, + // Unit Price Gross + doc.text(formatCurrency(unitGross), colXPrices.gross, textY, { + width: colWidthPrices.gross, align: 'right', }); // Tax Rate - doc.text(`${taxrate}%`, colXPrices.tax, y, { + doc.text(`${taxrate}%`, colXPrices.tax, textY, { width: colWidthPrices.tax, align: 'right', }); - // Line Total Net + // Line Total Gross doc.font('Helvetica-Bold') - .text(formatCurrency(lineNet), colXPrices.total, y, { + .text(formatCurrency(lineGross), colXPrices.total, textY, { width: colWidthPrices.total, align: 'right', }); - let subY = y + 12; + let subY = textY + nameHeight + 2; if (hasRebate) { doc.font('Helvetica') .fontSize(7.5) .fillColor(secondaryColor) - .text(`Rabatt: ${rebate}% (effektiver E-Preis: ${formatCurrency(unitNetAfterRebate)})`, colXPrices.name, subY, { - width: colWidthPrices.name, + .text(rebateText, colXPrices.name, subY, { + width: nameWidth, }); - subY += 10; + subY += rebateHeight; } if (hasComment) { doc.font('Helvetica-Oblique') .fontSize(7.5) .fillColor(mutedColor) .text(line.comment, colXPrices.name, subY, { - width: colWidthPrices.name, + width: nameWidth, }); } } else { + doc.font('Helvetica') + .fontSize(8.5) + .fillColor(textColor); + // Without Prices - doc.text(String(index + 1), colXNoPrices.pos + 3, y, { width: colWidthNoPrices.pos }); - doc.text(line.sku || '–', colXNoPrices.sku, y, { width: colWidthNoPrices.sku, ellipsis: true }); - doc.text((line.name || 'Position') + puSuffix, colXNoPrices.name, y, { - width: colWidthNoPrices.name, - ellipsis: true, - }); + doc.text(String(index + 1), colXNoPrices.pos + 3, textY, { width: colWidthNoPrices.pos }); + doc.text(line.sku || '–', colXNoPrices.sku, textY, { width: colWidthNoPrices.sku, ellipsis: true }); + doc.text(nameText, colXNoPrices.name, textY, { width: nameWidth }); doc.font('Helvetica-Bold') - .text(formatQuantity(amount) + ' Stk', colXNoPrices.amount, y, { + .text(formatQuantity(amount) + ' Stk', colXNoPrices.amount, textY, { width: colWidthNoPrices.amount, align: 'right', }); @@ -502,8 +537,8 @@ export function generateManifestPdf({ doc.font('Helvetica-Oblique') .fontSize(7.5) .fillColor(mutedColor) - .text(line.comment, colXNoPrices.name, y + 12, { - width: colWidthNoPrices.name, + .text(line.comment, colXNoPrices.name, textY + nameHeight + 2, { + width: nameWidth, }); } } @@ -657,6 +692,7 @@ export function generateManifestPdf({ const range = doc.bufferedPageRange(); for (let i = 0; i < range.count; i++) { doc.switchToPage(i); + doc.page.margins.bottom = 0; // Footer rule doc.moveTo(45, 785)