components.js

10.38 KB
02/11/2025 06:44
JS
components.js
function renderComponent(comp) {
  const el = document.createElement('div');
  el.className = 'dropped-component';
  el.id = comp.id;
  el.innerHTML = `
    <div class="component-controls">
      <button class="control-btn edit" onclick="selectComponent('${comp.id}')"><i class="fa-solid fa-pen"></i></button>
      <button class="control-btn delete" onclick="deleteComponent('${comp.id}')"><i class="fa-solid fa-trash"></i></button>
    </div>
    ${renderComponentContent(comp)}
  `;
  el.addEventListener('click', (e) => {if (!e.target.closest('.component-controls')) selectComponent(comp.id);});
  document.getElementById('drop-zone').appendChild(el);
}

function renderComponentContent(comp) {
  const d = comp.data;
  switch (comp.type) {
    case 'heading':
      return `<div class="comp-heading ${d.inline ? 'editable' : ''}" contenteditable="${d.inline ? 'true' : 'false'}" data-key="text" style="text-align:${d.align};font-size:${d.size};font-weight:${d.bold ? '700' : '400'}">${d.text}</div>`;
    case 'text':
      return `<div class="comp-text ${d.inline ? 'editable' : ''}" contenteditable="${d.inline ? 'true' : 'false'}" data-key="text" style="text-align:${d.align};font-size:${d.size}">${(d.text || '').replace(/\n/g, '<br>')}</div>`;
    case 'logo':
      const logoUrl = d.url || Pro.data.store.logoUrl;
      return `<div class="comp-logo" style="text-align:${d.align}">${logoUrl ? `<img src="${logoUrl}" style="width:${d.width}">` : `<div style=\"padding:20px;background:#f0f0f0;border-radius:4px\">โลโก้</div>`}</div>`;
    case 'image':
      return `<div class="comp-logo" style="text-align:${d.align}">${d.url ? `<img src="${d.url}" style="width:${d.width}">` : `<div style=\"padding:20px;background:#f0f0f0;border-radius:4px\">รูปภาพ</div>`}</div>`;
    case 'divider':
      return `<div class="comp-divider" style="border-top:${d.thickness || '1px'} ${d.style} ${d.color}"></div>`;
    case 'spacer':
      return `<div style="height:${d.height}"></div>`;
    case 'store-info':
      const s = Pro.data.store;
      return `<div class="comp-text" style="text-align:center">
        <div class="editable" contenteditable data-scope="store" data-key="name" style="font-size:18px;font-weight:700">${s.name || ''}</div>
        <div class="editable" contenteditable data-scope="store" data-key="branch" style="font-size:14px">${s.branch || ''}</div>
        <div class="editable" contenteditable data-scope="store" data-key="address" style="font-size:12px">${s.address || ''}</div>
        <div style="font-size:12px">โทร: <span class="editable" contenteditable data-scope="store" data-key="phone">${s.phone || ''}</span> | Tax: <span class="editable" contenteditable data-scope="store" data-key="taxId">${s.taxId || ''}</span></div>
      </div>`;
    case 'receipt-info':
      const r = Pro.data.receipt;
      return `<div class="comp-text" style="font-size:13px">
        <strong>ใบเสร็จ:</strong> <span class="editable" contenteditable data-scope="receipt" data-key="id">${r.id || ''}</span><br>
        <strong>วันที่:</strong> ${r.date || ''} ${r.time || ''}<br>
        <strong>โต๊ะ:</strong> <span class="editable" contenteditable data-scope="receipt" data-key="table">${r.table || ''}</span> | <strong>พนักงาน:</strong> <span class="editable" contenteditable data-scope="receipt" data-key="staff">${r.staff || ''}</span>
      </div>`;
    case 'items-table':
      const rows = (Pro.items.length ? Pro.items : [{name: 'ไม่มีรายการ', qty: 0, price: 0}]).map(it => {
        const t = (Number(it.qty) || 0) * (Number(it.price) || 0);
        return `<tr>
          <td>${it.name}${it.note ? `<br><small style="color:#999">${it.note}</small>` : ''}</td>
          <td style="text-align:center">${it.qty}</td>
          <td style="text-align:right">฿${(Number(it.price) || 0).toFixed(2)}</td>
          <td style="text-align:right">฿${t.toFixed(2)}</td>
        </tr>`;
      }).join('');
      return `<table class="comp-table"><thead><tr><th>รายการ</th><th style="text-align:center;width:50px">จน.</th><th style="text-align:right;width:70px">ราคา</th><th style="text-align:right;width:70px">รวม</th></tr></thead><tbody>${rows}</tbody></table>`;
    case 'summary':
      const sum = Pro.calcSummary();
      return `<div class="comp-text" style="text-align:right;font-size:14px">
        <div>ยอดรวม: ฿${sum.subtotal.toFixed(2)}</div>
        <div>ภาษี ${Number(Pro.data.vatRate) || 0}%: ฿${sum.vat.toFixed(2)}</div>
        <div style="font-size:18px;font-weight:700;margin-top:5px;color:#e74c3c">รวมสุทธิ: ฿${sum.total.toFixed(2)}</div>
      </div>`;
    case 'payment-info':
      const sm = Pro.calcSummary();
      const cash = parseFloat(d.cashReceived);
      const showCash = !isNaN(cash);
      const change = showCash ? cash - sm.total : 0;
      return `<div class="comp-text" style="font-size:13px">
        <strong>ชำระโดย:</strong> ${d.method || 'เงินสด'}<br>
        ${showCash ? `<strong>รับเงิน:</strong> ฿${cash.toFixed(2)}<br>` : ''}
        ${showCash ? `<strong>เงินทอน:</strong> ฿${Math.max(change, 0).toFixed(2)}` : ''}
      </div>`;
    case 'barcode':
      return `<div class="comp-barcode"><svg class="barcode-svg" data-value="${d.value || Pro.data.receipt.id}" data-height="${d.height || '50'}" data-width="${d.width || '1'}"></svg></div>`;

    case 'promptpay':
      const id = Pro.data.store.promptpayId || '';
      const sum2 = Pro.calcSummary();
      const amt = (d.useTotal ? sum2.total : parseFloat(d.amount || ''));
      const amtStr = (!isNaN(amt) && amt > 0) ? `/${amt.toFixed(2)}` : '';
      const src = id ? `https://promptpay.io/${encodeURIComponent(id)}${amtStr}.png` : '';
      return `<div class=\"comp-qrcode\">${src ? `<img src=\"${src}\" alt=\"PromptPay\" style=\"width:${d.size || 150}px;height:${d.size || 150}px;object-fit:contain\">` : '<div style="text-align:center;color:#999">ตั้งค่า PromptPay ID ในข้อมูลร้าน</div>'}</div>`;
    case 'footer':
      return `<div class="comp-text ${d.inline ? 'editable' : ''}" contenteditable="${d.inline ? 'true' : 'false'}" data-key="text" style="text-align:${d.align};font-size:${d.size}">${(d.text || '').replace(/\n/g, '<br>')}</div>`;
    case 'box':
      const boxStyle = `
        padding: ${d.padding || 10}px;
        margin: ${d.margin || 5}px 0;
        border: ${d.borderWidth || 1}px ${d.borderStyle || 'solid'} ${d.borderColor || '#ddd'};
        border-radius: ${d.borderRadius || 4}px;
        background: ${d.backgroundColor || 'transparent'};
        ${d.width ? `width: ${d.width}px;` : ''}
        ${d.height ? `height: ${d.height}px;` : ''}
        display: ${d.layout || 'block'};
        ${d.layout === 'flex' ? `
          flex-direction: ${d.flexDirection || 'row'};
          justify-content: ${d.justifyContent || 'flex-start'};
          align-items: ${d.alignItems || 'stretch'};
          gap: ${d.gap || 10}px;
        ` : ''}
        ${d.layout === 'grid' ? `
          grid-template-columns: ${d.gridColumns || 'repeat(2, 1fr)'};
          gap: ${d.gap || 10}px;
        ` : ''}
      `;
      const childrenHtml = (comp.children || []).map(child => {
        const childComp = Pro.components.find(c => c.id === child.id);
        return childComp ? `<div class="box-child" data-child-id="${child.id}">${renderComponentContent(childComp)}</div>` : '';
      }).join('');
      return `<div class="comp-box" style="${boxStyle}" data-box-id="${comp.id}">
        ${childrenHtml}
        <div class="box-drop-zone" style="min-height: 40px; border: 2px dashed #ccc; border-radius: 4px; display: flex; align-items: center; justify-content: center; color: #999; font-size: 12px; margin: 5px 0;">
          ลาก component มาวางในกล่อง
        </div>
      </div>`;
    default:
      return `<div>Unknown: ${comp.type}</div>`;
  }
}

function selectComponent(id) {
  document.querySelectorAll('.dropped-component').forEach(el => el.classList.remove('selected'));
  const el = document.getElementById(id);
  if (el) el.classList.add('selected');
  Pro.selected = Pro.components.find(c => c.id === id) || null;
  showProperties(Pro.selected);
}

function deleteComponent(id) {
  // Remove from any box that contains this component
  Pro.components.forEach(comp => {
    if (comp.type === 'box' && comp.children) {
      comp.children = comp.children.filter(child => child.id !== id);
    }
  });

  // Remove the component itself
  Pro.components = Pro.components.filter(c => c.id !== id);
  document.getElementById(id)?.remove();

  if (!Pro.components.length) showEmptyMessage();
  if (Pro.selected?.id === id) {Pro.selected = null; showProperties(null);}

  // Refresh all box components
  refreshAll();
}

function refreshAll() {
  Pro.components.forEach(c => {
    const el = document.getElementById(c.id);
    if (el) {
      const ctrl = el.querySelector('.component-controls')?.outerHTML || '';
      el.innerHTML = ctrl + renderComponentContent(c);
    }
  });
  setTimeout(() => {renderBarcodes(); bindInlineEditors(); setupBoxDropZones();}, 20);
}

function renderBarcodes() {
  document.querySelectorAll('.barcode-svg').forEach(svg => {
    const value = svg.dataset.value;
    const height = parseInt(svg.dataset.height) || 50;
    const width = Math.max(0.5, Math.min(2, parseFloat(svg.dataset.width) || 1));
    const finalValue = value || (Pro?.data?.receipt?.id || '');
    if (finalValue) {try {JsBarcode(svg, finalValue, {format: 'CODE128', width, height, displayValue: true, fontSize: 12});} catch (e) {} }
  });
}

// removed qrcode renderer; using PromptPay image instead

function bindInlineEditors() {
  document.querySelectorAll('.editable[contenteditable="true"]').forEach(el => {
    el.onblur = (e) => {
      const compEl = e.target.closest('.dropped-component');
      if (!compEl) return;
      const comp = Pro.components.find(c => c.id === compEl.id);
      if (!comp) return;
      const scope = e.target.dataset.scope;
      const key = e.target.dataset.key;
      const text = e.target.innerText.trim();
      if (scope) {
        Pro.data[scope][key] = text;
        if (scope === 'store') {
          try {localStorage.setItem('pro.store', JSON.stringify(Pro.data.store));} catch (e) {}
        }
      } else if (key) {
        comp.data[key] = text;
      }
      showProperties(Pro.selected);
      refreshAll();
    };
  });
}