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