invoice-craft
Version:
Customizable, browser-first invoice PDF generator library with modern TypeScript API
627 lines (626 loc) • 32.2 kB
JavaScript
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);