Cafe Billing Suite
Loading your cafe data…
New Order
Current Bill
No table selected
Attach Customer (optional)
πŸ›’
Tap menu items to add
Invoice #Date & TimeTableCustomerItemsSubtotalGSTTotalPaymentActions
Live kitchen order tickets β€” updates in real time
NamePhoneEmailNotesVisitsTotal SpentLast VisitActions
1
Upload File
2
Map Columns
3
Preview & Import
Import Past Customer Data
Upload a CSV or Excel file exported from your old POS, spreadsheet, or any database. We'll help you map the columns.
πŸ“
Drop your file here or click to browse
Supports CSV (.csv), Excel (.xlsx, .xls), and tab-separated (.tsv)
CSV XLSX TSV
πŸ’‘ What should my file look like?
Your file can have columns in any order and any name β€” you'll map them in the next step. Common columns include:
Name, Phone, Email, Address, Notes, Total Spent, Visit Count, Last Visit Date
The only requirement is a Name or Phone column.
Or paste CSV text directly:
πŸ“‹ Download Sample Template
Not sure about the format? Download our ready-made template and fill it in.
Click a table to toggle status. Green = free, Gold = occupied.
πŸ’°
Today's Revenue
β‚Ή0
0 orders
πŸ“‹
Total Orders
0
πŸ‘₯
Customers
0
Registered
🧾
Avg Order Value
β‚Ή0
Per transaction
Weekly Revenue
Top Selling Items
Recent Orders
Sales by Category
Invoice #DateTableItemsSubtotalTaxDiscountTotalPayment
β˜• Cafe Profile
πŸ’΅ Tax & Billing
πŸ–¨οΈ Receipt
πŸ‘€ Face Login
Register your face to log in without a password. Uses your device camera.
Status: Loading...
β€”
Step 1: Click "Register Face" below
Step 2: Allow camera access
Step 3: Look at camera until face is detected
Step 4: Click "Capture & Save Face"
πŸ” Two-Factor Authentication
Extra security β€” every login will require a code from Google Authenticator app.
Status: Loading...
β€”
'); w.document.close(); w.print(); } function printBill(){ if(!state.bill.length){showToast('No items','error');return;} showReceipt(buildTempOrder()); } function buildTempOrder(){ const sub=state.bill.reduce((s,i)=>s+i.price*i.qty,0); const gst=parseFloat(document.getElementById('settGST')?.value||state.settings.gst)||5; const disc=parseFloat(document.getElementById('discountInput')?.value||0)||0; const gstAmt=sub*gst/100;const discAmt=sub*disc/100; return {id:'PREVIEW',date:new Date().toISOString(),table:state.selectedTable,customer:state.selectedCustomer,items:JSON.parse(JSON.stringify(state.bill)),subtotal:sub,gst:gstAmt,discount:discAmt,total:sub+gstAmt-discAmt,payment:'-',status:'pending'}; } function downloadBill(){ if(!state.bill.length){showToast('No items','error');return;} downloadReceipt(buildTempOrder()); } function downloadReceipt(order){ if(!order) order=state.viewingOrder; if(!order) return; const html=`${order.id}
Cafe Billing Suite
Loading your cafe data…
${buildReceiptHTML(order)}`; const b=new Blob([html],{type:'text/html'}); const a=document.createElement('a'); a.href=URL.createObjectURL(b); a.download=`${order.id}.html`; a.click(); showToast('Downloaded!','success'); } // ==================== KOT ==================== function openKOT(){ if(!state.bill.length){showToast('No items','error');return;} if(!state.selectedTable){showToast('Select a table first','error');return;} const kot={id:'KOT-'+Date.now(),table:state.selectedTable,items:JSON.parse(JSON.stringify(state.bill)),time:new Date().toLocaleTimeString('en-IN',{hour:'2-digit',minute:'2-digit'}),status:'pending'}; state.kots.unshift(kot); const t=state.tables.find(t=>t.name===state.selectedTable); if(t) t.status='occupied'; saveState(); showToast('KOT sent to kitchen 🍳','success'); } function renderKOT(){ const grid=document.getElementById('kotGrid'); if(!state.kots.length){grid.innerHTML='
No active KOTs
';return;} grid.innerHTML=state.kots.map((kot,i)=>`
πŸ“ ${kot.table}
${kot.time}
${kot.items.map(item=>`
${item.emoji} ${item.name} Γ— ${item.qty}
`).join('')}
`).join(''); } function resolveKOT(i){ state.kots.splice(i,1); saveState(); renderKOT(); showToast('KOT done','success'); } function deleteKOT(i){ state.kots.splice(i,1); saveState(); renderKOT(); } function clearKOTs(){ state.kots=[]; saveState(); renderKOT(); } // ==================== ORDERS ==================== function renderOrders(){ const body=document.getElementById('ordersBody'); const df=document.getElementById('orderDateFilter').value; const sf=document.getElementById('orderStatusFilter').value; const orders=state.orders.filter(o=>(!df||o.date.startsWith(df))&&(!sf||o.status===sf)); if(!orders.length){body.innerHTML='No orders found';return;} body.innerHTML=orders.map(o=>{ const ri=state.orders.indexOf(o); const custName=o.customer!==null&&state.customers[o.customer]?state.customers[o.customer].name:'Walk-in'; return ` ${o.id} ${new Date(o.date).toLocaleString('en-IN')} ${o.table||'β€”'} ${custName} ${o.items.length} item${o.items.length!==1?'s':''} ${state.settings.currency}${o.subtotal.toFixed(2)} ${state.settings.currency}${o.gst.toFixed(2)} ${state.settings.currency}${o.total.toFixed(2)} ${o.payment}
`; }).join(''); document.getElementById('orderBadge').textContent=state.orders.length; } function viewOrder(i){ const o=state.orders[i]; state.viewingOrder=o; document.getElementById('orderDetailContent').innerHTML=buildReceiptHTML(o); document.getElementById('orderDetailModal').classList.add('open'); } function exportOrders(){ const rows=[['Invoice','Date','Table','Customer','Items','Subtotal','GST','Discount','Total','Payment','Status']]; state.orders.forEach(o=>{ rows.push([o.id,new Date(o.date).toLocaleString('en-IN'),o.table||'',o.customer!==null&&state.customers[o.customer]?state.customers[o.customer].name:'Walk-in',o.items.map(i=>i.name+'x'+i.qty).join(';'),o.subtotal.toFixed(2),o.gst.toFixed(2),o.discount.toFixed(2),o.total.toFixed(2),o.payment,o.status]); }); downloadCSV(rows,'orders.csv'); showToast('Exported!','success'); } // ==================== MENU MANAGER ==================== function renderMenuManager(){ const body=document.getElementById('menuManagerBody'); const cf=document.getElementById('menuCatFilter').value; const items=cf?state.menuItems.filter(i=>i.cat===cf):state.menuItems; body.innerHTML=items.map(item=>{ const ri=state.menuItems.indexOf(item); return ` ${item.emoji||'🍽️'}${item.name}${item.desc?`
${item.desc}
`:''} ${item.cat} ${state.settings.currency}${item.price} Available
`; }).join(''); if(!items.length) body.innerHTML='No items'; const mcf=document.getElementById('menuCatFilter'); const cv=mcf.value; mcf.innerHTML=''+state.categories.map(c=>``).join(''); mcf.value=cv; } function openMenuItemModal(ei){ document.getElementById('editItemIdx').value=ei!==undefined?ei:''; document.getElementById('menuModalTitle').textContent=ei!==undefined?'Edit Item':'Add Menu Item'; document.getElementById('miCat').innerHTML=state.categories.map(c=>``).join(''); if(ei!==undefined){ const it=state.menuItems[ei]; document.getElementById('miName').value=it.name; document.getElementById('miPrice').value=it.price; document.getElementById('miCat').value=it.cat; document.getElementById('miEmoji').value=it.emoji||''; document.getElementById('miDesc').value=it.desc||''; } else { ['miName','miPrice','miEmoji','miDesc'].forEach(id=>document.getElementById(id).value=''); } document.getElementById('menuItemModal').classList.add('open'); } function editMenuItem(i){ openMenuItemModal(i); } function saveMenuItem(){ const name=document.getElementById('miName').value.trim(); const price=parseFloat(document.getElementById('miPrice').value)||0; if(!name||!price){showToast('Name and price required','error');return;} const ei=document.getElementById('editItemIdx').value; const obj={name,price,cat:document.getElementById('miCat').value,emoji:document.getElementById('miEmoji').value||'🍽️',desc:document.getElementById('miDesc').value}; if(ei!=='') state.menuItems[parseInt(ei)]=obj; else state.menuItems.push(obj); saveState(); closeModal('menuItemModal'); renderMenuManager(); initBilling(); showToast(ei!==''?'Item updated':'Item added','success'); } function deleteMenuItem(i){ if(confirm(`Delete "${state.menuItems[i].name}"?`)){ state.menuItems.splice(i,1); saveState(); renderMenuManager(); initBilling(); showToast('Deleted','success'); } } function openCategoryModal(){ document.getElementById('catName').value=''; document.getElementById('categoryModal').classList.add('open'); } function saveCategory(){ const n=document.getElementById('catName').value.trim(); if(!n){showToast('Enter a name','error');return;} if(state.categories.includes(n)){showToast('Already exists','error');return;} state.categories.push(n); saveState(); closeModal('categoryModal'); renderMenuManager(); initBilling(); showToast('Category added','success'); } // ==================== CUSTOMERS ==================== function renderCustomers(){ const body=document.getElementById('customersBody'); const q=(document.getElementById('custSearch').value||'').toLowerCase(); const custs=state.customers.filter(c=>!q||c.name.toLowerCase().includes(q)||(c.phone||'').includes(q)||(c.email||'').toLowerCase().includes(q)); body.innerHTML=custs.map(c=>{ const ri=state.customers.indexOf(c); const initials=c.name.split(' ').map(w=>w[0]).join('').slice(0,2).toUpperCase(); return `
${initials}
${c.name}
${c.phone||'β€”'}${c.email||'β€”'} ${c.notes||'β€”'} ${c.visits||0} ${state.settings.currency}${(c.spent||0).toFixed(2)} ${c.lastVisit||'Never'}
`; }).join(''); if(!custs.length) body.innerHTML='No customers found'; } function openCustomerModal(){ document.getElementById('custModalTitle').textContent='Add Customer'; document.getElementById('editCustIdx').value=''; ['custName','custPhone','custEmail','custNotes'].forEach(id=>document.getElementById(id).value=''); document.getElementById('customerModal').classList.add('open'); } function editCustomer(i){ const c=state.customers[i]; document.getElementById('custModalTitle').textContent='Edit Customer'; document.getElementById('editCustIdx').value=i; document.getElementById('custName').value=c.name; document.getElementById('custPhone').value=c.phone; document.getElementById('custEmail').value=c.email||''; document.getElementById('custNotes').value=c.notes||''; document.getElementById('customerModal').classList.add('open'); } function saveCustomer(){ const name=document.getElementById('custName').value.trim(); const phone=document.getElementById('custPhone').value.trim(); if(!name||!phone){showToast('Name and phone required','error');return;} const ei=document.getElementById('editCustIdx').value; if(ei!==''){ Object.assign(state.customers[parseInt(ei)],{name,phone,email:document.getElementById('custEmail').value,notes:document.getElementById('custNotes').value}); showToast('Updated','success'); } else { state.customers.push({name,phone,email:document.getElementById('custEmail').value,notes:document.getElementById('custNotes').value,visits:0,spent:0,lastVisit:null}); showToast('Customer added','success'); } saveState(); closeModal('customerModal'); renderCustomers(); } function deleteCustomer(i){ if(confirm('Delete?')){ state.customers.splice(i,1); saveState(); renderCustomers(); showToast('Deleted','success'); } } function openCustomerPicker(){ document.getElementById('pickCustSearch').value=''; renderPickerList(); document.getElementById('customerPickerModal').classList.add('open'); } function renderPickerList(){ const q=(document.getElementById('pickCustSearch').value||'').toLowerCase(); const custs=state.customers.filter(c=>!q||c.name.toLowerCase().includes(q)||(c.phone||'').includes(q)); document.getElementById('pickerList').innerHTML=custs.map(c=>{ const ri=state.customers.indexOf(c); const ini=c.name.split(' ').map(w=>w[0]).join('').slice(0,2).toUpperCase(); return `
${ini}
${c.name}
${c.phone}
`; }).join('')||'
No customers found
'; } function selectCustomer(i){ state.selectedCustomer=i; document.getElementById('selectedCustomerLabel').textContent='πŸ‘€ '+state.customers[i].name; closeModal('customerPickerModal'); } function clearCustomer(){ state.selectedCustomer=null; document.getElementById('selectedCustomerLabel').textContent='Attach Customer (optional)'; closeModal('customerPickerModal'); } // ==================== TABLES ==================== function renderTables(){ const g=document.getElementById('tablesGrid'); g.innerHTML=state.tables.map((t,i)=>`
${t.status==='occupied'?'πŸ”΄':'🟒'}
${t.name}
Seats: ${t.cap}
${t.status}
`).join(''); } function toggleTableStatus(i){ state.tables[i].status=state.tables[i].status==='free'?'occupied':'free'; saveState(); renderTables(); } function openTableModal(){ document.getElementById('tableName').value=''; document.getElementById('tableCap').value=4; document.getElementById('tableModal').classList.add('open'); } function saveTable(){ const name=document.getElementById('tableName').value.trim(); if(!name){showToast('Enter table name','error');return;} state.tables.push({name,cap:parseInt(document.getElementById('tableCap').value)||4,status:'free'}); saveState(); closeModal('tableModal'); renderTables(); initBilling(); showToast('Table added','success'); } function deleteTable(i){ if(confirm('Remove?')){ state.tables.splice(i,1); saveState(); renderTables(); initBilling(); showToast('Removed','success'); } } // ==================== DASHBOARD ==================== function renderDashboard(){ const today=new Date().toISOString().split('T')[0]; const tOrd=state.orders.filter(o=>o.date.startsWith(today)); const tRev=tOrd.reduce((s,o)=>s+o.total,0); const avg=state.orders.length?state.orders.reduce((s,o)=>s+o.total,0)/state.orders.length:0; document.getElementById('dashRevenue').textContent=`${state.settings.currency}${tRev.toFixed(0)}`; document.getElementById('dashRevSub').textContent=`${tOrd.length} order${tOrd.length!==1?'s':''}`; document.getElementById('dashOrders').textContent=state.orders.length; document.getElementById('dashOrderSub').textContent=`${state.orders.filter(o=>o.status==='paid').length} paid`; document.getElementById('dashCustomers').textContent=state.customers.length; document.getElementById('dashAvg').textContent=`${state.settings.currency}${avg.toFixed(0)}`; // Weekly chart const days=['Sun','Mon','Tue','Wed','Thu','Fri','Sat'], now=new Date(), wd=Array(7).fill(0); state.orders.forEach(o=>{ const d=new Date(o.date); const diff=Math.floor((now-d)/(86400000)); if(diff>=0&&diff<7) wd[d.getDay()]+=o.total; }); const max=Math.max(...wd,1); document.getElementById('weeklyChart').innerHTML=`
${wd.map((v,i)=>`
`).join('')}
`; document.getElementById('weeklyLabels').innerHTML=days.map(d=>`
${d}
`).join(''); // Top items const iSales={}; state.orders.forEach(o=>o.items.forEach(i=>{ iSales[i.name]=(iSales[i.name]||0)+i.qty; })); const sorted=Object.entries(iSales).sort((a,b)=>b[1]-a[1]).slice(0,6); const ms=sorted[0]?sorted[0][1]:1; document.getElementById('topItems').innerHTML=sorted.length?sorted.map(([name,qty])=>`
${name}${qty} sold
`).join(''):'
No sales yet
'; // Recent orders document.getElementById('recentOrders').innerHTML=state.orders.slice(0,6).map(o=>`
${o.id}${new Date(o.date).toLocaleDateString('en-IN')}${state.settings.currency}${o.total.toFixed(2)}
`).join('')||'
No orders yet
'; // Cat sales const cSales={}; state.orders.forEach(o=>o.items.forEach(i=>{ const cat=(state.menuItems.find(m=>m.name===i.name)||{}).cat||'Other'; cSales[cat]=(cSales[cat]||0)+i.price*i.qty; })); const cMax=Math.max(...Object.values(cSales),1); document.getElementById('catSales').innerHTML=Object.entries(cSales).sort((a,b)=>b[1]-a[1]).map(([cat,t])=>`
${cat}${state.settings.currency}${t.toFixed(0)}
`).join('')||'
No data yet
'; } // ==================== REPORTS ==================== function initReports(){ const today=new Date().toISOString().split('T')[0]; const ms=today.slice(0,7)+'-01'; document.getElementById('repFrom').value=ms; document.getElementById('repTo').value=today; generateReport(); } function generateReport(){ const from=document.getElementById('repFrom').value, to=document.getElementById('repTo').value; const f=state.orders.filter(o=>{ const d=o.date.split('T')[0]; return d>=from&&d<=to; }); const rev=f.reduce((s,o)=>s+o.total,0), tax=f.reduce((s,o)=>s+o.gst,0), disc=f.reduce((s,o)=>s+o.discount,0); document.getElementById('reportStats').innerHTML=`
πŸ’°
Revenue
${state.settings.currency}${rev.toFixed(2)}
πŸ“‹
Orders
${f.length}
🧾
Tax Collected
${state.settings.currency}${tax.toFixed(2)}
🏷️
Discounts
${state.settings.currency}${disc.toFixed(2)}
`; document.getElementById('reportBody').innerHTML=f.map(o=>` ${o.id} ${new Date(o.date).toLocaleDateString('en-IN')} ${o.table||'β€”'} ${o.items.map(i=>i.name).join(', ').slice(0,40)} ${state.settings.currency}${o.subtotal.toFixed(2)} ${state.settings.currency}${o.gst.toFixed(2)} -${state.settings.currency}${o.discount.toFixed(2)} ${state.settings.currency}${o.total.toFixed(2)} ${o.payment} `).join('')||'No data in range'; } function exportReportCSV(){ const from=document.getElementById('repFrom').value, to=document.getElementById('repTo').value; const f=state.orders.filter(o=>{ const d=o.date.split('T')[0]; return d>=from&&d<=to; }); const rows=[['Invoice','Date','Table','Subtotal','GST','Discount','Total','Payment']]; f.forEach(o=>rows.push([o.id,new Date(o.date).toLocaleDateString('en-IN'),o.table||'',o.subtotal.toFixed(2),o.gst.toFixed(2),o.discount.toFixed(2),o.total.toFixed(2),o.payment])); downloadCSV(rows,'report.csv'); showToast('Exported!','success'); } // ==================== SETTINGS ==================== function saveSettings(){ state.settings={cafeName:document.getElementById('settCafeName').value,phone:document.getElementById('settPhone').value,email:document.getElementById('settEmail').value,address:document.getElementById('settAddress').value,gstin:document.getElementById('settGSTIN').value,gst:parseFloat(document.getElementById('settGST').value)||5,currency:document.getElementById('settCurrency').value||'β‚Ή',footer:document.getElementById('settFooter').value,header:document.getElementById('settHeader').value,upi:document.getElementById('settUPI').value}; document.getElementById('sidebarCafeName').textContent=state.settings.cafeName; saveState(); showToast('Settings saved!','success'); recalc(); } function loadSettingsForm(){ const s=state.settings; document.getElementById('settCafeName').value=s.cafeName||'';document.getElementById('settPhone').value=s.phone||'';document.getElementById('settEmail').value=s.email||'';document.getElementById('settAddress').value=s.address||'';document.getElementById('settGSTIN').value=s.gstin||'';document.getElementById('settGST').value=s.gst||5;document.getElementById('settCurrency').value=s.currency||'β‚Ή';document.getElementById('settFooter').value=s.footer||'';document.getElementById('settHeader').value=s.header||'';document.getElementById('settUPI').value=s.upi||''; } // ==================== IMPORT CUSTOMERS ==================== let importData = { headers:[], rows:[], mapping:{} }; const IMPORT_FIELDS = [ {key:'name',label:'Full Name',required:true}, {key:'phone',label:'Phone Number',required:true}, {key:'email',label:'Email Address',required:false}, {key:'notes',label:'Notes / Preferences',required:false}, {key:'visits',label:'Visit Count',required:false}, {key:'spent',label:'Total Spent (β‚Ή)',required:false}, {key:'lastVisit',label:'Last Visit Date',required:false}, ]; function resetImport(){ importData={headers:[],rows:[],mapping:{}}; goImportStep(1); document.getElementById('fileInput').value=''; document.getElementById('csvPasteArea').value=''; document.getElementById('importStatusMsg').innerHTML=''; } function goImportStep(n){ [1,2,3].forEach(s=>{ document.getElementById('importStep'+s).style.display=s===n?'block':'none'; const ind=document.getElementById('step-ind-'+s); ind.className='step '+(sl.trim()); if(lines.length<2){ showToast('File must have at least a header row and one data row','error'); return; } const headers=parseCSVLine(lines[0],delimiter).map(h=>h.trim().replace(/^"|"$/g,'')); const rows=lines.slice(1).map(l=>parseCSVLine(l,delimiter).map(c=>c.trim().replace(/^"|"$/g,''))); importData.headers=headers; importData.rows=rows; autoMapColumns(); document.getElementById('detectedCols').textContent=`${headers.length} column${headers.length!==1?'s':''}`; renderColumnMapper(); goImportStep(2); showToast(`Loaded ${rows.length} rows from file`,'success'); } function parseCSVLine(line, delimiter){ const result=[], d=delimiter||','; let cur='', inQ=false; for(let i=0;icommas&&tabs>semis) return '\t'; if(semis>commas) return ';'; return ','; } function parsePastedCSV(){ const text=document.getElementById('csvPasteArea').value.trim(); if(!text){ showToast('Paste some CSV data first','error'); return; } parseCSVText(text); } function parseXLSXSimple(buffer){ // Simple XLSX parser - reads shared strings and sheet data try { const uint8=new Uint8Array(buffer); // Convert to string for basic parsing let text=''; for(let i=0;i'); const shStart=text.indexOf(''); const shEnd=text.indexOf(''); if(ssStart===-1||shStart===-1){ showToast('Could not parse Excel file. Please save as CSV and try again.','error'); return; } // Parse shared strings const ssXml=text.slice(ssStart,ssEnd+6); const strings=[]; const siRegex=/.*?<\/si>/gs; const tRegex=/]*>(.*?)<\/t>/s; let siM; while((siM=siRegex.exec(ssXml))!==null){ const tm=tRegex.exec(siM[0]); strings.push(tm?tm[1].replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'):''); } // Parse sheet rows const shXml=text.slice(shStart,shEnd+12); const rowRegex=/]*>(.*?)<\/row>/gs; const cellRegex=/]*r="([A-Z]+)\d+"[^>]*t="([^"]*)"[^>]*>.*?(.*?)<\/v>.*?<\/c>|]*r="([A-Z]+)\d+"[^>]*>.*?(.*?)<\/v>.*?<\/c>/gs; const rows=[]; let rowM; while((rowM=rowRegex.exec(shXml))!==null){ const row=[]; let cellM; while((cellM=cellRegex.exec(rowM[1]))!==null){ const type=cellM[2]; const val=cellM[3]||cellM[5]||''; row.push(type==='s'?(strings[parseInt(val)]||''):val); } if(row.length>0) rows.push(row); } if(rows.length<2){ showToast('Excel file is empty or has only headers','error'); return; } importData.headers=rows[0].map(h=>(h||'').toString().trim()); importData.rows=rows.slice(1).map(r=>r.map(c=>(c||'').toString().trim())); autoMapColumns(); document.getElementById('detectedCols').textContent=`${importData.headers.length} column${importData.headers.length!==1?'s':''}`; renderColumnMapper(); goImportStep(2); showToast(`Loaded ${importData.rows.length} rows from Excel`,'success'); } catch(e){ showToast('Could not parse Excel. Try saving as CSV.','error'); console.error(e); } } function autoMapColumns(){ // Smart auto-detection of columns const hints={ name:['name','full name','customer name','client name','guest name','customer','naam'], phone:['phone','mobile','contact','number','tel','telephone','mob','cell','phone number','mobile number'], email:['email','e-mail','mail','email address'], notes:['notes','note','preference','preferences','remarks','comment','allergy','special'], visits:['visits','visit count','total visits','visits count','count'], spent:['spent','total spent','amount','revenue','total amount','total spend','lifetime value','ltv'], lastVisit:['last visit','lastvisit','last date','last order','last seen','recent visit'], }; importData.mapping={}; IMPORT_FIELDS.forEach(field=>{ const h=hints[field.key]||[field.label.toLowerCase()]; const found=importData.headers.findIndex(col=>h.some(hint=>col.toLowerCase().includes(hint))); importData.mapping[field.key]=found>=0?found:-1; }); } function renderColumnMapper(){ const html=IMPORT_FIELDS.map(field=>`
`).join(''); document.getElementById('columnMapper').innerHTML=html; } function goImportStep(n){ if(n===2&&!importData.headers.length){ showToast('Upload a file first','error'); return; } if(n===3){ // Validate required fields const nameIdx=importData.mapping['name']; const phoneIdx=importData.mapping['phone']; if(nameIdx===-1&&phoneIdx===-1){ showToast('Map at least Name or Phone column','error'); return; } renderPreview(); } [1,2,3].forEach(s=>{ document.getElementById('importStep'+s).style.display=s===n?'block':'none'; const ind=document.getElementById('step-ind-'+s); ind.className='step '+(sm[f.key]!==-1); // Check dupes const existingPhones=new Set(state.customers.map(c=>(c.phone||'').replace(/\D/g,''))); let dupeCount=0; document.getElementById('previewHead').innerHTML=fields.map(f=>`${f.label}`).join('')+'Status'; document.getElementById('previewBody').innerHTML=preview.map(row=>{ const phone=(m.phone!==-1?row[m.phone]||'':'').replace(/\D/g,''); const isDupe=phone&&existingPhones.has(phone); if(isDupe) dupeCount++; const cells=fields.map(f=>`${m[f.key]!==-1?row[m[f.key]]||'β€”':'β€”'}`).join(''); return `${cells}${isDupe?'Duplicate':'New'}`; }).join(''); document.getElementById('previewSummary').textContent=`${importData.rows.length} total records, ${Math.min(10,importData.rows.length)} shown`; document.getElementById('dupWarning').style.display=dupeCount>0?'inline-flex':'none'; document.getElementById('dupWarning').textContent=`⚠ ${dupeCount} duplicate${dupeCount!==1?'s':''} detected`; } function executeImport(){ const m=importData.mapping; const skipDupes=document.getElementById('skipDupes').checked; const existingPhones=new Set(state.customers.map(c=>(c.phone||'').replace(/\D/g,''))); let added=0, skipped=0, errors=0; importData.rows.forEach(row=>{ try{ const name=m.name!==-1?(row[m.name]||'').trim():''; const phone=m.phone!==-1?(row[m.phone]||'').trim():''; if(!name&&!phone){ errors++; return; } const phoneClean=phone.replace(/\D/g,''); if(skipDupes&&phoneClean&&existingPhones.has(phoneClean)){ skipped++; return; } const cust={ name:name||'Unknown', phone:phone||'', email:m.email!==-1?(row[m.email]||''):'', notes:m.notes!==-1?(row[m.notes]||''):'', visits:m.visits!==-1?parseInt(row[m.visits])||0:0, spent:m.spent!==-1?parseFloat(row[m.spent])||0:0, lastVisit:m.lastVisit!==-1?(row[m.lastVisit]||''):null, }; state.customers.push(cust); if(phoneClean) existingPhones.add(phoneClean); added++; } catch(e){ errors++; } }); saveState(); const msg=`
βœ… ${added} customer${added!==1?'s':''} imported β€” ${skipped} skipped (duplicates)${errors>0?`, ${errors} errors`:''}
`; document.getElementById('importStatusMsg').innerHTML=msg; document.getElementById('importBtn').disabled=true; document.getElementById('importBtn').textContent='βœ“ Import Complete'; showToast(`${added} customers imported successfully!`,'success'); setTimeout(()=>{ showPage('customers'); },2200); } function downloadTemplate(){ const rows=[['Name','Phone','Email','Notes','Visit Count','Total Spent','Last Visit Date'], ['Priya Sharma','+91 98765 43210','priya@example.com','No sugar',8,2450,'2025-04-09'], ['Rahul Mehta','+91 87654 32109','rahul@example.com','',3,890,'2025-04-07'], ['Anjali Nair','+91 76543 21098','anjali@example.com','Vegan',12,3200,'2025-04-10'], ]; downloadCSV(rows,'brewdesk-customer-template.csv'); showToast('Template downloaded!','success'); } // ==================== UTILS ==================== function downloadCSV(rows, filename){ const csv=rows.map(r=>r.map(c=>'"'+String(c||'').replace(/"/g,'""')+'"').join(',')).join('\n'); const b=new Blob([csv],{type:'text/csv'}); const a=document.createElement('a'); a.href=URL.createObjectURL(b); a.download=filename; a.click(); } function closeModal(id){ document.getElementById(id).classList.remove('open'); } document.querySelectorAll('.modal-overlay').forEach(m=>m.addEventListener('click',function(e){ if(e.target===this) this.classList.remove('open'); })); function showToast(msg, type=''){ const c=document.getElementById('toastContainer'); const t=document.createElement('div'); t.className=`toast ${type}`; const icon=type==='success'?'βœ“':type==='error'?'βœ•':'β„Ή'; t.innerHTML=`${icon}${msg}`; c.appendChild(t); setTimeout(()=>{ t.style.opacity='0'; t.style.transform='translateX(20px)'; t.style.transition='all 0.3s'; setTimeout(()=>t.remove(),300); }, 2800); } // ==================== MOBILE UI ==================== function setMobileNav(page) { document.querySelectorAll('.mobile-nav-item').forEach(i => i.classList.remove('active')); const el = document.getElementById('mnav-' + page); if (el) el.classList.add('active'); // Update mobile header title const titles = {billing:'New Order',orders:'Order History',dashboard:'Dashboard',customers:'Customers',kot:'Kitchen (KOT)',menu:'Menu Manager',tables:'Tables',reports:'Reports',settings:'Settings',import:'Import Customers'}; const titleEl = document.getElementById('mobilePageTitle'); if (titleEl) titleEl.textContent = titles[page] || page; // Hide more menu document.getElementById('mobileMoreMenu').style.display = 'none'; // Show/hide bill toggle button const billBtn = document.getElementById('billToggleBtn'); if (billBtn) billBtn.style.display = page === 'billing' ? 'flex' : 'none'; } function toggleMobileSidebar() { // On mobile, show sidebar as overlay const sidebar = document.querySelector('.sidebar'); const overlay = document.getElementById('mobileSidebarOverlay'); if (!sidebar) return; const isOpen = sidebar.style.display === 'flex'; sidebar.style.cssText = isOpen ? '' : 'display:flex;position:fixed;left:0;top:0;bottom:0;z-index:700;width:224px;'; overlay.classList.toggle('open', !isOpen); } function toggleMobileMore() { const menu = document.getElementById('mobileMoreMenu'); menu.style.display = menu.style.display === 'none' ? 'block' : 'none'; } function toggleMobileBill() { const billPanel = document.querySelector('.billing-right'); if (!billPanel) return; billPanel.classList.toggle('show-bill'); const btn = document.getElementById('billToggleBtn'); const count = state.bill.reduce((s,i) => s + i.qty, 0); btn.textContent = billPanel.classList.contains('show-bill') ? 'βœ•' : 'πŸ›’'; if (count > 0 && !billPanel.classList.contains('show-bill')) { btn.textContent = 'πŸ›’ ' + count; } } // Update bill toggle badge after render function updateBillBadge() { const count = state.bill ? state.bill.reduce((s,i) => s + i.qty, 0) : 0; const btn = document.getElementById('billToggleBtn'); if (btn) { const billPanel = document.querySelector('.billing-right'); const isOpen = billPanel && billPanel.classList.contains('show-bill'); btn.textContent = isOpen ? 'βœ•' : (count > 0 ? 'πŸ›’ ' + count : 'πŸ›’'); } } // Close more menu when clicking outside document.addEventListener('click', function(e) { const moreMenu = document.getElementById('mobileMoreMenu'); const moreBtn = document.getElementById('mnav-more'); if (moreMenu && moreBtn && !moreMenu.contains(e.target) && !moreBtn.contains(e.target)) { moreMenu.style.display = 'none'; } }); // showPage already updated via setMobileNav calls in mobile nav items // ==================== INIT ==================== async function init(){ await loadState(); initBilling(); loadSettingsForm(); // Always show the registered cafe name from auth (not default state) const cafe = JSON.parse(localStorage.getItem('brewdesk_cafe') || '{}'); // If settings.cafeName is still the default, replace with the registered name const defaultNames = ['The Golden Cup', 'My Cafe', '']; const displayName = (!state.settings.cafeName || defaultNames.includes(state.settings.cafeName)) ? (cafe.name || 'My Cafe') : state.settings.cafeName; document.getElementById('sidebarCafeName').textContent = displayName; document.getElementById('cafeEmailDisplay').textContent = cafe.email || ''; // Also update state.settings.cafeName so receipts use correct name if (!state.settings.cafeName || defaultNames.includes(state.settings.cafeName)) { state.settings.cafeName = cafe.name || 'My Cafe'; state.settings.header = cafe.name || 'My Cafe'; } document.getElementById('orderBadge').textContent = state.orders.length; setDate(); // Hide loading screen const ls = document.getElementById('loadingScreen'); ls.style.opacity = '0'; setTimeout(function(){ ls.style.display='none'; }, 450); } init();