diff --git a/index.html b/index.html
index 3249b3d..4ff7c5a 100644
--- a/index.html
+++ b/index.html
@@ -201,6 +201,12 @@
Import / Export
+
diff --git a/src/js/main.js b/src/js/main.js
index 2b31866..21acda3 100644
--- a/src/js/main.js
+++ b/src/js/main.js
@@ -126,6 +126,13 @@ function attachEventHandlers() {
syncFromForm({ render: shouldRender });
});
+ // Explicitly handle template change for immediate feedback
+ document.getElementById('templateId').addEventListener('change', () => {
+ if (isInvoiceVisible()) {
+ syncFromForm({ render: true });
+ }
+ });
+
document.getElementById('addItemBtn').addEventListener('click', () => {
addItemRow({ description: '', quantity: 1, unitPrice: 0 });
syncFromForm({ render: isInvoiceVisible() });
diff --git a/src/js/modules/form.js b/src/js/modules/form.js
index 59bde9e..34e6180 100644
--- a/src/js/modules/form.js
+++ b/src/js/modules/form.js
@@ -17,7 +17,8 @@ import { renderInvoice, isInvoiceVisible } from './invoice.js';
export function getDataFromForm() {
const settings = {
currency: getValue('currencyCode', 'INR').trim() || 'INR',
- locale: 'en-IN'
+ locale: 'en-IN',
+ templateId: getValue('templateId', 'default')
};
const items = collectItemsFromForm(settings);
@@ -121,6 +122,7 @@ export function applyDataToForm(data, syncCallback) {
setValue('invoiceNumber', data.invoice?.number ?? '');
setValue('currencyCode', data.settings?.currency ?? 'INR');
+ setValue('templateId', data.settings?.templateId ?? 'default');
renderItemsForm(data.items || []);
diff --git a/src/js/modules/invoice.js b/src/js/modules/invoice.js
index a042b8a..6a01ae1 100644
--- a/src/js/modules/invoice.js
+++ b/src/js/modules/invoice.js
@@ -21,6 +21,13 @@ export function isInvoiceVisible() {
*/
export function renderInvoice(data) {
const { settings, seller, billTo, invoice, items, totals } = data;
+ const templateId = settings.templateId || 'default';
+
+ const invoiceEl = document.getElementById('invoice');
+ // Remove existing template classes
+ invoiceEl.className = invoiceEl.className.replace(/invoice-template-[^\s]+/, '').trim();
+ // Add new template class
+ invoiceEl.classList.add(`invoice-template-${templateId}`);
setText('displaySellerName', seller.name);
setText('displaySellerAddress', seller.address || '');
diff --git a/src/js/modules/pdf.js b/src/js/modules/pdf.js
index c4430d6..e86ea5d 100644
--- a/src/js/modules/pdf.js
+++ b/src/js/modules/pdf.js
@@ -3,8 +3,8 @@
* PDF generation and sharing functionality using pdfmake
*/
+import { getTemplate } from './templates/template-registry.js';
import { getDataFromForm } from './form.js';
-import { formatCurrency, formatDate } from './formatters.js';
/**
* Generate PDF Document Definition from data
@@ -12,234 +12,9 @@ import { formatCurrency, formatDate } from './formatters.js';
* @returns {Object} pdfmake document definition
*/
function generateDocumentDefinition(data) {
- const { seller, billTo, invoice, items, totals, settings } = data;
- const currency = settings.currency;
- const locale = settings.locale;
-
- // Helper to format currency
- const fmt = (amount) => formatCurrency(amount, currency, locale);
-
- // Prepare table body
- const tableBody = [];
-
- // Header
- tableBody.push([
- { text: 'DESCRIPTION', style: 'tableHeader' },
- { text: 'QTY', style: 'tableHeader', alignment: 'center' },
- { text: 'UNIT PRICE', style: 'tableHeader', alignment: 'right' },
- { text: 'TOTAL', style: 'tableHeader', alignment: 'right' }
- ]);
-
- // Items
- items.forEach(item => {
- tableBody.push([
- { text: item.description || '', style: 'tableCell' },
- { text: item.quantity, style: 'tableCell', alignment: 'center' },
- { text: fmt(item.unitPrice), style: 'tableCell', alignment: 'right' },
- { text: fmt(item.total), style: 'tableCell', alignment: 'right' }
- ]);
- });
-
- // Totals section (rows)
- const totalsRows = [];
-
- // Only show Subtotal if there are adjustments (tax or discount)
- if (totals.taxAmount > 0 || totals.discount > 0) {
- totalsRows.push([
- { text: 'Subtotal', alignment: 'right' },
- { text: fmt(totals.subtotal), alignment: 'right' }
- ]);
-
- if (totals.taxAmount > 0) {
- totalsRows.push([
- { text: `Tax (${totals.taxRate}%)`, alignment: 'right' },
- { text: fmt(totals.taxAmount), alignment: 'right' }
- ]);
- }
-
- if (totals.discount > 0) {
- totalsRows.push([
- { text: 'Discount', alignment: 'right' },
- { text: `-${fmt(totals.discount)}`, alignment: 'right' }
- ]);
- }
- }
-
- // Total Row
- totalsRows.push([
- { text: 'Total', bold: true, fontSize: 12, alignment: 'right' },
- { text: fmt(totals.total), bold: true, fontSize: 12, alignment: 'right' }
- ]);
-
- const invoiceNumber = invoice.number || 'draft';
- const customerName = billTo.name || 'Customer';
-
- return {
- // Moderate margins: [left, top, right, bottom]
- pageMargins: [30, 40, 30, 40],
-
- info: {
- title: `Invoice ${invoiceNumber}`,
- author: seller.name || 'Rebill',
- subject: `Invoice for ${customerName}`,
- creator: 'Rebill - Invoice Generator'
- },
- content: [
- // Header Section
- {
- columns: [
- // Seller Info (Left)
- {
- width: '*',
- stack: [
- { text: seller.name || 'Business Name', style: 'businessName' },
- { text: seller.address || '', style: 'normalText' },
- seller.email ? { text: seller.email, style: 'normalText' } : null,
- seller.phone ? { text: seller.phone, style: 'normalText' } : null
- ].filter(Boolean)
- },
- // Invoice Title & Meta (Right)
- {
- width: 'auto',
- alignment: 'right',
- stack: [
- { text: invoice.title || 'INVOICE', style: 'invoiceTitle' },
- {
- text: [
- { text: 'Invoice #: ', bold: true },
- invoiceNumber
- ],
- style: 'metaText'
- },
- {
- text: [
- { text: 'Date: ', bold: true },
- formatDate(invoice.date)
- ],
- style: 'metaText'
- },
- invoice.dueDate ? {
- text: [
- { text: 'Due Date: ', bold: true },
- formatDate(invoice.dueDate)
- ],
- style: 'metaText'
- } : null
- ].filter(Boolean)
- }
- ],
- columnGap: 20,
- marginBottom: 40
- },
-
- // Bill To Section
- {
- text: 'BILL TO',
- style: 'sectionLabel',
- marginBottom: 5
- },
- {
- stack: [
- { text: customerName, style: 'customerName' },
- { text: billTo.address || '', style: 'normalText' },
- billTo.email ? { text: billTo.email, style: 'normalText' } : null,
- billTo.phone ? { text: billTo.phone, style: 'normalText' } : null
- ].filter(Boolean),
- marginBottom: 40
- },
-
- // Items Table
- {
- table: {
- headerRows: 1,
- widths: ['*', 50, 80, 80],
- body: tableBody
- },
- layout: {
- hLineWidth: (i, node) => 0.5,
- vLineWidth: (i, node) => 0.5,
- hLineColor: (i, node) => '#E5E7EB',
- vLineColor: (i, node) => '#E5E7EB',
- paddingLeft: (i) => 8,
- paddingRight: (i) => 8,
- paddingTop: (i) => 4,
- paddingBottom: (i) => 4
- },
- marginBottom: 20
- },
-
- // Totals
- {
- columns: [
- { width: '*', text: '' }, // Spacer
- {
- width: 'auto',
- table: {
- widths: ['auto', 100],
- body: totalsRows
- },
- layout: {
- // Add a top border line only above the last row (Total)
- hLineWidth: (i, node) => {
- const lastRowIndex = node.table.body.length;
- return i === lastRowIndex - 1 ? 0.5 : 0;
- },
- vLineWidth: () => 0,
- hLineColor: () => '#9CA3AF',
- paddingLeft: () => 8,
- paddingRight: () => 0,
- paddingTop: (i, node) => {
- const lastRowIndex = node.table.body.length;
- return i === lastRowIndex - 1 ? 6 : 2;
- },
- paddingBottom: () => 2
- }
- }
- ],
- marginBottom: 40
- },
-
- // Footer Section: Center aligned, standard font size, bold "Thank you"
- {
- stack: [
- // Notes (Thank you message)
- invoice.notes ? {
- text: invoice.notes,
- style: 'footerNote',
- alignment: 'center',
- marginBottom: 10
- } : null,
-
- // Instructions (Bank details etc)
- invoice.instructions ? {
- text: invoice.instructions,
- style: 'footerText',
- alignment: 'center'
- } : null
- ].filter(Boolean)
- }
- ].filter(Boolean),
-
- styles: {
- businessName: { fontSize: 18, bold: true, marginBottom: 4, color: '#111827' },
- invoiceTitle: { fontSize: 24, bold: true, marginBottom: 8, color: '#111827' },
- sectionLabel: { fontSize: 10, bold: true, color: '#6B7280', letterSpacing: 1, marginBottom: 4 },
- customerName: { fontSize: 14, bold: true, marginBottom: 4, color: '#111827' },
- normalText: { fontSize: 10, lineHeight: 1.4, color: '#4B5563' },
- metaText: { fontSize: 10, marginBottom: 4, alignment: 'right', color: '#4B5563' },
-
- // Table styles
- tableHeader: { fontSize: 10, bold: true, color: '#111827', fillColor: '#F3F4F6', margin: [0, 2, 0, 2] },
- tableCell: { fontSize: 10, color: '#111827', margin: [0, 2, 0, 2] },
-
- // Footer styles
- footerNote: { fontSize: 14, bold: true, color: '#111827' },
- footerText: { fontSize: 10, color: '#111827', lineHeight: 1.5 }
- },
- defaultStyle: {
- font: 'Roboto'
- }
- };
+ const templateId = data.settings?.templateId || 'default';
+ const template = getTemplate(templateId);
+ return template.generateDocumentDefinition(data);
}
/**
diff --git a/src/js/modules/templates/default.js b/src/js/modules/templates/default.js
new file mode 100644
index 0000000..278acc3
--- /dev/null
+++ b/src/js/modules/templates/default.js
@@ -0,0 +1,247 @@
+/**
+ * Default Template Module
+ * Standard invoice layout
+ */
+
+import { formatCurrency, formatDate } from '../formatters.js';
+
+export default {
+ id: 'default',
+ name: 'Default Standard',
+
+ /**
+ * Generate PDF Document Definition
+ * @param {Object} data - Invoice data
+ * @returns {Object} pdfmake document definition
+ */
+ generateDocumentDefinition(data) {
+ const { seller, billTo, invoice, items, totals, settings } = data;
+ const currency = settings.currency;
+ const locale = settings.locale;
+
+ // Helper to format currency
+ const fmt = (amount) => formatCurrency(amount, currency, locale);
+
+ // Prepare table body
+ const tableBody = [];
+
+ // Header
+ tableBody.push([
+ { text: 'DESCRIPTION', style: 'tableHeader' },
+ { text: 'QTY', style: 'tableHeader', alignment: 'center' },
+ { text: 'UNIT PRICE', style: 'tableHeader', alignment: 'right' },
+ { text: 'TOTAL', style: 'tableHeader', alignment: 'right' }
+ ]);
+
+ // Items
+ items.forEach(item => {
+ tableBody.push([
+ { text: item.description || '', style: 'tableCell' },
+ { text: item.quantity, style: 'tableCell', alignment: 'center' },
+ { text: fmt(item.unitPrice), style: 'tableCell', alignment: 'right' },
+ { text: fmt(item.total), style: 'tableCell', alignment: 'right' }
+ ]);
+ });
+
+ // Totals section (rows)
+ const totalsRows = [];
+
+ // Only show Subtotal if there are adjustments (tax or discount)
+ if (totals.taxAmount > 0 || totals.discount > 0) {
+ totalsRows.push([
+ { text: 'Subtotal', alignment: 'right' },
+ { text: fmt(totals.subtotal), alignment: 'right' }
+ ]);
+
+ if (totals.taxAmount > 0) {
+ totalsRows.push([
+ { text: `Tax (${totals.taxRate}%)`, alignment: 'right' },
+ { text: fmt(totals.taxAmount), alignment: 'right' }
+ ]);
+ }
+
+ if (totals.discount > 0) {
+ totalsRows.push([
+ { text: 'Discount', alignment: 'right' },
+ { text: `-${fmt(totals.discount)}`, alignment: 'right' }
+ ]);
+ }
+ }
+
+ // Total Row
+ totalsRows.push([
+ { text: 'Total', bold: true, fontSize: 12, alignment: 'right' },
+ { text: fmt(totals.total), bold: true, fontSize: 12, alignment: 'right' }
+ ]);
+
+ const invoiceNumber = invoice.number || 'draft';
+ const customerName = billTo.name || 'Customer';
+
+ return {
+ // Moderate margins: [left, top, right, bottom]
+ pageMargins: [30, 40, 30, 40],
+
+ info: {
+ title: `Invoice ${invoiceNumber}`,
+ author: seller.name || 'Rebill',
+ subject: `Invoice for ${customerName}`,
+ creator: 'Rebill - Invoice Generator'
+ },
+ content: [
+ // Header Section
+ {
+ columns: [
+ // Seller Info (Left)
+ {
+ width: '*',
+ stack: [
+ { text: seller.name || 'Business Name', style: 'businessName' },
+ { text: seller.address || '', style: 'normalText' },
+ seller.email ? { text: seller.email, style: 'normalText' } : null,
+ seller.phone ? { text: seller.phone, style: 'normalText' } : null
+ ].filter(Boolean)
+ },
+ // Invoice Title & Meta (Right)
+ {
+ width: 'auto',
+ alignment: 'right',
+ stack: [
+ { text: invoice.title || 'INVOICE', style: 'invoiceTitle' },
+ {
+ text: [
+ { text: 'Invoice #: ', bold: true },
+ invoiceNumber
+ ],
+ style: 'metaText'
+ },
+ {
+ text: [
+ { text: 'Date: ', bold: true },
+ formatDate(invoice.date)
+ ],
+ style: 'metaText'
+ },
+ invoice.dueDate ? {
+ text: [
+ { text: 'Due Date: ', bold: true },
+ formatDate(invoice.dueDate)
+ ],
+ style: 'metaText'
+ } : null
+ ].filter(Boolean)
+ }
+ ],
+ columnGap: 20,
+ marginBottom: 40
+ },
+
+ // Bill To Section
+ {
+ text: 'BILL TO',
+ style: 'sectionLabel',
+ marginBottom: 5
+ },
+ {
+ stack: [
+ { text: customerName, style: 'customerName' },
+ { text: billTo.address || '', style: 'normalText' },
+ billTo.email ? { text: billTo.email, style: 'normalText' } : null,
+ billTo.phone ? { text: billTo.phone, style: 'normalText' } : null
+ ].filter(Boolean),
+ marginBottom: 40
+ },
+
+ // Items Table
+ {
+ table: {
+ headerRows: 1,
+ widths: ['*', 50, 80, 80],
+ body: tableBody
+ },
+ layout: {
+ hLineWidth: (i, node) => 0.5,
+ vLineWidth: (i, node) => 0.5,
+ hLineColor: (i, node) => '#E5E7EB',
+ vLineColor: (i, node) => '#E5E7EB',
+ paddingLeft: (i) => 8,
+ paddingRight: (i) => 8,
+ paddingTop: (i) => 4,
+ paddingBottom: (i) => 4
+ },
+ marginBottom: 20
+ },
+
+ // Totals
+ {
+ columns: [
+ { width: '*', text: '' }, // Spacer
+ {
+ width: 'auto',
+ table: {
+ widths: ['auto', 100],
+ body: totalsRows
+ },
+ layout: {
+ // Add a top border line only above the last row (Total)
+ hLineWidth: (i, node) => {
+ const lastRowIndex = node.table.body.length;
+ return i === lastRowIndex - 1 ? 0.5 : 0;
+ },
+ vLineWidth: () => 0,
+ hLineColor: () => '#9CA3AF',
+ paddingLeft: () => 8,
+ paddingRight: () => 0,
+ paddingTop: (i, node) => {
+ const lastRowIndex = node.table.body.length;
+ return i === lastRowIndex - 1 ? 6 : 2;
+ },
+ paddingBottom: () => 2
+ }
+ }
+ ],
+ marginBottom: 40
+ },
+
+ // Footer Section
+ {
+ stack: [
+ // Notes
+ invoice.notes ? {
+ text: invoice.notes,
+ style: 'footerNote',
+ alignment: 'center',
+ marginBottom: 10
+ } : null,
+
+ // Instructions
+ invoice.instructions ? {
+ text: invoice.instructions,
+ style: 'footerText',
+ alignment: 'center'
+ } : null
+ ].filter(Boolean)
+ }
+ ].filter(Boolean),
+
+ styles: {
+ businessName: { fontSize: 18, bold: true, marginBottom: 4, color: '#111827' },
+ invoiceTitle: { fontSize: 24, bold: true, marginBottom: 8, color: '#111827' },
+ sectionLabel: { fontSize: 10, bold: true, color: '#6B7280', letterSpacing: 1, marginBottom: 4 },
+ customerName: { fontSize: 14, bold: true, marginBottom: 4, color: '#111827' },
+ normalText: { fontSize: 10, lineHeight: 1.4, color: '#4B5563' },
+ metaText: { fontSize: 10, marginBottom: 4, alignment: 'right', color: '#4B5563' },
+
+ // Table styles
+ tableHeader: { fontSize: 10, bold: true, color: '#111827', fillColor: '#F3F4F6', margin: [0, 2, 0, 2] },
+ tableCell: { fontSize: 10, color: '#111827', margin: [0, 2, 0, 2] },
+
+ // Footer styles
+ footerNote: { fontSize: 14, bold: true, color: '#111827' },
+ footerText: { fontSize: 10, color: '#111827', lineHeight: 1.5 }
+ },
+ defaultStyle: {
+ font: 'Roboto'
+ }
+ };
+ }
+};
diff --git a/src/js/modules/templates/minimal.js b/src/js/modules/templates/minimal.js
new file mode 100644
index 0000000..9cceff4
--- /dev/null
+++ b/src/js/modules/templates/minimal.js
@@ -0,0 +1,233 @@
+/**
+ * Minimalist Template Module
+ * Simple, clean layout with less emphasis on borders/boxes
+ */
+
+import { formatCurrency, formatDate } from '../formatters.js';
+
+export default {
+ id: 'minimal',
+ name: 'Minimalist',
+
+ /**
+ * Generate PDF Document Definition
+ * @param {Object} data - Invoice data
+ * @returns {Object} pdfmake document definition
+ */
+ generateDocumentDefinition(data) {
+ const { seller, billTo, invoice, items, totals, settings } = data;
+ const currency = settings.currency;
+ const locale = settings.locale;
+
+ // Helper to format currency
+ const fmt = (amount) => formatCurrency(amount, currency, locale);
+
+ // Prepare table body
+ const tableBody = [];
+
+ // Header
+ tableBody.push([
+ { text: 'Description', style: 'tableHeader', alignment: 'left' },
+ { text: 'Quantity', style: 'tableHeader', alignment: 'center' },
+ { text: 'Price', style: 'tableHeader', alignment: 'right' },
+ { text: 'Total', style: 'tableHeader', alignment: 'right' }
+ ]);
+
+ // Items
+ items.forEach(item => {
+ tableBody.push([
+ { text: item.description || '', style: 'tableCell' },
+ { text: item.quantity, style: 'tableCell', alignment: 'center' },
+ { text: fmt(item.unitPrice), style: 'tableCell', alignment: 'right' },
+ { text: fmt(item.total), style: 'tableCell', alignment: 'right' }
+ ]);
+ });
+
+ // Totals section (rows)
+ const totalsRows = [];
+
+ // Subtotal
+ if (totals.taxAmount > 0 || totals.discount > 0) {
+ totalsRows.push([
+ { text: 'Subtotal', alignment: 'right', color: '#666666' },
+ { text: fmt(totals.subtotal), alignment: 'right', color: '#666666' }
+ ]);
+ }
+
+ // Tax
+ if (totals.taxAmount > 0) {
+ totalsRows.push([
+ { text: `Tax (${totals.taxRate}%)`, alignment: 'right', color: '#666666' },
+ { text: fmt(totals.taxAmount), alignment: 'right', color: '#666666' }
+ ]);
+ }
+
+ // Discount
+ if (totals.discount > 0) {
+ totalsRows.push([
+ { text: 'Discount', alignment: 'right', color: '#666666' },
+ { text: `-${fmt(totals.discount)}`, alignment: 'right', color: '#666666' }
+ ]);
+ }
+
+ // Total
+ totalsRows.push([
+ { text: 'Total', bold: true, fontSize: 16, alignment: 'right', margin: [0, 5, 0, 0] },
+ { text: fmt(totals.total), bold: true, fontSize: 16, alignment: 'right', margin: [0, 5, 0, 0] }
+ ]);
+
+ const invoiceNumber = invoice.number || 'draft';
+
+ return {
+ pageMargins: [40, 60, 40, 60],
+
+ info: {
+ title: `Invoice ${invoiceNumber}`,
+ author: seller.name || 'Rebill',
+ creator: 'Rebill'
+ },
+
+ content: [
+ // Top Header: Brand Left, Invoice Info Right
+ {
+ columns: [
+ {
+ width: '*',
+ text: seller.name || 'Business Name',
+ style: 'headerBusinessName'
+ },
+ {
+ width: 'auto',
+ stack: [
+ { text: 'INVOICE', style: 'headerLabel' },
+ { text: invoiceNumber, style: 'headerValue' }
+ ],
+ alignment: 'right'
+ }
+ ],
+ marginBottom: 40
+ },
+
+ // Addresses: Side by Side
+ {
+ columns: [
+ // From
+ {
+ width: '*',
+ stack: [
+ { text: 'FROM', style: 'sectionLabel' },
+ { text: seller.name || '', bold: true, marginBottom: 2 },
+ { text: seller.address || '', style: 'addressText' },
+ seller.email ? { text: seller.email, style: 'addressText' } : null,
+ seller.phone ? { text: seller.phone, style: 'addressText' } : null
+ ].filter(Boolean)
+ },
+ // To
+ {
+ width: '*',
+ stack: [
+ { text: 'TO', style: 'sectionLabel' },
+ { text: billTo.name || 'Customer Name', bold: true, marginBottom: 2 },
+ { text: billTo.address || '', style: 'addressText' },
+ billTo.email ? { text: billTo.email, style: 'addressText' } : null,
+ billTo.phone ? { text: billTo.phone, style: 'addressText' } : null
+ ].filter(Boolean),
+ alignment: 'right'
+ }
+ ],
+ columnGap: 40,
+ marginBottom: 40
+ },
+
+ // Dates
+ {
+ columns: [
+ { width: '*', text: '' },
+ {
+ width: 'auto',
+ columns: [
+ {
+ stack: [
+ { text: 'ISSUED', style: 'sectionLabel' },
+ { text: formatDate(invoice.date), style: 'dateValue' }
+ ],
+ width: 100
+ },
+ invoice.dueDate ? {
+ stack: [
+ { text: 'DUE', style: 'sectionLabel' },
+ { text: formatDate(invoice.dueDate), style: 'dateValue' }
+ ],
+ width: 100,
+ alignment: 'right'
+ } : null
+ ].filter(Boolean)
+ }
+ ],
+ marginBottom: 40
+ },
+
+ // Items Table - Minimalist (no borders, just simple lines)
+ {
+ table: {
+ headerRows: 1,
+ widths: ['*', 60, 80, 80],
+ body: tableBody
+ },
+ layout: {
+ hLineWidth: (i, node) => i === 1 ? 1 : 0, // line under header only
+ vLineWidth: () => 0,
+ hLineColor: () => '#000000',
+ paddingLeft: () => 0,
+ paddingRight: () => 0,
+ paddingTop: (i) => i === 0 ? 0 : 8,
+ paddingBottom: (i) => 8
+ },
+ marginBottom: 20
+ },
+
+ // Divider
+ { canvas: [{ type: 'line', x1: 0, y1: 0, x2: 515, y2: 0, lineWidth: 1 }], marginBottom: 10 },
+
+ // Totals
+ {
+ columns: [
+ { width: '*', text: '' },
+ {
+ width: 'auto',
+ table: {
+ widths: ['auto', 100],
+ body: totalsRows
+ },
+ layout: 'noBorders'
+ }
+ ],
+ marginBottom: 60
+ },
+
+ // Footer
+ {
+ stack: [
+ invoice.notes ? { text: invoice.notes, style: 'normalText', marginBottom: 20 } : null,
+ invoice.instructions ? { text: invoice.instructions, style: 'normalText' } : null
+ ].filter(Boolean)
+ }
+ ],
+
+ styles: {
+ headerBusinessName: { fontSize: 20, bold: true, letterSpacing: -0.5 },
+ headerLabel: { fontSize: 10, color: '#999', letterSpacing: 2, marginBottom: 2 },
+ headerValue: { fontSize: 14, bold: true },
+ sectionLabel: { fontSize: 9, color: '#999', bold: true, letterSpacing: 1, marginBottom: 5 },
+ addressText: { fontSize: 10, lineHeight: 1.4, color: '#333' },
+ dateValue: { fontSize: 11, bold: true },
+ tableHeader: { fontSize: 10, bold: true, color: '#999', marginBottom: 5 },
+ tableCell: { fontSize: 10, color: '#333' },
+ normalText: { fontSize: 10, color: '#333', lineHeight: 1.5 }
+ },
+ defaultStyle: {
+ font: 'Roboto'
+ }
+ };
+ }
+};
diff --git a/src/js/modules/templates/template-registry.js b/src/js/modules/templates/template-registry.js
new file mode 100644
index 0000000..1dd93e2
--- /dev/null
+++ b/src/js/modules/templates/template-registry.js
@@ -0,0 +1,30 @@
+/**
+ * Template Registry
+ * Manages available invoice templates
+ */
+
+import DefaultTemplate from './default.js';
+import MinimalTemplate from './minimal.js';
+
+const templates = [
+ { id: 'default', name: 'Default Standard', strategy: DefaultTemplate },
+ { id: 'minimal', name: 'Minimalist', strategy: MinimalTemplate }
+];
+
+/**
+ * Get all available templates
+ * @returns {Array} List of template objects
+ */
+export function getAvailableTemplates() {
+ return templates;
+}
+
+/**
+ * Get template definition by ID
+ * @param {string} id - Template ID
+ * @returns {Object|null} Template object or null if not found
+ */
+export function getTemplate(id) {
+ const template = templates.find(t => t.id === id) || templates[0];
+ return template.strategy;
+}
diff --git a/tests/rebill.spec.js b/tests/rebill.spec.js
index ea452f6..64beafc 100644
--- a/tests/rebill.spec.js
+++ b/tests/rebill.spec.js
@@ -330,3 +330,31 @@ test.describe('Reset Draft', () => {
await expect(page.locator('#sellerName')).toHaveValue('Persistent Seller');
});
});
+
+test.describe('Template System', () => {
+ test('should switch templates and apply correct styles', async ({ page }) => {
+ await page.goto('/');
+
+ // Fill basic data
+ await page.fill('#sellerName', 'Test Seller');
+ await page.fill('#sellerAddress', '123 Test St');
+ await page.fill('#billToName', 'Test Customer');
+ await page.fill('#billToAddress', '456 Client Rd');
+ await page.fill('#invoiceNumber', '300');
+ await page.fill('.item-description', 'Service');
+ await page.fill('.item-quantity', '1');
+ await page.fill('.item-unit-price', '100');
+
+ // Initial Generate
+ await page.click('button:has-text("Generate Bill")');
+ await expect(page.locator('#invoice')).toBeVisible();
+ await expect(page.locator('#invoice')).toHaveClass(/invoice-template-default/);
+
+ // Switch to Minimalist
+ await page.selectOption('#templateId', 'minimal');
+ await page.click('button:has-text("Generate Bill")');
+
+ // Verify class changed
+ await expect(page.locator('#invoice')).toHaveClass(/invoice-template-minimal/);
+ });
+});