>>0).toString(36);}',
' b.id=String(b.id).replace(/[^A-Za-z0-9_-]/g,"");',
' if(!b.updatedAt)b.updatedAt=b.paidAt||b.savedAt||new Date(0).toISOString();',
' return b;',
'}',
'function newId(){return Date.now().toString(36)+Math.random().toString(36).slice(2,8);}',
'function sortKey(x){return x.paidAt||x.savedAt||x.updatedAt||"";}',
'function mergeLists(a,b,cap){',
' const map={};',
' a.concat(b).forEach(x=>{stampItem(x);const o=map[x.id];if(!o||x.updatedAt>o.updatedAt)map[x.id]=x;});',
' const all=Object.values(map).sort((p,q)=>sortKey(q).localeCompare(sortKey(p)));',
' const cutoff=Date.now()-30*24*60*60*1000;',
' const live=all.filter(x=>!x.deleted).slice(0,cap);',
' const dead=all.filter(x=>x.deleted&&new Date(x.updatedAt).getTime()>cutoff);',
' return live.concat(dead).sort((p,q)=>sortKey(q).localeCompare(sortKey(p)));',
'}',
'function readList(kind){try{return JSON.parse(localStorage.getItem(KEYS[kind])||"[]").map(stampItem);}catch(e){return[];}}',
'function writeList(kind,list){localStorage.setItem(KEYS[kind],JSON.stringify(list));}',
'function liveList(kind){return readList(kind).filter(x=>!x.deleted);}',
'function commitList(kind,list){writeList(kind,mergeLists(list,[],CAPS[kind]));scheduleSync(kind);}',
'function tomb(b){return{id:b.id,deleted:true,updatedAt:new Date().toISOString(),paidAt:b.paidAt,savedAt:b.savedAt};}',
'function setBadge(cls,txt){const b=document.getElementById("syncBadge");if(!b)return;b.className="sync-badge"+(cls?" "+cls:"");b.textContent=txt||"";}',
// Debounced: a burst of edits becomes one server write (one git commit), not one per click
'function scheduleSync(kind){',
' clearTimeout(syncTimers[kind]);',
' syncTimers[kind]=setTimeout(()=>syncKind(kind),2500);',
'}',
'async function syncKind(kind){',
' clearTimeout(syncTimers[kind]);',
' setBadge("busy",l==="EN"?"Syncing…":"กำลังซิงก์…");',
' try{',
' const r=await fetch("/api/sync",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({shopSlug:SLUG,kind,items:readList(kind)})});',
' if(!r.ok)throw new Error("HTTP "+r.status);',
' const j=await r.json();',
' writeList(kind,mergeLists(readList(kind),j.items||[],CAPS[kind]));',
' setBadge("busy",l==="EN"?"Synced":"ซิงก์แล้ว");setTimeout(()=>setBadge("",""),1500);',
' if(kind==="bills")renderBills();else renderPaidBills();',
' return true;',
' }catch(e){',
' setBadge("err",l==="EN"?"Sync failed - will retry":"ซิงก์ล้มเหลว จะลองใหม่");',
' syncTimers[kind]=setTimeout(()=>syncKind(kind),30000);',
' return false;',
' }',
'}',
// Pull the fresh server copy (GitHub API, not the static Pages copy), merge it with the local
// one, and push back anything the server does not have yet (e.g. bills made while offline).
'async function pullKind(kind){',
' try{',
' const r=await fetch("/api/data?shop="+encodeURIComponent(SLUG)+"&kind="+kind,{cache:"no-store"});',
' if(!r.ok)return false;',
' const server=await r.json();if(!Array.isArray(server))return false;',
' const merged=mergeLists(readList(kind),server,CAPS[kind]);',
' writeList(kind,merged);',
' const known={};server.forEach(x=>{stampItem(x);known[x.id]=x.updatedAt;});',
' if(merged.some(x=>known[x.id]!==x.updatedAt))scheduleSync(kind);',
' return true;',
' }catch(e){return false;}',
'}',
'window.addEventListener("online",()=>{scheduleSync("bills");scheduleSync("paid");});',
'function saveDraft(){',
' const rows=[];',
' document.querySelectorAll(".item-row").forEach(row=>{',
' const sel=row.querySelector("select");',
' const qty=parseInt(row.querySelector(".q").value)||0;',
' if(sel.value&&qty>0)rows.push({id:sel.value,qty});',
' });',
' localStorage.setItem("pos_draft_"+SLUG,JSON.stringify({',
' rows,',
' disc:document.getElementById("disc").value,',
' sc:document.getElementById("sc").value,',
' pm:document.getElementById("pm").value,',
' tab:document.getElementById("tab").value,',
' note:document.getElementById("note").value',
' }));',
'}',
'function restoreDraft(){',
' try{',
' const raw=localStorage.getItem("pos_draft_"+SLUG);',
' if(!raw)return false;',
' const d=JSON.parse(raw);',
' if(!d.rows||!d.rows.length)return false;',
' document.getElementById("disc").value=d.disc||"0";',
' document.getElementById("sc").value=d.sc||"0";',
' if(d.pm)document.getElementById("pm").value=d.pm;',
' document.getElementById("tab").value=d.tab||"Table 01";',
' document.getElementById("note").value=d.note||"";',
' document.getElementById("lines").innerHTML="";',
' const prevF=f;f="ALL";',
' const added=[];',
' d.rows.forEach(r=>{',
' const row=addNoUpd(); added.push({row,r});',
' });',
' up(true);',
' added.forEach(({row,r})=>applyRow(row,r));',
' f=prevF;',
' if(prevF!=="ALL")fb();',
' calc();return true;',
' }catch(e){return false;}',
'}',
// Named bill save / recall
'function collectOrder(){',
' const rows=[];',
' document.querySelectorAll(".item-row").forEach(row=>{',
' const sel=row.querySelector("select");',
' const qty=parseInt(row.querySelector(".q").value)||0;',
' if(sel.value&&qty>0){',
' const item=dataset.find(x=>x.id===sel.value);',
' rows.push({id:sel.value,qty,',
' nameEN:item?item.nameEN:"",nameTH:item?item.nameTH:"",',
' price:parseFloat(sel.dataset.price)||0});',
' }',
' });',
' return{rows,',
' disc:document.getElementById("disc").value,',
' sc:document.getElementById("sc").value,',
' pm:document.getElementById("pm").value,',
' tab:document.getElementById("tab").value,',
' note:document.getElementById("note").value,',
' total:parseFloat(document.getElementById("totalBox").textContent.replace(/[^0-9.]/g,""))||0};',
'}',
'function saveBill(){',
' const order=collectOrder();',
' if(!order.rows.length){alert(l==="EN"?"No items to save.":"ยังไม่มีรายการ");return;}',
' const label=document.getElementById("billLabel").value.trim()||',
' ("Bill "+new Date().toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit"}));',
' const now=new Date().toISOString();',
' const bills=readList("bills");',
' bills.unshift({id:newId(),label,...order,savedAt:now,updatedAt:now});',
' commitList("bills",bills);',
' clearOrder();',
' renderBills();',
'}',
'function recallBill(idx){',
' const b=liveList("bills")[idx];if(!b)return;',
' document.getElementById("disc").value=b.disc||"0";',
' document.getElementById("sc").value=b.sc||"0";',
' if(b.pm)document.getElementById("pm").value=b.pm;',
' document.getElementById("tab").value=b.tab||"";',
' document.getElementById("note").value=b.note||"";',
' document.getElementById("lines").innerHTML="";',
' const prevF=f;f="ALL";',
' const added=[];',
' b.rows.forEach(r=>{',
' const row=addNoUpd(); added.push({row,r});',
' });',
' up(true);',
' added.forEach(({row,r})=>applyRow(row,r));',
' f=prevF;',
' if(prevF!=="ALL")fb();',
' calc();m("calc");',
'}',
'function deleteBill(idx){',
' const b=liveList("bills")[idx];if(!b)return;',
' commitList("bills",readList("bills").map(x=>x.id===b.id?tomb(x):x));',
' renderBills();',
'}',
'function renderBills(){',
' const el=document.getElementById("savedBills");',
' const bills=liveList("bills");',
' if(!bills.length){el.innerHTML=`${l==="EN"?"No saved bills yet.":"ยังไม่มีบิลที่บันทึกไว้"}
`;return;}',
' el.innerHTML=bills.map((b,i)=>`',
' ',
'
',
' ${esc(b.label)} ',
' ${new Date(b.savedAt).toLocaleString("en-GB",{day:"2-digit",month:"short",hour:"2-digit",minute:"2-digit"})} ',
'
',
'
↩ Recall ',
'
✕ ',
'
`).join("");',
'}',
'function clearOrder(){',
' document.getElementById("lines").innerHTML="";',
' document.getElementById("disc").value="0";',
' document.getElementById("sc").value="0";',
' document.getElementById("billLabel").value="";',
' add();calc();',
' localStorage.removeItem("pos_draft_"+SLUG);',
'}',
'function markAsPaid(pmOverride){',
' const order=collectOrder();',
' if(!order.rows.length){alert(l==="EN"?"No items on this bill.":"ยังไม่มีรายการ");return;}',
' if(pmOverride)order.pm=pmOverride;',
' const label=document.getElementById("billLabel").value.trim()||',
' ("Bill "+new Date().toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit"}));',
' const now=new Date().toISOString();',
' const paid=readList("paid");',
' paid.unshift({id:newId(),label,...order,paidAt:now,updatedAt:now});',
' commitList("paid",paid);',
' clearOrder();',
' m("hist");',
'}',
'function getPaidBills(unfiltered){',
' const all=liveList("paid");',
' if(unfiltered)return all;',
' const cutoff=Date.now()-30*24*60*60*1000;',
' return all.filter(b=>new Date(b.paidAt).getTime()>cutoff);',
'}',
'function deletePaidBill(id){',
' if(!confirm(uiDict[l].histDelConfirm))return;',
' commitList("paid",readList("paid").map(x=>x.id===id?tomb(x):x));',
' renderPaidBills();',
'}',
'function filterPaidBills(bills,mode){',
' const now=new Date();',
' if(mode==="today")return bills.filter(b=>new Date(b.paidAt).toDateString()===now.toDateString());',
' if(mode==="week"){',
' const offset=(now.getDay()+6)%7;',
' const start=new Date(now);start.setHours(0,0,0,0);start.setDate(now.getDate()-offset);',
' return bills.filter(b=>new Date(b.paidAt).getTime()>=start.getTime());',
' }',
' if(mode==="month")return bills.filter(b=>{const d=new Date(b.paidAt);return d.getFullYear()===now.getFullYear()&&d.getMonth()===now.getMonth();});',
' return bills;',
'}',
'function renderPaidBills(){',
' const el=document.getElementById("paidBills");',
' const sumEl=document.getElementById("histSummary");',
' const selEl=document.getElementById("histFilter");',
' const mode=selEl?selEl.value:"all";',
' const ui=uiDict[l];',
' const bills=filterPaidBills(getPaidBills(),mode);',
' if(!bills.length){',
' sumEl.style.display="none";',
' el.innerHTML=`${ui.histNone}
`;',
' return;',
' }',
' const grandTotal=bills.reduce((s,b)=>s+(b.total||0),0);',
' sumEl.style.display="flex";',
' sumEl.innerHTML=`${ui.histTotal} (${bills.length} ${ui.histBills}) ${CURRENCY}${Math.round(grandTotal)}.00 `;',
' // Group by month, then by day within each month, tracking subtotals at both levels',
' const monthGroups={};',
' bills.forEach(b=>{',
' const d=new Date(b.paidAt);',
' const mKey=d.toLocaleDateString("en-GB",{month:"long",year:"numeric"});',
' const dKey=d.toLocaleDateString("en-GB",{weekday:"short",day:"2-digit",month:"short",year:"numeric"});',
' if(!monthGroups[mKey])monthGroups[mKey]={total:0,days:{}};',
' monthGroups[mKey].total+=(b.total||0);',
' if(!monthGroups[mKey].days[dKey])monthGroups[mKey].days[dKey]=[];',
' monthGroups[mKey].days[dKey].push(b);',
' });',
' el.innerHTML=Object.entries(monthGroups).map(([month,mg])=>{',
' const dayHtml=Object.entries(mg.days).map(([day,dayBills])=>{',
' const dayTotal=dayBills.reduce((s,b)=>s+(b.total||0),0);',
' const cards=dayBills.map(b=>`',
' ',
' ',
'
${new Date(b.paidAt).toLocaleTimeString("en-GB",{hour:"2-digit",minute:"2-digit"})} · ${esc(b.pm)} · ${esc(b.tab)}${b.note?" · "+esc(b.note):""}
',
'
${b.rows.map(r=>`${r.qty}× ${esc(l==="EN"?r.nameEN:r.nameTH)} (${CURRENCY}${Math.round(r.price)})`).join(", ")}
',
'
${ui.histView}
',
'
`).join("");',
' return `${day} ${CURRENCY}${Math.round(dayTotal)}.00
${cards}`;',
' }).join("");',
' return `${month} ${CURRENCY}${Math.round(mg.total)}.00
${dayHtml}`;',
' }).join("");',
'}',
// Open a single paid bill preview in a popup with print button
'function viewPaidBill(id){',
' const b=getPaidBills(true).find(x=>x.id===id);if(!b)return;',
' const labels=dict[l];',
' const dStr=new Date(b.paidAt).toLocaleString("en-GB");',
' let rows="";',
' b.rows.forEach(r=>{rows+=`${esc(l==="EN"?r.nameEN:r.nameTH)} ${r.qty} ${Math.round(r.price)}.00 ${Math.round(r.qty*r.price)}.00 `;});',
' const win=window.open("","_blank");if(!win)return;',
' win.document.write(`${esc(b.label)} ',
' ${labels.title} ',
' ${dStr} · ${esc(b.tab)} · ${esc(b.pm)}${b.note?" · "+esc(b.note):""}
',
' ',
' ${labels.total} ${CURRENCY}${Math.round(b.total||0)}.00
',
' 🖨 Print Close
',
' `);',
' win.document.close();',
'}',
'function downloadBlob(content,filename,mime){',
' const blob=new Blob([content],{type:mime});',
' const url=URL.createObjectURL(blob);',
' const a=document.createElement("a");a.href=url;a.download=filename;document.body.appendChild(a);a.click();a.remove();URL.revokeObjectURL(url);',
'}',
'function getHistorySelected(){',
' const mode=document.getElementById("histFilter")?document.getElementById("histFilter").value:"all";',
' return filterPaidBills(getPaidBills(),mode);',
'}',
'function exportFileName(ext){',
' const now=new Date();',
' const stamp=now.getFullYear()+"-"+String(now.getMonth()+1).padStart(2,"0")+"-"+String(now.getDate()).padStart(2,"0");',
' return `${SLUG}-bills-${stamp}.${ext}`;',
'}',
'function downloadHistoryJson(){',
' const bills=getHistorySelected();',
' if(!bills.length){alert(l==="EN"?"No bills to export.":"ไม่มีบิลจะส่งออก");return;}',
' downloadBlob(JSON.stringify({slug:SLUG,exportedAt:new Date().toISOString(),bills},null,2),exportFileName("json"),"application/json");',
'}',
'function downloadHistoryCsv(){',
' const bills=getHistorySelected();',
' if(!bills.length){alert(l==="EN"?"No bills to export.":"ไม่มีบิลจะส่งออก");return;}',
' const rows=[["billId","label","paidAt","payment","table","note","total","nameEN","qty","price"]];',
' const q=String.fromCharCode(34);',
' const esc=v=>q+String(v||"").split(q).join(q+q)+q;',
' bills.forEach(b=>{',
' b.rows.forEach((r,i)=>{',
' if(i===0)rows.push([esc(b.id),esc(b.label),esc(b.paidAt),esc(b.pm),esc(b.tab),esc(b.note),esc(b.total),esc(r.nameEN),esc(r.qty),esc(r.price)]);',
' else rows.push(["","","","","","","",esc(r.nameEN),esc(r.qty),esc(r.price)]);',
' });',
' if(!b.rows.length)rows.push([esc(b.id),esc(b.label),esc(b.paidAt),esc(b.pm),esc(b.tab),esc(b.note),esc(b.total),"","",""]);',
' });',
' const csv=rows.map(r=>r.join(",")).join("\\n");',
' downloadBlob(csv,exportFileName("csv"),"text/csv");',
'}',
'function printHistorySummary(){',
' const bills=getHistorySelected();',
' if(!bills.length){alert(l==="EN"?"No bills to print.":"ไม่มีบิลจะพิมพ์");return;}',
' const labels=dict[l];const ui=uiDict[l];',
' const grandTotal=bills.reduce((s,b)=>s+(b.total||0),0);',
' let rows="";',
' bills.forEach(b=>{',
' rows+=`${esc(b.label)}${new Date(b.paidAt).toLocaleString("en-GB")} ${esc(b.pm)} ${CURRENCY}${Math.round(b.total||0)}.00 `;',
' });',
' const win=window.open("","_blank");if(!win)return;',
' win.document.write(`${ui.histTotal} ',
' ${labels.title} ',
' ${SLUG} · ${new Date().toLocaleString("en-GB")}
',
' ${ui.histTotal} ${ui.pm} ${ui.histTotal} ${rows}
',
' ${CURRENCY}${Math.round(grandTotal)}.00 · ${bills.length} ${ui.histBills}
',
' `);',
' win.document.close();',
' setTimeout(()=>win.print(),400);',
'}',
// Init — restore draft after live menu loads so item IDs resolve correctly
'fb();',
'loadLiveMenu().then(()=>{ Promise.all([pullKind("bills"),pullKind("paid")]).then(()=>{ if(!restoreDraft())add(); renderBills(); renderPaidBills(); }); });',
'window.addEventListener("beforeinstallprompt",e=>{e.preventDefault();window.__installEvt=e;const b=document.getElementById("installBtn");if(b){b.style.display="inline-block";b.onclick=()=>{e.prompt();e.userChoice.then(()=>{b.style.display="none";window.__installEvt=null;});};};});'
].join('\n');
// "" -> "<\/" so no user text (menu names, shop name) can close the script tag early
return html + '\n