invoice-craft
Version:
Customizable, browser-first invoice PDF generator library with modern TypeScript API
363 lines (362 loc) • 15.9 kB
JavaScript
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
}
}