UNPKG

invoice-craft

Version:

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

363 lines (362 loc) 15.9 kB
var _a; import * as pdfMake from "pdfmake/build/pdfmake"; import * as pdfFonts from "pdfmake/build/vfs_fonts"; import { calculateTotals } from "../core/calculations"; import { getLabels } from "../utils/localization"; import { fetchBase64Image } from "./assets"; // Initialize fonts immediately try { // Handle different ways pdfMake might be exported const pdfMakeInstance = pdfMake.default || pdfMake; const fontsInstance = pdfFonts.default || pdfFonts; // Try different font structures if ((_a = fontsInstance === null || fontsInstance === void 0 ? void 0 : fontsInstance.pdfMake) === null || _a === void 0 ? void 0 : _a.vfs) { pdfMakeInstance.vfs = fontsInstance.pdfMake.vfs; } else if (fontsInstance === null || fontsInstance === void 0 ? void 0 : fontsInstance.vfs) { pdfMakeInstance.vfs = fontsInstance.vfs; } else { console.warn('Could not load pdfMake fonts properly, PDF may not render correctly'); } console.log('pdfMake fonts initialized'); } catch (error) { console.error('Font initialization error:', error); } export async function buildPdf(invoice, options = {}) { var _a, _b, _c, _d, _e; console.log('buildPdf: Starting PDF generation...'); (_b = (_a = options.hooks) === null || _a === void 0 ? void 0 : _a.beforeRender) === null || _b === void 0 ? void 0 : _b.call(_a, invoice); console.log('buildPdf: Before render hook completed'); console.log('buildPdf: Calculating totals...'); const totals = calculateTotals(invoice); console.log('buildPdf: Totals calculated:', totals); console.log('buildPdf: Getting labels...'); const labels = getLabels(options.labels); console.log('buildPdf: Labels retrieved'); const layoutStyle = options.layoutStyle || 'default'; const brandColor = options.brandColor || invoice.from.brandColor || "#1976d2"; let logoImg = options.logoUrl || invoice.from.logoUrl; console.log(`buildPdf: Using layout style: ${layoutStyle}`); if (logoImg && !logoImg.startsWith("data:")) { console.log('buildPdf: Fetching logo image...'); logoImg = await fetchBase64Image(logoImg); console.log('buildPdf: Logo image fetched'); } console.log('buildPdf: Processing invoice items...'); const allItemKeys = Array.from(new Set(invoice.items.flatMap(item => Object.keys(item)))); console.log('buildPdf: Item keys:', allItemKeys); const itemHeaderRow = allItemKeys.map(key => labels[key] || key); const itemRows = invoice.items.map(item => allItemKeys.map(k => { var _a; return (_a = item[k]) !== null && _a !== void 0 ? _a : ""; })); console.log('buildPdf: Item rows processed'); // Create document definition based on layout style console.log('buildPdf: Creating document definition...'); // Validate required data before creating document definition if (!((_c = invoice.from) === null || _c === void 0 ? void 0 : _c.name)) { throw new Error('Invoice from.name is required'); } if (!((_d = invoice.to) === null || _d === void 0 ? void 0 : _d.name)) { throw new Error('Invoice to.name is required'); } if (!invoice.items || invoice.items.length === 0) { throw new Error('Invoice must have at least one item'); } if (!invoice.invoiceNumber) { throw new Error('Invoice number is required'); } if (!invoice.invoiceDate) { throw new Error('Invoice date is required'); } const docDefinition = createDocumentDefinition(invoice, options, totals, labels, brandColor, logoImg, allItemKeys, itemHeaderRow, itemRows, layoutStyle); console.log('buildPdf: Document definition created, calling pdfMake...'); console.log('buildPdf: Document definition structure:', { hasContent: !!docDefinition.content, contentLength: (_e = docDefinition.content) === null || _e === void 0 ? void 0 : _e.length, hasStyles: !!docDefinition.styles, hasDefaultStyle: !!docDefinition.defaultStyle }); return new Promise((resolve, reject) => { // Add timeout for PDF generation const timeout = setTimeout(() => { reject(new Error('PDF generation timeout - document definition may be invalid')); }, 15000); try { const pdfMakeInstance = pdfMake.default || pdfMake; console.log('buildPdf: pdfMake instance obtained, creating PDF...'); const pdfDocGenerator = pdfMakeInstance.createPdf(docDefinition); // Add error handling for PDF generation pdfDocGenerator.getBlob((blob) => { var _a, _b; clearTimeout(timeout); console.log('buildPdf: PDF blob created successfully, size:', blob.size); (_b = (_a = options.hooks) === null || _a === void 0 ? void 0 : _a.afterRender) === null || _b === void 0 ? void 0 : _b.call(_a, blob); resolve(blob); }, (error) => { clearTimeout(timeout); console.error('buildPdf: Error in getBlob:', error); reject(new Error(`PDF generation failed: ${error.message || error}`)); }); } catch (error) { clearTimeout(timeout); console.error('buildPdf: Error in PDF creation:', error); reject(error); } }); } function createDocumentDefinition(invoice, options, totals, labels, brandColor, logoImg, allItemKeys, itemHeaderRow, itemRows, layoutStyle) { // Modern header section with logo and company info const headerSection = { columns: [ { width: '*', stack: [ { text: invoice.from.name, style: "companyName", color: brandColor }, invoice.from.address && { text: invoice.from.address, style: "companyInfo" }, invoice.from.email && { text: invoice.from.email, style: "companyInfo" }, invoice.from.phone && { text: invoice.from.phone, style: "companyInfo" } ].filter(Boolean) }, logoImg && { width: 120, image: logoImg, alignment: "right", margin: [0, 0, 0, 0] } ].filter(Boolean), margin: [0, 0, 0, 30] }; // Modern invoice details section const invoiceDetailsSection = { columns: [ { width: '*', stack: [ { text: labels.invoice.toUpperCase(), style: "invoiceTitle", color: brandColor }, { text: `${labels.invoice} #: ${invoice.invoiceNumber}`, style: "invoiceDetail" }, { text: `${labels.date}: ${invoice.invoiceDate}`, style: "invoiceDetail" }, invoice.dueDate && { text: `${labels.dueDate}: ${invoice.dueDate}`, style: "invoiceDetail" } ].filter(Boolean) }, { width: 200, stack: [ { text: labels.to.toUpperCase(), style: "sectionTitle", color: brandColor }, { text: invoice.to.name, style: "clientName" }, invoice.to.address && { text: invoice.to.address, style: "clientInfo" }, invoice.to.email && { text: invoice.to.email, style: "clientInfo" }, invoice.to.phone && { text: invoice.to.phone, style: "clientInfo" } ].filter(Boolean) } ], margin: [0, 0, 0, 30] }; // Extra sections with modern styling const extraSectionsContent = options.extraSections && options.extraSections.length > 0 ? { stack: options.extraSections.map(sec => ({ text: `${sec.title}: ${sec.content}`, style: "extraSection", margin: [0, 5, 0, 5] })), margin: [0, 0, 0, 20] } : null; // Modern items table const itemsTable = { table: { headerRows: 1, widths: Array(allItemKeys.length).fill("*"), body: [ itemHeaderRow.map(header => ({ text: header, style: "tableHeader" })), ...itemRows.map(row => row.map(cell => ({ text: cell || '', style: "tableCell" }))) ] }, layout: getTableLayout(layoutStyle), margin: [0, 0, 0, 30] }; // Modern totals section const totalsSection = { columns: [ { width: '*', text: '' }, { width: 200, stack: [ { text: `${labels.subtotal}: ${invoice.currency || "USD"} ${totals.subtotal.toFixed(2)}`, style: "totalLine" }, invoice.discount && { text: `${labels.discount}: ${invoice.currency || "USD"} ${invoice.discount.toFixed(2)}`, style: "totalLine" }, totals.totalTax > 0 && { text: `${labels.tax}: ${invoice.currency || "USD"} ${totals.totalTax.toFixed(2)}`, style: "totalLine" }, { text: `${labels.total}: ${invoice.currency || "USD"} ${totals.total.toFixed(2)}`, style: "grandTotal", color: brandColor, margin: [0, 10, 0, 0] } ].filter(Boolean) } ] }; // Notes and terms with modern styling const notesSection = options.notes ? { text: `${labels.notes}: ${options.notes}`, style: "notes", margin: [0, 20, 0, 0] } : null; const termsSection = invoice.terms ? { text: `${labels.terms}: ${invoice.terms}`, style: "terms", margin: [0, 10, 0, 0] } : null; // Build content array, filtering out null/undefined values const content = []; if (headerSection) content.push(headerSection); if (invoiceDetailsSection) content.push(invoiceDetailsSection); if (extraSectionsContent) content.push(extraSectionsContent); if (itemsTable) content.push(itemsTable); if (totalsSection) content.push(totalsSection); if (notesSection) content.push(notesSection); if (termsSection) content.push(termsSection); return { content: content, styles: getStyles(layoutStyle, brandColor), defaultStyle: getDefaultStyle(layoutStyle), pageMargins: getPageMargins(layoutStyle) }; } function getTableLayout(layoutStyle) { const modernLayout = { fillColor: function (rowIndex) { // Header row with brand color if (rowIndex === 0) { return '#3b82f6'; // Modern blue for headers } // Alternating row colors for better readability return (rowIndex % 2 === 0) ? null : '#f8fafc'; }, hLineWidth: function (i) { return (i === 0 || i === 1) ? 0 : 0.5; }, vLineWidth: function () { return 0; }, hLineColor: function () { return '#e2e8f0'; }, paddingLeft: function () { return 8; }, paddingRight: function () { return 8; }, paddingTop: function () { return 8; }, paddingBottom: function () { return 8; } }; switch (layoutStyle) { case 'modern': return modernLayout; case 'minimal': return { fillColor: function (rowIndex) { return rowIndex === 0 ? '#f9fafb' : null; }, hLineWidth: function (i, node) { return (i === 0 || i === 1 || i === node.table.body.length) ? 1 : 0; }, vLineWidth: function () { return 0; }, hLineColor: function () { return '#e5e7eb'; }, paddingLeft: function () { return 0; }, paddingRight: function () { return 0; }, paddingTop: function () { return 8; }, paddingBottom: function () { return 8; } }; case 'creative': return { fillColor: function (rowIndex) { if (rowIndex === 0) return '#8b5cf6'; return (rowIndex % 2 === 0) ? null : '#faf5ff'; }, hLineWidth: function (i) { return (i === 0 || i === 1) ? 0 : 0.5; }, vLineWidth: function () { return 0; }, hLineColor: function () { return '#e5e7eb'; }, paddingLeft: function () { return 8; }, paddingRight: function () { return 8; }, paddingTop: function () { return 8; }, paddingBottom: function () { return 8; } }; default: return modernLayout; } } function getStyles(layoutStyle, brandColor) { const modernStyles = { // Company header styles companyName: { fontSize: 24, bold: true, margin: [0, 0, 0, 8] }, companyInfo: { fontSize: 11, margin: [0, 2, 0, 0], color: '#64748b' }, // Invoice title and details invoiceTitle: { fontSize: 28, bold: true, margin: [0, 0, 0, 10] }, invoiceDetail: { fontSize: 12, margin: [0, 2, 0, 0] }, // Section titles sectionTitle: { fontSize: 12, bold: true, margin: [0, 0, 0, 8] }, clientName: { fontSize: 14, bold: true, margin: [0, 0, 0, 5] }, clientInfo: { fontSize: 11, margin: [0, 2, 0, 0], color: '#64748b' }, // Table styles tableHeader: { fontSize: 11, bold: true, margin: [8, 8, 8, 8], color: 'white', fillColor: brandColor }, tableCell: { fontSize: 11, margin: [8, 6, 8, 6] }, // Totals styles totalLine: { fontSize: 12, alignment: 'right', margin: [0, 3, 0, 3] }, grandTotal: { fontSize: 16, bold: true, alignment: 'right' }, // Notes and terms extraSection: { fontSize: 11, margin: [0, 2, 0, 2] }, notes: { fontSize: 11, color: '#64748b', margin: [0, 0, 0, 0] }, terms: { fontSize: 10, color: '#64748b', margin: [0, 0, 0, 0] } }; switch (layoutStyle) { case 'modern': return modernStyles; case 'minimal': return { ...modernStyles, companyName: { fontSize: 20, margin: [0, 0, 0, 5] }, invoiceTitle: { fontSize: 24, bold: true, margin: [0, 0, 0, 8] }, tableHeader: { fontSize: 10, bold: true, margin: [0, 8, 0, 8], letterSpacing: 0.5, color: 'white', fillColor: brandColor }, tableCell: { fontSize: 11, margin: [0, 6, 0, 6] } }; case 'creative': return { ...modernStyles, companyName: { fontSize: 22, bold: true, margin: [0, 0, 0, 8] }, invoiceTitle: { fontSize: 30, bold: true, margin: [0, 0, 0, 12] } }; default: return modernStyles; } } function getDefaultStyle(layoutStyle) { const modernDefault = { fontSize: 11, lineHeight: 1.4 // Remove font specification to use pdfMake's default fonts }; switch (layoutStyle) { case 'modern': return modernDefault; case 'minimal': return { ...modernDefault, fontSize: 10, lineHeight: 1.5 }; case 'creative': return { ...modernDefault, fontSize: 11, lineHeight: 1.3 }; default: return modernDefault; } } function getPageMargins(layoutStyle) { switch (layoutStyle) { case 'modern': return [40, 50, 40, 50]; // More balanced margins case 'minimal': return [60, 70, 60, 70]; // More whitespace case 'creative': return [35, 45, 35, 45]; // Tighter for creative layouts default: return [40, 50, 40, 50]; // Modern default } }