UNPKG

invoice-craft

Version:

Customizable, browser-first invoice PDF generator library with modern TypeScript API

364 lines (326 loc) 9.94 kB
import { calculateTotals } from "../core/calculations"; import { getLabels } from "../utils/localization"; export function generatePreviewHTML(invoice, options = {}) { const totals = calculateTotals(invoice); const labels = getLabels(); const theme = options.theme || 'light'; const responsive = options.responsive !== false; const showGrid = options.showGrid || false; const styles = generatePreviewStyles(theme, responsive, showGrid); const content = generatePreviewContent(invoice, totals, labels); return ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Invoice Preview - ${invoice.invoiceNumber}</title> ${options.includeStyles !== false ? `<style>${styles}</style>` : ''} </head> <body> <div class="invoice-preview ${theme}-theme ${showGrid ? 'show-grid' : ''}"> ${content} </div> </body> </html>`; } function generatePreviewStyles(theme, responsive, showGrid) { const baseStyles = ` * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: ${theme === 'dark' ? '#e0e0e0' : '#333'}; background-color: ${theme === 'dark' ? '#1a1a1a' : '#f5f5f5'}; padding: 20px; } .invoice-preview { max-width: 800px; margin: 0 auto; background: ${theme === 'dark' ? '#2d2d2d' : '#ffffff'}; padding: 40px; border-radius: 8px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); position: relative; } ${showGrid ? ` .invoice-preview::before { content: ''; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background-image: linear-gradient(rgba(0,0,0,0.1) 1px, transparent 1px), linear-gradient(90deg, rgba(0,0,0,0.1) 1px, transparent 1px); background-size: 20px 20px; pointer-events: none; opacity: 0.3; }` : ''} .invoice-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 40px; border-bottom: 2px solid ${theme === 'dark' ? '#404040' : '#e0e0e0'}; padding-bottom: 20px; } .company-info h1 { font-size: 2.5em; color: var(--brand-color, #1976d2); margin-bottom: 10px; } .company-info p { margin: 5px 0; color: ${theme === 'dark' ? '#b0b0b0' : '#666'}; } .invoice-details { text-align: right; } .invoice-details h2 { font-size: 2em; color: var(--brand-color, #1976d2); margin-bottom: 10px; } .invoice-details p { margin: 5px 0; font-size: 1.1em; } .parties { display: grid; grid-template-columns: 1fr 1fr; gap: 40px; margin-bottom: 40px; } .party h3 { font-size: 1.3em; margin-bottom: 15px; color: var(--brand-color, #1976d2); border-bottom: 1px solid ${theme === 'dark' ? '#404040' : '#e0e0e0'}; padding-bottom: 5px; } .party p { margin: 8px 0; color: ${theme === 'dark' ? '#b0b0b0' : '#666'}; } .items-table { width: 100%; border-collapse: collapse; margin-bottom: 30px; background: ${theme === 'dark' ? '#333' : '#fff'}; } .items-table th, .items-table td { padding: 15px; text-align: left; border-bottom: 1px solid ${theme === 'dark' ? '#404040' : '#e0e0e0'}; } .items-table th { background-color: var(--brand-color, #1976d2); color: white; font-weight: 600; text-transform: uppercase; font-size: 0.9em; letter-spacing: 0.5px; } .items-table td { color: ${theme === 'dark' ? '#e0e0e0' : '#333'}; } .items-table .number { text-align: right; font-family: 'Courier New', monospace; } .totals { margin-left: auto; width: 300px; background: ${theme === 'dark' ? '#333' : '#f9f9f9'}; padding: 20px; border-radius: 8px; } .totals .total-row { display: flex; justify-content: space-between; margin: 10px 0; padding: 8px 0; } .totals .total-row.final { border-top: 2px solid var(--brand-color, #1976d2); font-weight: bold; font-size: 1.2em; color: var(--brand-color, #1976d2); } .notes-terms { margin-top: 40px; padding-top: 20px; border-top: 1px solid ${theme === 'dark' ? '#404040' : '#e0e0e0'}; } .notes-terms h4 { color: var(--brand-color, #1976d2); margin-bottom: 10px; } .notes-terms p { color: ${theme === 'dark' ? '#b0b0b0' : '#666'}; line-height: 1.6; } `; const responsiveStyles = responsive ? ` @media (max-width: 768px) { .invoice-preview { padding: 20px; margin: 10px; } .invoice-header { flex-direction: column; gap: 20px; } .invoice-details { text-align: left; } .parties { grid-template-columns: 1fr; gap: 20px; } .items-table { font-size: 0.9em; } .items-table th, .items-table td { padding: 10px 8px; } .totals { width: 100%; margin: 20px 0; } } @media (max-width: 480px) { .company-info h1 { font-size: 2em; } .invoice-details h2 { font-size: 1.5em; } .items-table { font-size: 0.8em; } } ` : ''; return baseStyles + responsiveStyles; } function generatePreviewContent(invoice, totals, labels) { var _a, _b, _c; const brandColor = invoice.from.brandColor || '#1976d2'; return ` <style> :root { --brand-color: ${brandColor}; } </style> <div class="invoice-header"> <div class="company-info"> ${invoice.from.logoUrl ? `<img src="${invoice.from.logoUrl}" alt="Logo" style="max-height: 60px; margin-bottom: 15px;">` : ''} <h1>${invoice.from.name}</h1> <p>${((_a = invoice.from.address) === null || _a === void 0 ? void 0 : _a.replace(/\n/g, '<br>')) || ''}</p> ${invoice.from.email ? `<p>Email: ${invoice.from.email}</p>` : ''} ${invoice.from.phone ? `<p>Phone: ${invoice.from.phone}</p>` : ''} </div> <div class="invoice-details"> <h2>${labels.invoice}</h2> <p><strong>${labels.invoiceNumber}:</strong> ${invoice.invoiceNumber}</p> <p><strong>${labels.date}:</strong> ${invoice.invoiceDate}</p> ${invoice.dueDate ? `<p><strong>${labels.dueDate}:</strong> ${invoice.dueDate}</p>` : ''} </div> </div> <div class="parties"> <div class="party"> <h3>${labels.billTo}</h3> <p><strong>${invoice.to.name}</strong></p> <p>${((_b = invoice.to.address) === null || _b === void 0 ? void 0 : _b.replace(/\n/g, '<br>')) || ''}</p> ${invoice.to.email ? `<p>Email: ${invoice.to.email}</p>` : ''} ${invoice.to.phone ? `<p>Phone: ${invoice.to.phone}</p>` : ''} </div> <div class="party"> <h3>${labels.billFrom}</h3> <p><strong>${invoice.from.name}</strong></p> <p>${((_c = invoice.from.address) === null || _c === void 0 ? void 0 : _c.replace(/\n/g, '<br>')) || ''}</p> ${invoice.from.email ? `<p>Email: ${invoice.from.email}</p>` : ''} ${invoice.from.phone ? `<p>Phone: ${invoice.from.phone}</p>` : ''} </div> </div> <table class="items-table"> <thead> <tr> <th>${labels.description}</th> <th>${labels.quantity}</th> <th>${labels.unitPrice}</th> <th>${labels.tax}</th> <th>${labels.total}</th> </tr> </thead> <tbody> ${invoice.items.map(item => { const itemTotal = item.quantity * item.unitPrice; const taxAmount = itemTotal * (item.taxRate || 0); const totalWithTax = itemTotal + taxAmount; return ` <tr> <td>${item.description}</td> <td class="number">${item.quantity}</td> <td class="number">${formatCurrency(item.unitPrice, invoice.currency)}</td> <td class="number">${((item.taxRate || 0) * 100).toFixed(1)}%</td> <td class="number">${formatCurrency(totalWithTax, invoice.currency)}</td> </tr> `; }).join('')} </tbody> </table> <div class="totals"> <div class="total-row"> <span>${labels.subtotal}:</span> <span>${formatCurrency(totals.subtotal, invoice.currency)}</span> </div> ${totals.totalTax > 0 ? ` <div class="total-row"> <span>${labels.tax}:</span> <span>${formatCurrency(totals.totalTax, invoice.currency)}</span> </div> ` : ''} ${invoice.discount ? ` <div class="total-row"> <span>${labels.discount}:</span> <span>-${formatCurrency(invoice.discount, invoice.currency)}</span> </div> ` : ''} <div class="total-row final"> <span>${labels.total}:</span> <span>${formatCurrency(totals.total, invoice.currency)}</span> </div> </div> ${invoice.notes || invoice.terms ? ` <div class="notes-terms"> ${invoice.notes ? ` <div> <h4>Notes:</h4> <p>${invoice.notes}</p> </div> ` : ''} ${invoice.terms ? ` <div style="margin-top: 20px;"> <h4>Terms & Conditions:</h4> <p>${invoice.terms}</p> </div> ` : ''} </div> ` : ''} `; } function formatCurrency(amount, currency = 'USD') { return new Intl.NumberFormat('en-US', { style: 'currency', currency: currency }).format(amount); }