// ── DATA ───────────────────────────────────────────────────────────── const EMPLOYEES = ["Kate","Venus","Mohammed","Arcilie"]; const MASSAGE_TYPES = ["Relax Massage","Medical Massage","Lymphatic Massage","Swedish Massage","Thai Massage","Pregnancy Massage","After Birth Massage","Candle Massage","Pearl VIP Massage","Deep Tissue Massage"]; const SERVICE_CATS = ["Massage","Nails","Facial","Waxing","Combo Package"]; const EXPENSE_CATS = ["Supplies","Equipment","Utilities","Marketing","Other"]; const PRICES = {"Relax Massage":300,"Medical Massage":380,"Lymphatic Massage":420,"Swedish Massage":320,"Thai Massage":350,"Pregnancy Massage":400,"After Birth Massage":400,"Candle Massage":380,"Pearl VIP Massage":600,"Deep Tissue Massage":450,"Nails":180,"Facial":280,"Waxing":150,"Combo Package":550}; const DELIVERY = {"No Delivery":0,"Riyadh - Near (0-10km)":50,"Riyadh - Mid (10-20km)":80,"Riyadh - Far (20-30km)":120,"Outside Riyadh":200}; const PAY_METHODS = ["Cash","Card","Bank Transfer","STC Pay","Mada"]; const MONTHS = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]; const VAT = 0.15; const ADVANCE = 100; const today = new Date().toISOString().split("T")[0]; let db = { employees: EMPLOYEES.map(n=>({id:n,name:n,salary:0})), clients: [ {id:1,name:"Sarah Al-Zahrani",phone:"0501234567",cat:"Massage",mtype:"Relax Massage",visits:5,notes:"Prefers light pressure"}, {id:2,name:"Mia Chen",phone:"0559876543",cat:"Nails",mtype:"",visits:3,notes:"Gel manicure"}, ], expenses: [ {id:1,cat:"Supplies",amount:250,note:"Nail polish set",date:"2026-04-01",emp:""}, {id:2,cat:"Utilities",amount:180,note:"Electric bill",date:"2026-04-05",emp:""}, ], appts: [ {id:1,client:"Sarah Al-Zahrani",cat:"Massage",mtype:"Relax Massage",emp:"Kate",date:"2026-04-10",time:"10:00",status:"completed",price:300,delzone:"No Delivery",delfee:0,discount:0,advance:true}, {id:2,client:"Mia Chen",cat:"Nails",mtype:"",emp:"Venus",date:"2026-04-12",time:"14:00",status:"completed",price:180,delzone:"No Delivery",delfee:0,discount:0,advance:true}, {id:3,client:"Sarah Al-Zahrani",cat:"Massage",mtype:"Swedish Massage",emp:"Kate",date:today,time:"11:00",status:"confirmed",price:320,delzone:"Riyadh - Near (0-10km)",delfee:50,discount:0,advance:true}, ], revenue: [ {id:1,client:"Sarah Al-Zahrani",cat:"Massage",mtype:"Relax Massage",amount:300,delfee:0,discount:0,date:"2026-04-10",method:"Cash",advance:true}, {id:2,client:"Mia Chen",cat:"Nails",mtype:"",amount:180,delfee:0,discount:0,date:"2026-04-12",method:"Card",advance:true}, ], promos: [ {id:1,name:"Spring Special",type:"percent",value:20,services:["Relax Massage"],active:true,expiry:"2026-05-01",note:"Spring offer"}, ], giftCards: [ {id:1,code:"PEARL-2026-0001",recipientName:"Hessa Al-Otaibi",recipientPhone:"0501112233",gifterName:"Reem Al-Otaibi",value:300,balance:300,service:"Any Service",issued:today,expiry:"2026-12-31",note:"Birthday gift"}, ], reminders: [ {id:1,client:"Sarah Al-Zahrani",phone:"0501234567",type:"Follow-up",date:today,time:"10:00",note:"Check in after massage",done:false}, {id:2,client:"Mia Chen",phone:"0559876543",type:"Appointment Reminder",date:today,time:"13:00",note:"Confirm tomorrow's nails",done:false}, ], dailyIncome: [], stockLog: [], transportZones: [ {id:1, name:"No Delivery", fee:0}, {id:2, name:"Riyadh - Near (0-10km)", fee:50}, {id:3, name:"Riyadh - Mid (10-20km)", fee:80}, {id:4, name:"Riyadh - Far (20-30km)", fee:120}, {id:5, name:"Outside Riyadh", fee:200}, ], stock: [ {id:1, name:"Panty & Bra Set", unit:"Package", pkgQty:1, totalPkg:0, pcsPerClient:1, alertAt:5, notes:"1 set per client per session"}, {id:2, name:"Bed Sheet Roll", unit:"Roll", pkgQty:100, totalPkg:0, pcsPerClient:2, alertAt:1, notes:"2 sheets per client (cover+base)"}, {id:3, name:"Eye Mask", unit:"Pack", pkgQty:50, totalPkg:0, pcsPerClient:1, alertAt:1, notes:"1 mask per client"}, {id:4, name:"Massage Oil", unit:"Gallon", pkgQty:250, totalPkg:0, pcsPerClient:30, alertAt:1, notes:"~30ml per session"}, {id:5, name:"Slippers", unit:"Box", pkgQty:100, totalPkg:0, pcsPerClient:1, alertAt:1, notes:"1 pair per client"}, {id:6, name:"Big Disposable Towel", unit:"Pack", pkgQty:50, totalPkg:0, pcsPerClient:2, alertAt:1, notes:"2 per client per session"}, {id:7, name:"Short Disposable Towel", unit:"Pack", pkgQty:50, totalPkg:0, pcsPerClient:2, alertAt:1, notes:"2 per client per session"}, ], }; let state = { tab:"dashboard", salaryTarget:null, invoiceClientId:null, rmFilter:"All", gcRedeemAmounts:{} }; // ── HELPERS ────────────────────────────────────────────────────────── const SAR = n => `${(+n||0).toLocaleString("en-SA",{minimumFractionDigits:2,maximumFractionDigits:2})} ﷼`; const eid = id => document.getElementById(id); const genCode = () => `PEARL-${new Date().getFullYear()}-${Math.floor(1000+Math.random()*9000)}`; const svcLabel = (cat,mtype) => cat==="Massage"&&mtype ? mtype : cat; const bcls = cat => cat==="Massage"?"bm":cat==="Nails"?"bn":cat==="Facial"?"bf":"bo"; const scol = s => s==="completed"?"var(--green)":s==="confirmed"?"var(--gold)":s==="cancelled"?"var(--red)":"var(--purple)"; const rmTypeCls = t => t==="Appointment Reminder"?"rm-type-appt":t==="Follow-up"?"rm-type-followup":t==="Birthday"?"rm-type-birthday":"rm-type-general"; function totals(){ const tExpB = db.expenses.reduce((s,e)=>s+e.amount,0); const tExpV = tExpB*VAT; const tExp = tExpB+tExpV; const tSal = db.employees.reduce((s,e)=>s+e.salary,0); const tRevB = db.revenue.reduce((s,r)=>s+r.amount,0); const tDel = db.revenue.reduce((s,r)=>s+(r.delfee||0),0); const tRevV = tRevB*VAT; const tRev = tRevB+tRevV+tDel; const net = tRev-tExp-tSal; return {tExpB,tExpV,tExp,tSal,tRevB,tDel,tRevV,tRev,net}; } // ── MODALS ─────────────────────────────────────────────────────────── function openModal(id){ eid(id).classList.add("open"); } function closeModal(id){ eid(id).classList.remove("open"); } document.querySelectorAll(".ov").forEach(ov=>{ ov.addEventListener("click",e=>{ if(e.target===ov) ov.classList.remove("open"); }); }); // ── TABS ───────────────────────────────────────────────────────────── const TABS = [ ["dashboard","🏠 Home"],["appointments","📅 Appts"],["daily","📋 Daily"], ["revenue","💰 Revenue"],["employees","👥 Staff"],["expenses","💸 Expenses"], ["clients","👤 Clients"],["promos","🎁 Promos"],["giftcards","🎀 Gift Cards"], ["reminders","🔔 Reminders"],["transport","🚗 Transport"],["stock","📦 Stock"],["reports","📊 Reports"] ]; function renderNav(){ eid("nav").innerHTML = TABS.map(([k,l])=>``).join("") + `
`; } function switchTab(t){ state.tab=t; renderNav(); TABS.forEach(([k])=>{ eid("tab-"+k).className="tab-section"+(k===t?" active":""); }); renderTab(t); } function renderTab(t){ if(t==="dashboard") renderDashboard(); else if(t==="appointments") renderAppointments(); else if(t==="daily") renderDaily(); else if(t==="revenue") renderRevenue(); else if(t==="employees") renderEmployees(); else if(t==="expenses") renderExpenses(); else if(t==="clients") renderClients(); else if(t==="promos") renderPromos(); else if(t==="giftcards") renderGiftCards(); else if(t==="reminders") renderReminders(); else if(t==="transport") renderTransport(); else if(t==="stock") renderStock(); else if(t==="reports") renderReports(); } // ── HEADER STATS ───────────────────────────────────────────────────── function renderHeader(){ const {tRev,tExp,tSal,net} = totals(); eid("hstats").innerHTML = [ {l:"Revenue",v:SAR(tRev),c:"var(--green)"}, {l:"Outflow",v:SAR(tExp+tSal),c:"var(--gold-dk)"}, {l:"Net Profit",v:SAR(net),c:net>=0?"var(--green)":"var(--red)"}, ].map(s=>`
${s.l}${s.v}
`).join(""); } // ── STAT CARD HTML ──────────────────────────────────────────────────── function sc(label,value,cls,sub=""){ return `
${label}
${value}
${sub?`
${sub}
`:""}
`; } // ── DASHBOARD ───────────────────────────────────────────────────────── function renderDashboard(){ const {tRev,tExp,tSal,tDel,net} = totals(); const todayAppts = db.appts.filter(a=>a.date===today); const todayDI = db.dailyIncome.filter(d=>d.date===today); eid("tab-dashboard").innerHTML = `

Overview

${sc("Revenue (incl. VAT)",SAR(tRev),"c-green")} ${sc("Expenses (incl. VAT)",SAR(tExp),"c-gold")} ${sc("Salaries",SAR(tSal),"c-gold")} ${sc("Delivery Income",SAR(tDel),"c-green")} ${sc("Net Profit",SAR(net),net>=0?"c-green":"c-red")}
${sc("Total Clients",db.clients.length,"c-gold",`${db.clients.reduce((s,c)=>s+c.visits,0)} total visits`)} ${sc("Appointments",db.appts.length,"c-gold",`${db.appts.filter(a=>a.status==="confirmed").length} upcoming`)} ${sc("Gift Cards",db.giftCards.length,"c-gold",`${db.giftCards.filter(g=>!g.redeemed).length} active`)} ${sc("Reminders",db.reminders.filter(r=>!r.done).length,"c-gold",`${db.reminders.filter(r=>r.date===today&&!r.done).length} due today`)}
Today's Appointments
${todayAppts.length===0?`
No appointments scheduled today
`: todayAppts.map(a=>`
${a.client}
${a.time} · ${svcLabel(a.cat,a.mtype)} · ${a.emp}
${a.status}
`).join("") }
Today's Income
${todayDI.length===0?`
No entries today
`: todayDI.map(d=>{const p=d.total-d.vat-d.exp; return `
${d.client||"—"}
${svcLabel(d.cat,d.mtype)}
${SAR(d.total)}
Profit: ${SAR(p)}
`}).join("") }
`; } // ── APPOINTMENTS ────────────────────────────────────────────────────── function renderAppointments(){ const statuses = ["confirmed","pending","completed","cancelled"]; let html = ``; statuses.forEach(st=>{ const g = db.appts.filter(a=>a.status===st).sort((a,b)=>a.date.localeCompare(b.date)); if(!g.length) return; html += `
${st} (${g.length})
${g.map(a=>``).join("")}
Date/TimeClientServiceTypeStaffPriceDeliveryDiscountAdvanceStatus
${a.date} ${a.time} ${a.client} ${a.cat} ${a.mtype||"—"} ${a.emp} ${SAR(a.price)} ${a.delfee>0?SAR(a.delfee):"—"} ${a.discount>0?"-"+SAR(a.discount):"—"} ${a.advance?"✓ Paid":"Pending"}
`; }); if(!db.appts.length) html += `

No appointments yet.

`; eid("tab-appointments").innerHTML = html; } // ── DAILY INCOME ────────────────────────────────────────────────────── function renderDaily(){ const tRev = db.dailyIncome.reduce((s,d)=>s+d.total,0); const tVAT = db.dailyIncome.reduce((s,d)=>s+d.vat,0); const tExp = db.dailyIncome.reduce((s,d)=>s+d.exp,0); const tProfit = tRev-tVAT-tExp; let html = `
${sc("Total Revenue",SAR(tRev),"c-green")} ${sc("Total VAT",SAR(tVAT),"c-gold")} ${sc("Total Expenses",SAR(tExp),"c-gold")} ${sc("Total Profit",SAR(tProfit),tProfit>=0?"c-green":"c-red")}
${db.dailyIncome.length===0?``: db.dailyIncome.map(d=>{ const profit=d.total-d.vat-d.exp; return ``; }).join("") }
DateClientServiceTypeTotalVAT (−)Expenses (−)DeliveryDiscountAdvanceMethod= Profit
No entries yet — click "+ Add Entry" to start tracking
${d.date} ${d.client||"—"} ${d.cat} ${d.mtype||"—"} ${SAR(d.total)} −${SAR(d.vat)} −${SAR(d.exp)} ${d.delfee>0?SAR(d.delfee):"—"} ${d.discount>0?"-"+SAR(d.discount):"—"} ${d.advance?"✓ "+SAR(ADVANCE):"—"} ${d.method} ${SAR(profit)}
`; if(db.dailyIncome.length>0) html += `
Revenue: ${SAR(tRev)} VAT: ${SAR(tVAT)} Expenses: ${SAR(tExp)} = Profit: ${SAR(tProfit)}
`; eid("tab-daily").innerHTML = html; } // ── REVENUE ─────────────────────────────────────────────────────────── function renderRevenue(){ const tRevB = db.revenue.reduce((s,r)=>s+r.amount,0); const tDel = db.revenue.reduce((s,r)=>s+(r.delfee||0),0); const tRevV = tRevB*VAT; const tRev = tRevB+tRevV+tDel; eid("tab-revenue").innerHTML = `
${sc("Gross Revenue",SAR(tRevB),"c-gold")} ${sc("VAT Collected",SAR(tRevV),"c-gold")} ${sc("Delivery Income",SAR(tDel),"c-green")} ${sc("Total incl. VAT",SAR(tRev),"c-green")}
${db.revenue.length===0?``: db.revenue.map(r=>``).join("") }
DateClientServiceTypeMethodAmountDeliveryDiscountVATAdvance
No revenue logged yet
${r.date} ${r.client} ${r.cat} ${r.mtype||"—"} ${r.method} ${SAR(r.amount)} ${r.delfee>0?"+"+SAR(r.delfee):"—"} ${r.discount>0?"-"+SAR(r.discount):"—"} ${SAR(r.amount*VAT)} ${r.advance?"✓ "+SAR(ADVANCE):"—"}
`; } // ── EMPLOYEES ───────────────────────────────────────────────────────── function renderEmployees(){ const staffRev = n => db.appts.filter(a=>a.emp===n&&a.status==="completed").reduce((s,a)=>s+a.price,0); const staffExp = n => db.expenses.filter(e=>e.emp===n).reduce((s,e)=>s+e.amount,0); const staffAppts = n => db.appts.filter(a=>a.emp===n).length; eid("tab-employees").innerHTML = `

Staff & Salaries

${db.employees.map(e=>`
${e.name[0]}
${e.name}
${staffAppts(e.name)} appointments · Spa Artist
Monthly Salary
${SAR(e.salary)}
Revenue: ${SAR(staffRev(e.name))} Expenses: ${SAR(staffExp(e.name))}
`).join("")}
`; } // ── EXPENSES ────────────────────────────────────────────────────────── function renderExpenses(){ const {tExpB,tExpV,tExp} = totals(); const catTotals = EXPENSE_CATS.map(c=>({c,t:db.expenses.filter(e=>e.cat===c).reduce((s,e)=>s+e.amount,0)})).filter(x=>x.t>0); eid("tab-expenses").innerHTML = `
${catTotals.map(x=>`
${x.c}
${SAR(x.t*1.15)}
`).join("")}
${db.expenses.length===0?``: db.expenses.map(e=>``).join("") }
DateCategoryNoteEmployeeAmount
No expenses recorded
${e.date} ${e.cat} ${e.note||"—"} ${e.emp||"—"} −${SAR(e.amount)}
Subtotal: ${SAR(tExpB)}  |  VAT 15%: ${SAR(tExpV)}  |  Total: ${SAR(tExp)}
`; } // ── CLIENTS ─────────────────────────────────────────────────────────── function renderClients(search=""){ const list = db.clients.filter(c=>!search||c.name.toLowerCase().includes(search.toLowerCase())||c.phone.includes(search)); eid("tab-clients").innerHTML = `
${list.map(c=>`
${c.name[0]}
${c.name}
${c.phone}
${c.cat}${c.mtype?`${c.mtype}`:""}
${c.visits} visits
${c.defTransport&&c.defTransport!=="No Delivery"?`
🚗 ${c.defTransport}
`:""} ${c.notes?`
${c.notes}
`:""}
`).join("")} ${list.length===0?`
No clients found.
`:""}
`; } // ── PROMOTIONS ──────────────────────────────────────────────────────── function renderPromos(){ eid("tab-promos").innerHTML = `
${db.promos.map(p=>`
${p.name}
${p.note||""}
${p.active?"Active":"Inactive"}
${p.type==="percent"?p.value+"% OFF":SAR(p.value)+" OFF"}
${p.expiry?`
Expires: ${p.expiry}
`:""}
Applies to: ${p.services.length?p.services.join(", "):"All services"}
`).join("")} ${db.promos.length===0?`
No promotions yet.
`:""}
`; } // ── GIFT CARDS ──────────────────────────────────────────────────────── function renderGiftCards(){ const total = db.giftCards.reduce((s,g)=>s+g.value,0); const balance = db.giftCards.reduce((s,g)=>s+g.balance,0); eid("tab-giftcards").innerHTML = `
${sc("Total Issued",db.giftCards.length,"c-gold")} ${sc("Total Value",SAR(total),"c-gold")} ${sc("Remaining Balance",SAR(balance),"c-green")} ${sc("Fully Redeemed",db.giftCards.filter(g=>g.redeemed).length,"c-gold")}
${db.giftCards.map(g=>`
Home Spa · Gift Card
${g.redeemed?"Redeemed":g.balance
${SAR(g.balance)}
For ${g.recipientName}
${g.code}
Balance ${SAR(g.balance)} / ${SAR(g.value)}
${[["For",g.recipientName],["From",g.gifterName||"—"],["Phone",g.recipientPhone||"—"],["Service",g.service],["Issued",g.issued],["Expires",g.expiry||"No expiry"]].map(([k,v])=> `
${k}${v}
` ).join("")}
${g.note?`
${g.note}
`:""} ${!g.redeemed&&g.balance>0?`
`:`
${g.redeemed?"Fully redeemed":"Balance depleted"}
`}
`).join("")} ${db.giftCards.length===0?`
No gift cards issued yet.
`:""}
`; } // ── REMINDERS ───────────────────────────────────────────────────────── function renderReminders(){ const filter = state.rmFilter||"All"; const overdue = db.reminders.filter(r=>!r.done&&r.date!r.done&&r.date===today); const filtered = db.reminders.filter(r=>filter==="All"?true:filter==="Pending"?!r.done:r.done) .sort((a,b)=>a.date.localeCompare(b.date)); let html = `
${sc("Total",db.reminders.length,"c-gold")} ${sc("Pending",db.reminders.filter(r=>!r.done).length,"c-gold")} ${sc("Due Today",dueToday.length,dueToday.length>0?"c-red":"c-green")} ${sc("Completed",db.reminders.filter(r=>r.done).length,"c-green")}
`; // Overdue alert if(overdue.length>0){ html += `
⚠ Overdue (${overdue.length})
${overdue.map(r=>`
${r.client} · ${r.phone}
${r.date} ${r.time} · ${r.type}${r.note?" — "+r.note:""}
`).join("")}
`; } // Today alert if(dueToday.length>0){ html += `
⚡ Due Today (${dueToday.length})
${dueToday.map(r=>`
${r.client} · ${r.phone}
${r.time} · ${r.type}${r.note?" — "+r.note:""}
`).join("")}
`; } // Filter tabs html += `
${["All","Pending","Completed"].map(f=>``).join("")}
${filtered.map(r=>`
${r.type} ${r.done?`✓ Done`:""} ${!r.done&&r.date===today?`Today`:""} ${!r.done&&r.dateOverdue`:""}
${r.client[0]}
${r.client}
${r.phone}
📅 ${r.date}🕐 ${r.time}
${r.note?`
${r.note}
`:""}
`).join("")} ${filtered.length===0?`
No reminders in this category.
`:""}
`; eid("tab-reminders").innerHTML = html; } // ── REPORTS ─────────────────────────────────────────────────────────── function renderReports(){ const selMonth = state.reportMonth ?? new Date().getMonth(); const year = new Date().getFullYear(); const mRev = Array(12).fill(0).map((_,m)=>db.revenue.filter(r=>new Date(r.date).getMonth()===m).reduce((s,r)=>s+r.amount,0)); const mExp = Array(12).fill(0).map((_,m)=>db.expenses.filter(e=>new Date(e.date).getMonth()===m).reduce((s,e)=>s+e.amount,0)); const maxB = Math.max(...mRev,...mExp,1); const staffRev = EMPLOYEES.map(n=>({name:n,appts:db.appts.filter(a=>a.emp===n).length,rev:db.appts.filter(a=>a.emp===n&&a.status==="completed").reduce((s,a)=>s+a.price,0)})); const totalRevB = db.revenue.reduce((s,r)=>s+r.amount,0); eid("tab-reports").innerHTML = `
Revenue vs Expenses — ${year}
${MONTHS.map((m,i)=>`
${m}
`).join("")}
■ Revenue ■ Expenses
${sc(MONTHS[selMonth]+" Revenue",SAR(mRev[selMonth]),"c-green")} ${sc(MONTHS[selMonth]+" Expenses",SAR(mExp[selMonth]),"c-gold")} ${sc(MONTHS[selMonth]+" Net",SAR(mRev[selMonth]-mExp[selMonth]),mRev[selMonth]>=mExp[selMonth]?"c-green":"c-red")}
Massage Types Performance
${MASSAGE_TYPES.map(mt=>{ const cnt=db.appts.filter(a=>a.mtype===mt).length; const tot=db.revenue.filter(r=>r.mtype===mt).reduce((s,r)=>s+r.amount,0); if(!cnt)return ""; return `
${mt} ${SAR(tot)} (${cnt}×)
`; }).join("")||`
No massage data yet
`}
Staff Performance
${staffRev.map(s=>`
${s.name[0]}
${s.name}
${s.appts} appointments
${SAR(s.rev)}
revenue
`).join("")}
`; } // ── SERVICE SELECTOR HTML ───────────────────────────────────────────── function svcSelectorHTML(prefix, cat, mtype){ return `
${MASSAGE_TYPES.map(m=>`
${m} ${SAR(PRICES[m]||0)}
`).join("")}
`; } function onCatChange(prefix){ const cat = eid(prefix+"-cat").value; eid(prefix+"-mtype-wrap").style.display = cat==="Massage"?"block":"none"; if(prefix==="appt") { eid("appt-price").value = PRICES[cat]||300; } } function selectMassage(prefix, mtype){ document.querySelectorAll(`#${prefix}-mtype-wrap .svc-opt`).forEach(el=>{ el.classList.toggle("sel", el.querySelector(".svc-name").textContent===mtype); }); if(prefix==="appt"){ eid("appt-price").value = PRICES[mtype]||300; } } function getSelectedMassage(prefix){ const sel = document.querySelector(`#${prefix}-mtype-wrap .svc-opt.sel`); return sel ? sel.querySelector(".svc-name").textContent : ""; } // ── LOGO HTML ───────────────────────────────────────────────────────── const logoHTML = `
PEARL
HOME SPA
`; // ── MODAL: APPOINTMENT ──────────────────────────────────────────────── function openApptModal(){ eid("md-appt").innerHTML = `
${logoHTML}Book Appointment
${svcSelectorHTML("appt","Massage","Relax Massage")}
`; openModal("ov-appt"); } function prefillApptClient(id){ const c = db.clients.find(c=>c.id===+id); if(!c)return; eid("appt-name").value = c.name; eid("appt-cat").value = c.cat; onCatChange("appt"); if(c.mtype) selectMassage("appt",c.mtype); eid("appt-price").value = PRICES[c.mtype||c.cat]||300; } function updateApptDelFee(){ const zid=+eid("appt-del").value; const z=db.transportZones.find(x=>x.id===zid)||{fee:0,name:""}; eid("appt-delfee-hint").textContent=z.fee>0?`Delivery fee: ${SAR(z.fee)}`:""; } function saveAppt(){ const name = eid("appt-name").value.trim(); if(!name)return; const cat = eid("appt-cat").value; const mtype = cat==="Massage" ? getSelectedMassage("appt") : ""; const zid=+eid("appt-del").value; const z=db.transportZones.find(x=>x.id===zid)||{fee:0,name:"No Delivery"}; db.appts.push({id:Date.now(),client:name,cat,mtype,emp:eid("appt-emp").value, date:eid("appt-date").value,time:eid("appt-time").value,status:eid("appt-status").value, price:+eid("appt-price").value||0,delzone:z.name,delfee:z.fee, discount:+eid("appt-disc").value||0,advance:eid("appt-advance").checked}); closeModal("ov-appt"); refresh(); } function updateApptStatus(id,status){ const a = db.appts.find(x=>x.id===id); if(!a)return; if(status==="completed"&&a.status!=="completed"){ db.revenue.push({id:Date.now(),client:a.client,cat:a.cat,mtype:a.mtype,amount:a.price,delfee:a.delfee||0,discount:a.discount||0,date:a.date,method:"Cash",advance:a.advance}); } a.status=status; refresh(); } function deleteAppt(id){ db.appts=db.appts.filter(x=>x.id!==id); refresh(); } // ── MODAL: DAILY INCOME ─────────────────────────────────────────────── function openDailyModal(){ eid("md-daily").innerHTML = `
${logoHTML}Daily Income Entry
📦 Stock Auto-Deduction
Saving this entry will automatically deduct 1 session worth of supplies from stock:
${db.stock.filter(it=>(it.pcsPerClient||0)>0).map(it=>`${it.name}: −${it.pcsPerClient} pcs`).join("")}
${svcSelectorHTML("di","Massage","Relax Massage")}
`; openModal("ov-daily"); } function updateProfitPreview(){ const total=+eid("di-total")?.value||0; if(!total){eid("di-preview").style.display="none";return;} const vat=+eid("di-vat")?.value||0; const exp=+eid("di-exp")?.value||0; const profit=total-vat-exp; eid("di-preview").style.display="block"; eid("pp-total").textContent=SAR(total); eid("pp-vat").textContent="-"+SAR(vat); eid("pp-exp").textContent="-"+SAR(exp); eid("pp-profit").textContent=SAR(profit); eid("pp-profit").style.color=profit>=0?"var(--green)":"var(--red)"; } function saveDailyIncome(){ const total=+eid("di-total").value||0; if(!total)return; const cat=eid("di-cat").value; const mtype=cat==="Massage"?getSelectedMassage("di"):""; const zid=+eid("di-del").value; const z=db.transportZones.find(x=>x.id===zid)||{fee:0,name:"No Delivery"}; const entryId = Date.now(); const entryDate = eid("di-date").value; // Auto-deduct 1 session worth of stock const deductions = []; db.stock.forEach(it=>{ const ppc = it.pcsPerClient||0; if(ppc<=0) return; // pieces available in total const totalPcs = it.totalPkg * it.pkgQty; const newTotalPcs = Math.max(0, totalPcs - ppc); const newTotalPkg = newTotalPcs / it.pkgQty; const actualDeducted = totalPcs - newTotalPcs; if(actualDeducted>0){ deductions.push({itemId:it.id, itemName:it.name, pcsDeducted:actualDeducted, unit:it.unit, pkgQty:it.pkgQty}); } db.stock = db.stock.map(s=>s.id===it.id?{...s,totalPkg:newTotalPkg}:s); }); // Log each stock deduction if(!db.stockLog) db.stockLog=[]; deductions.forEach(d=>{ db.stockLog.push({ id:Date.now()+Math.random(), date:entryDate, itemId:d.itemId, itemName:d.itemName, units: d.pcsDeducted/d.pkgQty, pkgQty:d.pkgQty, unit:d.unit, note:`Auto-deducted for session — ${eid("di-client").value||"client"}`, entryId, auto:true }); }); db.dailyIncome.push({ id:entryId, date:entryDate, client:eid("di-client").value, cat,mtype,total, vat:+eid("di-vat").value||0, exp:+eid("di-exp").value||0, delfee:z.fee, discount:+eid("di-disc").value||0, method:eid("di-method").value, advance:eid("di-advance").checked, note:eid("di-note").value, stockDeducted: deductions.length }); closeModal("ov-daily"); refresh(); } function deleteDailyEntry(id){ // Restore stock when a daily entry is deleted const entry = db.dailyIncome.find(x=>x.id===id); if(entry && db.stockLog){ const entryLogs = db.stockLog.filter(l=>l.entryId===id&&l.auto); entryLogs.forEach(l=>{ const pcsToRestore = l.units * l.pkgQty; db.stock = db.stock.map(it=>it.id===l.itemId?{...it,totalPkg:it.totalPkg+(pcsToRestore/it.pkgQty)}:it); }); db.stockLog = db.stockLog.filter(l=>l.entryId!==id||!l.auto); } db.dailyIncome=db.dailyIncome.filter(x=>x.id!==id); refresh(); } // ── MODAL: REVENUE ──────────────────────────────────────────────────── function openRevModal(){ eid("md-rev").innerHTML = `
${logoHTML}Log Payment
${svcSelectorHTML("rev","Massage","Relax Massage")}
`; openModal("ov-rev"); } function saveRevenue(){ const amount=+eid("rev-amount").value||0; if(!amount)return; const cat=eid("rev-cat").value; const mtype=cat==="Massage"?getSelectedMassage("rev"):""; const zid=+eid("rev-del").value; const z=db.transportZones.find(x=>x.id===zid)||{fee:0,name:"No Delivery"}; db.revenue.push({id:Date.now(),client:eid("rev-client").value,cat,mtype,amount, delfee:z.fee,discount:+eid("rev-disc").value||0, date:eid("rev-date").value,method:eid("rev-method").value,advance:eid("rev-advance").checked}); closeModal("ov-rev"); refresh(); } function deleteRev(id){ db.revenue=db.revenue.filter(x=>x.id!==id); refresh(); } // ── MODAL: CLIENT ───────────────────────────────────────────────────── function openClientModal(){ eid("md-client").innerHTML = `
${logoHTML}New Client
${svcSelectorHTML("cl","Massage","Relax Massage")}
`; openModal("ov-client"); } function saveClient(){ const name=eid("cl-name").value.trim(); if(!name)return; const cat=eid("cl-cat").value; const mtype=cat==="Massage"?getSelectedMassage("cl"):""; db.clients.push({id:Date.now(),name,phone:eid("cl-phone").value,cat,mtype,visits:0,defTransport:eid("cl-transport").value,notes:eid("cl-notes").value}); closeModal("ov-client"); refresh(); } function addVisit(id){ db.clients=db.clients.map(c=>c.id===id?{...c,visits:c.visits+1}:c); refresh(); } function deleteClient(id){ db.clients=db.clients.filter(x=>x.id!==id); refresh(); } // ── MODAL: EXPENSE ──────────────────────────────────────────────────── function openExpenseModal(){ eid("md-expense").innerHTML = `
${logoHTML}Add Expense
`; openModal("ov-expense"); } function saveExpense(){ const amount=+eid("ex-amount").value||0; if(!amount)return; db.expenses.push({id:Date.now(),cat:eid("ex-cat").value,amount,note:eid("ex-note").value,date:eid("ex-date").value,emp:eid("ex-emp").value}); closeModal("ov-expense"); refresh(); } function deleteExpense(id){ db.expenses=db.expenses.filter(x=>x.id!==id); refresh(); } // ── MODAL: SALARY ───────────────────────────────────────────────────── function openSalaryModal(name,current){ state.salaryTarget=name; eid("md-salary").innerHTML = `
${logoHTML}Edit Salary

${name}

`; openModal("ov-salary"); } function saveSalary(){ db.employees=db.employees.map(e=>e.name===state.salaryTarget?{...e,salary:+eid("sal-amount").value||0}:e); closeModal("ov-salary"); refresh(); } // ── MODAL: PROMO ────────────────────────────────────────────────────── function openPromoModal(){ eid("md-promo").innerHTML = `
${logoHTML}New Promotion
${[...MASSAGE_TYPES,...SERVICE_CATS.filter(s=>s!=="Massage")].map(m=>`
${m}
`).join("")}
Applies to all services
`; // update check display on click eid("pr-svc-list").querySelectorAll(".svc-opt").forEach(el=>{ el.addEventListener("click",()=>{ const chk=el.querySelector(".svc-check"); const span=chk.querySelector("span"); if(el.classList.contains("sel")){chk.style.background="var(--purple)";chk.style.borderColor="var(--purple)";span.style.display="block";} else{chk.style.background="transparent";chk.style.borderColor="var(--muted)";span.style.display="none";} const cnt=eid("pr-svc-list").querySelectorAll(".svc-opt.sel").length; eid("pr-sel-count").textContent=cnt===0?"Applies to all services":cnt+" service(s) selected"; }); }); openModal("ov-promo"); } function savePromo(){ const name=eid("pr-name").value.trim(); const value=+eid("pr-value").value; if(!name||!value)return; const services=[...eid("pr-svc-list").querySelectorAll(".svc-opt.sel")].map(el=>el.dataset.svc); db.promos.push({id:Date.now(),name,type:eid("pr-type").value,value,services,active:eid("pr-active").checked,expiry:eid("pr-expiry").value,note:eid("pr-note").value}); closeModal("ov-promo"); refresh(); } function togglePromo(id){ db.promos=db.promos.map(p=>p.id===id?{...p,active:!p.active}:p); refresh(); } function deletePromo(id){ db.promos=db.promos.filter(x=>x.id!==id); refresh(); } // ── MODAL: GIFT CARD ────────────────────────────────────────────────── function openGCModal(){ eid("md-gc").innerHTML = `
${logoHTML}Issue Gift Card
PEARL
HOME SPA · GIFT CARD
For —
PEARL-XXXX-XXXX

• Gift cards are redeemable for the selected service

• A unique code will be auto-generated

• Track balance and redemptions in the Gift Cards tab

`; openModal("ov-gc"); } function updateGCPreview(){ const v=+eid("gc-value")?.value||0; const n=eid("gc-rname")?.value||"—"; if(eid("gc-prev-amount")) eid("gc-prev-amount").textContent = v>0?SAR(v):"—"; if(eid("gc-prev-name")) eid("gc-prev-name").textContent = "For "+n; } function saveGiftCard(){ const rname=eid("gc-rname").value.trim(); const value=+eid("gc-value").value; if(!rname||!value)return; db.giftCards.push({id:Date.now(),code:genCode(),recipientName:rname,recipientPhone:eid("gc-rphone").value, gifterName:eid("gc-gname").value,value,balance:value,service:eid("gc-svc").value, issued:eid("gc-issued").value,expiry:eid("gc-expiry").value,note:eid("gc-note").value,redeemed:false}); closeModal("ov-gc"); refresh(); } function redeemGC(id){ const inp=eid("gc-redeem-"+id); if(!inp)return; const amount=+inp.value; const g=db.giftCards.find(x=>x.id===id); if(!g||!amount||amount<=0||amount>g.balance)return; g.balance=Math.max(g.balance-amount,0); if(g.balance===0)g.redeemed=true; refresh(); } function deleteGC(id){ db.giftCards=db.giftCards.filter(x=>x.id!==id); refresh(); } // ── MODAL: REMINDER ─────────────────────────────────────────────────── function openRMModal(){ eid("md-rm").innerHTML = `
${logoHTML}New Reminder
`; openModal("ov-rm"); } function prefillRM(id){ const c=db.clients.find(c=>c.id===+id); if(!c)return; eid("rm-client").value=c.name; eid("rm-phone").value=c.phone; } function saveReminder(){ const client=eid("rm-client").value.trim(); if(!client)return; db.reminders.push({id:Date.now(),client,phone:eid("rm-phone").value,type:eid("rm-type").value, date:eid("rm-date").value,time:eid("rm-time").value,note:eid("rm-note").value,done:false}); closeModal("ov-rm"); refresh(); } function toggleReminder(id){ db.reminders=db.reminders.map(r=>r.id===id?{...r,done:!r.done}:r); refresh(); } function deleteReminder(id){ db.reminders=db.reminders.filter(x=>x.id!==id); refresh(); } // ── INVOICE ─────────────────────────────────────────────────────────── function openInvoice(clientId){ const c=db.clients.find(x=>x.id===clientId); if(!c)return; const cR=db.revenue.filter(r=>r.client===c.name); const sub=cR.length?cR.reduce((s,r)=>s+r.amount,0):(PRICES[c.mtype||c.cat]||300); const deliv=cR.reduce((s,r)=>s+(r.delfee||0),0); const disc=cR.reduce((s,r)=>s+(r.discount||0),0); const vat=sub*VAT; const hasAdv=cR.some(r=>r.advance); const total=sub+vat+deliv-disc-(hasAdv?ADVANCE:0); const invN=`PHS-${String(Date.now()).slice(-6)}`; eid("md-invoice").innerHTML = `
PEARL
HOME SPA
فاتورة ضريبية · Tax Invoice
VAT Reg. No: 300XXXXXXXXX
Invoice
#${invN}
${today}
Bill To / فاتورة إلى
${c.name}
📞 ${c.phone}
${["Service / Type","Date","Qty","Unit Price","Total"].map(h=>``).join("")} ${cR.length===0?``:cR.map(r=>``).join("")} ${deliv>0?``:""} ${disc>0?``:""} ${hasAdv?``:""}
${h}
${c.mtype||c.cat}${today} 1 ${SAR(sub)} ${SAR(sub)}
${r.mtype||r.cat}${r.date} 1 ${SAR(r.amount)} ${SAR(r.amount)}
Delivery Fee${SAR(deliv)}
Promotion Discount 🎁-${SAR(disc)}
Advance Deposit Received-${SAR(ADVANCE)}
Subtotal (excl. VAT)${SAR(sub)}
VAT 15% · ضريبة القيمة${SAR(vat)}
${deliv>0?`
Delivery Fee${SAR(deliv)}
`:""} ${disc>0?`
Discount-${SAR(disc)}
`:""} ${hasAdv?`
Advance Paid-${SAR(ADVANCE)}
`:""}
Total${SAR(total)}
`; openModal("ov-invoice"); } // ── TRANSPORT FEES TAB ──────────────────────────────────────────────── function renderTransport(){ const html = `
Active Zones
${db.transportZones.length}
${db.transportZones.filter(z=>z.fee>0).length} zones with fees
Fee Range
${SAR(Math.min(...db.transportZones.filter(z=>z.fee>0).map(z=>z.fee).concat([0])))}  –  ${SAR(Math.max(...db.transportZones.map(z=>z.fee).concat([0])))}
Min – Max delivery fee
${db.transportZones.map(z=>` `).join("")}
Zone Name (click to edit) Fee (﷼ SAR) (click to edit) Appointments Using This Zone VAT on Fee
${z.id===1 ? `${z.name}` : `` } ${z.id===1 ? `` : `
` }
${db.appts.filter(a=>a.delzone===z.name).length} appts ${z.fee>0?SAR(z.fee*0.15):"—"} ${z.id===1?"":``}
💡 Tip: Click any zone name or fee to edit. New fees apply immediately to all future bookings. Existing appointments keep their original fees.
`; eid("tab-transport").innerHTML = html; } function addTransportZone(){ db.transportZones.push({id:Date.now(),name:"New Zone",fee:0}); renderTransport(); } function updateZoneName(id,val){ db.transportZones=db.transportZones.map(z=>z.id===id?{...z,name:val}:z); } function updateZoneFee(id,val){ db.transportZones=db.transportZones.map(z=>z.id===id?{...z,fee:+val||0}:z); } function deleteZone(id){ db.transportZones=db.transportZones.filter(z=>z.id!==id); renderTransport(); } // ── STOCK TAB ───────────────────────────────────────────────────────── function renderStock(){ if(!db.stockLog) db.stockLog=[]; const totalUnits=db.stock.reduce((s,it)=>s+(it.totalPkg*it.pkgQty),0); const outOf=db.stock.filter(it=>it.totalPkg===0).length; const totalSessions=db.dailyIncome.length; // ── Discrepancy analysis ── // Expected pieces used = sessions × pcsPerClient // Actual pieces used = from stockLog auto entries const alerts = db.stock.map(it=>{ const expectedUsed = totalSessions * (it.pcsPerClient||0); const actualUsed = db.stockLog.filter(l=>l.itemId===it.id&&l.auto).reduce((s,l)=>s+(l.units*l.pkgQty),0); const diff = actualUsed - expectedUsed; // positive = more used than expected = suspicious return {...it, expectedUsed, actualUsed, diff}; }).filter(it=>(it.pcsPerClient||0)>0); const suspicious = alerts.filter(a=>a.diff>0); const html=`
Total Items
${db.stock.length}
Total Pieces
${Math.floor(totalUnits).toLocaleString()}
Sessions Logged
${totalSessions}
Out of Stock
${outOf}
⚠ Discrepancies
${suspicious.length}
${suspicious.length>0?`
⚠️
Stock Discrepancy Detected
${suspicious.length} item(s) show more usage than expected based on ${totalSessions} logged sessions. This could indicate over-use, theft, or unrecorded sales.
${suspicious.map(a=>``).join("")}
Item Sessions Logged Expected Used (${db.stock.find(x=>x.id===suspicious[0]?.id)?.pcsPerClient||"N"} pcs/session) Actually Used Extra / Missing ⚠ Equivalent Sessions
${a.name} ${totalSessions} ${a.expectedUsed.toFixed(1)} pcs ${a.actualUsed.toFixed(1)} pcs +${a.diff.toFixed(1)} pcs extra ≈ ${(a.pcsPerClient?Math.round(a.diff/a.pcsPerClient):0)} extra sessions worth
`: totalSessions>0?`
Stock usage matches ${totalSessions} logged sessions — no discrepancies detected.
`:""} ${totalSessions>0?`
Usage vs Expectation — Per Item
${alerts.map(a=>{ const pct = a.expectedUsed>0 ? Math.min((a.actualUsed/a.expectedUsed)*100,200) : 0; const over = a.diff>0; return `
${a.name}
${over?"⚠ Over-used":a.diff===0?"✓ Exact":"Under-used"}
Expected
${a.expectedUsed.toFixed(1)}
pieces
Actual
${a.actualUsed.toFixed(1)}
pieces
Difference
${a.diff>0?"+":""}${a.diff.toFixed(1)}
pieces
${pct.toFixed(0)}% of expected usage · ${a.pcsPerClient} pcs/session setting
`; }).join("")}
`:""}
Stock Levels & Settings
${db.stock.map(it=>{ const totalPcs=it.totalPkg*it.pkgQty; const sessionsLeft = it.pcsPerClient>0 ? Math.floor(totalPcs/it.pcsPerClient) : "∞"; const isOut=it.totalPkg===0; const isLow=!isOut&&it.totalPkg>0&&it.totalPkg<=(it.alertAt||2); return ``; }).join("")}
Item Name Unit Pcs / Unit Pcs Per Session (used per client) Units in Stock Total Pieces Capacity (sessions left) Alert At Notes Use
pcs
pcs
${Math.floor(totalPcs).toLocaleString()} pcs ${isOut?`
✗ Out of stock
`:""} ${isLow?`
⚠ Low
`:""}
${sessionsLeft} ${sessionsLeft!=="∞"?"sessions":""}
${it.pcsPerClient>0?`
${it.pcsPerClient} pcs/session
`:""}
${db.stockLog.length>0?`
Stock Usage Log
${db.stockLog.slice().reverse().slice(0,20).map(l=>``).join("")}
DateItemPieces UsedSourceNote
${l.date} ${l.itemName} −${(l.units*l.pkgQty).toFixed(1)} pcs ${l.auto?"📋 Session Auto":"✋ Manual"} ${l.note||"—"}
`:""}
📋 How the link works:
Every time you log a Daily Income entry (one client session), the platform automatically deducts Pcs Per Session from each item's stock. If the actual stock used is more than expected based on your session count — the discrepancy panel above will flag it. This helps you catch over-usage, employee theft, or unrecorded sales immediately. If you delete a daily entry, the stock is automatically restored.
`; eid("tab-stock").innerHTML=html; } function updateStock(id,field,val){ db.stock=db.stock.map(it=>it.id===id?{...it,[field]:val}:it); } function adjustStock(id,delta){ db.stock=db.stock.map(it=>it.id===id?{...it,totalPkg:Math.max(0,it.totalPkg+delta)}:it); renderStock(); } function addStockItem(){ db.stock.push({id:Date.now(),name:"New Item",unit:"Pack",pkgQty:1,totalPkg:0,alertAt:0,notes:""}); renderStock(); } function deleteStockItem(id){ db.stock=db.stock.filter(x=>x.id!==id); renderStock(); } function openUseStockModal(id){ const it=db.stock.find(x=>x.id===id); if(!it)return; const ov=document.createElement("div"); ov.className="ov open"; ov.innerHTML=`
PEARL
HOME SPA
Record Stock Usage
${it.name}
Available: ${it.totalPkg} ${it.unit}(s)  =  ${(it.totalPkg*it.pkgQty).toLocaleString()} pieces
= ${it.pkgQty} pieces total
`; ov.addEventListener("click",e=>{if(e.target===ov)ov.remove();}); document.body.appendChild(ov); } function confirmUseStock(id,pkgQty,unit,itemName){ const qty=+document.getElementById("use-qty")?.value||0; if(!qty)return; const note=document.getElementById("use-note")?.value||""; const date=document.getElementById("use-date")?.value||today; db.stock=db.stock.map(it=>it.id===id?{...it,totalPkg:Math.max(0,it.totalPkg-qty)}:it); if(!db.stockLog)db.stockLog=[]; db.stockLog.push({id:Date.now(),date,itemId:id,itemName,units:qty,pkgQty,unit,note}); document.querySelector(".ov.open:last-of-type")?.remove(); renderStock(); } // ── REFRESH ─────────────────────────────────────────────────────────── function refresh(){ renderHeader(); renderTab(state.tab); } // ── SAVE / LOAD / AUTOSAVE ──────────────────────────────────────────── function saveData(){ try { const payload = JSON.stringify({version:1, savedAt: new Date().toISOString(), db}, null, 2); const blob = new Blob([payload], {type:"application/json"}); const url = URL.createObjectURL(blob); const a = document.createElement("a"); const dateStr = new Date().toISOString().slice(0,10); a.href = url; a.download = `PearlHomeSpa_${dateStr}.json`; a.click(); URL.revokeObjectURL(url); // Show saved indicator const ind = eid("save-indicator"); if(ind){ ind.style.display="inline"; setTimeout(()=>ind.style.display="none", 2500); } } catch(e){ alert("Error saving: " + e.message); } } function loadData(event){ const file = event.target.files[0]; if(!file) return; const reader = new FileReader(); reader.onload = function(e){ try { const parsed = JSON.parse(e.target.result); if(!parsed.db) throw new Error("Invalid file — not a Pearl Home Spa backup"); // Confirm before overwriting if(!confirm(`Load data saved on ${parsed.savedAt ? new Date(parsed.savedAt).toLocaleString() : "unknown date"}?\n\nThis will replace all current data. Make sure you have saved the current data first.`)) return; // Restore db — merge defaults for any new fields added since last save Object.keys(parsed.db).forEach(key => { db[key] = parsed.db[key]; }); // Ensure new fields exist if(!db.stockLog) db.stockLog = []; if(!db.transportZones) db.transportZones = [{id:1,name:"No Delivery",fee:0}]; db.stock = (db.stock||[]).map(it=>({pcsPerClient:0,alertAt:2,...it})); refresh(); autoSave(); alert("✅ Data loaded successfully! Your Pearl Home Spa data has been restored."); } catch(err){ alert("❌ Could not load file: " + err.message); } }; reader.readAsText(file); // Reset input so same file can be loaded again if needed event.target.value = ""; } // Auto-save to browser localStorage every time data changes function autoSave(){ try { localStorage.setItem("pearlhomespa_db", JSON.stringify(db)); localStorage.setItem("pearlhomespa_saved", new Date().toISOString()); } catch(e){ /* localStorage might be full or unavailable */ } } function autoLoad(){ try { const saved = localStorage.getItem("pearlhomespa_db"); if(saved){ const parsed = JSON.parse(saved); Object.keys(parsed).forEach(key=>{ db[key] = parsed[key]; }); if(!db.stockLog) db.stockLog=[]; if(!db.transportZones) db.transportZones=[{id:1,name:"No Delivery",fee:0}]; db.stock=(db.stock||[]).map(it=>({pcsPerClient:0,alertAt:2,...it})); console.log("Pearl Home Spa: data auto-loaded from browser storage"); } } catch(e){ console.log("Auto-load skipped:", e.message); } } // Patch refresh to also auto-save const _origRefresh = refresh; function refresh(){ _origRefresh(); autoSave(); } // ── INIT ────────────────────────────────────────────────────────────── autoLoad(); // ← Restore data from last session automatically renderNav(); renderHeader(); renderDashboard();