Dashboard
● Online
// ═══ POS TERMINAL ════════════════════════════════════════════ function renderPOS(c){ c.style.cssText='padding:0;overflow:hidden;display:flex;height:100%'; const o=S.ORDER; const ctx=[]; const icons={dine_in:'🍽️',takeaway:'πŸ₯‘',delivery:'🚚',car_pickup:'πŸš—',online:'πŸ“±',party:'πŸŽ‰'}; if(o.type)ctx.push(`${icons[o.type]||'πŸ“‹'} ${esc((o.type||'').replace('_',' '))}`); if(o.table_name)ctx.push(`🍽️ ${esc(o.table_name)}`); if(o.token)ctx.push(`🎫 #${esc(o.token)}`); if(o.customer_name)ctx.push(`πŸ‘€ ${esc(o.customer_name)}`); if(o.car_number)ctx.push(`πŸš— ${esc(o.car_number)}`); if(o.online_platform)ctx.push(`πŸ“± ${esc(o.online_platform)}`); const delInfo=o.type==='delivery'&&o.address_text? `
🚚 ${esc(o.address_text)}
`:''; const carInfo=o.type==='car_pickup'? `
πŸš— Car: ${esc(o.car_number||'β€”')}
`:''; const partyInfo=o.type==='party'? `
πŸŽ‰ ${esc(o.party_date||'')} Β· ${o.party_guests||0} guests
`:''; c.innerHTML=`
${ctx.join('')}
${o.customer_id?``:''}
πŸ›’
${delInfo}${carInfo}${partyInfo}
πŸ›’

Tap items to add

SubtotalQAR 0.00
Delivery Fee${fmt(o.delivery_fee||0)}
VAT 5%QAR 0.00
TOTALQAR 0.00
πŸ“Ά Offline mode β€” orders saved locally
`; loadPosMenu(); } function filterItems(q){S.posSearch=q;renderPosItems();} async function loadPosMenu(){ try{ const [cr,ir]=await Promise.all([GET('/menu/categories'),GET('/menu/items?per_page=500')]); S.cats=cr.data||[];S.items=ir.data||[]; store.set(LS.cats,S.cats);store.set(LS.menu,S.items); }catch{S.cats=store.get(LS.cats)||[];S.items=store.get(LS.menu)||[];} buildPosChips();renderPosItems();buildSuggestions(); } function buildPosChips(){ const ce=$('pos-cats');if(!ce)return;ce.innerHTML=''; const all=el('div','chip on','All'); all.addEventListener('click',()=>{S.posCat=null;renderPosItems();setChip(all);}); ce.appendChild(all); S.cats.forEach(cat=>{ const ch=el('div','chip',esc(cat.name_en)); ch.addEventListener('click',()=>{S.posCat=cat.id;renderPosItems();setChip(ch);}); ce.appendChild(ch); }); } function setChip(a){document.querySelectorAll('.pos-cats .chip').forEach(c=>c.classList.remove('on'));a.classList.add('on');} function renderPosItems(){ const g=$('pos-grid');if(!g)return; let items=S.items.filter(i=>i.is_active!=0&&i.is_active!=='0'); if(S.posCat)items=items.filter(i=>i.category_id==S.posCat); if(S.posSearch){const q=S.posSearch.toLowerCase();items=items.filter(i=>(i.name_en||'').toLowerCase().includes(q)||(i.name_ar||'').toLowerCase().includes(q));} if(!items.length){g.innerHTML=`
🍽️

No items found

`;return;} g.innerHTML=''; items.forEach(item=>{ const promo=item.discount_price&&+item.discount_price>0&&+item.discount_price<+item.base_price; const price=promo?+item.discount_price:+item.base_price; const t=el('div','itm'); t.innerHTML=`
${esc(item.icon||'🍽️')}
${esc(item.name_en)}
${promo?`${fmt(item.base_price)} `:''}${fmt(price)}
${promo?'
OFFER
':''}`; t.addEventListener('click',()=>addToCart(item,price)); g.appendChild(t); }); } async function addToCart(item,price){ // Check modifiers let groups=item.modifier_groups||[]; if(!groups.length){ try{const r=await GET('/menu/items/'+item.id+'/modifiers').catch(()=>({data:[]}));groups=r.data||[];}catch{} } if(groups&&groups.length){openModifierModal(item,groups,price);return;} doCartAdd({...item,price,selectedMods:[],itemNote:''}); } function doCartAdd(item){ const existing=!item.selectedMods?.length?S.cart.find(l=>l.id===item.id&&!l.selectedMods?.length):null; if(existing){existing.qty++;renderCart();return;} S.cart.push({...item,qty:1,_cid:Date.now()}); renderCart(); } function cartAdj(cid,d){ const l=S.cart.find(l=>l._cid===cid); if(!l)return;l.qty+=d;if(l.qty<=0)S.cart=S.cart.filter(x=>x._cid!==cid); renderCart(); } function renderCart(){ const bd=$('cart-bd'),cnt=$('cart-cnt');if(!bd)return; const total=S.cart.reduce((s,l)=>s+l.qty,0); cnt.textContent=total;cnt.style.display=total?'':'none'; if(!S.cart.length){ bd.innerHTML=`
πŸ›’

Tap items to add

`; updTotals(0,0,0,0);return; } bd.innerHTML=''; S.cart.forEach(line=>{ const row=el('div','cart-row'); const modsHtml=line.selectedMods?.length?`
${line.selectedMods.map(m=>`${esc(m.name)}${m.price_adjustment&&+m.price_adjustment!==0?` +${fmt(m.price_adjustment)}`:''}`).join('')}
`:''; const noteHtml=line.itemNote?`
πŸ“ ${esc(line.itemNote)}
`:''; row.innerHTML=`
${esc(line.name_en)}
${fmt(line.price)} each
${modsHtml}${noteHtml}
βˆ’
${line.qty}
+
`; bd.appendChild(row); }); bd.querySelectorAll('.qb').forEach(b=>b.addEventListener('click',()=>cartAdj(+b.dataset.cid,+b.dataset.d))); const sub=S.cart.reduce((s,l)=>s+l.price*l.qty,0); const disc=S.ORDER.discount_amount||0,del=S.ORDER.delivery_fee||0; const vat=(sub-disc+del)*0.05; updTotals(sub,disc,del,vat); } function updTotals(sub,disc,del,vat){ const set=(id,v)=>{const e=$(id);if(e)e.textContent=v;}; set('ct-sub',fmt(sub));set('ct-disc','-'+fmt(disc));set('ct-vat',fmt(vat)); set('ct-tot',fmt(sub-disc+del+vat));set('ct-del',fmt(del)); const dr=$('ct-disc-r'),dlr=$('ct-del-r'); if(dr)dr.style.display=disc?'flex':'none'; if(dlr)dlr.style.display=del?'flex':'none'; } // Item note modal function openItemNote(cid){ const item=S.cart.find(l=>l._cid===cid);if(!item)return; const d=$('mo-item-detail-inner'); d.innerHTML=`

✏️ ${esc(item.name_en)}

${item.selectedMods?.length?`
Modifiers:
${item.selectedMods.map(m=>`${esc(m.name)}`).join('')}
`:''}
Line Total:${fmt(item.price*item.qty)}
`; $('mo-item-detail').style.display='flex'; } function saveItemNote(cid){ const item=S.cart.find(l=>l._cid===cid); if(item){item.itemNote=$('idn-note')?.value?.trim()||'';} $('mo-item-detail').style.display='none';renderCart(); } // Modifier modal function openModifierModal(item,groups,price){ const d=$('mo-modifiers-inner'); d.innerHTML=`

${esc(item.name_en)}

${fmt(price)}
${(groups||[]).map(g=>`
${esc(g.name_en||g.name||'Options')} ${g.is_required?'Required':'Optional'} ${g.max_selections>1?`Pick up to ${g.max_selections}`:''}
${(g.options||g.modifier_options||[]).map(opt=>` `).join('')}
`).join('')}
Total: ${fmt(price)}
`; $('mo-modifiers').style.display='flex'; // Recalculate total on change document.querySelectorAll('#mod-groups-wrap input').forEach(inp=>{ inp.addEventListener('change',()=>{ let adj=0;document.querySelectorAll('#mod-groups-wrap input:checked').forEach(c=>adj+=parseFloat(c.dataset.price)||0); const t=$('mod-total');if(t)t.textContent=fmt(price+adj); }); }); } function confirmMods(itemRef,basePrice){ const item=typeof itemRef==='string'?JSON.parse(itemRef):itemRef; const price=parseFloat(basePrice); // Validate required groups let valid=true; document.querySelectorAll('#mod-groups-wrap .mod-group').forEach(g=>{ g.style.border='';g.style.borderRadius=''; if(g.dataset.required==='1'&&!g.querySelector('input:checked')){ g.style.border='1px solid var(--r)';g.style.borderRadius='.4rem';valid=false; } }); if(!valid){toast('Please select required options','a-r');return;} const selectedMods=[];let priceAdj=0; document.querySelectorAll('#mod-groups-wrap input:checked').forEach(inp=>{ selectedMods.push({id:+inp.value,name:inp.dataset.name,price_adjustment:parseFloat(inp.dataset.price)||0}); priceAdj+=parseFloat(inp.dataset.price)||0; }); const note=$('mod-note')?.value?.trim()||''; // Get full item from state const fullItem=S.items.find(i=>i.id===item.id)||item; doCartAdd({...fullItem,price:price+priceAdj,selectedMods,itemNote:note}); $('mo-modifiers').style.display='none'; } // Suggestions bar function buildSuggestions(){ const bar=$('sug-bar');if(!bar)return; let items=[]; // Show top items by category or best sellers if(S.ORDER.customer_id){ items=S.items.filter(i=>i.is_active!=0).slice(0,8); } else { items=S.items.filter(i=>i.is_active!=0).slice(0,8); } if(!items.length){bar.style.display='none';return;} bar.style.display='flex'; bar.innerHTML='✨'; items.forEach(item=>{ const promo=item.discount_price&&+item.discount_price>0&&+item.discount_price<+item.base_price; const price=promo?+item.discount_price:+item.base_price; const ch=el('div','sug-chip',`${esc(item.icon||'🍽️')} ${esc(item.name_en)} ${fmt(price)}`); ch.addEventListener('click',()=>addToCart(item,price)); bar.appendChild(ch); }); } function viewCustHistory(id){openCustomerDetail(id);} // Coupon async function openCoupon(){ const code=prompt('Enter coupon / discount code:');if(!code)return; try{ const r=await GET('/discounts?code='+encodeURIComponent(code)); const d=r.data?.data||r.data; const disc=Array.isArray(d)?d.find(x=>x.code===code):d; if(!disc){toast('Coupon not found or expired','a-r');return;} const sub=S.cart.reduce((s,l)=>s+l.price*l.qty,0); const amt=disc.type==='percent'?(sub*disc.value/100):+disc.value; S.ORDER.discount_id=disc.id;S.ORDER.discount_code=code;S.ORDER.discount_amount=amt; const strip=$('cart-coupon-strip'); if(strip){strip.style.display='block';strip.innerHTML=`
🏷️ ${esc(code)} applied-${fmt(amt)}
`;} renderCart();toast('βœ“ Coupon applied! -'+fmt(amt),'a-g'); }catch(e){toast('Invalid coupon: '+e.message,'a-r');} } function removeCoupon(){ S.ORDER.discount_id=null;S.ORDER.discount_code=null;S.ORDER.discount_amount=0; const s=$('cart-coupon-strip');if(s)s.style.display='none';renderCart(); } // Payment modal function openPayment(){ if(!S.cart.length){toast('Cart is empty','a-r');return;} const sub=S.cart.reduce((s,l)=>s+l.price*l.qty,0); const disc=S.ORDER.discount_amount||0,del=S.ORDER.delivery_fee||0; const vat=(sub-disc+del)*0.05,total=sub-disc+del+vat; const d=$('mo-payment-inner'); d.innerHTML=`

πŸ’³ Payment

${fmt(sub)}
Subtotal
${fmt(total)}
Total Due
${[['cash','πŸ’΅','Cash'],['card','πŸ’³','Card'],['transfer','πŸ“²','Transfer'],['split','βœ‚οΈ','Split']].map(([v,i,l])=>`
${i}
${l}
`).join('')}
`; $('mo-payment').style.display='flex';S._payMethod='cash'; $('pay-recv')?.addEventListener('input',()=>{ const rcv=parseFloat($('pay-recv').value)||0,chg=rcv-total; const row=$('pay-change-row'),cel=$('pay-change'); if(row&&cel){row.style.display=rcv>total?'flex':'none';cel.textContent=fmt(Math.max(0,chg));} }); } function setPayMethod(v){ document.querySelectorAll('#pay-methods .ot-card').forEach(c=>c.classList.toggle('sel',c.dataset.pm===v)); S._payMethod=v; $('pay-split-wrap').style.display=v==='split'?'block':'none'; $('pay-cash-wrap').style.display=v==='split'?'none':'block'; } async function placeOrder(){ if(!S.cart.length)return; const btn=$('pay-go-btn'),err=$('pay-err'); btn.disabled=true;btn.innerHTML='';if(err)err.style.display='none'; const o=S.ORDER; const sub=S.cart.reduce((s,l)=>s+l.price*l.qty,0); const disc=o.discount_amount||0,del=o.delivery_fee||0; const vat=(sub-disc+del)*0.05,total=sub-disc+del+vat; const typeMap={dine_in:'dine_in',takeaway:'takeaway',delivery:'delivery',car_pickup:'car_pickup',online:'delivery',party:'dine_in'}; const srcMap={dine_in:'pos',takeaway:'pos',delivery:'pos',car_pickup:'pos',online:'online',party:'pos'}; const noteParts=[S.cartNote||'',o.car_number?'Car: '+o.car_number:'',o.token?'Token: #'+o.token:'',o.online_ref?'Ref: '+o.online_ref:''].filter(Boolean); const payload={ order_type:typeMap[o.type||'dine_in'],source:srcMap[o.type||'dine_in'], table_id:o.table_id||null,customer_id:o.customer_id||null, customer_name:o.customer_name||null,customer_phone:o.customer_phone||null, discount_id:o.discount_id||null,notes:noteParts.join(' | ')||null, items:S.cart.map(l=>({item_id:l.id,quantity:l.qty,notes:l.itemNote||null})), }; if(!S.online){ Q.push({...payload,_pm:S._payMethod||'cash',_total:total}); updateSyncBadge();S.cart=[];S.ORDER.discount_amount=0;S.ORDER.discount_id=null; renderCart();$('mo-payment').style.display='none'; toast('βœ“ Saved offline β€” will sync when back online','a-y'); btn.disabled=false;btn.textContent='βœ“ Confirm & Pay';return; } try{ const res=await POST('/orders',payload); const orderId=res.data?.id||res.data?.order?.id; const serverTotal=res.data?.total||res.data?.grand_total||total; if(orderId){ let payments; if(S._payMethod==='split'){ const ca=parseFloat($('pay-cash-sp')?.value)||0; const cd=parseFloat($('pay-card-sp')?.value)||0; payments=[{method:'cash',amount:+ca.toFixed(3)},{method:'card',amount:+cd.toFixed(3)}]; }else{ payments=[{method:S._payMethod||'cash',amount:+parseFloat(serverTotal).toFixed(3)}]; } await POST('/payments',{order_id:orderId,payments}); // Save delivery order record if((o.type==='delivery'||o.type==='online')&&o.address_text){ try{await POST('/delivery/orders',{order_id:orderId,address:o.address_text, lat:o.address_lat,lng:o.address_lng,delivery_fee:o.delivery_fee||0});}catch{} } } const no=res.data?.order_no||orderId||''; S.cart=[];S.ORDER.discount_amount=0;S.ORDER.discount_id=null; renderCart();$('mo-payment').style.display='none'; toast(`βœ“ Order #${no} confirmed!`,'a-g'); }catch(e){ if(err)showAlert(err,e.message||'Payment failed'); Q.push({...payload,_pm:S._payMethod||'cash',_total:total}); updateSyncBadge();S.cart=[];renderCart();$('mo-payment').style.display='none'; toast('Saved offline as fallback','a-y'); }finally{btn.disabled=false;btn.textContent='βœ“ Confirm & Pay';} } async function holdOrder(){ if(!S.cart.length){toast('Cart is empty','a-r');return;} try{ const res=await POST('/orders',{order_type:(S.ORDER.type||'dine_in').replace('online','delivery'), source:'pos',table_id:S.ORDER.table_id||null,customer_id:S.ORDER.customer_id||null, customer_name:S.ORDER.customer_name||null,customer_phone:S.ORDER.customer_phone||null, notes:(S.cartNote||'')+' [ON HOLD]', items:S.cart.map(l=>({item_id:l.id,quantity:l.qty,notes:l.itemNote||null}))}); const id=res.data?.id; if(id)try{await POST('/orders/'+id+'/hold',{});}catch{} S.cart=[];renderCart();toast('βœ“ Order held β€” #'+(res.data?.order_no||id||''),'a-y'); }catch(e){toast('Hold failed: '+e.message,'a-r');} } function printKOT(){ if(!S.cart.length){toast('Cart is empty','a-r');return;} const o=S.ORDER;const w=window.open('','_blank','width=300,height=500'); w.document.write(`KOT

** KITCHEN ORDER **

${new Date().toLocaleString()}
Type: ${esc((o.type||'dine_in').replace('_',' ').toUpperCase())}
${o.table_name?`
Table: ${esc(o.table_name)}
`:''} ${o.token?`
Token: #${esc(o.token)}
`:''} ${o.customer_name?`
Customer: ${esc(o.customer_name)}
`:''} ${o.car_number?`
Car: ${esc(o.car_number)}
`:''}
${S.cart.map(l=>`
${l.qty}x ${esc(l.name_en)} ${l.selectedMods?.length?'
'+l.selectedMods.map(m=>esc(m.name)).join(', ')+'
':''} ${l.itemNote?'
β†’ '+esc(l.itemNote)+'
':''}
`).join('
')} ${S.cartNote?`
Order note: ${esc(S.cartNote)}
`:''} `); w.print(); } // ═══ DASHBOARD ═══════════════════════════════════════════════ function renderDashboard(c){ c.innerHTML=`

🏠 Dashboard

πŸ“‹ Recent Orders
`; loadDash(); } async function loadDash(){ try{ const r=await GET('/analytics/dashboard'); const k=r.data?.today||r.data?.kpis||r.data||{}; $('dash-kpis').innerHTML=[ ['πŸ’³','Revenue Today',fmt(k.revenue||0),'ty'], ['πŸ“‹','Orders Today',k.orders||0,'tp'], ['πŸ›’','Avg Order',fmt(k.avg_order||0),'tg'], ['πŸ‘₯','New Customers',k.new_customers||0,'to'], ].map(([ic,lbl,val,cls])=>`
${ic}
${esc(String(val))}
${lbl}
`).join(''); }catch(e){ if(e.message&&e.message.includes('Session expired')){ const c=$('dash-kpis'); if(c)c.innerHTML='
⚠️ Auth issue β€” try or
'; }else if($('dash-kpis'))$('dash-kpis').innerHTML=''; } try{ const r=await GET('/orders?per_page=12');const rows=r.data?.data||r.data||[]; $('dash-orders').innerHTML=rows.length ?`${rows.map(o=>``).join('')}
#TypeCustomerTotalStatusTime
#${esc(o.order_no||o.id)} ${esc((o.order_type||'').replace('_',' '))} ${esc(o.customer_name||'β€”')} ${fmt(o.total||o.grand_total||0)} ${esc(o.status)} ${fmtT(o.created_at)} ${['pending','confirmed','new','preparing'].includes(o.status)? `` :''}
` :`
πŸ“­

No orders today

`; }catch{$('dash-orders').innerHTML='

Could not load orders

';} } async function quickPay(id,total){ if(!confirm('Process cash payment of '+fmt(total)+'?'))return; try{await POST('/payments',{order_id:id,payments:[{method:'cash',amount:+parseFloat(total).toFixed(3)}]}); toast('βœ“ Payment processed','a-g');loadDash();} catch(e){toast('Payment failed: '+e.message,'a-r');} } // ═══ ORDERS ══════════════════════════════════════════════════ function renderOrders(c){ c.innerHTML=`

πŸ“‹ Orders

`; ['of-type','of-status','of-date'].forEach(id=>$(id)?.addEventListener('change',loadOrders)); let t;$('of-q')?.addEventListener('input',()=>{clearTimeout(t);t=setTimeout(loadOrders,300);}); loadOrders(); } async function loadOrders(){ const c=$('ord-list');if(!c)return; c.innerHTML='
'; const type=$('of-type')?.value||'',status=$('of-status')?.value||''; const date=$('of-date')?.value||'',q=$('of-q')?.value||''; let url='/orders?per_page=60'; if(type)url+=`&order_type=${type}`;if(status)url+=`&status=${status}`; if(date)url+=`&date=${date}`;if(q)url+=`&q=${encodeURIComponent(q)}`; try{ const r=await GET(url);const rows=r.data?.data||r.data||[]; if(!rows.length){c.innerHTML='
πŸ“­

No orders found

';return;} c.innerHTML=`
${rows.map(o=>``).join('')}
#TypeCustomerItemsTotalStatusTime
#${esc(o.order_no||o.id)} ${esc((o.order_type||'').replace('_',' '))} ${esc(o.customer_name||'β€”')} ${o.items_count||'β€”'} ${fmt(o.total||o.grand_total||0)} ${esc(o.status)} ${fmtT(o.created_at)} ${['pending','confirmed','new','preparing'].includes(o.status)?``:''} ${!['voided','completed','paid'].includes(o.status)?``:''}
`; }catch{c.innerHTML='

Failed to load orders

';} } async function voidOrd(id){ const reason=prompt('Void reason (required):');if(!reason)return; try{await POST('/orders/'+id+'/void',{reason});toast('Order voided','a-y');loadOrders();} catch(e){toast('Failed: '+e.message,'a-r');} } // ═══ KDS ═════════════════════════════════════════════════════ function renderKDS(c){ c.innerHTML=`

πŸ“Ÿ Kitchen Display

0 active
`; clearInterval(kdsInterval);kdsInterval=setInterval(loadKDS,15000);loadKDS(); } async function loadKDS(){ const grid=$('kds-grid'),cnt=$('kds-cnt');if(!grid){clearInterval(kdsInterval);return;} try{const r=await GET('/kds/orders');S.kdsOrders=r.data||[];}catch{S.kdsOrders=[];} if(cnt)cnt.textContent=S.kdsOrders.length+' active'; if(!S.kdsOrders.length){ grid.innerHTML='
βœ…

All clear! Kitchen is empty.

';return;} grid.innerHTML='';grid.className='kds-grid'; const colors={bar:'#3b82f6',kitchen:'#f75555',grill:'#f7743b',juice:'#f7b731',cold:'#8b5cf6',default:'#4f8ef7'}; S.kdsOrders.forEach(o=>{ const oid=o.order_id||o.id; const color=colors[o.station]||colors.default; const m=o.age_minutes??mins(o.created_at)??0; const tc=m<5?'t-g':m<10?'t-y':'t-r'; const tick=el('div','ticket');tick.style.borderTopColor=color; tick.innerHTML=`
#${esc(o.order_no||oid)} ${esc((o.order_type||'').replace('_',' '))} ${o.table_name?`${esc(o.table_name)}`:''} ${o.customer_name?`πŸ‘€${esc(o.customer_name)}`:''} ${m}m
${(o.items||[]).map(item=>`
${['ready','served'].includes(item.status)?'βœ…':'⬜'}
${item.quantity||1}Γ— ${esc(item.item_name||item.name||'Item')}
${item.notes?`
πŸ“ ${esc(item.notes)}
`:''} ${item.modifier_summary?`
${esc(item.modifier_summary)}
`:''}
`).join('')}
`; tick.querySelectorAll('.tk-r').forEach(row=>{ row.addEventListener('click',()=>{ const s=row.querySelector('span');s.textContent=s.textContent==='βœ…'?'⬜':'βœ…'; row.classList.toggle('done',s.textContent==='βœ…'); }); }); tick.querySelector('.kds-bump-btn').addEventListener('click',async e=>{ const b=e.currentTarget;b.disabled=true;b.innerHTML=''; try{await POST('/kds/order/bump',{order_id:+b.dataset.oid});loadKDS();} catch{b.disabled=false;b.textContent='βœ“ Bump β€” All Ready';} }); grid.appendChild(tick); }); } // ═══ DELIVERY BOARD ══════════════════════════════════════════ function renderDeliveryBoard(c){ c.innerHTML=`

🚚 Live Delivery Board

`; loadDelBoard(); } async function loadDelBoard(){ const lc=$('del-orders'),stats=$('del-stats');if(!lc)return; try{ const r=await GET('/delivery/orders?per_page=60');const orders=r.data?.data||r.data||[]; const c={pending:0,assigned:0,on_way:0,delivered:0}; orders.forEach(o=>c[o.status]=(c[o.status]||0)+1); if(stats)stats.innerHTML=[ ['πŸ•','Pending',c.pending||0,'by'],['πŸ›΅','On Way',(c.assigned||0)+(c.on_way||0),'bb'], ['βœ…','Delivered',c.delivered||0,'bg'],['❌','Failed',c.failed||0,'br'], ].map(([i,l,v,cls])=>`
${i}
${v}
${l}
`).join(''); lc.innerHTML=orders.length?`
${orders.map(o=>``).join('')}
Order #CustomerAreaAddressDriverStatusMap
#${esc(o.order_no||o.order_id)} ${esc(o.customer_name||'β€”')} ${esc(o.area||'β€”')} ${esc(o.address||'β€”')} ${esc(o.driver_name||'Unassigned')} ${esc(o.status)} ${o.lat&&o.lng?``:'β€”'}
` :'
🚚

No active deliveries

'; }catch{lc.innerHTML='

Failed to load

';} } // ═══ CUSTOMERS ═══════════════════════════════════════════════ function renderCustomers(c){ c.innerHTML=`

πŸ‘₯ Customers

`; let t; $('cust-q').addEventListener('input',()=>{clearTimeout(t);t=setTimeout(()=>loadCusts($('cust-q').value,''),300);}); $('cust-phone-q').addEventListener('input',()=>{clearTimeout(t);t=setTimeout(()=>loadCusts('',$('cust-phone-q').value),300);}); loadCusts(); } async function loadCusts(q='',phone=''){ const lc=$('cust-list');if(!lc)return; lc.innerHTML='
'; try{ let url='/customers?per_page=60'; if(q)url+=`&search=${encodeURIComponent(q)}`; if(phone)url+=`&phone=${encodeURIComponent(phone)}`; const r=await GET(url);const rows=r.data?.data||r.data||[]; if(!rows.length){lc.innerHTML='
πŸ‘₯

No customers found

';return;} lc.innerHTML=`
${rows.map(cu=>``).join('')}
NamePhoneOrdersTotal SpentPointsLast Order
${esc(cu.name)} ${esc(cu.phone||'β€”')} ${cu.total_orders||0} ${fmt(cu.total_spent||0)} ${cu.loyalty_points||0} pts ${fmtD(cu.last_order_at)}
`; }catch{lc.innerHTML='

Failed to load

';} } function showAddCustomerModal(){$('cust-add-mo').style.display='flex';$('ca-name').focus();} async function saveNewCust(){ const n=$('ca-name')?.value?.trim(),p=$('ca-phone')?.value?.trim(); if(!n||!p){showAlert($('ca-err'),'Name and phone required');return;} try{ await POST('/customers',{name:n,phone:p,email:$('ca-email')?.value||null,notes:$('ca-notes')?.value||null}); $('cust-add-mo').style.display='none'; ['ca-name','ca-phone','ca-email','ca-notes'].forEach(id=>{if($(id))$(id).value='';}); toast('Customer added','a-g');loadCusts(); }catch(e){showAlert($('ca-err'),e.message||'Save failed');} } async function openCustomerDetail(id){ const d=$('mo-customer-inner'); d.innerHTML='
'; $('mo-customer').style.display='flex'; try{ const [cr,or,ar]=await Promise.all([ GET('/customers/'+id), GET('/orders?customer_id='+id+'&per_page=25'), GET('/customers/'+id+'/address').catch(()=>({data:[]})), ]); const cu=cr.data;const orders=or.data?.data||or.data||[];const addrs=ar.data||[]; d.innerHTML=`
${initials(cu.name)}
${esc(cu.name)}
${esc(cu.phone)}
${cu.total_orders||0}
Orders
${fmt(cu.total_spent||0)}
Total Spent
${cu.loyalty_points||0}
Points
${fmtD(cu.last_order_at)}
Last Order
Orders (${orders.length})
Addresses (${addrs.length})
Edit Info
${orders.length?`${orders.map(o=>``).join('')}
#TypeTotalStatusDate
#${esc(o.order_no||o.id)} ${esc((o.order_type||'').replace('_',' '))} ${fmt(o.total||o.grand_total||0)} ${esc(o.status)} ${fmtD(o.created_at)}
` :'
πŸ“­

No orders

'}
${addrs.map(a=>`
${a.label==='Home'?'🏠':a.label==='Work'?'🏒':'πŸ“'}
${esc(a.label||'Address')}
${esc(a.address)}
${a.area?`
${esc(a.area)}
`:''} ${a.lat&&a.lng?`πŸ“ View map`:''}
${a.is_default?'Default':''}
`).join('')||'

No saved addresses

'}
`; }catch(e){d.innerHTML=`
${esc(e.message)}
`;} } function swCustTab(tab,btn){ ['orders','addresses','info'].forEach(t=>{const e=$('ct-'+t);if(e)e.classList.toggle('on',t===tab);}); document.querySelectorAll('#mo-customer-inner .tab').forEach(t=>t.classList.remove('on')); btn.classList.add('on'); } async function updateCust(id){ try{await PUT('/customers/'+id,{email:$('ci-email')?.value,status:$('ci-status')?.value,notes:$('ci-notes')?.value}); toast('Saved','a-g');}catch(e){toast('Failed: '+e.message,'a-r');} } function orderForCust(phone,name,id){ S.ORDER.customer_id=id;S.ORDER.customer_name=name;S.ORDER.customer_phone=phone; openNewOrder(); } // ═══ PARTY ORDERS ════════════════════════════════════════════ function renderPartyOrders(c){ c.innerHTML=`

πŸŽ‰ Party Orders

`; loadPartyList(); } async function loadPartyList(){ const lc=$('party-list');if(!lc)return; try{ const r=await GET('/orders?per_page=60'); const rows=(r.data?.data||r.data||[]).filter(o=>o.notes&&o.notes.includes('[PARTY]')); if(!rows.length){ lc.innerHTML=`
πŸŽ‰

No party orders yet

`;return;} lc.innerHTML=`
${rows.map(o=>{ const n=o.notes||''; const dm=n.match(/Date:([^\s|]+(?:\s\S+)?)/),gm=n.match(/Guests:(\d+)/),vm=n.match(/Venue:([^|]+)/); return `
πŸŽ‰ #${esc(o.order_no||o.id)}
${esc(o.status)}
${esc(o.customer_name||'Guest')} Β· ${esc(o.customer_phone||'')}
${dm?`
πŸ“… ${dm[1].trim()}
`:''} ${gm?`
πŸ‘₯ ${gm[1]} guests
`:''} ${vm?`
πŸ›οΈ ${vm[1].trim()}
`:''}
${fmt(o.total||o.grand_total||0)}
`}).join('')}
`; }catch{lc.innerHTML='

Failed to load

';} } // ═══ PROMOTIONS ══════════════════════════════════════════════ function renderPromotions(c){ c.innerHTML=`

🏷️ Promotions & Coupons

`; loadPromos(); } async function loadPromos(){ const lc=$('promo-list');if(!lc)return; try{ const r=await GET('/discounts?per_page=60');const rows=r.data?.data||r.data||[]; lc.innerHTML=rows.length?`
${rows.map(d=>``).join('')}
NameTypeValueCodeMin OrderUsedStatus
${esc(d.name)} ${esc(d.type)} ${d.type==='percent'?d.value+'%':fmt(d.value)} ${d.code?`${esc(d.code)}`:'auto-apply'} ${d.min_order?fmt(d.min_order):'β€”'} ${d.used_count||0} ${d.is_active?'Active':'Off'}
` :'
🏷️

No promotions yet

'; }catch{lc.innerHTML='

Failed

';} } function showPromoModal(){$('promo-mo').style.display='flex';} async function savePromo(){ const name=$('pm-name')?.value?.trim();if(!name){showAlert($('pm-err'),'Name required');return;} try{ await POST('/discounts',{name,type:$('pm-type')?.value,value:+($('pm-val')?.value||0), code:$('pm-code')?.value?.toUpperCase()||null,min_order:+($('pm-min')?.value||0), max_discount:+($('pm-max')?.value||0),valid_from:$('pm-from')?.value||null, valid_until:$('pm-to')?.value||null,is_active:1}); $('promo-mo').style.display='none';toast('Promotion saved','a-g');loadPromos(); }catch(e){showAlert($('pm-err'),e.message||'Save failed');} } async function delPromo(id){if(!confirm('Delete promotion?'))return; try{await DEL('/discounts/'+id);toast('Deleted','a-y');loadPromos();}catch(e){toast(e.message,'a-r');} } // ═══ MENU MANAGEMENT ═════════════════════════════════════════ function renderMenu(c){ c.innerHTML=`

🍽️ Menu & Items

All Items
Categories
Sizes
`; loadMenuItems(); } async function loadMenuItems(){ const lc=$('menu-content');if(!lc)return; try{ const [ir,cr]=await Promise.all([GET('/menu/items?per_page=300'),GET('/menu/categories')]); const items=ir.data||[];const cm={};(cr.data||[]).forEach(c=>cm[c.id]=c.name_en); lc.innerHTML=`
${items.map(i=>``).join('')}
ItemCategoryBase PricePromo PriceStatus
${esc(i.icon||'🍽️')}
${esc(i.name_en)}
${i.name_ar?`
${esc(i.name_ar)}
`:''}
${esc(cm[i.category_id]||'β€”')} ${fmt(i.base_price)} ${i.discount_price&&+i.discount_price>0?`${fmt(i.discount_price)}`:'β€”'} ${i.is_active?'Active':'Hidden'}
`; }catch{lc.innerHTML='

Failed to load

';} } function menuTab(tab,btn){ document.querySelectorAll('#content .tab').forEach(t=>t.classList.remove('on'));btn.classList.add('on'); if(tab==='items')loadMenuItems(); else if(tab==='cats')loadMenuCats(); else loadMenuSizes(); } async function loadMenuCats(){ const lc=$('menu-content');if(!lc)return; try{const r=await GET('/menu/categories');const cats=r.data||[]; lc.innerHTML=`
${cats.map(c=>``).join('')}
Name ENName ARSort
${esc(c.name_en)}${esc(c.name_ar||'β€”')} ${c.sort_order||0}
`; }catch{$('menu-content').innerHTML='

Failed

';} } async function loadMenuSizes(){ $('menu-content').innerHTML='
Sizes are managed per-item. Edit an item to add sizes.
'; } async function addCat(){ const n=prompt('Category name (English):');if(!n)return; const ar=prompt('Arabic name (optional):',''); try{await POST('/admin/categories',{name_en:n,name_ar:ar||'',sort_order:0});toast('Added','a-g');loadMenuCats();} catch(e){toast(e.message,'a-r');} } async function delCat(id){if(!confirm('Delete category?'))return; try{await DEL('/admin/categories/'+id);toast('Deleted','a-y');loadMenuCats();}catch(e){toast(e.message,'a-r');} } async function delItem(id){if(!confirm('Delete item?'))return; try{await DEL('/admin/items/'+id);toast('Deleted','a-y');loadMenuItems();}catch(e){toast(e.message,'a-r');} } async function openItemModal(id){ let item=null,cats=[]; try{const cr=await GET('/menu/categories');cats=cr.data||[];}catch{} if(id){try{const r=await GET('/menu/items/'+id);item=r.data;}catch(e){toast(e.message,'a-r');return;}} const d=$('item-mo-inner'); d.innerHTML=`

${item?'Edit':'Add'} Menu Item

Active / Visible in POS
`; $('item-mo').style.display='flex'; } async function saveItem(id){ const n=$('im-ne')?.value?.trim(),p=parseFloat($('im-price')?.value); if(!n||isNaN(p)){showAlert($('im-err'),'Name and price required');return;} const data={name_en:n,name_ar:$('im-na')?.value||'',category_id:+$('im-cat')?.value, base_price:p,discount_price:parseFloat($('im-promo')?.value)||null, icon:$('im-icon')?.value||'🍽️',description:$('im-desc')?.value||'', calories:parseInt($('im-cal')?.value)||null,prep_time:parseInt($('im-prep')?.value)||null, is_active:$('im-active')?.checked?1:0}; try{ if(id&&id!=='null')await PUT('/admin/items/'+id,data);else await POST('/admin/items',data); $('item-mo').style.display='none';toast('Saved','a-g');loadMenuItems(); }catch(e){showAlert($('im-err'),e.message||'Save failed');} } // ═══ TABLES & TOKENS ═════════════════════════════════════════ function renderTables(c){ c.innerHTML=`

πŸ—ΊοΈ Tables & Tokens

Table Map
Active Tokens
Sessions
`; loadTableMap2(); } async function loadTableMap2(){ const lc=$('tables-content');if(!lc)return; try{ const [sr,tr]=await Promise.all([GET('/tables/sections'),GET('/tables')]); const secs=sr.data||[];const tables=tr.data||[]; const byS={};tables.forEach(t=>{const k=t.section_id||0;(byS[k]=byS[k]||[]).push(t);}); const occ=tables.filter(t=>t.status==='occupied').length; lc.innerHTML=`
${tables.length}
Total Tables
${occ}
Occupied
${tables.length-occ}
Available
${secs.map(s=>`
${esc(s.name)}
${(byS[s.id]||[]).map(t=>tblCard(t)).join('')}
`).join('')} ${byS[0]?.length?`
Tables
${byS[0].map(t=>tblCard(t)).join('')}
`:''}`; lc.querySelectorAll('.tbl-card').forEach(c=>{ c.addEventListener('click',()=>{ const tid=+c.dataset.tid,tname=c.dataset.tname; if(confirm(`Open order at ${tname}?`)){S.ORDER.type='dine_in';S.ORDER.table_id=tid;S.ORDER.table_name=tname;navigate('pos');} }); }); }catch{lc.innerHTML='

Failed to load

';} } function tblCard(t){ const s=t.status||'free'; return `
${esc(t.name||'T'+t.id)}
${esc(s)}${t.seats?' Β· '+t.seats+'p':''}
`; } function tablesTab(tab,btn){ document.querySelectorAll('#content .tab').forEach(t=>t.classList.remove('on'));btn.classList.add('on'); if(tab==='map')loadTableMap2();else if(tab==='tokens')renderTokensBoard();else renderSessionsList(); } async function renderTokensBoard(){ const lc=$('tables-content');if(!lc)return; lc.innerHTML=`
🎫 Token / Page Management
Tokens are assigned when creating orders. Currently active tokens from open orders are shown below.

Print Standalone Token
`; try{ const r=await GET('/orders?status=preparing&per_page=40'); const orders=(r.data?.data||r.data||[]).filter(o=>o.notes&&/Token:\s*#\w+/.test(o.notes)); const g=$('token-grid'); if(!orders.length){if(g)g.innerHTML='
No active tokens
';return;} if(g)g.innerHTML=orders.map(o=>{ const m=(o.notes||'').match(/Token:\s*#(\w+)/);const token=m?m[1]:'?'; return `
#${esc(token)}
${esc(o.customer_name||o.order_no||'')}
${esc(o.status)}
`; }).join(''); }catch{} } function printToken2(){ const num=$('tk-num')?.value;const name=$('tk-name')?.value||''; if(!num){toast('Enter token number','a-r');return;} const w=window.open('','_blank','width=200,height=220'); w.document.write(`Token
TOKEN
#${num}
${name}
`);w.print(); } async function addTable(){ const n=prompt('Table name/number:');if(!n)return; const seats=parseInt(prompt('Seats:','4'))||4; try{await POST('/tables',{name:n,seats,status:'free'});toast('Table added','a-g');loadTableMap2();} catch(e){toast(e.message,'a-r');} } async function openNewSession(){ try{await POST('/sessions/open',{notes:'Opened from POS'});toast('Session opened','a-g');} catch(e){toast('Session failed: '+e.message,'a-r');} } async function renderSessionsList(){ const lc=$('tables-content');if(!lc)return; try{const r=await GET('/sessions/history?per_page=20';const rows=r.data?.data||r.data||[]; lc.innerHTML=rows.length?`
${rows.map(s=>``).join('')}
SessionOpened ByOrdersSalesStatusDate
#${s.id}${esc(s.opened_by_name||'β€”')} ${s.order_count||0}${fmt(s.total_sales||0)} ${s.closed_at?'Closed':'Open'} ${fmtD(s.opened_at)}
` :'

No sessions

'; }catch{lc.innerHTML='

Failed

';} } // ═══ INVENTORY ═══════════════════════════════════════════════ function renderInventory(c){ c.innerHTML=`

πŸ“¦ Stock & Inventory

Stock Levels
Waste Log
Purchase Orders
`; loadStock(); } async function loadStock(){ const lc=$('inv-content');if(!lc)return; try{const r=await GET('/inventory/ingredients');const rows=r.data?.data||r.data||[]; const low=rows.filter(r=>+r.qty<=+r.min_qty).length; lc.innerHTML=`${low?`
⚠️ ${low} item(s) below minimum stock level
`:''}
${rows.map(r=>``).join('')}
IngredientUnitStockMinCost/UnitStatus
${esc(r.name)}${esc(r.unit)} ${(+r.qty||0).toFixed(2)} ${r.min_qty||0} ${r.cost_per_unit?fmt(r.cost_per_unit):'β€”'} ${+r.qty<=+r.min_qty?'⚠ Low':'OK'}
`; }catch{lc.innerHTML='

Failed

';} } function invTab(tab,btn){document.querySelectorAll('#content .tab').forEach(t=>t.classList.remove('on'));btn.classList.add('on'); if(tab==='stock')loadStock();else if(tab==='waste')loadWaste();else loadPOs();} async function loadWaste(){ const lc=$('inv-content');if(!lc)return; try{const r=await GET('/inventory/waste/summary';const rows=r.data?.data||r.data||[]; lc.innerHTML=rows.length?`
${rows.map(w=>``).join('')}
IngredientQtyReasonByDate
${esc(w.ingredient_name||'β€”')} ${w.qty} ${esc(w.unit||'')} ${esc(w.reason||'β€”')} ${esc(w.recorded_by_name||'β€”')} ${fmtD(w.created_at)}
` :'

No waste records

'; }catch{lc.innerHTML='

Failed

';} } async function loadPOs(){ const lc=$('inv-content');if(!lc)return; try{const r=await GET('/inventory/purchase-orders?per_page=30';const rows=r.data?.data||r.data||[]; lc.innerHTML=rows.length?`
${rows.map(p=>``).join('')}
PO #SupplierTotalStatusDate
${esc(p.po_no||p.id)}${esc(p.supplier_name||'β€”')} ${fmt(p.total||0)} ${esc(p.status)} ${fmtD(p.created_at)}
` :'

No purchase orders

'; }catch{lc.innerHTML='

Failed

';} } function showIngModal(){ $('ing-mo-inner').innerHTML=`

Add Ingredient / Stock Item

`; $('ing-mo').style.display='flex'; } async function saveIng(){ const n=$('ing-name')?.value?.trim();if(!n){showAlert($('ing-err'),'Name required');return;} try{await POST('/inventory/ingredients',{name:n,unit:$('ing-unit')?.value, min_qty:+($('ing-min')?.value||0),qty:+($('ing-qty')?.value||0),cost_per_unit:+($('ing-cost')?.value||0)}); $('ing-mo').style.display='none';toast('Saved','a-g');loadStock();} catch(e){showAlert($('ing-err'),e.message);} } async function adjStock(id,name){ const qty=parseFloat(prompt(`New stock quantity for "${name}":`));if(isNaN(qty))return; try{await PUT('/inventory/ingredients/'+id,{qty});toast('Updated','a-g');loadStock();} catch(e){toast(e.message,'a-r');} } // ═══ VENDORS ═════════════════════════════════════════════════ function renderVendors(c){ c.innerHTML=`

🏭 Vendors & Purchasing

`; loadVendors(); } async function loadVendors(){ const lc=$('vendor-list');if(!lc)return; try{const r=await GET('/inventory/suppliers?per_page=60';const rows=r.data?.data||r.data||[]; lc.innerHTML=rows.length?`
${rows.map(s=>``).join('')}
VendorContactPhoneEmailTerms
${esc(s.name)}${esc(s.contact_name||'β€”')} ${esc(s.phone||'β€”')}${esc(s.email||'β€”')} ${s.payment_terms||0} days
` :'
🏭

No vendors yet

'; }catch{lc.innerHTML='

Failed

';} } async function addVendor(){ const n=prompt('Vendor / company name:');if(!n)return; const p=prompt('Phone:','');const e=prompt('Email:',''); try{await POST('/inventory/suppliers',{name:n,phone:p,email:e,payment_terms:30});toast('Added','a-g');loadVendors();} catch(e){toast(e.message,'a-r');} } async function delVendor(id){if(!confirm('Delete vendor?'))return; try{await DEL('/inventory/suppliers/'+id);toast('Deleted','a-y');loadVendors();}catch(e){toast(e.message,'a-r');} } // ═══ DELIVERY MGMT ═══════════════════════════════════════════ function renderDeliveryMgmt(c){ c.innerHTML=`

πŸ›΅ Delivery Management

Delivery Zones
Drivers
`; loadZones(); } async function loadZones(){ const lc=$('del-mgmt-content');if(!lc)return; try{const r=await GET('/delivery/zones');const rows=r.data||[]; lc.innerHTML=`
${rows.map(z=>``).join('')}
Zone / AreaFee (QAR)Min OrderEst. Time
${esc(z.name)}${fmt(z.fee||0)} ${fmt(z.min_order||0)}${z.estimated_min||0} min
`; }catch{lc.innerHTML='

Failed

';} } async function loadDrivers(){ const lc=$('del-mgmt-content');if(!lc)return; try{const r=await GET('/delivery/drivers');const rows=r.data||[]; lc.innerHTML=rows.length?`
${rows.map(d=>``).join('')}
DriverPhoneVehicleStatusActive
${esc(d.name)}${esc(d.phone||'β€”')} ${esc(d.vehicle_no||'β€”')} ${d.is_online?'Online':'Offline'} ${d.active_orders||0}
` :'

No drivers configured

'; }catch{lc.innerHTML='

Failed

';} } function delTab(tab,btn){document.querySelectorAll('#content .tab').forEach(t=>t.classList.remove('on'));btn.classList.add('on'); if(tab==='zones')loadZones();else loadDrivers();} async function addZone(){ const n=prompt('Zone name:');if(!n)return; const fee=parseFloat(prompt('Delivery fee (QAR):','2.000'))||0; const min=parseFloat(prompt('Minimum order (QAR):','10.000'))||0; const eta=parseInt(prompt('Estimated minutes:','30'))||30; try{await POST('/delivery/zones',{name:n,fee,min_order:min,estimated_min:eta});S.zones=[];toast('Zone added','a-g');loadZones();} catch(e){toast(e.message,'a-r');} } async function delZone(id){if(!confirm('Delete zone?'))return; try{await DEL('/delivery/zones/'+id);S.zones=[];toast('Deleted','a-y');loadZones();}catch(e){toast(e.message,'a-r');} } // ═══ HR ══════════════════════════════════════════════════════ function renderHR(c){ c.innerHTML=`

πŸ‘” HR & Employees

Employees
Attendance
Payroll
Leave Requests
`; loadEmps(); } async function loadEmps(){ const lc=$('hr-content');if(!lc)return; try{const r=await GET('/hr/employees?per_page=60');const rows=r.data?.data||r.data||[]; lc.innerHTML=rows.length?`
${rows.map(e=>``).join('')}
NamePositionPhoneSalaryStatus
${esc(e.name)}${esc(e.position||e.role||'β€”')} ${esc(e.phone||'β€”')} ${e.salary?fmt(e.salary):'β€”'} ${esc(e.status)}
` :'
πŸ‘”

No employees

'; }catch{lc.innerHTML='

Failed

';} } function hrTab(tab,btn){document.querySelectorAll('#content .tab').forEach(t=>t.classList.remove('on'));btn.classList.add('on'); if(tab==='emp')loadEmps();else $('hr-content').innerHTML='

'+tab+' records β€” coming soon

';} async function addEmp(){ const n=prompt('Employee name:');if(!n)return; const p=prompt('Phone:','');const pos=prompt('Position/Role:','Cashier'); const sal=parseFloat(prompt('Monthly salary (QAR):','0'))||0; try{await POST('/hr/employees',{name:n,phone:p,position:pos,salary:sal,status:'active'}); toast('Employee added','a-g');loadEmps();}catch(e){toast(e.message,'a-r');} } // ═══ FINANCE ═════════════════════════════════════════════════ function renderFinance(c){ c.innerHTML=`

πŸ’° Finance & Bank

Expenses
VAT Summary
Bank Contra
P&L
`; loadExpenses(); } async function loadExpenses(){ const lc=$('fin-content');if(!lc)return; try{const r=await GET('/finance/expenses?per_page=60');const rows=r.data?.data||r.data||[]; const total=rows.reduce((s,e)=>s+(+e.amount||0),0); lc.innerHTML=`
Month Total${fmt(total)}
${rows.map(e=>``).join('')}
CategoryDescriptionAmountDate
${esc(e.category||'β€”')} ${esc(e.description||'β€”')} ${fmt(e.amount)} ${fmtD(e.expense_date||e.created_at)}
`; }catch{lc.innerHTML='

Failed

';} } function finTab(tab,btn){document.querySelectorAll('#content .tab').forEach(t=>t.classList.remove('on'));btn.classList.add('on'); if(tab==='exp')loadExpenses();else if(tab==='vat')loadVAT();else if(tab==='bank')renderBankContra();else renderPL();} async function loadVAT(){ const lc=$('fin-content');if(!lc)return; try{const r=await GET('/finance/vat?period=month');const d=r.data||{}; lc.innerHTML=`
${fmt(d.total_sales||0)}
Total Sales
${fmt(d.vat_collected||d.vat_amount||0)}
VAT Collected (5%)
`; }catch{lc.innerHTML='

No VAT data available

';} } function renderBankContra(){ const lc=$('fin-content');if(!lc)return; lc.innerHTML=`
πŸ’³ Daily Cash / Bank Reconciliation
Reconcile end-of-day cash collected vs bank deposit to identify discrepancies.
`; GET('/analytics/dashboard').then(r=>{ const cash=r.data?.today?.cash_sales||0; const e=$('bc-exp');if(e)e.value=parseFloat(cash).toFixed(3); }).catch(()=>{}); } function calcContra(){ const exp=parseFloat($('bc-exp')?.value)||0,dep=parseFloat($('bc-dep')?.value)||0; const diff=dep-exp;const lc=$('bc-diff');if(!lc)return; lc.textContent=`Difference: ${diff>=0?'+':''}${fmt(diff)}`; lc.className='fw7 ts mt2 '+(Math.abs(diff)<0.01?'tg':diff>0?'tp':'tr-c'); } function renderPL(){ const lc=$('fin-content');if(!lc)return; lc.innerHTML='

P&L report β€” connects to Finance API, coming in next update

'; } async function addExpense(){ const cat=prompt('Category (Utilities/Supplies/Rent/Other):');if(!cat)return; const desc=prompt('Description:',''); const amt=parseFloat(prompt('Amount (QAR):'));if(isNaN(amt))return; try{await POST('/finance/expenses',{category:cat,description:desc,amount:amt,expense_date:new Date().toISOString().slice(0,10)}); toast('Expense added','a-g');loadExpenses();}catch(e){toast(e.message,'a-r');} } // ═══ REPORTS ═════════════════════════════════════════════════ function renderReports(c){ c.innerHTML=`

πŸ“Š Reports

to
Summary
Top Items
By Type
`; loadRpt(); } async function loadRpt(){ const lc=$('rpt-content');if(!lc)return; lc.innerHTML='
'; const from=$('rpt-from')?.value||'',to=$('rpt-to')?.value||''; try{ const r=await GET('/analytics/dashboard?date_from='+from+'&date_to='+to); const d=r.data||{};const k=d.today||d.kpis||d;const pk=d.prev_kpis||{}; const pct=(a,b)=>!+b?'β€”':((+a-+b)/(+b)*100>0?'↑+':'↓')+Math.abs(((+a-+b)/(+b)*100)).toFixed(1)+'%'; lc.innerHTML=`
${fmt(k.revenue||0)}
Total Revenue
${pct(k.revenue,pk.revenue)} vs prev
${k.orders||0}
Total Orders
${pct(k.orders,pk.orders)} vs prev
${fmt(k.avg_order||0)}
Avg Order Value
${k.new_customers||0}
New Customers
${d.by_type?`
Orders by Type
${Object.entries(d.by_type).map(([t,v])=>`
${v}
${t.replace('_',' ')}
`).join('')}
`:''} ${d.top_items?.length?`
πŸ† Best Selling Items
${d.top_items.slice(0,12).map((i,n)=>``).join('')}
#ItemQtyRevenue
${n+1} ${esc(i.name||i.item_name||'β€”')} ${i.qty||i.quantity||0} ${fmt(i.revenue||i.total||0)}
`:''}`; }catch(e){lc.innerHTML=`

Report failed: ${esc(e.message)}

`;} } function rptTab(btn){document.querySelectorAll('#content .tab').forEach(t=>t.classList.remove('on'));btn.classList.add('on');loadRpt();} function emailRpt(){ const email=prompt('Send report to email:');if(!email)return; toast('Report scheduled to: '+email,'a-b'); } // ═══ ANALYTICS ═══════════════════════════════════════════════ function renderAnalytics(c){ c.innerHTML=`

πŸ“ˆ Analytics & Trends

`; loadAnalytics(); } async function loadAnalytics(){ const lc=$('analytics-content');if(!lc)return; try{ const r=await GET('/analytics/dashboard');const d=r.data||{}; const hours=d.hourly||d.heatmap||[];const maxH=hours.reduce((m,h)=>Math.max(m,+h.orders||+h.count||0),1); lc.innerHTML=`
⏰ Hourly Volume
${hours.length?hours.map(h=>{ const v=+h.orders||+h.count||0,p=Math.max(4,Math.round(v/maxH*100)); const label=(h.hour!=null?h.hour+':00':(h.label||'')); return `
`; }).join(''):'
No data
'}
12am6am12pm6pm11pm
🍽️ Order Type Split
${d.by_type?Object.entries(d.by_type).map(([t,v])=>{ const total=Object.values(d.by_type).reduce((s,n)=>s+(+n||0),0); const pct=total?Math.round((+v/total)*100):0; return `
${t.replace('_',' ')}
${pct}%
`; }).join(''):'
No breakdown data
'}
🎯 Key Metrics
${[ ['Avg Revenue/Order',fmt(d.today?.avg_order||0),'ty'], ['Busiest Hour',(hours.sort((a,b)=>(+b.orders||0)-(+a.orders||0))[0]?.hour??'β€”')+':00','tp'], ['Top Item',d.top_items?.[0]?.name||'β€”','tg'], ['New Customers',d.today?.new_customers||0,'to'], ].map(([l,v,cls])=>`
${esc(String(v))}
${l}
`).join('')}
`; }catch(e){lc.innerHTML=`

Analytics failed: ${esc(e.message)}

`;} } // ═══ SETTINGS ════════════════════════════════════════════════ function renderSettings(c){ c.innerHTML=`

βš™οΈ Settings

General
Users
Roles & Permissions
Receipt
`; loadGenSettings(); } async function loadGenSettings(){ const lc=$('sett-content');if(!lc)return; try{const r=await GET('/settings');const s=r.data||{}; lc.innerHTML=`
Restaurant Information
`; }catch{lc.innerHTML='

Failed to load settings

';} } async function saveGenSettings(){ try{await PUT('/settings',{ app_name:$('s-name')?.value,currency:$('s-cur')?.value, vat_rate:+($('s-vat')?.value||5),service_charge_rate:+($('s-sc')?.value||0), phone:$('s-phone')?.value,address:$('s-addr')?.value}); toast('Settings saved','a-g');}catch(e){toast(e.message,'a-r');} } function settTab(tab,btn){document.querySelectorAll('#content .tab').forEach(t=>t.classList.remove('on'));btn.classList.add('on'); if(tab==='gen')loadGenSettings();else if(tab==='users')loadUsers();else if(tab==='roles')loadRoles();else loadReceiptSettings();} async function loadUsers(){ const lc=$('sett-content');if(!lc)return; try{const r=await GET('/users?per_page=60');const rows=r.data?.data||r.data||[]; lc.innerHTML=`
${rows.map(u=>``).join('')}
NameEmailRoleStatus
${esc(u.name)}${esc(u.email)} ${esc(u.role_name||u.role||'β€”')} ${esc(u.status)} ${u.id!==S.user?.id?``:'(you)'}
`; }catch{lc.innerHTML='

Failed

';} } async function addUser(){ const n=prompt('Full name:');if(!n)return; const e=prompt('Email:');if(!e)return; const p=prompt('Password:','password123');if(!p)return; const roles=['super_admin','manager','cashier','chef','driver','waiter']; const role=prompt('Role ('+roles.join(' / ')+'):','cashier'); try{await POST('/users',{name:n,email:e,password:p,role_id:roles.indexOf(role)+1||3,status:'active'}); toast('User added','a-g');loadUsers();}catch(e){toast(e.message,'a-r');} } async function delUser(id){if(!confirm('Delete user?'))return; try{await DEL('/users/'+id);toast('Deleted','a-y');loadUsers();}catch(e){toast(e.message,'a-r');} } async function loadRoles(){ const lc=$('sett-content');if(!lc)return; try{const r=await GET('/roles');const rows=r.data||[]; lc.innerHTML=`
Roles define what each user can access. Edit permissions in code or via the API.
${rows.map(r=>``).join('')}
RoleDisplay NamePermissions
${esc(r.slug||r.name)}${esc(r.display_name||r.name)} ${esc(JSON.stringify(r.permissions).replace(/["\[\]]/g,'').slice(0,80))}
`; }catch{lc.innerHTML='

Failed to load roles

';} } function loadReceiptSettings(){ const lc=$('sett-content');if(!lc)return; lc.innerHTML=`
Receipt / Thermal Printer Settings
Show VAT breakdown on receipt
`; } // ═══ OFFLINE SYNC ════════════════════════════════════════════ function updateSyncBadge(){ const q=Q.get();const banner=$('sync-banner'),btn=$('sync-btn'); if(banner)banner.style.display=q.length?'block':'none'; if(btn){ if(q.length&&S.online){ btn.style.display=''; btn.textContent=S.syncing?'Syncing...':'↑ Sync ('+q.length+')'; }else btn.style.display='none'; } } async function syncQueue(){ if(S.syncing||!S.online)return; const queue=Q.get();if(!queue.length)return; S.syncing=true;updateSyncBadge();let synced=0; for(const order of queue){ try{ const{_lid,_pm,_total,...payload}=order; const res=await POST('/orders',payload); const oid=res.data?.id||res.data?.order?.id; if(oid){await POST('/payments',{order_id:oid,payments:[{method:_pm||'cash',amount:+parseFloat(_total||0).toFixed(3)}]});} Q.remove(_lid);synced++; }catch{} } S.syncing=false;updateSyncBadge(); if(synced)toast('βœ“ Synced '+synced+' offline order(s)','a-g'); } // ═══ CLOCK & NETWORK ═════════════════════════════════════════ function startClock(){ const tick=()=>{const c=$('clock');if(c)c.textContent=new Date().toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'});}; tick();setInterval(tick,1000); } function updateNetStatus(){ const e=$('net-status');if(!e)return; if(S.online){e.className='net-badge nb-on';e.textContent='● Online';} else{e.className='net-badge nb-off';e.textContent='β—Œ Offline';} updateSyncBadge(); const po=$('pos-off');if(po)po.style.display=S.online?'none':'flex'; } window.addEventListener('online',()=>{S.online=true;updateNetStatus();syncQueue();}); window.addEventListener('offline',()=>{S.online=false;updateNetStatus();}); // ═══ BOOT ═════════════════════════════════════════════════════ function boot(){ $('li-btn').addEventListener('click',doLogin); $('li-pass').addEventListener('keyup',e=>e.key==='Enter'&&doLogin()); $('li-email').addEventListener('keyup',e=>e.key==='Enter'&&$('li-pass').focus()); $('logout-btn').addEventListener('click',logout); $('sync-btn')?.addEventListener('click',syncQueue); // Close modals on bg click ['mo-new-order','mo-modifiers','mo-item-detail','mo-payment','mo-customer','mo-pin'].forEach(id=>{ const mo=$(id);if(!mo)return; mo.addEventListener('click',e=>{if(e.target===mo)mo.style.display='none';}); }); // Restore session const tok=store.get(LS.tok),usr=store.get(LS.usr); if(tok&&usr){S.user=usr;initApp();showView('v-app');navigate('dashboard');} else showView('v-login'); } document.addEventListener('DOMContentLoaded',boot);