refactoring 1. step
This commit is contained in:
205
src/services/pdf-service.js
Normal file
205
src/services/pdf-service.js
Normal file
@@ -0,0 +1,205 @@
|
||||
/**
|
||||
* 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();
|
||||
await page.setContent(html, { waitUntil: 'networkidle0', timeout: 60000 });
|
||||
|
||||
const pdf = await page.pdf({
|
||||
format,
|
||||
printBackground,
|
||||
margin
|
||||
});
|
||||
|
||||
await page.close();
|
||||
return pdf;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = `<img src="data:image/png;base64,${logoBase64}" alt="Company Logo" class="logo logo-size">`;
|
||||
} 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 `
|
||||
<tr>
|
||||
<td class="qty">${item.quantity}</td>
|
||||
<td class="description">${item.description}</td>
|
||||
<td class="rate">${rateFormatted}</td>
|
||||
<td class="amount">${item.amount}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
// Add subtotal
|
||||
const subtotal = invoice ? invoice.subtotal : 0;
|
||||
itemsHTML += `
|
||||
<tr class="footer-row">
|
||||
<td colspan="3" class="total-label">Subtotal:</td>
|
||||
<td class="total-amount">$${formatMoney(subtotal)}</td>
|
||||
</tr>`;
|
||||
|
||||
// Add tax if not exempt
|
||||
if (invoice && !invoice.tax_exempt) {
|
||||
itemsHTML += `
|
||||
<tr class="footer-row">
|
||||
<td colspan="3" class="total-label">Tax (${invoice.tax_rate}%):</td>
|
||||
<td class="total-amount">$${formatMoney(invoice.tax_amount)}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
// Add total
|
||||
const amountPaid = invoice ? (parseFloat(invoice.amount_paid) || 0) : 0;
|
||||
const total = invoice ? parseFloat(invoice.total) : 0;
|
||||
const balanceDue = total - amountPaid;
|
||||
|
||||
itemsHTML += `
|
||||
<tr class="footer-row">
|
||||
<td colspan="3" class="total-label" style="font-size: 16px;">TOTAL:</td>
|
||||
<td class="total-amount" style="font-size: 16px;">$${formatMoney(total)}</td>
|
||||
</tr>`;
|
||||
|
||||
// Add downpayment/balance if partial
|
||||
if (amountPaid > 0) {
|
||||
itemsHTML += `
|
||||
<tr class="footer-row">
|
||||
<td colspan="3" class="total-label" style="color: #059669;">Downpayment:</td>
|
||||
<td class="total-amount" style="color: #059669;">-$${formatMoney(amountPaid)}</td>
|
||||
</tr>
|
||||
<tr class="footer-row">
|
||||
<td colspan="3" class="total-label" style="font-weight: bold; font-size: 16px; border-top: 2px solid #333; padding-top: 8px;">BALANCE DUE:</td>
|
||||
<td class="total-amount" style="font-weight: bold; font-size: 16px; border-top: 2px solid #333; padding-top: 8px;">$${formatMoney(balanceDue)}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
// Thank you message
|
||||
itemsHTML += `
|
||||
<tr class="footer-row">
|
||||
<td colspan="4" class="thank-you">Thank you for your business!</td>
|
||||
</tr>`;
|
||||
|
||||
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 `
|
||||
<tr>
|
||||
<td class="qty">${item.quantity}</td>
|
||||
<td class="description">${item.description}</td>
|
||||
<td class="rate">${rateFormatted}</td>
|
||||
<td class="amount">${item.amount}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
// Add subtotal
|
||||
const subtotal = quote ? quote.subtotal : 0;
|
||||
itemsHTML += `
|
||||
<tr class="footer-row">
|
||||
<td colspan="3" class="total-label">Subtotal</td>
|
||||
<td class="total-amount">$${formatMoney(subtotal)}</td>
|
||||
</tr>`;
|
||||
|
||||
// Add tax if not exempt
|
||||
if (quote && !quote.tax_exempt) {
|
||||
itemsHTML += `
|
||||
<tr class="footer-row">
|
||||
<td colspan="3" class="total-label">Tax (${quote.tax_rate}%)</td>
|
||||
<td class="total-amount">$${formatMoney(quote.tax_amount)}</td>
|
||||
</tr>`;
|
||||
}
|
||||
|
||||
// Add total
|
||||
const total = quote ? quote.total : 0;
|
||||
itemsHTML += `
|
||||
<tr class="footer-row">
|
||||
<td colspan="3" class="total-label" style="font-size: 16px;">TOTAL</td>
|
||||
<td class="total-amount" style="font-size: 16px;">$${formatMoney(total)}</td>
|
||||
</tr>
|
||||
<tr class="footer-row">
|
||||
<td colspan="4" class="thank-you">This quote is valid for 14 days. We appreciate your business </td>
|
||||
</tr>`;
|
||||
|
||||
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('<br>');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
setBrowser,
|
||||
getBrowser,
|
||||
generatePdfFromHtml,
|
||||
getLogoHtml,
|
||||
renderInvoiceItems,
|
||||
renderQuoteItems,
|
||||
formatAddressLines
|
||||
};
|
||||
Reference in New Issue
Block a user