app.js

15.71 KB
02/11/2025 07:36
JS
app.js
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 => `
    <div class="component-group">
      <h3>${group.g}</h3>
      ${group.items.map(it => `<div class="component-item" draggable="true" data-type="${it.type}"><i class="fa-solid fa-grip"></i> ${it.name}</div>`).join('')}
    </div>
  `).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 = '<div class="drop-zone-empty"><div style="font-size:36px">📋</div><div>ลากคอมโพเนนต์มาวางที่นี่ หรือเลือก Template</div></div>';
  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 = '<div class="drop-zone-empty"><div style="font-size:36px">📋</div><div>ลากคอมโพเนนต์มาวางที่นี่ หรือเลือก Template</div></div>';
}

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 => `<div class="template-card"><div class="template-name"><i class="fa-solid fa-receipt"></i> ${r.id}</div><div class="template-desc">${new Date(r.ts).toLocaleString('th-TH')}</div><div style="margin-top:6px;display:flex;gap:6px"><button class="btn btn-sm btn-primary" onclick="Storage.loadReceipt('${r.id}')"><i class=\"fa-solid fa-rotate\"></i> โหลด</button></div></div>`).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);
}