π½οΈ
Restaurant POS
Sign in to your account
Email
Password
Sign In β
admin@pos.local / password
// βββ 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=`
${delInfo}${carInfo}${partyInfo}
Subtotal QAR 0.00
Discount
Delivery Fee ${fmt(o.delivery_fee||0)}
VAT 5% QAR 0.00
TOTAL QAR 0.00
πΆ Offline mode β orders saved locally
π³ Pay / Checkout
βΈ Hold
π¨ KOT
π Reset
`;
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=``;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=``;
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}
βοΈ
`;
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 Note / Special Request
${item.selectedMods?.length?`
Modifiers:
${item.selectedMods.map(m=>`${esc(m.name)} `).join('')}
`:''}
Line Total: ${fmt(item.price*item.qty)}
π Remove
Cancel
Save Note
`;
$('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=>`
${esc(opt.name_en||opt.name||opt.name_ar||'')}
${opt.price_adjustment&&+opt.price_adjustment!==0?`${+opt.price_adjustment>0?'+':''}${fmt(opt.price_adjustment)} `:''}
`).join('')}
`).join('')}
Item Note (optional)
Total:
${fmt(price)}
Cancel
Add to Cart
`;
$('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
β
Payment Method
${[['cash','π΅','Cash'],['card','π³','Card'],['transfer','π²','Transfer'],['split','βοΈ','Split']].map(([v,i,l])=>`
`).join('')}
Cancel
β Confirm & Pay
`;
$('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
π New Order
`;
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 Re-login or Retry
';
}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
?`# Type Customer Total Status Time
${rows.map(o=>`
#${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)?
`π³ Pay `
:''} `).join('')}
`
:`π
No orders today
Create First Order `;
}catch{$('dash-orders').innerHTML='';}
}
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
π New
π
All Types
${['dine_in','takeaway','delivery','car_pickup'].map(t=>`${t.replace('_',' ')} `).join('')}
All Status
${['pending','confirmed','preparing','ready','completed','voided','on_hold'].map(s=>`${s} `).join('')}
`;
['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='';return;}
c.innerHTML=`
# Type Customer Items Total Status Time
${rows.map(o=>`
#${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)?`β `:''}
`).join('')}
`;
}catch{c.innerHTML='';}
}
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('')}
β Bump β All Ready
`;
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])=>``).join('');
lc.innerHTML=orders.length?`
Order # Customer Area Address Driver Status Map
${orders.map(o=>`
#${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?`π `:'β'}
`).join('')}
`
:'';
}catch{lc.innerHTML='';}
}
// βββ CUSTOMERS βββββββββββββββββββββββββββββββββββββββββββββββ
function renderCustomers(c){
c.innerHTML=`
π₯ Customers
+ Add
Add Customer β
Cancel
Save Customer
`;
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='';return;}
lc.innerHTML=`
Name Phone Orders Total Spent Points Last Order
${rows.map(cu=>`
${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)}
π View
π
`).join('')}
`;
}catch{lc.innerHTML='';}
}
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?`
# Type Total Status Date
${orders.map(o=>`
#${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)} `).join('')}
`
:'
'}
${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('')||'
'}
Close
π New Order
`;
}catch(e){d.innerHTML=``;}
}
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
π New Party
`;
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
Create Party Order `;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='';}
}
// βββ PROMOTIONS ββββββββββββββββββββββββββββββββββββββββββββββ
function renderPromotions(c){
c.innerHTML=`
π·οΈ Promotions & Coupons
+ New Promotion
`;
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?`
Name Type Value Code Min Order Used Status
${rows.map(d=>`
${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'}
π
`).join('')}
`
:'';
}catch{lc.innerHTML='';}
}
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
+ Category
+ Item
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=`
Item Category Base Price Promo Price Status
${items.map(i=>`
${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'}
βοΈ
π
`).join('')}
`;
}catch{lc.innerHTML='';}
}
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=`
Name EN Name AR Sort
${cats.map(c=>`
${esc(c.name_en)} ${esc(c.name_ar||'β')}
${c.sort_order||0}
π
`).join('')}
`;
}catch{$('menu-content').innerHTML='';}
}
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
β
Cancel
Save Item
`;
$('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
π Open Session
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
${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='';}
}
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=``;
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?`
Session Opened By Orders Sales Status Date
${rows.map(s=>`
#${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)} `).join('')}
`
:'';
}catch{lc.innerHTML='';}
}
// βββ INVENTORY βββββββββββββββββββββββββββββββββββββββββββββββ
function renderInventory(c){
c.innerHTML=`
π¦ Stock & Inventory
+ Add Item
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
`:''}
Ingredient Unit Stock Min Cost/Unit Status
${rows.map(r=>`
${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'}
Β± Adjust
`).join('')}
`;
}catch{lc.innerHTML='';}
}
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?`
Ingredient Qty Reason By Date
${rows.map(w=>`
${esc(w.ingredient_name||'β')}
${w.qty} ${esc(w.unit||'')}
${esc(w.reason||'β')}
${esc(w.recorded_by_name||'β')}
${fmtD(w.created_at)} `).join('')}
`
:'';
}catch{lc.innerHTML='';}
}
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?`
PO # Supplier Total Status Date
${rows.map(p=>`
${esc(p.po_no||p.id)} ${esc(p.supplier_name||'β')}
${fmt(p.total||0)}
${esc(p.status)}
${fmtD(p.created_at)} `).join('')}
`
:'';
}catch{lc.innerHTML='';}
}
function showIngModal(){
$('ing-mo-inner').innerHTML=`
Add Ingredient / Stock Item
β
Cancel
Save
`;
$('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
+ Add Vendor
`;
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?`
Vendor Contact Phone Email Terms
${rows.map(s=>`
${esc(s.name)} ${esc(s.contact_name||'β')}
${esc(s.phone||'β')} ${esc(s.email||'β')}
${s.payment_terms||0} days
π
`).join('')}
`
:'';
}catch{lc.innerHTML='';}
}
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
`;
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=`
Zone / Area Fee (QAR) Min Order Est. Time
${rows.map(z=>`
${esc(z.name)} ${fmt(z.fee||0)}
${fmt(z.min_order||0)} ${z.estimated_min||0} min
π
`).join('')}
`;
}catch{lc.innerHTML='';}
}
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?`
Driver Phone Vehicle Status Active
${rows.map(d=>`
${esc(d.name)} ${esc(d.phone||'β')}
${esc(d.vehicle_no||'β')}
${d.is_online?'Online':'Offline'}
${d.active_orders||0} `).join('')}
`
:'';
}catch{lc.innerHTML='';}
}
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
+ Add Employee
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?`
Name Position Phone Salary Status
${rows.map(e=>`
${esc(e.name)} ${esc(e.position||e.role||'β')}
${esc(e.phone||'β')}
${e.salary?fmt(e.salary):'β'}
${esc(e.status)}
`).join('')}
`
:'';
}catch{lc.innerHTML='';}
}
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
+ Expense
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)}
Category Description Amount Date
${rows.map(e=>`
${esc(e.category||'β')}
${esc(e.description||'β')}
${fmt(e.amount)}
${fmtD(e.expense_date||e.created_at)} `).join('')}
`;
}catch{lc.innerHTML='';}
}
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%)
π₯ Download VAT Report
`;
}catch{lc.innerHTML='';}
}
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.
Date
Record Entry
`;
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=`
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
# Item Qty Revenue
${d.top_items.slice(0,12).map((i,n)=>`
${n+1}
${esc(i.name||i.item_name||'β')}
${i.qty||i.quantity||0}
${fmt(i.revenue||i.total||0)} `).join('')}
`:''}`;
}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
'}
12am 6am 12pm 6pm 11pm
π½οΈ 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])=>`
`).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
Phone
Address
Save Settings
`;
}catch{lc.innerHTML='';}
}
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=`
Name Email Role Status
${rows.map(u=>`
${esc(u.name)} ${esc(u.email)}
${esc(u.role_name||u.role||'β')}
${esc(u.status)}
${u.id!==S.user?.id?`π `:'(you) '}
`).join('')}
`;
}catch{lc.innerHTML='';}
}
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.
Role Display Name Permissions
${rows.map(r=>`
${esc(r.slug||r.name)} ${esc(r.display_name||r.name)}
${esc(JSON.stringify(r.permissions).replace(/["\[\]]/g,'').slice(0,80))}
`).join('')}
`;
}catch{lc.innerHTML='';}
}
function loadReceiptSettings(){
const lc=$('sett-content');if(!lc)return;
lc.innerHTML=``;
}
// βββ 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);