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/); + }); +});