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