const Templates = { list: [], loadAll() { const custom = JSON.parse(localStorage.getItem('pro.customTemplates') || '[]'); const builtin = [ { id: 'modern-80', name: 'โมเดิร์น 80mm', paperSize: '80mm', components: [ {type: 'logo', data: {url: '', width: '120px', align: 'center'}}, {type: 'store-info', data: {inline: true}}, {type: 'divider', data: {style: 'solid', color: '#333', thickness: '2px'}}, {type: 'receipt-info', data: {inline: true}}, {type: 'items-table', data: {}}, {type: 'divider', data: {style: 'dashed', color: '#ccc'}}, {type: 'summary', data: {}}, {type: 'payment-info', data: {method: 'เงินสด'}}, {type: 'footer', data: {text: 'ขอบคุณที่ใช้บริการ', align: 'center', size: '12px', inline: true}}, {type: 'promptpay', data: {useTotal: true}} ] }, { id: 'invoice-a4', name: 'Invoice A4', paperSize: 'a4', components: [ {type: 'logo', data: {url: '', width: '180px', align: 'center'}}, {type: 'heading', data: {text: 'INVOICE', align: 'right', size: '28px', inline: true}}, {type: 'divider', data: {style: 'solid', color: '#4a90e2'}}, {type: 'store-info', data: {inline: true}}, {type: 'receipt-info', data: {inline: true}}, {type: 'items-table', data: {}}, {type: 'summary', data: {}}, {type: 'footer', data: {text: 'ชำระเงินภายใน 7 วัน', align: 'left', size: '12px', inline: true}}, {type: 'barcode', data: {}} ] } ]; this.list = [...builtin, ...custom]; }, saveCurrent(name, desc) { const tpl = { id: 'custom-' + Date.now(), name, desc, paperSize: document.getElementById('paper-size').value, components: Pro.components.map(c => ({type: c.type, data: {...c.data}})), // store snapshot as part of this custom template so template can include shop defaults store: {...Pro.data.store}, vatRate: Pro.data.vatRate }; const arr = JSON.parse(localStorage.getItem('pro.customTemplates') || '[]'); arr.push(tpl); localStorage.setItem('pro.customTemplates', JSON.stringify(arr)); // Best-effort: also save template to server storage/templates/ try { fetch('api/save_template.php', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(tpl)}).then(r => r.json()).then(res => { // Optionally: could store returned path in template metadata }).catch(() => {}); } catch (e) {} return tpl; }, renderList() { const el = document.getElementById('templates-list'); el.innerHTML = this.list.map(t => `
${t.name}
${t.paperSize.toUpperCase()}
`).join(''); el.querySelectorAll('.template-card').forEach(card => { card.onclick = () => loadTemplateById(card.dataset.id); }); } }; function loadTemplateById(id) { const tpl = Templates.list.find(t => t.id === id); if (!tpl) return; clearCanvas(true); document.getElementById('paper-size').value = tpl.paperSize; changePaperSize(); Pro.components = (tpl.components || []).map(c => ({id: 'comp-' + (Pro.idCounter++), type: c.type, data: {...c.data}})); document.getElementById('drop-zone').innerHTML = ''; Pro.components.forEach(renderComponent); // If template contains store info, merge it into current store so templates load with shop defaults if (tpl.store) { // Merge but prefer existing non-empty values; only apply template store values that are non-empty const safe = Object.fromEntries(Object.entries(tpl.store).filter(([k, v]) => v !== undefined && v !== '')); Pro.data.store = {...Pro.data.store, ...safe}; try {localStorage.setItem('pro.store', JSON.stringify(Pro.data.store));} catch (e) {} } if (tpl.vatRate !== undefined) {Pro.data.vatRate = tpl.vatRate; try {localStorage.setItem('pro.vatRate', String(Pro.data.vatRate));} catch (e) {} } fillDataForm(); setTimeout(() => {renderBarcodes(); renderQRCodes(); bindInlineEditors();}, 30); localStorage.setItem('pro.lastTemplate', id); }