/** * PDF Generation Service * Handles HTML to PDF conversion using Puppeteer */ const path = require('path'); const fs = require('fs').promises; const { formatMoney, formatDate } = require('../utils/helpers'); // Initialize browser - will be set from main app let browserInstance = null; function setBrowser(browser) { browserInstance = browser; } async function getBrowser() { return browserInstance; } /** * Generate PDF from HTML template */ async function generatePdfFromHtml(html, options = {}) { const { format = 'Letter', margin = { top: '0.5in', right: '0.5in', bottom: '0.5in', left: '0.5in' }, printBackground = true } = options; const browser = await getBrowser(); if (!browser) { throw new Error('Browser not initialized'); } const page = await browser.newPage(); try { // Erhöhtes Timeout: 5 Sekunden sind unter Docker manchmal zu wenig. // Besser auf 15 Sekunden (15000) setzen, um den Fehler von vornherein zu vermeiden. await page.setContent(html, { waitUntil: 'load', timeout: 15000 }); const pdf = await page.pdf({ format, printBackground, margin }); return pdf; } finally { // Dieser Block wird IMMER ausgeführt, selbst wenn oben ein Fehler fliegt. // Der Tab wird also zu 100% wieder geschlossen. if (page) { await page.close(); } } } /** * Get company logo as base64 HTML */ async function getLogoHtml() { let logoHTML = ''; try { const logoPath = path.join(__dirname, '..', '..', 'public', 'uploads', 'company-logo.png'); const logoData = await fs.readFile(logoPath); const logoBase64 = logoData.toString('base64'); logoHTML = ``; } catch (err) { // No logo found } return logoHTML; } /** * Render invoice items to HTML table rows */ function renderInvoiceItems(items, invoice = null) { let itemsHTML = items.map(item => { let rateFormatted = item.rate; if (item.rate.toUpperCase() !== 'TBD' && !item.rate.includes('/')) { const rateNum = parseFloat(item.rate.replace(/[^0-9.]/g, '')); if (!isNaN(rateNum)) rateFormatted = rateNum.toFixed(2); } return ` ${item.quantity} ${item.description} ${rateFormatted} ${item.amount} `; }).join(''); // Add subtotal const subtotal = invoice ? invoice.subtotal : 0; itemsHTML += ` Subtotal: $${formatMoney(subtotal)} `; // Add tax if not exempt if (invoice && !invoice.tax_exempt) { itemsHTML += ` Tax (${invoice.tax_rate}%): $${formatMoney(invoice.tax_amount)} `; } // Add total const amountPaid = invoice ? (parseFloat(invoice.amount_paid) || 0) : 0; const total = invoice ? parseFloat(invoice.total) : 0; const balanceDue = total - amountPaid; itemsHTML += ` TOTAL: $${formatMoney(total)} `; // Add downpayment/balance if partial // Add downpayment/balance if partial if (amountPaid > 0) { const isFullyPaid = balanceDue <= 0.01; // allow for rounding const paymentLabel = isFullyPaid ? 'Payment:' : 'Downpayment:'; itemsHTML += ` ${paymentLabel} -$${formatMoney(amountPaid)} `; // Only show BALANCE DUE row if there's actually a remaining balance if (!isFullyPaid) { itemsHTML += ` BALANCE DUE: $${formatMoney(balanceDue)} `; } } // Thank you message itemsHTML += ` Thank you for your business! `; return itemsHTML; } /** * Render quote items to HTML table rows */ function renderQuoteItems(items, quote = null) { let itemsHTML = items.map(item => { let rateFormatted = item.rate; if (item.rate.toUpperCase() !== 'TBD' && !item.rate.includes('/')) { const rateNum = parseFloat(item.rate.replace(/[^0-9.]/g, '')); if (!isNaN(rateNum)) rateFormatted = rateNum.toFixed(2); } return ` ${item.quantity} ${item.description} ${rateFormatted} ${item.amount} `; }).join(''); // Add subtotal const subtotal = quote ? quote.subtotal : 0; itemsHTML += ` Subtotal $${formatMoney(subtotal)} `; // Add tax if not exempt if (quote && !quote.tax_exempt) { itemsHTML += ` Tax (${quote.tax_rate}%) $${formatMoney(quote.tax_amount)} `; } // Add total const total = quote ? quote.total : 0; itemsHTML += ` TOTAL $${formatMoney(total)} This quote is valid for 14 days. We appreciate your business `; return itemsHTML; } /** * Format address lines for template */ function formatAddressLines(line1, line2, line3, line4, customerName) { const addressLines = []; if (line1 && line1.trim().toLowerCase() !== (customerName || '').trim().toLowerCase()) { addressLines.push(line1); } if (line2) addressLines.push(line2); if (line3) addressLines.push(line3); if (line4) addressLines.push(line4); return addressLines.join('
'); } module.exports = { setBrowser, getBrowser, generatePdfFromHtml, getLogoHtml, renderInvoiceItems, renderQuoteItems, formatAddressLines };