document.addEventListener('DOMContentLoaded', () => { // Init data const now = Pro.now(); Pro.data.receipt.date = now.date; Pro.data.receipt.time = now.time; // Load persisted store info (and basic settings) from localStorage if present try { const savedStore = JSON.parse(localStorage.getItem('pro.store') || 'null'); if (savedStore) Pro.data.store = {...Pro.data.store, ...savedStore}; } catch (e) { /* ignore invalid json */} const savedVat = localStorage.getItem('pro.vatRate'); if (savedVat !== null) Pro.data.vatRate = parseFloat(savedVat) || Pro.data.vatRate; // Try to load authoritative store from server if available; merge with local values (server overrides empty local) (async function loadServerStore() { try { const res = await fetch('api/get_store.php'); const j = await res.json(); if (j && j.ok && j.data) { const server = j.data; // server may contain {store: {...}, vatRate: n} if (server.store) { // merge: prefer non-empty server values, but do not overwrite filled local values unless local is empty const merged = {...Pro.data.store}; Object.keys(server.store).forEach(k => { const val = server.store[k]; if (val !== undefined && val !== null && val !== '') { // overwrite if local empty or different if (!merged[k] || merged[k] === '') merged[k] = val; else merged[k] = val; } }); Pro.data.store = merged; try {localStorage.setItem('pro.store', JSON.stringify(Pro.data.store));} catch (e) {} } if (server.vatRate !== undefined) {Pro.data.vatRate = server.vatRate; try {localStorage.setItem('pro.vatRate', String(Pro.data.vatRate));} catch (e) {} } } } catch (e) { // ignore fetch errors } finally { fillDataForm(); } })(); // Debounced server save helper window._saveStoreTimer = null; window.scheduleSaveStore = function() { if (window._saveStoreTimer) clearTimeout(window._saveStoreTimer); window._saveStoreTimer = setTimeout(async () => { try { await apiSaveStore({store: Pro.data.store, vatRate: Pro.data.vatRate}); } catch (e) { // ignore network errors; client still has localStorage fallback } }, 600); }; // Setup tabs document.querySelectorAll('.panel-tab').forEach(btn => { btn.onclick = () => { const cont = btn.closest('.left-panel,.right-panel'); cont.querySelectorAll('.panel-tab').forEach(b => b.classList.remove('active')); cont.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active')); btn.classList.add('active'); cont.querySelector('#tab-' + btn.dataset.tab).classList.add('active'); }; }); // Paper size document.getElementById('paper-size').onchange = changePaperSize; // Toolbar buttons document.getElementById('btn-clear').onclick = () => clearCanvas(); document.getElementById('btn-print').onclick = printReceipt; document.getElementById('btn-export').onclick = () => { const data = Storage.exportAll(); const blob = new Blob([JSON.stringify(data, null, 2)], {type: 'application/json'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'receipt-' + Date.now() + '.json'; a.click(); URL.revokeObjectURL(url); }; document.getElementById('import-file').onchange = (e) => { const f = e.target.files?.[0]; if (!f) return; const r = new FileReader(); r.onload = () => {try {Storage.importAll(JSON.parse(r.result)); alert('นำเข้าข้อมูลสำเร็จ');} catch (e) {alert('ไฟล์ไม่ถูกต้อง');} e.target.value = '';}; r.readAsText(f); }; document.getElementById('btn-save-template').onclick = () => { const name = prompt('ชื่อ Template'); if (!name) return; const desc = prompt('คำอธิบาย') || ''; Templates.saveCurrent(name, desc); Templates.loadAll(); Templates.renderList(); alert('บันทึก Template สำเร็จ'); }; document.getElementById('btn-add-item').onclick = addItem; const logoUpload = document.getElementById('upload-store-logo'); if (logoUpload) logoUpload.onchange = handleStoreLogoUpload; document.getElementById('btn-save-receipt').onclick = async () => { const snap = Storage.saveReceipt(); renderReceipts(); // Optional API save // const res = await apiSaveReceipt(snap); alert('บันทึกใบเสร็จแล้ว: ' + snap.id); }; // Components list renderComponentsPalette(); setupDnD(); // Templates Templates.loadAll(); Templates.renderList(); const last = localStorage.getItem('pro.lastTemplate'); if (last) loadTemplateById(last); else loadTemplateById('modern-80'); }); function renderComponentsPalette() { const list = [ { g: 'พื้นฐาน', items: [ {type: 'heading', name: 'หัวข้อ'}, {type: 'text', name: 'ข้อความ'}, {type: 'logo', name: 'โลโก้'}, {type: 'image', name: 'รูปภาพ'}, {type: 'divider', name: 'เส้นแบ่ง'}, {type: 'spacer', name: 'ช่องว่าง'} ] }, { g: 'ข้อมูล', items: [ {type: 'store-info', name: 'ข้อมูลร้าน'}, {type: 'receipt-info', name: 'ข้อมูลใบเสร็จ'}, {type: 'items-table', name: 'ตารางรายการ'}, {type: 'summary', name: 'สรุปยอด'}, {type: 'payment-info', name: 'ชำระเงิน'} ] }, { g: 'พิเศษ', items: [ {type: 'barcode', name: 'Barcode'}, {type: 'promptpay', name: 'PromptPay QR'}, {type: 'footer', name: 'ข้อความท้าย'} ] } ]; const wrap = document.getElementById('components-list'); wrap.innerHTML = list.map(group => `

${group.g}

${group.items.map(it => `
${it.name}
`).join('')}
`).join(''); wrap.querySelectorAll('.component-item').forEach(el => { el.addEventListener('dragstart', (e) => {Pro.draggedType = el.dataset.type; e.dataTransfer.effectAllowed = 'copy';}); }); } function setupDnD() { const dz = document.getElementById('drop-zone'); dz.addEventListener('dragover', (e) => {e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; dz.classList.add('drag-over');}); dz.addEventListener('dragleave', () => dz.classList.remove('drag-over')); dz.addEventListener('drop', (e) => {e.preventDefault(); dz.classList.remove('drag-over'); if (Pro.draggedType) addComponent(Pro.draggedType); Pro.draggedType = null;}); new Sortable(dz, {animation: 150, handle: '.dropped-component', onEnd: updateOrder}); // Setup box drop zones setupBoxDropZones(); } function setupBoxDropZones() { document.querySelectorAll('.box-drop-zone').forEach(zone => { zone.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); e.dataTransfer.dropEffect = 'copy'; zone.closest('.comp-box').classList.add('drag-over'); }); zone.addEventListener('dragleave', (e) => { e.stopPropagation(); zone.closest('.comp-box').classList.remove('drag-over'); }); zone.addEventListener('drop', (e) => { e.preventDefault(); e.stopPropagation(); zone.closest('.comp-box').classList.remove('drag-over'); if (Pro.draggedType) { const boxId = zone.closest('.comp-box').dataset.boxId; addComponentToBox(Pro.draggedType, boxId); Pro.draggedType = null; } }); }); } function addComponent(type) { const id = 'comp-' + (Pro.idCounter++); const comp = {id, type, data: Pro.defaults(type)}; if (type === 'box') { comp.children = []; } Pro.components.push(comp); hideEmpty(); renderComponent(comp); setTimeout(() => {renderBarcodes(); bindInlineEditors(); setupBoxDropZones();}, 10); } function addComponentToBox(type, boxId) { const id = 'comp-' + (Pro.idCounter++); const comp = {id, type, data: Pro.defaults(type)}; const boxComp = Pro.components.find(c => c.id === boxId); if (boxComp && boxComp.type === 'box') { if (!boxComp.children) boxComp.children = []; boxComp.children.push({id}); Pro.components.push(comp); // Re-render the box component const boxEl = document.getElementById(boxId); if (boxEl) { const ctrl = boxEl.querySelector('.component-controls')?.outerHTML || ''; boxEl.innerHTML = ctrl + renderComponentContent(boxComp); } setTimeout(() => {renderBarcodes(); bindInlineEditors(); setupBoxDropZones();}, 10); } } function updateOrder() { const dz = document.getElementById('drop-zone'); const ids = Array.from(dz.querySelectorAll('.dropped-component')).map(el => el.id); Pro.components = ids.map(id => Pro.components.find(c => c.id === id)).filter(Boolean); } function changePaperSize() { const canvas = document.getElementById('receipt-canvas'); const size = document.getElementById('paper-size').value; canvas.className = `receipt-canvas paper-${size}`; } function clearCanvas(silent = false) { if (!silent && !confirm('ล้างทั้งหมด?')) return; Pro.components = []; document.getElementById('drop-zone').innerHTML = '
📋
ลากคอมโพเนนต์มาวางที่นี่ หรือเลือก Template
'; Pro.selected = null; showProperties(null); } function hideEmpty() { const empty = document.querySelector('.drop-zone-empty'); if (empty) empty.remove(); } function showEmptyMessage() { document.getElementById('drop-zone').innerHTML = '
📋
ลากคอมโพเนนต์มาวางที่นี่ หรือเลือก Template
'; } function fillDataForm() { const s = Pro.data.store, r = Pro.data.receipt; document.getElementById('data-store-name').value = s.name || ''; document.getElementById('data-store-branch').value = s.branch || ''; document.getElementById('data-store-address').value = s.address || ''; document.getElementById('data-store-phone').value = s.phone || ''; document.getElementById('data-store-tax').value = s.taxId || ''; const logoEl = document.getElementById('data-store-logo'); if (logoEl) logoEl.value = s.logoUrl || ''; const ppEl = document.getElementById('data-store-promptpay'); if (ppEl) ppEl.value = s.promptpayId || ''; document.getElementById('data-receipt-id').value = r.id || ''; document.getElementById('data-table').value = r.table || ''; document.getElementById('data-staff').value = r.staff || ''; document.getElementById('data-vat-rate').value = Pro.data.vatRate || 7; } // Bind data tab inputs ['data-store-name', 'data-store-branch', 'data-store-address', 'data-store-phone', 'data-store-tax', 'data-store-logo', 'data-store-promptpay', 'data-receipt-id', 'data-table', 'data-staff', 'data-vat-rate'].forEach(id => { document.addEventListener('input', (e) => { if (e.target.id !== id) return; const s = Pro.data.store, r = Pro.data.receipt; if (id === 'data-store-name') s.name = e.target.value; if (id === 'data-store-branch') s.branch = e.target.value; if (id === 'data-store-address') s.address = e.target.value; if (id === 'data-store-phone') s.phone = e.target.value; if (id === 'data-store-tax') s.taxId = e.target.value; if (id === 'data-store-logo') s.logoUrl = e.target.value; if (id === 'data-store-promptpay') s.promptpayId = e.target.value; if (id === 'data-receipt-id') r.id = e.target.value; if (id === 'data-table') r.table = e.target.value; if (id === 'data-staff') r.staff = e.target.value; if (id === 'data-vat-rate') Pro.data.vatRate = parseFloat(e.target.value) || 0; // Persist store info and VAT rate to localStorage so it's available as default across templates try {localStorage.setItem('pro.store', JSON.stringify(s));} catch (err) { /* ignore */} try {localStorage.setItem('pro.vatRate', String(Pro.data.vatRate));} catch (err) { /* ignore */} // Schedule upload to server (best-effort). If server unavailable, localStorage remains as fallback. try {scheduleSaveStore();} catch (err) {} refreshAll(); }, true); }); function renderReceipts() { const list = Storage.listReceipts(); const el = document.getElementById('receipts-list'); el.innerHTML = list.map(r => `
${r.id}
${new Date(r.ts).toLocaleString('th-TH')}
`).join(''); } // Make setupBoxDropZones available globally window.setupBoxDropZones = setupBoxDropZones; // Upload helpers function handleStoreLogoUpload(e) { const file = e.target.files && e.target.files[0]; if (!file) return; // Upload file to server as multipart/form-data const fd = new FormData(); fd.append('file', file); fetch('api/upload_logo.php', {method: 'POST', body: fd}).then(r => r.json()).then(res => { if (res && res.ok && res.url) { Pro.data.store.logoUrl = res.url; const el = document.getElementById('data-store-logo'); if (el) { el.value = Pro.data.store.logoUrl; try {el.dispatchEvent(new Event('input', {bubbles: true}));} catch (err) {} } try {localStorage.setItem('pro.store', JSON.stringify(Pro.data.store));} catch (err) {} try {scheduleSaveStore();} catch (err) {} refreshAll(); } else { // fallback: try to read as dataURL and persist locally const reader = new FileReader(); reader.onload = () => { Pro.data.store.logoUrl = reader.result; const el = document.getElementById('data-store-logo'); if (el) {el.value = Pro.data.store.logoUrl; try {el.dispatchEvent(new Event('input', {bubbles: true}));} catch (err) {} } try {localStorage.setItem('pro.store', JSON.stringify(Pro.data.store));} catch (err) {} try {scheduleSaveStore();} catch (err) {} refreshAll(); }; reader.readAsDataURL(file); } }).catch(() => { // network error -> persist locally as fallback const reader = new FileReader(); reader.onload = () => { Pro.data.store.logoUrl = reader.result; const el = document.getElementById('data-store-logo'); if (el) {el.value = Pro.data.store.logoUrl; try {el.dispatchEvent(new Event('input', {bubbles: true}));} catch (err) {} } try {localStorage.setItem('pro.store', JSON.stringify(Pro.data.store));} catch (err) {} try {scheduleSaveStore();} catch (err) {} refreshAll(); }; reader.readAsDataURL(file); }); } // Upload store data to server (best-effort). Expects {store: {...}, vatRate: number} async function apiSaveStore(payload) { try { const res = await fetch('api/save_store.php', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload)}); return await res.json(); } catch (e) { return {ok: false, error: String(e)}; } } function handleImageUpload(input, compId, key) { const file = input.files && input.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => {const comp = Pro.components.find(c => c.id === compId); if (!comp) return; comp.data[key] = reader.result; const el = document.getElementById(compId); if (el) {const ctrl = el.querySelector('.component-controls')?.outerHTML || ''; el.innerHTML = ctrl + renderComponentContent(comp);} setTimeout(() => {bindInlineEditors();}, 10);}; reader.readAsDataURL(file); }