UNPKG

invoice-craft

Version:

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

627 lines (626 loc) 32.2 kB
import { calculateTotals } from "../core/calculations"; import { getLabels } from "../utils/localization"; import { fetchBase64Image } from "./assets"; // Global template registry const templateRegistry = {}; /** * Register a new template in the registry */ export function registerTemplate(template) { templateRegistry[template.id] = template; } /** * Get a template by ID */ export function getTemplateById(templateId) { return templateRegistry[templateId]; } /** * Get all available templates */ export function getAvailableTemplates() { return Object.values(templateRegistry); } /** * Get templates by category */ export function getTemplatesByCategory(category) { return Object.values(templateRegistry).filter(template => template.category === category); } /** * Default template - extracted from current pdfmake.ts implementation */ export const defaultTemplate = { id: 'default', name: 'Default Template', description: 'Classic business invoice template with clean layout', category: 'business', supportedFeatures: { logo: true, brandColor: true, rtl: true, extraSections: true }, render: async (invoice, options = {}) => { const totals = calculateTotals(invoice); const labels = getLabels(options.labels); const brandColor = options.brandColor || invoice.from.brandColor || "#1976d2"; let logoImg = options.logoUrl || invoice.from.logoUrl; if (logoImg && !logoImg.startsWith("data:")) { logoImg = await fetchBase64Image(logoImg); } const allItemKeys = Array.from(new Set(invoice.items.flatMap(item => Object.keys(item)))); 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 : ""; })); return { content: [ logoImg && { image: logoImg, width: 120, alignment: "right", margin: [0, 0, 0, 10] }, { text: invoice.from.name, color: brandColor, style: "header" }, invoice.from.address && { text: invoice.from.address }, invoice.from.email && { text: "Email: " + invoice.from.email }, invoice.from.phone && { text: "Phone: " + invoice.from.phone }, { text: " " }, { text: labels.invoice, style: "subheader", color: brandColor }, { text: `${labels.date}: ${invoice.invoiceDate}` }, invoice.dueDate && { text: `${labels.dueDate}: ${invoice.dueDate}` }, { text: `${labels.invoice} #: ${invoice.invoiceNumber}` }, options.extraSections && options.extraSections.map(sec => ({ text: `${sec.title}: ${sec.content}`, margin: [0, 5, 0, 0] })), { text: " " }, { text: labels.to, style: "subheader", color: brandColor }, { text: invoice.to.name }, invoice.to.address && { text: invoice.to.address }, invoice.to.email && { text: "Email: " + invoice.to.email }, invoice.to.phone && { text: "Phone: " + invoice.to.phone }, { text: " " }, { table: { headerRows: 1, widths: Array(allItemKeys.length).fill("*"), body: [itemHeaderRow, ...itemRows] }, layout: "lightHorizontalLines" }, { text: " " }, { text: `${labels.subtotal}: ${invoice.currency || "USD"} ${totals.subtotal.toFixed(2)}`, alignment: "right" }, invoice.discount && { text: `${labels.discount}: ${invoice.currency || "USD"} ${invoice.discount.toFixed(2)}`, alignment: "right" }, totals.totalTax > 0 && { text: `${labels.tax}: ${invoice.currency || "USD"} ${totals.totalTax.toFixed(2)}`, alignment: "right" }, { text: `${labels.total}: ${invoice.currency || "USD"} ${totals.total.toFixed(2)}`, alignment: "right", style: "subheader", color: brandColor }, options.notes && { text: `${labels.notes}: ${options.notes}`, margin: [0, 10, 0, 0] }, invoice.terms && { text: `${labels.terms}: ${invoice.terms}`, margin: [0, 5, 0, 0] } ].flat().filter(Boolean), styles: { header: { fontSize: 18, bold: true, margin: [0, 0, 0, 8] }, subheader: { fontSize: 14, margin: [0, 10, 0, 5] } }, defaultStyle: { fontSize: 12 }, pageMargins: [40, 60, 40, 60], pageOrientation: options.rtl ? "rtl" : "ltr" }; } }; /** * Modern template - clean, contemporary design with better spacing */ export const modernTemplate = { id: 'modern', name: 'Modern Template', description: 'Contemporary design with clean lines and modern typography', category: 'modern', supportedFeatures: { logo: true, brandColor: true, rtl: true, extraSections: true }, render: async (invoice, options = {}) => { const totals = calculateTotals(invoice); const labels = getLabels(options.labels); const brandColor = options.brandColor || invoice.from.brandColor || "#3b82f6"; let logoImg = options.logoUrl || invoice.from.logoUrl; if (logoImg && !logoImg.startsWith("data:")) { logoImg = await fetchBase64Image(logoImg); } const allItemKeys = Array.from(new Set(invoice.items.flatMap(item => Object.keys(item)))); const itemHeaderRow = allItemKeys.map(key => ({ text: labels[key] || key, style: 'tableHeader', fillColor: brandColor, color: 'white' })); const itemRows = invoice.items.map(item => allItemKeys.map(k => { var _a; return ({ text: (_a = item[k]) !== null && _a !== void 0 ? _a : "", style: 'tableCell' }); })); return { content: [ // Header section with logo and company info in columns { 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" } ] }, logoImg && { width: 120, image: logoImg, alignment: "right" } ].filter(Boolean), margin: [0, 0, 0, 30] }, // Invoice title and details { 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" } ] }, { width: '*', 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" } ] } ], margin: [0, 0, 0, 30] }, // Extra sections if provided 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] }, // Items table { table: { headerRows: 1, widths: Array(allItemKeys.length).fill("*"), body: [itemHeaderRow, ...itemRows] }, layout: { fillColor: function (rowIndex) { return (rowIndex % 2 === 0) ? null : '#f8fafc'; }, hLineWidth: function () { return 0.5; }, vLineWidth: function () { return 0; }, hLineColor: function () { return '#e2e8f0'; } }, margin: [0, 0, 0, 20] }, // Totals section { 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 options.notes && { text: `${labels.notes}: ${options.notes}`, style: "notes", margin: [0, 20, 0, 0] }, invoice.terms && { text: `${labels.terms}: ${invoice.terms}`, style: "terms", margin: [0, 10, 0, 0] } ].filter(Boolean), styles: { companyName: { fontSize: 24, bold: true, margin: [0, 0, 0, 5] }, companyInfo: { fontSize: 11, margin: [0, 2, 0, 0], color: '#64748b' }, invoiceTitle: { fontSize: 28, bold: true, margin: [0, 0, 0, 10] }, invoiceDetail: { fontSize: 12, margin: [0, 2, 0, 0] }, 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' }, tableHeader: { fontSize: 11, bold: true, margin: [8, 8, 8, 8] }, tableCell: { fontSize: 11, margin: [8, 6, 8, 6] }, totalLine: { fontSize: 12, alignment: 'right', margin: [0, 3, 0, 3] }, grandTotal: { fontSize: 16, bold: true, alignment: 'right' }, extraSection: { fontSize: 11, margin: [0, 2, 0, 2] }, notes: { fontSize: 11, color: '#64748b' }, terms: { fontSize: 10, color: '#64748b' } }, defaultStyle: { fontSize: 11, lineHeight: 1.3 }, pageMargins: [50, 60, 50, 60], pageOrientation: options.rtl ? "rtl" : "ltr" }; } }; /** * Minimal template - ultra-clean design with maximum white space */ export const minimalTemplate = { id: 'minimal', name: 'Minimal Template', description: 'Ultra-clean design with maximum white space and minimal styling', category: 'minimal', supportedFeatures: { logo: true, brandColor: true, rtl: false, // Minimal template works best with LTR extraSections: true }, render: async (invoice, options = {}) => { const totals = calculateTotals(invoice); const labels = getLabels(options.labels); const brandColor = options.brandColor || invoice.from.brandColor || "#000000"; let logoImg = options.logoUrl || invoice.from.logoUrl; if (logoImg && !logoImg.startsWith("data:")) { logoImg = await fetchBase64Image(logoImg); } const allItemKeys = Array.from(new Set(invoice.items.flatMap(item => Object.keys(item)))); const itemHeaderRow = allItemKeys.map(key => ({ text: (labels[key] || key).toUpperCase(), style: 'minimalTableHeader' })); const itemRows = invoice.items.map(item => allItemKeys.map(k => { var _a; return ({ text: (_a = item[k]) !== null && _a !== void 0 ? _a : "", style: 'minimalTableCell' }); })); return { content: [ // Minimal header logoImg && { image: logoImg, width: 80, alignment: "left", margin: [0, 0, 0, 40] }, { text: invoice.from.name, style: "minimalCompany" }, { text: " ", margin: [0, 0, 0, 30] }, // Invoice info in simple layout { text: `${labels.invoice.toUpperCase()} ${invoice.invoiceNumber}`, style: "minimalInvoiceNumber" }, { text: `${labels.date}: ${invoice.invoiceDate}`, style: "minimalDate" }, invoice.dueDate && { text: `${labels.dueDate}: ${invoice.dueDate}`, style: "minimalDate" }, { text: " ", margin: [0, 0, 0, 30] }, // Client info { text: labels.to.toUpperCase(), style: "minimalLabel" }, { text: invoice.to.name, style: "minimalClient" }, invoice.to.address && { text: invoice.to.address, style: "minimalClientInfo" }, invoice.to.email && { text: invoice.to.email, style: "minimalClientInfo" }, invoice.to.phone && { text: invoice.to.phone, style: "minimalClientInfo" }, { text: " ", margin: [0, 0, 0, 40] }, // Extra sections options.extraSections && options.extraSections.length > 0 && { stack: options.extraSections.map(sec => [ { text: sec.title.toUpperCase(), style: "minimalLabel" }, { text: sec.content, style: "minimalExtraContent", margin: [0, 5, 0, 20] } ]).flat(), margin: [0, 0, 0, 20] }, // Simple table { table: { headerRows: 1, widths: Array(allItemKeys.length).fill("*"), body: [itemHeaderRow, ...itemRows] }, layout: { hLineWidth: function (i, node) { return (i === 0 || i === 1 || i === node.table.body.length) ? 1 : 0; }, vLineWidth: function () { return 0; }, hLineColor: function () { return brandColor; }, paddingLeft: function () { return 0; }, paddingRight: function () { return 0; }, paddingTop: function () { return 8; }, paddingBottom: function () { return 8; } }, margin: [0, 0, 0, 40] }, // Minimal totals { text: `${labels.subtotal}: ${invoice.currency || "USD"} ${totals.subtotal.toFixed(2)}`, style: "minimalTotal" }, invoice.discount && { text: `${labels.discount}: ${invoice.currency || "USD"} ${invoice.discount.toFixed(2)}`, style: "minimalTotal" }, totals.totalTax > 0 && { text: `${labels.tax}: ${invoice.currency || "USD"} ${totals.totalTax.toFixed(2)}`, style: "minimalTotal" }, { text: `${labels.total}: ${invoice.currency || "USD"} ${totals.total.toFixed(2)}`, style: "minimalGrandTotal", margin: [0, 10, 0, 0] }, // Notes and terms with more spacing options.notes && [ { text: " ", margin: [0, 0, 0, 30] }, { text: labels.notes.toUpperCase(), style: "minimalLabel" }, { text: options.notes, style: "minimalNotes" } ], invoice.terms && [ { text: " ", margin: [0, 0, 0, 20] }, { text: labels.terms.toUpperCase(), style: "minimalLabel" }, { text: invoice.terms, style: "minimalTerms" } ] ].flat().filter(Boolean), styles: { minimalCompany: { fontSize: 16, margin: [0, 0, 0, 5] }, minimalInvoiceNumber: { fontSize: 20, bold: true, margin: [0, 0, 0, 10] }, minimalDate: { fontSize: 11, margin: [0, 3, 0, 0] }, minimalLabel: { fontSize: 9, bold: true, margin: [0, 0, 0, 5], letterSpacing: 1 }, minimalClient: { fontSize: 13, margin: [0, 0, 0, 5] }, minimalClientInfo: { fontSize: 11, margin: [0, 2, 0, 0] }, minimalTableHeader: { fontSize: 9, bold: true, margin: [0, 8, 0, 8], letterSpacing: 0.5 }, minimalTableCell: { fontSize: 11, margin: [0, 6, 0, 6] }, minimalTotal: { fontSize: 11, alignment: 'right', margin: [0, 3, 0, 3] }, minimalGrandTotal: { fontSize: 14, bold: true, alignment: 'right' }, minimalExtraContent: { fontSize: 11 }, minimalNotes: { fontSize: 10, margin: [0, 5, 0, 0] }, minimalTerms: { fontSize: 9, margin: [0, 5, 0, 0] } }, defaultStyle: { fontSize: 11, lineHeight: 1.4 }, pageMargins: [60, 80, 60, 80], pageOrientation: "portrait" }; } }; /** * Creative template - bold design with creative layout and colors */ export const creativeTemplate = { id: 'creative', name: 'Creative Template', description: 'Bold and creative design with unique layout and vibrant styling', category: 'creative', supportedFeatures: { logo: true, brandColor: true, rtl: true, extraSections: true }, render: async (invoice, options = {}) => { const totals = calculateTotals(invoice); const labels = getLabels(options.labels); const brandColor = options.brandColor || invoice.from.brandColor || "#8b5cf6"; const accentColor = "#f59e0b"; // Complementary accent color let logoImg = options.logoUrl || invoice.from.logoUrl; if (logoImg && !logoImg.startsWith("data:")) { logoImg = await fetchBase64Image(logoImg); } const allItemKeys = Array.from(new Set(invoice.items.flatMap(item => Object.keys(item)))); const itemHeaderRow = allItemKeys.map(key => ({ text: labels[key] || key, style: 'creativeTableHeader', fillColor: brandColor, color: 'white' })); const itemRows = invoice.items.map((item, index) => allItemKeys.map(k => { var _a; return ({ text: (_a = item[k]) !== null && _a !== void 0 ? _a : "", style: 'creativeTableCell', fillColor: index % 2 === 0 ? null : '#faf5ff' }); })); return { content: [ // Creative header with colored background { table: { widths: ['*'], body: [[{ stack: [ { columns: [ { width: '*', stack: [ { text: invoice.from.name, style: "creativeCompanyName" }, invoice.from.address && { text: invoice.from.address, style: "creativeCompanyInfo" }, invoice.from.email && { text: invoice.from.email, style: "creativeCompanyInfo" }, invoice.from.phone && { text: invoice.from.phone, style: "creativeCompanyInfo" } ] }, logoImg && { width: 100, image: logoImg, alignment: "right" } ].filter(Boolean) } ], fillColor: brandColor, color: 'white', margin: [20, 20, 20, 20] }]] }, layout: 'noBorders', margin: [0, 0, 0, 30] }, // Invoice title with creative styling { table: { widths: ['*', '*'], body: [[ { stack: [ { text: labels.invoice, style: "creativeInvoiceTitle" }, { text: `# ${invoice.invoiceNumber}`, style: "creativeInvoiceNumber", color: brandColor } ], border: [false, false, false, false] }, { stack: [ { text: `${labels.date}: ${invoice.invoiceDate}`, style: "creativeDate" }, invoice.dueDate && { text: `${labels.dueDate}: ${invoice.dueDate}`, style: "creativeDate" } ].filter(Boolean), border: [false, false, false, false], alignment: 'right' } ]] }, layout: 'noBorders', margin: [0, 0, 0, 20] }, // Client section with accent color { table: { widths: ['*'], body: [[{ stack: [ { text: labels.to, style: "creativeSectionTitle", color: accentColor }, { text: invoice.to.name, style: "creativeClientName" }, invoice.to.address && { text: invoice.to.address, style: "creativeClientInfo" }, invoice.to.email && { text: invoice.to.email, style: "creativeClientInfo" }, invoice.to.phone && { text: invoice.to.phone, style: "creativeClientInfo" } ].filter(Boolean), fillColor: '#fffbeb', margin: [15, 15, 15, 15] }]] }, layout: 'noBorders', margin: [0, 0, 0, 25] }, // Extra sections with creative styling options.extraSections && options.extraSections.length > 0 && { stack: options.extraSections.map(sec => ({ table: { widths: ['*'], body: [[{ stack: [ { text: sec.title, style: "creativeExtraTitle", color: brandColor }, { text: sec.content, style: "creativeExtraContent" } ], fillColor: '#f8fafc', margin: [15, 10, 15, 10] }]] }, layout: 'noBorders', margin: [0, 5, 0, 5] })), margin: [0, 0, 0, 20] }, // Creative items table { table: { headerRows: 1, widths: Array(allItemKeys.length).fill("*"), body: [itemHeaderRow, ...itemRows] }, layout: { hLineWidth: function (i) { return (i === 0 || i === 1) ? 2 : 0.5; }, vLineWidth: function () { return 0; }, hLineColor: function (i) { return (i === 0 || i === 1) ? brandColor : '#e5e7eb'; } }, margin: [0, 0, 0, 25] }, // Creative totals section { table: { widths: ['*', 200], body: [[ { text: '', border: [false, false, false, false] }, { stack: [ { text: `${labels.subtotal}: ${invoice.currency || "USD"} ${totals.subtotal.toFixed(2)}`, style: "creativeTotalLine" }, invoice.discount && { text: `${labels.discount}: ${invoice.currency || "USD"} ${invoice.discount.toFixed(2)}`, style: "creativeTotalLine" }, totals.totalTax > 0 && { text: `${labels.tax}: ${invoice.currency || "USD"} ${totals.totalTax.toFixed(2)}`, style: "creativeTotalLine" }, { table: { widths: ['*'], body: [[{ text: `${labels.total}: ${invoice.currency || "USD"} ${totals.total.toFixed(2)}`, style: "creativeGrandTotal", fillColor: accentColor, color: 'white', margin: [10, 10, 10, 10] }]] }, layout: 'noBorders', margin: [0, 10, 0, 0] } ].filter(Boolean), border: [false, false, false, false] } ]] }, layout: 'noBorders' }, // Notes and terms with creative styling options.notes && { table: { widths: ['*'], body: [[{ stack: [ { text: labels.notes, style: "creativeNotesTitle", color: brandColor }, { text: options.notes, style: "creativeNotesContent" } ], fillColor: '#f1f5f9', margin: [15, 15, 15, 15] }]] }, layout: 'noBorders', margin: [0, 20, 0, 0] }, invoice.terms && { table: { widths: ['*'], body: [[{ stack: [ { text: labels.terms, style: "creativeTermsTitle", color: accentColor }, { text: invoice.terms, style: "creativeTermsContent" } ], fillColor: '#fffbeb', margin: [15, 10, 15, 10] }]] }, layout: 'noBorders', margin: [0, 10, 0, 0] } ].filter(Boolean), styles: { creativeCompanyName: { fontSize: 20, bold: true, margin: [0, 0, 0, 8] }, creativeCompanyInfo: { fontSize: 11, margin: [0, 2, 0, 0] }, creativeInvoiceTitle: { fontSize: 32, bold: true }, creativeInvoiceNumber: { fontSize: 24, bold: true, margin: [0, 5, 0, 0] }, creativeDate: { fontSize: 12, margin: [0, 2, 0, 0] }, creativeSectionTitle: { fontSize: 14, bold: true, margin: [0, 0, 0, 8] }, creativeClientName: { fontSize: 16, bold: true, margin: [0, 0, 0, 5] }, creativeClientInfo: { fontSize: 11, margin: [0, 2, 0, 0] }, creativeTableHeader: { fontSize: 12, bold: true, margin: [10, 10, 10, 10] }, creativeTableCell: { fontSize: 11, margin: [10, 8, 10, 8] }, creativeTotalLine: { fontSize: 12, alignment: 'right', margin: [0, 4, 0, 4] }, creativeGrandTotal: { fontSize: 16, bold: true, alignment: 'center' }, creativeExtraTitle: { fontSize: 12, bold: true, margin: [0, 0, 0, 5] }, creativeExtraContent: { fontSize: 11 }, creativeNotesTitle: { fontSize: 12, bold: true, margin: [0, 0, 0, 8] }, creativeNotesContent: { fontSize: 11 }, creativeTermsTitle: { fontSize: 12, bold: true, margin: [0, 0, 0, 8] }, creativeTermsContent: { fontSize: 10 } }, defaultStyle: { fontSize: 11, lineHeight: 1.3 }, pageMargins: [40, 50, 40, 50], pageOrientation: options.rtl ? "rtl" : "ltr" }; } }; // Initialize templates registry with built-in templates registerTemplate(defaultTemplate); registerTemplate(modernTemplate); registerTemplate(minimalTemplate); registerTemplate(creativeTemplate);