Logo
๐Ÿ“ธ Face Recognition Attendance
๐Ÿ“ GPS Location + Geo-Fence Rules
โฐ Configurable Attendance Policies
๐Ÿ’ฐ Auto Salary Calculation + LOP
๐Ÿ“„ Salary Slip with PF/ESI/TDS
๐Ÿงพ Invoice Customization
๐Ÿ“‹ AMC Agreement Generator
๐Ÿ“Š Quotation with Custom T&C

Sign In

FieldOPS Service Management

โŒ Invalid credentials
`);w.document.close()}; // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // AUTH // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function doLogin(){ const u=v('lu'),p=v('lp'),r=v('lr2'); const e=g('lerr');e.style.display='none'; const user=DB.users.find(x=>x.username===u&&x.password===p&&x.role===r); if(!user){e.innerHTML='โŒ Invalid credentials';e.style.display='block';return;} if(user.status===0){e.innerHTML='โŒ Account is deactivated';e.style.display='block';return;} CU=user; g('LS').style.display='none';g('APP').style.display='flex'; g('tAv').textContent=CU.avatar;g('tAv').style.background=CU.color+'22';g('tAv').style.color=CU.color; g('tNm').textContent=CU.name; ['navA','navE','navS'].forEach(id=>{if(g(id))g(id).style.display='none';}); if(CU.role==='admin'){g('tRl').textContent='ADMIN';g('tRl').className='rb ra';g('navA').style.display='block';nav('dash');} else if(CU.role==='sales'){g('tRl').textContent='SALES';g('tRl').className='rb rs';g('navS').style.display='block';nav('salesDash');} else{g('tRl').textContent='ENGINEER';g('tRl').className='rb re';g('navE').style.display='block';nav('engDash');} startClock();populateSalMonths();populateAttFilters(); } document.addEventListener('keydown',e=>{if(e.key==='Enter'&&g('LS').style.display!=='none')doLogin()}); function logout(){ if(camStream){camStream.getTracks().forEach(t=>t.stop());camStream=null;} CU=null;g('APP').style.display='none';g('LS').style.display='flex'; g('lu').value='';g('lp').value='';g('lr2').value=''; } function startClock(){ setInterval(()=>{ const n=new Date(); const ct=g('clockTime');if(ct)ct.textContent=n.toLocaleTimeString('en-IN',{hour12:false}); const cd=g('clockDate');if(cd)cd.textContent=n.toLocaleDateString('en-IN',{weekday:'long',year:'numeric',month:'long',day:'numeric'}); },1000); } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // NAVIGATION // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function nav(page){ document.querySelectorAll('.page').forEach(p=>p.classList.remove('active')); document.querySelectorAll('.ni').forEach(n=>n.classList.remove('active')); const el=g('page-'+page);if(el)el.classList.add('active'); document.querySelectorAll('.ni').forEach(n=>{if(n.onclick&&n.getAttribute('onclick')?.includes("'"+page+"'"))n.classList.add('active');}); updBadges();rPage(page); } function rPage(p){ const r={dash:rDash,tickets:rTickets,reports:rReports,todo:()=>rTodo('admin'),customers:rCustomers,pipeline:rPipeline,engineers:rEngineers,attendance:rAttAdmin,salary:rSalaryAdmin,settings:rSettings,log:rLog,engDash:rEngDash,myTickets:rMyTickets,myTodo:()=>rTodo('engineer'),myAttendance:rMyAtt,mySalary:rMySalary, // CRM routes salesDash:rSalesDash,leads:rLeads,quotations:rQuotations,sites:rSites,amcTrack:rAmcTrack,followups:rFollowups,meetings:rMeetings,fieldReports:rFieldReports,engLog:rEngLog,users:rUsers}; r[p]&&r[p](); } function updBadges(){ s2('nb-open',DB.tickets.filter(t=>t.status==='Open').length); s2('nb-todo',DB.todos.admin.filter(t=>!t.done).length); s2('nb-myopen',CU?DB.tickets.filter(t=>t.assignedId===CU.id&&t.status!=='Done'&&t.status!=='Closed').length:0); s2('nb-mytodo',DB.todos.engineer.filter(t=>!t.done).length); s2('ndot',DB.log.length); // CRM badges const openLeads=(DB.leads||[]).filter(l=>l.stage!=='Closed').length; const pendQuot=(DB.quotations||[]).filter(q=>q.status==='Pending').length; const expAMC=(DB.amcTrack||[]).filter(a=>{const d=daysLeft(a.endDate);return d!==null&&d<=30&&a.renewalStatus==='Active';}).length; const todayFu=(DB.followups||[]).filter(f=>f.followUpDate===today()&&f.status==='Pending').length; const todayMtg=(DB.meetings||[]).filter(m=>m.date===today()&&m.status==='Scheduled').length; ['nb-leads','nb-leads-s'].forEach(id=>s2(id,openLeads)); ['nb-quot','nb-quot-s'].forEach(id=>s2(id,pendQuot)); s2('nb-amc',expAMC); ['nb-fu','nb-fu-s'].forEach(id=>s2(id,todayFu)); ['nb-mtg','nb-mtg-s'].forEach(id=>s2(id,todayMtg)); } function daysLeft(d){if(!d)return null;return Math.ceil((new Date(d)-new Date())/86400000);} // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // DASHBOARD // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function rDash(){ const t=DB.tickets,c=DB.customers,p=DB.pipeline; const leads=DB.leads||[],quot=DB.quotations||[],amc=DB.amcTrack||[],fu=DB.followups||[],mtg=DB.meetings||[]; const pipelineVal=leads.filter(l=>l.stage!=='Closed').reduce((s,l)=>s+(parseFloat(l.value)||0),0); g('dashStats').innerHTML=[ {i:'๐ŸŽซ',v:t.length,l:'Tickets',c:'teal'},{i:'๐Ÿ“‚',v:t.filter(x=>x.status==='Open').length,l:'Open',c:'blue'}, {i:'โš™๏ธ',v:t.filter(x=>x.status==='In Progress').length,l:'In Progress',c:'amber'}, {i:'๐Ÿข',v:c.filter(x=>x.status==='active').length,l:'Active AMC',c:'green'}, {i:'๐Ÿ’ฐ',v:leads.filter(l=>l.stage!=='Closed').length,l:'Open Leads',c:'purple'}, {i:'๐Ÿ“‘',v:quot.filter(q=>q.status==='Pending').length,l:'Pending Quotes',c:'amber'}, {i:'๐Ÿ“…',v:amc.filter(a=>{const d=daysLeft(a.endDate);return d!==null&&d<=30&&a.renewalStatus==='Active';}).length,l:'AMC Exp. 30d',c:'red'}, {i:'๐Ÿ“†',v:mtg.filter(m=>m.date===today()&&m.status==='Scheduled').length,l:'Today Mtgs',c:'blue'}, {i:'๐Ÿ”',v:fu.filter(f=>f.followUpDate===today()&&f.status==='Pending').length,l:'Today F/U',c:'orange'}, {i:'๐Ÿ“ˆ',v:`โ‚น${(pipelineVal/1000).toFixed(0)}K`,l:'Lead Pipeline',c:'pink'}, {i:'๐Ÿ”ด',v:t.filter(x=>x.priority==='Urgent'&&x.status!=='Done').length,l:'Urgent',c:'red'}, {i:'๐ŸŸฃ',v:c.filter(x=>x.status==='lead').length,l:'New Cust Leads',c:'purple'}, ].map(s=>`
${s.i}
${s.v}
${s.l}
`).join(''); g('dashTbody').innerHTML=[...DB.tickets].sort((a,b)=>b.createdAt.localeCompare(a.createdAt)).slice(0,5).map(t=>tRow(t,true)).join('')||eRow(7,'No tickets'); } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // TICKETS // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function tRow(t,mini=false){ const eng=DB.users.find(u=>u.id===t.assignedId); return ` ${t.id} ${t.subject} ${t.customer} ${mini?'':`${t.location}`} ${eng?.avatar||'?'}${eng?.name||'?'} ${t.priority} ${t.status} ${mini?'':`${t.date}`} ${mini?'':``} `; } function rTickets(){let list=DB.tickets;if(tktF!=='all')list=list.filter(t=>t.status===tktF);list=[...list].sort((a,b)=>b.createdAt.localeCompare(a.createdAt));g('allTktBody').innerHTML=list.map(t=>tRow(t,false)).join('')||eRow(9,'No tickets');} function fTkt(f,el){tktF=f;g('tktF').querySelectorAll('.fb').forEach(b=>b.classList.remove('active'));el.classList.add('active');rTickets();} function openNewTicket(){g('mTktT').textContent='New Ticket';g('tId').value='';['tSubj','tCust','tLoc','tDesc'].forEach(x=>g(x).value='');g('tPri').value='High';g('tDate').value=today();g('tAssign').innerHTML=DB.users.filter(u=>u.role==='engineer').map(u=>``).join('');openM('mTicket');} function saveTicket(){ const subj=v('tSubj');if(!subj){toast('Subject required','e');return;} const now=nowStr(),tid=v('tId'); if(tid){const t=DB.tickets.find(x=>x.id===tid);Object.assign(t,{subject:subj,customer:v('tCust'),location:v('tLoc'),priority:v('tPri'),assignedId:v('tAssign'),category:v('tCat'),date:v('tDate'),desc:v('tDesc')});t.history.push({time:now,text:`Updated by ${CU.name}`,type:'update'});toast('Updated','s');} else{const nid='TK-'+(String(DB.tickets.length+1).padStart(3,'0'));DB.tickets.push({id:nid,subject:subj,customer:v('tCust'),location:v('tLoc'),priority:v('tPri'),assignedId:v('tAssign'),category:v('tCat'),date:v('tDate'),desc:v('tDesc'),status:'Open',createdAt:today(),history:[{time:now,text:`Created by ${CU.name}`,type:'create'}],checklist:[],parts:[],labor:0,remarks:''});addLog(`New ticket ${nid} created`,'๐ŸŽซ');toast('Ticket created','s');} saveDB();closeM('mTicket');rDash();updBadges(); } function delTkt(tid){if(!confirm('Delete '+tid+'?'))return;DB.tickets=DB.tickets.filter(t=>t.id!==tid);saveDB();toast('Deleted','a');rTickets();updBadges();} function chgStatus(tid,st){ const t=DB.tickets.find(x=>x.id===tid);if(!t)return; t.status=st;t.history.push({time:nowStr(),text:`Status โ†’ "${st}" by ${CU.name}`,type:st==='Done'?'done':'action'}); addLog(`${tid} โ†’ ${st}`,'๐Ÿ”„');saveDB();updBadges();toast(`${tid} โ†’ ${st}`,'s'); rPage(CU.role==='admin'?'dash':'engDash'); } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // JOB DETAIL MODAL // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function openJob(tid){ const t=DB.tickets.find(x=>x.id===tid);if(!t)return;jobId=tid; const eng=DB.users.find(u=>u.id===t.assignedId); g('mJobT').textContent=`${t.id} โ€” ${t.subject}`; g('mJobS').textContent=`${t.customer} ยท ${t.location} ยท ${t.category}`; g('mJobSt').textContent=t.status;g('mJobSt').className='badge '+sCls(t.status); g('jdGrid').innerHTML=`
${t.customer}
${t.location}
${t.category}
${t.date}
${t.priority}
${eng?.name||'?'}
`; g('jdDesc').textContent=t.desc; g('jdBtns').innerHTML=['Open','In Progress','Done','Closed'].filter(s=>s!==t.status).map(s=>``).join('')+``; rTL(t);rCL(t);rParts(t); document.querySelectorAll('.jd-tab').forEach(x=>x.classList.remove('active')); document.querySelectorAll('.jd-sec').forEach(x=>x.classList.remove('active')); document.querySelector('.jd-tab').classList.add('active');g('jd-info').classList.add('active'); openM('mJob'); } function jdTab(n,el){document.querySelectorAll('.jd-tab').forEach(x=>x.classList.remove('active'));document.querySelectorAll('.jd-sec').forEach(x=>x.classList.remove('active'));el.classList.add('active');g('jd-'+n).classList.add('active');if(n==='inv')rInvoice();} function editTktJob(tid){const t=DB.tickets.find(x=>x.id===tid);if(!t)return;closeM('mJob');g('mTktT').textContent='Edit โ€” '+tid;g('tId').value=tid;g('tSubj').value=t.subject;g('tCust').value=t.customer;g('tLoc').value=t.location;g('tPri').value=t.priority;g('tDate').value=t.date;g('tDesc').value=t.desc;g('tCat').value=t.category;g('tAssign').innerHTML=DB.users.filter(u=>u.role==='engineer').map(u=>``).join('');openM('mTicket');} // Timeline function rTL(t){g('jdTL').innerHTML=(t.history||[]).length?t.history.map(h=>`
${h.text}
${h.time}
`).join(''):`
No timeline entries
`;g('tlInput').value='';} function addTL(){const t=DB.tickets.find(x=>x.id===jobId);if(!t)return;const txt=g('tlInput').value.trim();if(!txt){toast('Enter note','e');return;}t.history.push({time:nowStr(),text:txt,type:'action'});saveDB();rTL(t);toast('Added','s');} // Checklist const DEF_CL=[{text:'Site inspection & safety check',cat:'Inspection'},{text:'Identify root cause of issue',cat:'Diagnosis'},{text:'Photograph before repair',cat:'Documentation'},{text:'Perform repair/replacement',cat:'Repair'},{text:'Test all components',cat:'Testing'},{text:'Clean work area',cat:'Completion'},{text:'Explain work to customer',cat:'Completion'},{text:'Get customer sign-off',cat:'Completion'}]; function rCL(t){const cl=t.checklist||[];const done=cl.filter(x=>x.done).length;g('clProg').textContent=`${done}/${cl.length} completed`;g('jdCL').innerHTML=cl.length?cl.map((i,idx)=>`
${i.done?'โœ“':''}
${i.text}${i.cat}
`).join(''):`
No items. Add or load default.
`;g('clInput').value='';g('clCat').value='';} function toggleCL(i){const t=DB.tickets.find(x=>x.id===jobId);if(!t)return;t.checklist[i].done=!t.checklist[i].done;saveDB();rCL(t);} function rmCL(i){const t=DB.tickets.find(x=>x.id===jobId);if(!t)return;t.checklist.splice(i,1);saveDB();rCL(t);} function addCL(){const t=DB.tickets.find(x=>x.id===jobId);if(!t)return;const txt=g('clInput').value.trim();if(!txt){toast('Enter item','e');return;}t.checklist.push({id:'c'+Date.now(),text:txt,cat:g('clCat').value.trim()||'General',done:false});saveDB();rCL(t);} function loadDefCL(){const t=DB.tickets.find(x=>x.id===jobId);if(!t)return;if(t.checklist.length&&!confirm('Replace checklist?'))return;t.checklist=DEF_CL.map((c,i)=>({id:'dc'+i,...c,done:false}));saveDB();rCL(t);toast('Default loaded','s');} // Parts function rParts(t){g('partsList').innerHTML=(t.parts||[]).map((p,i)=>`
`).join('');g('laborAmt').value=t.labor||0;g('workRemarks').value=t.remarks||'';calcParts();} function upP(i,f,val){const t=DB.tickets.find(x=>x.id===jobId);if(!t||!t.parts[i])return;t.parts[i][f]=f==='name'?val:parseFloat(val)||0;const rows=g('partsList').querySelectorAll('.part-row');if(rows[i])rows[i].querySelectorAll('input')[3].value=(t.parts[i].qty*t.parts[i].unit).toFixed(0);calcParts();} function addPart(){const t=DB.tickets.find(x=>x.id===jobId);if(!t)return;t.parts.push({name:'',qty:1,unit:0});saveDB();rParts(t);} function rmP(i){const t=DB.tickets.find(x=>x.id===jobId);if(!t)return;t.parts.splice(i,1);saveDB();rParts(t);} function calcParts(){const t=DB.tickets.find(x=>x.id===jobId);if(!t)return;const pt=t.parts.reduce((s,p)=>s+(p.qty*p.unit),0);const lab=parseFloat(g('laborAmt')?.value)||0;const sub=pt+lab;const gst=sub*0.18;const tot=sub+gst;g('partTotals').innerHTML=`Parts: โ‚น${pt.toLocaleString()}Labor: โ‚น${lab.toLocaleString()}GST: โ‚น${gst.toFixed(0)}Total: โ‚น${tot.toFixed(0)}`;} function saveJobReport(){const t=DB.tickets.find(x=>x.id===jobId);if(!t)return;t.labor=parseFloat(g('laborAmt')?.value)||0;t.remarks=g('workRemarks')?.value||'';saveDB();addLog(`Report saved for ${jobId}`,'๐Ÿ“‹');toast('Report saved','s');} // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // INVOICE // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function rInvoice(){ const t=DB.tickets.find(x=>x.id===jobId);if(!t)return; const eng=DB.users.find(u=>u.id===t.assignedId);const co=DB.settings.company;const inv=DB.settings.invoice; const parts=t.parts||[];const pt=parts.reduce((s,p)=>s+(p.qty*p.unit),0);const lab=t.labor||0; const sub=pt+lab;const gstPct=parseFloat(g('invGST')?.value)||18;const disc=parseFloat(g('invDisc')?.value)||0; const gst=(sub-disc)*(gstPct/100);const tot=sub-disc+gst;const invNo=`${inv.prefix}-${t.id}-${new Date().getFullYear()}`; g('invPreview').innerHTML=`

${co.logo} ${co.name}

${co.tagline}
${co.address}
${co.phone} ยท ${co.email}
GSTIN: ${co.gstin}

TAX INVOICE

${invNo}

Bill To
${t.customer}
${t.location}
Invoice No.:${invNo}Date:${today()}Service:${t.date}Engineer:${eng?.name||'โ€”'}
${parts.map((p,i)=>``).join('')} ${lab>0?``:''} ${parts.length===0&&lab===0?``:''}
#DescriptionQtyRateAmount
${i+1}${p.name}${p.qty}โ‚น${p.unit.toLocaleString()}โ‚น${(p.qty*p.unit).toLocaleString()}
${parts.length+1}Labor / Service Charges1โ‚น${lab.toLocaleString()}โ‚น${lab.toLocaleString()}
No items โ€” add parts in the Parts tab
Subtotalโ‚น${sub.toLocaleString()}
${disc>0?`
Discountโˆ’โ‚น${disc.toLocaleString()}
`:''}
GST @ ${gstPct}%โ‚น${gst.toFixed(0)}
TOTAL DUEโ‚น${tot.toFixed(0)}
${t.remarks?`
Work Summary: ${t.remarks}
`:''}
Terms: ${g('invNotes')?.value||inv.paymentTerms}
${inv.showBank?`
Bank: ${co.bank} | A/C: ${co.account} | IFSC: ${co.ifsc}
`:''}
${inv.footer}
`; } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // SERVICE REPORTS // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function rReports(){ let list=DB.tickets;if(repF!=='all')list=list.filter(t=>t.status===repF);list=[...list].sort((a,b)=>b.createdAt.localeCompare(a.createdAt)); if(!list.length){g('reportList').innerHTML=`
๐Ÿ“‹

No reports

`;return;} g('reportList').innerHTML=list.map(t=>{ const eng=DB.users.find(u=>u.id===t.assignedId);const pt=t.parts.reduce((s,p)=>s+(p.qty*p.unit),0);const tot=(pt+(t.labor||0))*1.18;const cl=t.checklist||[]; return `
${t.id} ยท ${t.date}
${t.subject}
๐Ÿข ${t.customer} ยท ๐Ÿ“ ${t.location} ยท ๐Ÿ‘ท ${eng?.name||'?'}
${t.status}${t.priority}
${cl.filter(c=>c.done).length}/${cl.length}
${t.parts.length}
โ‚น${pt.toLocaleString()}
โ‚น${(t.labor||0).toLocaleString()}
โ‚น${tot.toFixed(0)}
${t.remarks?`
๐Ÿ“ ${t.remarks}
`:''}
`; }).join(''); } function fRep(f,el){repF=f;g('repF')?.querySelectorAll('.fb').forEach(b=>b.classList.remove('active'));el.classList.add('active');rReports();} // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // CUSTOMERS // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function rCustomers(){ const counts={active:DB.customers.filter(c=>c.status==='active').length,lead:DB.customers.filter(c=>c.status==='lead').length,inactive:DB.customers.filter(c=>c.status==='inactive').length}; s2('cnt-ac',`(${counts.active})`);s2('cnt-ld',`(${counts.lead})`);s2('cnt-in',`(${counts.inactive})`); const list=DB.customers.filter(c=>c.status===custTab); if(!list.length){g('custGrid').innerHTML=`
๐Ÿข

No customers

`;return;} g('custGrid').innerHTML=`
${list.map(c=>{ const eng=DB.users.find(u=>u.id===c.engineerId); const sb=c.status==='active'?'bac':c.status==='inactive'?'bin':'blead'; const sl=c.status==='active'?'Active AMC':c.status==='inactive'?'Inactive AMC':'New Lead'; return `
${c.id}
${c.name}
${sl}
๐Ÿ‘ค ${c.contact}
๐Ÿ“ž ${c.phone}
${c.location?`
๐Ÿ“ ${c.location}
`:''} ${c.amcValue?`
๐Ÿ’ฐ โ‚น${c.amcValue.toLocaleString()}/yr
`:''} ${c.expiry?`
๐Ÿ“… Exp: ${c.expiry}
`:''}${eng?`
๐Ÿ‘ท ${eng.name}
`:''}
${c.notes?`
${c.notes}
`:''}
`; }).join('')}
`; } function setCustTab(tab,el){custTab=tab;g('custTabs').querySelectorAll('.tab').forEach(t=>t.classList.remove('active'));el.classList.add('active');rCustomers();} function openCustModal(){g('mCustT').textContent='Add Customer';g('cId').value='';['cName','cContact','cPhone','cEmail','cLoc','cNotes','cProducts'].forEach(x=>g(x).value='');g('cAMCval').value='';g('cExpiry').value='';g('cStatus').value='active';g('cEng').innerHTML=''+DB.users.filter(u=>u.role==='engineer').map(u=>``).join('');openM('mCust');} function editCust(cid){const c=DB.customers.find(x=>x.id===cid);if(!c)return;g('mCustT').textContent='Edit โ€” '+c.name;g('cId').value=cid;g('cName').value=c.name;g('cContact').value=c.contact;g('cPhone').value=c.phone;g('cEmail').value=c.email;g('cLoc').value=c.location;g('cStatus').value=c.status;g('cAMCval').value=c.amcValue||'';g('cExpiry').value=c.expiry||'';g('cProducts').value=c.products||'';g('cNotes').value=c.notes||'';g('cEng').innerHTML=''+DB.users.filter(u=>u.role==='engineer').map(u=>``).join('');openM('mCust');} function saveCust(){const name=v('cName');if(!name){toast('Name required','e');return;}const cid=v('cId');const d={name,contact:v('cContact'),phone:v('cPhone'),email:v('cEmail'),location:v('cLoc'),status:v('cStatus'),amcValue:parseFloat(v('cAMCval'))||0,expiry:v('cExpiry'),products:v('cProducts'),engineerId:v('cEng'),notes:v('cNotes')};if(cid){Object.assign(DB.customers.find(x=>x.id===cid),d);toast('Updated','s');}else{const nid='C-'+(String(DB.customers.length+1).padStart(3,'0'));DB.customers.push({id:nid,...d,createdAt:today()});toast('Added','s');}saveDB();closeM('mCust');rCustomers();} function delCust(cid){const c=DB.customers.find(x=>x.id===cid);if(!c||!confirm('Delete '+c.name+'?'))return;DB.customers=DB.customers.filter(x=>x.id!==cid);saveDB();toast('Deleted','a');rCustomers();} // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // PIPELINE // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• const STAGES=['Lead','Quotation','Proposal','Negotiation','Won']; const SC={Lead:'var(--purple)',Quotation:'var(--blue)',Proposal:'var(--amber)',Negotiation:'var(--orange)',Won:'var(--green)'}; function rPipeline(){ const tv=DB.pipeline.reduce((s,p)=>s+p.value,0),wv=DB.pipeline.filter(p=>p.stage==='Won').reduce((s,p)=>s+p.value,0); g('pipeTotals').innerHTML=`
Total Pipeline
โ‚น${tv.toLocaleString()}
Active Deals
โ‚น${(tv-wv).toLocaleString()}
Won
โ‚น${wv.toLocaleString()}
Total Deals
${DB.pipeline.length}
`; g('kanban').innerHTML=STAGES.map(st=>{ const deals=DB.pipeline.filter(p=>p.stage===st);const sv=deals.reduce((s,p)=>s+p.value,0); return `
${st}
${deals.length}ยทโ‚น${(sv/1000).toFixed(0)}K
${deals.map(d=>`
${d.title}
๐Ÿข ${d.customer}
โ‚น${d.value.toLocaleString()}
${STAGES.filter(s=>s!==st).map(s=>``).join('')}
`).join('')||`
No deals
`}
`; }).join(''); } function openPipeModal(){openPipeModalStage('Lead');} function openPipeModalStage(st){g('mPipeT').textContent='Add Deal';g('pId').value='';['pTitle','pCust','pContact','pProducts','pNotes'].forEach(x=>g(x).value='');g('pValue').value='';g('pClose').value='';g('pStage').value=st;g('pPri').value='Medium';g('pAssign').innerHTML=''+DB.users.filter(u=>u.role==='engineer').map(u=>``).join('');openM('mPipe');} function saveDeal(){const title=v('pTitle');if(!title){toast('Title required','e');return;}const pid=v('pId');const d={title,customer:v('pCust'),contact:v('pContact'),value:parseFloat(v('pValue'))||0,stage:v('pStage'),products:v('pProducts'),closeDate:v('pClose'),assignedId:v('pAssign'),priority:v('pPri'),notes:v('pNotes')};if(pid){Object.assign(DB.pipeline.find(x=>x.id===pid),d);toast('Updated','s');}else{const nid='P-'+(String(DB.pipeline.length+1).padStart(3,'0'));DB.pipeline.push({id:nid,...d,createdAt:today()});toast('Added','s');}saveDB();closeM('mPipe');rPipeline();} function moveDeal(did,st){const d=DB.pipeline.find(x=>x.id===did);if(!d)return;d.stage=st;saveDB();toast(`โ†’ ${st}`,'s');rPipeline();} function delDeal(did){const d=DB.pipeline.find(x=>x.id===did);if(!d||!confirm('Delete "'+d.title+'"?'))return;DB.pipeline=DB.pipeline.filter(x=>x.id!==did);saveDB();rPipeline();} // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // ENGINEERS // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function rEngineers(){ const engs=DB.users.filter(u=>u.role==='engineer'); if(!engs.length){g('engGrid').innerHTML=`
๐Ÿ‘ท

No engineers

`;return;} g('engGrid').innerHTML=engs.map(e=>{ const asgn=DB.tickets.filter(t=>t.assignedId===e.id);const open=asgn.filter(t=>t.status==='Open').length,inp=asgn.filter(t=>t.status==='In Progress').length,done=asgn.filter(t=>t.status==='Done').length; const todayAtt=DB.attendance.find(a=>a.userId===e.id&&a.date===today());const sal=e.sal||{basic:0,hra:0,ta:0,spl:0};const ctc=(sal.basic+sal.hra+sal.ta+sal.spl)*12; return `
${e.avatar}
${e.name}
@${e.username} ยท ${e.designation||'Engineer'}
${e.available?'Available':'Busy'}${todayAtt?`IN TODAY`:''}
${open}
OPEN
${inp}
IN PROG
${done}
DONE
${e.phone?`
๐Ÿ“ž ${e.phone}
`:''}${e.email?`
๐Ÿ“ง ${e.email}
`:''}${e.zone?`
๐Ÿ“ ${e.zone}
`:''} ${ctc>0?`
๐Ÿ’ฐ CTC: โ‚น${(ctc/100000).toFixed(1)}L/yr ยท Basic: โ‚น${sal.basic.toLocaleString()}/mo
`:''}
${(e.skills||[]).length?`
${e.skills.map(s=>`${s}`).join('')}
`:''}
${e.id!=='u1'?``:''}
`; }).join(''); } function openEngModal(){g('mEngT').textContent='Add Engineer';g('eId').value='';['eName','eUser','ePass','ePhone','eEmail','eZone','eSkills','eAva','eDesig'].forEach(x=>g(x).value='');g('eDOJ').value='';g('eSalBasic').value='';g('eSalHRA').value='';g('eSalTA').value='';g('eSalSpl').value='';g('eAvail').value='1';openM('mEng');} function editEng(eid){const e=DB.users.find(x=>x.id===eid);if(!e)return;g('mEngT').textContent='Edit โ€” '+e.name;g('eId').value=eid;g('eName').value=e.name;g('eUser').value=e.username;g('ePass').value=e.password;g('ePhone').value=e.phone||'';g('eEmail').value=e.email||'';g('eZone').value=e.zone||'';g('eSkills').value=(e.skills||[]).join(', ');g('eAva').value=e.avatar||'';g('eAvail').value=e.available?'1':'0';g('eDesig').value=e.designation||'';g('eDOJ').value=e.doj||'';if(e.sal){g('eSalBasic').value=e.sal.basic||'';g('eSalHRA').value=e.sal.hra||'';g('eSalTA').value=e.sal.ta||'';g('eSalSpl').value=e.sal.spl||'';}openM('mEng');} function saveEng(){ const name=v('eName'),user=v('eUser'),pass=v('ePass');if(!name||!user||!pass){toast('Name, username & password required','e');return;} const eid=v('eId');const skills=v('eSkills')?v('eSkills').split(',').map(s=>s.trim()).filter(Boolean):[]; const COLS=['#00d4aa','#3b82f6','#8b5cf6','#ec4899','#f97316','#f59e0b','#22c55e','#06b6d4']; const sal={basic:parseFloat(v('eSalBasic'))||0,hra:parseFloat(v('eSalHRA'))||0,ta:parseFloat(v('eSalTA'))||0,spl:parseFloat(v('eSalSpl'))||0}; const d={name,username:user,password:pass,phone:v('ePhone'),email:v('eEmail'),zone:v('eZone'),skills,avatar:v('eAva')||name.split(' ').map(w=>w[0]).join('').toUpperCase().slice(0,2),available:parseInt(v('eAvail')),designation:v('eDesig')||'Service Engineer',doj:v('eDOJ'),sal}; if(eid){const e=DB.users.find(x=>x.id===eid);Object.assign(e,d);addLog(`Engineer ${name} updated`,'๐Ÿ‘ท');toast('Updated','s');} else{if(DB.users.find(u=>u.username===user)){toast('Username exists','e');return;}d.id='u'+(DB.users.length+1);d.role='engineer';d.color=COLS[DB.users.length%COLS.length];DB.users.push(d);addLog(`Engineer ${name} added`,'๐Ÿ‘ท');toast('Added','s');} saveDB();closeM('mEng');rEngineers();populateAttFilters();populateSalMonths(); } function delEng(eid){const e=DB.users.find(x=>x.id===eid);if(!e||!confirm('Remove '+e.name+'?'))return;DB.users=DB.users.filter(x=>x.id!==eid);saveDB();toast('Removed','a');rEngineers();} function toggleAvail(eid){const e=DB.users.find(x=>x.id===eid);if(!e)return;e.available=e.available?0:1;saveDB();toast(`${e.name} โ†’ ${e.available?'Available':'Busy'}`,'s');rEngineers();} // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // ATTENDANCE โ€” ADMIN // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function populateAttFilters(){const sel=g('attEngF');if(!sel)return;sel.innerHTML=''+DB.users.filter(u=>u.role==='engineer').map(u=>``).join('');} function rAttAdmin(){ const ef=g('attEngF')?.value||'all',mf=g('attMonthF')?.value||''; let recs=DB.attendance;if(ef!=='all')recs=recs.filter(a=>a.userId===ef);if(mf)recs=recs.filter(a=>a.date.startsWith(mf));recs=[...recs].sort((a,b)=>b.date.localeCompare(a.date)); const engs=DB.users.filter(u=>u.role==='engineer');const td=today();const pToday=DB.attendance.filter(a=>a.date===td).length; const totalHrs=recs.filter(a=>a.hours).reduce((s,a)=>s+a.hours,0); g('attStats').innerHTML=[ {i:'๐Ÿ‘ท',v:engs.length,l:'Total Engineers',c:'teal'},{i:'โœ…',v:pToday,l:'Present Today',c:'green'}, {i:'โš ๏ธ',v:recs.filter(a=>a.late).length,l:'Late Arrivals',c:'amber'},{i:'๐Ÿ•',v:recs.filter(a=>a.halfDay).length,l:'Half Days',c:'blue'}, {i:'โฑ๏ธ',v:totalHrs.toFixed(0)+'h',l:'Total Hours',c:'purple'} ].map(s=>`
${s.i}
${s.v}
${s.l}
`).join(''); if(!recs.length){g('attBody').innerHTML=eRow(9,'No attendance records');return;} g('attBody').innerHTML=recs.map(a=>{ const eng=DB.users.find(u=>u.id===a.userId);const hrs=a.hours?a.hours.toFixed(1)+'h':'โ€”';const sc=a.status==='present'?'bd':a.status==='half-day'?'bp':a.status==='checked-in'?'bo':'bc'; return ` ${a.photo?``:`
๐Ÿ‘ค
`} ${a.date} ${eng?.avatar||'?'}${eng?.name||'?'} ${a.checkIn}${a.late?` LATE`:''} ${a.checkOut||'โ€”'} ${hrs} ${a.location||'โ€”'}${a.lat?`
${a.lat.toFixed(4)},${a.lng.toFixed(4)}`:''} ${a.late?`LATE`:a.halfDay?`HALF`:`โ€”`} ${a.status} `; }).join(''); } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // FACE ATTENDANCE โ€” ENGINEER // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function rMyAtt(){ if(!CU)return;const td=today();const rec=DB.attendance.find(a=>a.userId===CU.id&&a.date===td);const badge=g('attBadge'); if(rec&&rec.checkOut){badge.className='att-badge ab-out';badge.textContent=`โ— CHECKED OUT โ€” ${rec.checkIn} โ†’ ${rec.checkOut} (${rec.hours?.toFixed(1)}h)`;g('btnIn').disabled=true;g('btnOut').disabled=true;} else if(rec&&!rec.checkOut){badge.className='att-badge ab-in';badge.textContent=`โ— CHECKED IN โ€” Since ${rec.checkIn}`;g('btnIn').disabled=true;g('btnCapture').disabled=false;g('btnOut').disabled=false;} else{badge.className='att-badge ab-idle';badge.textContent='โ— NOT CHECKED IN';g('btnIn').disabled=!capturedPhoto;g('btnOut').disabled=true;} const myRecs=[...DB.attendance.filter(a=>a.userId===CU.id)].sort((a,b)=>b.date.localeCompare(a.date)).slice(0,10); const monthRecs=myRecs.filter(a=>a.date.startsWith(td.slice(0,7)));const totalH=monthRecs.filter(a=>a.hours).reduce((s,a)=>s+a.hours,0);const days=monthRecs.filter(a=>a.status==='present').length; g('myAttRecs').innerHTML=`
${days}
${totalH.toFixed(0)}h
${days?((totalH/days).toFixed(1)+'h'):'โ€”'}
${myRecs.length}
RECENT RECORDS
${myRecs.map(a=>`
${a.photo?``:`
๐Ÿ‘ค
`}${a.date}${a.checkIn}${a.late?' โš ๏ธ':''}${a.checkOut||'โ€”'}${a.hours?a.hours.toFixed(1)+'h':'Active'}${a.status}
`).join('')||`
๐Ÿ“…

No records yet

`}`; } function getLocation(){ if(!navigator.geolocation){g('locText').textContent='Not supported';return;} g('locText').textContent='Getting location...';g('locDot').className='loc-dot'; navigator.geolocation.getCurrentPosition(pos=>{ curLoc={lat:pos.coords.latitude,lng:pos.coords.longitude,text:`${pos.coords.latitude.toFixed(4)}, ${pos.coords.longitude.toFixed(4)}`}; g('locDot').className='loc-dot ok';g('locText').textContent='Location captured โœ“';g('locCoords').textContent=`${pos.coords.latitude.toFixed(6)}, ${pos.coords.longitude.toFixed(6)} (ยฑ${Math.round(pos.coords.accuracy)}m)`; checkGeoFence(pos.coords.latitude,pos.coords.longitude); },err=>{g('locDot').className='loc-dot err';g('locText').textContent='Location failed โ€” using default';g('locCoords').textContent=err.message;curLoc={lat:19.2183,lng:72.9781,text:'Default โ€” Thane Office'};},{enableHighAccuracy:true,timeout:10000}); } function checkGeoFence(lat,lng){const r=DB.settings.attRules;if(!r.geoFence||!r.officeLat)return;const R=6371,dLat=(r.officeLat-lat)*Math.PI/180,dLon=(r.officeLng-lng)*Math.PI/180,a=Math.sin(dLat/2)**2+Math.cos(lat*Math.PI/180)*Math.cos(r.officeLat*Math.PI/180)*Math.sin(dLon/2)**2;const dist=R*2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a))*1000;if(dist>r.geoRadius){toast(`โš ๏ธ ${Math.round(dist)}m from office (limit: ${r.geoRadius}m)`,'a');}else toast(`๐Ÿ“ In zone (${Math.round(dist)}m from office)`,'s');} async function startCam(){ try{camStream=await navigator.mediaDevices.getUserMedia({video:{facingMode:'user'},audio:false}); const vid=g('camVid');vid.srcObject=camStream;vid.style.display='block'; const fb=g('faceBox');fb.innerHTML='';fb.appendChild(vid); const ov=document.createElement('div');ov.className='face-overlay';ov.style.cssText='position:absolute;inset:0;display:flex;align-items:center;justify-content:center;pointer-events:none'; ov.innerHTML='
POSITION YOUR FACE IN THE RING
'; fb.appendChild(ov);vid.style.cssText='position:absolute;inset:0;width:100%;height:100%;object-fit:cover;';g('btnCapture').disabled=false;toast('Camera started','s');} catch(err){toast('Camera denied: '+err.message,'e');} } function captureFace(){ const vid=g('camVid');if(!vid||!camStream){toast('Start camera first','e');return;} const c=g('snapCanvas');const ctx=c.getContext('2d');ctx.drawImage(vid,0,0,320,240);capturedPhoto=c.toDataURL('image/jpeg',0.7); g('photoImg').src=capturedPhoto;g('capturedDiv').style.display='block'; if(camStream){camStream.getTracks().forEach(t=>t.stop());camStream=null;} const fb=g('faceBox');fb.innerHTML=``; g('btnCheckIn').disabled=false;g('btnCapture').disabled=true;toast('Face captured โœ“','s'); } function doCheckIn(){ if(!CU)return;const td=today();if(DB.attendance.find(a=>a.userId===CU.id&&a.date===td)){toast('Already checked in','a');return;} const now=new Date();const ts=now.toTimeString().slice(0,5);const r=DB.settings.attRules||{}; const [sh,sm]=(r.startTime||'09:00').split(':').map(Number);const late=(parseInt(r.lateMin)||30);const lateLimit=sh*60+sm+late; const [ch,cm]=ts.split(':').map(Number);const isLate=ch*60+cm>lateLimit; DB.attendance.push({id:'a'+Date.now(),userId:CU.id,date:td,checkIn:ts,checkOut:null,location:curLoc.text||CU.zone||'Field',lat:curLoc.lat,lng:curLoc.lng,hours:null,status:'checked-in',photo:capturedPhoto,late:isLate,halfDay:false}); addLog(`${CU.name} checked in at ${ts}${isLate?' (LATE)':''}`,'๐Ÿ“ธ');capturedPhoto=null;saveDB();rMyAtt();rAttAdmin(); toast(`Checked In ${ts}${isLate?' โ€” Late':''}`,isLate?'a':'s'); } function doCheckOut(){ if(!CU)return;const td=today();const rec=DB.attendance.find(a=>a.userId===CU.id&&a.date===td&&!a.checkOut);if(!rec){toast('Not checked in','e');return;} const now=new Date();const ts=now.toTimeString().slice(0,5);rec.checkOut=ts; const[ih,im]=rec.checkIn.split(':').map(Number);const[oh,om]=ts.split(':').map(Number);rec.hours=((oh*60+om)-(ih*60+im))/60; const r=DB.settings.attRules||{};const fh=parseFloat(r.fullDayHrs)||8,hh=parseFloat(r.halfDayHrs)||4; if(rec.hours>=fh)rec.status='present';else if(rec.hours>=hh){rec.status='half-day';rec.halfDay=true;}else rec.status='short'; addLog(`${CU.name} checked out at ${ts} (${rec.hours.toFixed(1)}h)`,'๐Ÿ');saveDB();rMyAtt();rAttAdmin();toast(`Checked Out ${ts} โ€” ${rec.hours.toFixed(1)}h`,'s'); } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // SALARY ENGINE // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function populateSalMonths(){const sel=g('salMonth');if(!sel)return;const now=new Date();const months=[];for(let i=0;i<12;i++){const d=new Date(now.getFullYear(),now.getMonth()-i,1);months.push({val:`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}`,lbl:`${MONTHS[d.getMonth()]} ${d.getFullYear()}`});}sel.innerHTML=months.map(m=>``).join('');} function computeAll(){const mo=g('salMonth')?.value||today().slice(0,7);DB.users.filter(u=>u.role==='engineer').forEach(e=>calcSlip(e.id,mo));saveDB();rSalaryAdmin();toast('All salaries computed','s');} function calcSlip(uid,month){ const e=DB.users.find(x=>x.id===uid);if(!e||!e.sal)return null; const sal=e.sal;const gross=sal.basic+sal.hra+sal.ta+sal.spl;const r=DB.settings.attRules||{};const sr=DB.settings.salRules||{}; const wdays=parseInt(r.workingDays)||26;const recs=DB.attendance.filter(a=>a.userId===uid&&a.date.startsWith(month)); const present=recs.filter(a=>a.status==='present').length;const half=recs.filter(a=>a.status==='half-day').length;const effDays=present+(half*0.5);const lopDays=Math.max(0,wdays-effDays); const lopDeduct=lopDays*(gross/wdays);const effGross=Math.max(0,gross-lopDeduct); const pfR=parseFloat(sr.pf)||12;const esiR=parseFloat(sr.esi)||0.75;const pt=parseFloat(sr.pt)||200;const tdsO=parseFloat(sr.tds)||0; const pf=Math.round(sal.basic*(pfR/100));const esi=gross<=21000?Math.round(effGross*(esiR/100)):0;const tds=tdsO||Math.max(0,Math.round((effGross-15000)*0.1/12));const totalDed=pf+esi+pt+tds;const net=Math.round(effGross-totalDed); const slip={id:'SL-'+Date.now(),userId:uid,month,name:e.name,designation:e.designation||'Engineer',avatar:e.avatar,color:e.color,doj:e.doj||'',wdays,present:Math.round(present),half,lop:Math.round(lopDays*2)/2,gross:Math.round(effGross),earnings:{basic:Math.round(sal.basic*(effDays/wdays)),hra:Math.round(sal.hra*(effDays/wdays)),ta:Math.round(sal.ta*(effDays/wdays)),spl:Math.round(sal.spl*(effDays/wdays))},deductions:{pf,esi,pt,tds},net,genAt:nowStr()}; DB.salarySlips=DB.salarySlips.filter(s=>!(s.userId===uid&&s.month===month));DB.salarySlips.push(slip);return slip; } function rSalaryAdmin(){ const mo=g('salMonth')?.value||today().slice(0,7);const[yr,mon]=mo.split('-').map(Number); const engs=DB.users.filter(u=>u.role==='engineer'); if(!engs.length){g('salList').innerHTML=`
๐Ÿ’ฐ

No engineers

`;return;} g('salList').innerHTML=engs.map(e=>{ const slip=DB.salarySlips.find(s=>s.userId===e.id&&s.month===mo);const sal=e.sal||{basic:0,hra:0,ta:0,spl:0};const gross=sal.basic+sal.hra+sal.ta+sal.spl; const recs=DB.attendance.filter(a=>a.userId===e.id&&a.date.startsWith(mo));const pres=recs.filter(a=>a.status==='present').length;const half=recs.filter(a=>a.status==='half-day').length; return `
${e.avatar}
${e.name}
${e.designation||'Engineer'} ยท ${MONTHS[mon-1]} ${yr}
${slip?`GENERATED`:''}
${pres}
${half}
โ‚น${gross.toLocaleString()}
${slip?`
โ‚น${slip.net.toLocaleString()}
`:''}
`; }).join(''); } function genSlip(uid){const mo=g('salMonth')?.value||today().slice(0,7);const slip=calcSlip(uid,mo);if(!slip){toast('No salary data','e');return;}saveDB();rSalaryAdmin();showSlip(slip);addLog(`Salary slip: ${slip.name} โ€” ${slip.month}`,'๐Ÿ’ฐ');} function showSlip(slip){ const co=DB.settings.company;const[yr,mon]=slip.month.split('-').map(Number);const slipNo=`SLP-${slip.userId.toUpperCase()}-${slip.month}`; const te=Object.values(slip.earnings).reduce((s,v)=>s+v,0);const td=Object.values(slip.deductions).reduce((s,v)=>s+v,0); g('mSlipT').textContent=`Salary Slip โ€” ${MONTHS[mon-1]} ${yr}`; g('slipPreview').innerHTML=`

${co.logo} ${co.name}

${co.tagline}
${co.address}
${co.phone} ยท ${co.email}

SALARY SLIP

${MONTHS[mon-1]} ${yr}
${slipNo}

${slip.name}
${slip.designation}
${slip.userId.toUpperCase()}
${slip.doj||'โ€”'}
${MONTHS[mon-1]} ${yr}
${slip.wdays}
${slip.present}
${slip.lop}
EARNINGS
Basic Salaryโ‚น${slip.earnings.basic.toLocaleString()}
HRAโ‚น${slip.earnings.hra.toLocaleString()}
Transport Allowanceโ‚น${slip.earnings.ta.toLocaleString()}
Special Allowanceโ‚น${slip.earnings.spl.toLocaleString()}
Total Earningsโ‚น${te.toLocaleString()}
DEDUCTIONS
Provident Fund (${DB.settings.salRules?.pf||12}%)โ‚น${slip.deductions.pf.toLocaleString()}
ESI (${DB.settings.salRules?.esi||0.75}%)โ‚น${slip.deductions.esi.toLocaleString()}
Professional Taxโ‚น${slip.deductions.pt.toLocaleString()}
TDSโ‚น${slip.deductions.tds.toLocaleString()}
Total Deductionsโ‚น${td.toLocaleString()}
NET PAY
${slip.present} days worked
โ‚น${slip.net.toLocaleString()}
`; openM('mSlip'); } function rMySalary(){ if(!CU)return;const slips=[...DB.salarySlips.filter(s=>s.userId===CU.id)].sort((a,b)=>b.month.localeCompare(a.month)); if(!slips.length){g('mySalList').innerHTML=`
๐Ÿ’ฐ

No salary slips yet. Admin will generate your monthly slip.

`;return;} g('mySalList').innerHTML=slips.map(s=>{const[yr,mon]=s.month.split('-').map(Number);return`
${MONTHS[mon-1]} ${yr}
Present: ${s.present} days ยท LOP: ${s.lop} days
โ‚น${s.net.toLocaleString()}
NET PAY
โ‚น${s.gross.toLocaleString()}
โ‚น${s.deductions.pf.toLocaleString()}
โ‚น${s.net.toLocaleString()}
`;}).join(''); } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // AMC DOCUMENT GENERATOR // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function openAMC(cust){const end=new Date();end.setFullYear(end.getFullYear()+1);g('amcCust').value=cust||'';g('amcStart').value=today();g('amcEnd').value=end.toISOString().slice(0,10);g('amcValue').value='';g('amcProducts').value='';g('amcSite').value='';g('amcPreview').innerHTML='';g('mAMCT').textContent='AMC Agreement'+(cust?' โ€” '+cust:'');openM('mAMC');} function rAMCDoc(){ const co=DB.settings.company;const a=DB.settings.amc;const cust=g('amcCust').value||'[Customer]';const val=parseFloat(g('amcValue').value)||0;const start=g('amcStart').value;const end=g('amcEnd').value;const prods=g('amcProducts').value||'[Products/Systems]';const site=g('amcSite').value||'[Site Address]';const amcNo=`AMC-${Date.now().toString().slice(-6)}`; g('amcPreview').innerHTML=`

${co.logo} ${co.name}

${co.tagline}
${co.address}
${co.phone} ยท ${co.email}
GSTIN: ${co.gstin}

ANNUAL MAINTENANCE CONTRACT
${amcNo}
${cust}
${site}
${start} to ${end}
โ‚น${val.toLocaleString()}
${prods}
${a.respTime}

SCOPE OF SERVICES

TERMS & CONDITIONS

${a.customTCs.map((tc,i)=>`
${i+1}.${tc}
`).join('')}
${a.customTCs.length+1}.Exclusions: ${a.exclusions}.
${a.customTCs.length+2}.Payment: ${a.payTerms}. Cancellation: ${a.cancellation}.
For ${cust}
Authorized Signatory & Date
For ${co.name}
Authorized Signatory & Date
`; } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // QUOTATION GENERATOR // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function openQuotation(cust){quotItemsArr=[{desc:'',qty:1,unit:0}];g('qCust').value=cust||'';g('qContact').value='';g('qSite').value='';g('qDate').value=today();const vd=new Date();vd.setDate(vd.getDate()+(parseInt(DB.settings.quotation.validDays)||30));g('qValid').value=vd.toISOString().slice(0,10);g('qGST').value=DB.settings.quotation.gstDefault||18;g('qCustomTC').value='';g('quotPreview').innerHTML='';rQItems();openM('mQuot');} function rQItems(){g('quotItems').innerHTML=quotItemsArr.map((it,i)=>`
`).join('');} function addQItem(){quotItemsArr.push({desc:'',qty:1,unit:0});rQItems();} function rQuotDoc(){ const co=DB.settings.company;const qt=DB.settings.quotation;const cust=g('qCust').value||'[Customer]';const contact=g('qContact').value;const site=g('qSite').value; const gstPct=parseFloat(g('qGST').value)||18;const date=g('qDate').value;const valid=g('qValid').value; const customTCs=g('qCustomTC').value.trim().split('\n').filter(x=>x.trim());const allTCs=[...qt.customTCs,...customTCs]; const sub=quotItemsArr.reduce((s,i)=>s+(i.qty*i.unit),0);const gst=sub*(gstPct/100);const tot=sub+gst;const qNo=`${qt.prefix}-${qt.nextNum||2001}`; g('quotPreview').innerHTML=`

${co.logo} ${co.name}

${co.tagline}
${co.address}
${co.phone} ยท ${co.email} ยท GSTIN: ${co.gstin}

QUOTATION

${qNo}

Quote To
${cust}${contact?`
${contact}`:''}${site?`
${site}`:''}
Quote No.:${qNo}Date:${date}Valid Until:${valid}
${quotItemsArr.filter(i=>i.desc||i.unit).map((i,idx)=>``).join('')||``}
#DescriptionQtyRate(โ‚น)Amount(โ‚น)
${idx+1}${i.desc||'Item'}${i.qty}โ‚น${i.unit.toLocaleString()}โ‚น${(i.qty*i.unit).toLocaleString()}
No items added
Subtotalโ‚น${sub.toLocaleString()}
GST @ ${gstPct}%โ‚น${gst.toFixed(0)}
TOTALโ‚น${tot.toFixed(0)}
Payment: ${qt.paySchedule}
Warranty: ${qt.warranty}
Delivery: ${qt.delivery}

TERMS & CONDITIONS

${allTCs.map((tc,i)=>`
${i+1}.${tc}
`).join('')}
Customer Acceptance
Signature, Name & Date
For ${co.name}
Authorized Signatory
`; } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // SETTINGS // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function stab(name,el){document.querySelectorAll('.stab').forEach(t=>t.classList.remove('active'));document.querySelectorAll('.sstab').forEach(t=>t.classList.remove('active'));el.classList.add('active');g('s-'+name).classList.add('active');rSettingsTab(name);} function rSettings(){document.querySelectorAll('.sstab').forEach(t=>t.classList.remove('active'));g('s-company').classList.add('active');rSettingsTab('company');} function rSettingsTab(n){({company:rSetCo,invoice:rSetInv,amc:rSetAMC,quot:rSetQuot,attR:rSetAttR,salR:rSetSalR})[n]?.();} const fi=(lbl,id,val,type='text',ph='')=>`
`; const fita=(lbl,id,val)=>`
`; function rSetCo(){const co=DB.settings.company;g('s-company').innerHTML=`
๐Ÿข Company Information
${fi('Company Name','co_name',co.name)}${fi('Tagline','co_tagline',co.tagline)}
${fi('Address','co_addr',co.address)}
${fi('Phone','co_phone',co.phone)}${fi('Email','co_email',co.email)}
${fi('Website','co_web',co.website)}${fi('Logo (emoji/text)','co_logo',co.logo)}
๐Ÿ›๏ธ Tax & Legal
${fi('GSTIN','co_gstin',co.gstin)}${fi('PAN','co_pan',co.pan)}
๐Ÿฆ Bank Details
${fi('Bank Name','co_bank',co.bank)}${fi('Account Number','co_account',co.account)}
${fi('IFSC Code','co_ifsc',co.ifsc)}${fi('Branch','co_branch',co.branch||'')}
`;} function saveCo(){const f=n=>g('s_co_'+n)?.value||'';DB.settings.company={name:f('name'),tagline:f('tagline'),address:f('addr'),phone:f('phone'),email:f('email'),website:f('web'),logo:f('logo'),gstin:f('gstin'),pan:f('pan'),bank:f('bank'),account:f('account'),ifsc:f('ifsc'),branch:f('branch')};saveDB();toast('Company settings saved','s');} function rSetInv(){const i=DB.settings.invoice;g('s-invoice').innerHTML=`
๐Ÿงพ Invoice Settings
${fi('Prefix','inv_prefix',i.prefix)}${fi('Next Number','inv_num',i.nextNum,'number')}
${fi('Default GST %','inv_gst',i.gstDefault,'number')}${fi('Currency Symbol','inv_cur','โ‚น')}
${fita('Payment Terms','inv_terms',i.paymentTerms)} ${fita('Invoice Footer','inv_footer',i.footer)}
`;} function saveInvSet(){DB.settings.invoice={prefix:g('s_inv_prefix')?.value||'INV',nextNum:parseInt(g('s_inv_num')?.value)||1001,gstDefault:parseFloat(g('s_inv_gst')?.value)||18,paymentTerms:g('s_inv_terms')?.value||'',footer:g('s_inv_footer')?.value||'',showBank:g('s_inv_bank')?.checked||false};saveDB();toast('Invoice settings saved','s');} function rSetAMC(){const a=DB.settings.amc;g('s-amc').innerHTML=`
๐Ÿ“„ AMC Default Settings
${fi('Visit Frequency','amc_vf',a.visitFreq)}${fi('Response Time','amc_rt',a.respTime)}
${fi('Service Hours','amc_h',a.hours)} ${fita('Exclusions','amc_ex',a.exclusions)}
${fi('Payment Terms','amc_pt',a.payTerms)}${fi('Cancellation Notice','amc_cn',a.cancellation)}
๐Ÿ“‹ AMC Terms & Conditions (editable โ€” changes apply to all new AMC documents)
`; rAMCTcList();} function rAMCTcList(){const el=g('amcTcList');if(!el)return;el.innerHTML=DB.settings.amc.customTCs.map((tc,i)=>`
${i+1}.
`).join('');} function addAMCTc(){DB.settings.amc.customTCs.push('New term โ€” edit this text');saveDB();rAMCTcList();} function rmAMCTc(i){DB.settings.amc.customTCs.splice(i,1);saveDB();rAMCTcList();} function saveAMCSet(){DB.settings.amc.visitFreq=g('s_amc_vf')?.value||'';DB.settings.amc.respTime=g('s_amc_rt')?.value||'';DB.settings.amc.hours=g('s_amc_h')?.value||'';DB.settings.amc.exclusions=g('s_amc_ex')?.value||'';DB.settings.amc.payTerms=g('s_amc_pt')?.value||'';DB.settings.amc.cancellation=g('s_amc_cn')?.value||'';DB.settings.amc.customTCs=DB.settings.amc.customTCs.map((tc,i)=>g(`amc_tc_${i}`)?.value||tc);saveDB();toast('AMC settings saved','s');} function rSetQuot(){const qt=DB.settings.quotation;g('s-quot').innerHTML=`
๐Ÿ“Š Quotation Settings
${fi('Prefix','qt_prefix',qt.prefix)}${fi('Next Number','qt_num',qt.nextNum,'number')}
${fi('Validity (days)','qt_vdays',qt.validDays,'number')}${fi('Default GST %','qt_gst',qt.gstDefault,'number')}
${fi('Payment Schedule','qt_pay',qt.paySchedule)} ${fi('Warranty Terms','qt_war',qt.warranty)} ${fi('Delivery Terms','qt_del',qt.delivery)}
๐Ÿ“‹ Default Terms & Conditions (applied to all new quotations + you can add more per quotation)
`; rQtTcList();} function rQtTcList(){const el=g('qtTcList');if(!el)return;el.innerHTML=DB.settings.quotation.customTCs.map((tc,i)=>`
${i+1}.
`).join('');} function addQtTc(){DB.settings.quotation.customTCs.push('New term โ€” edit this text');saveDB();rQtTcList();} function rmQtTc(i){DB.settings.quotation.customTCs.splice(i,1);saveDB();rQtTcList();} function saveQuotSet(){DB.settings.quotation.prefix=g('s_qt_prefix')?.value||'QT';DB.settings.quotation.nextNum=parseInt(g('s_qt_num')?.value)||2001;DB.settings.quotation.validDays=parseInt(g('s_qt_vdays')?.value)||30;DB.settings.quotation.gstDefault=parseFloat(g('s_qt_gst')?.value)||18;DB.settings.quotation.paySchedule=g('s_qt_pay')?.value||'';DB.settings.quotation.warranty=g('s_qt_war')?.value||'';DB.settings.quotation.delivery=g('s_qt_del')?.value||'';DB.settings.quotation.customTCs=DB.settings.quotation.customTCs.map((tc,i)=>g(`qt_tc_${i}`)?.value||tc);saveDB();toast('Quotation settings saved','s');} function rSetAttR(){const r=DB.settings.attRules||{};g('s-attR').innerHTML=`
๐Ÿ• Work Hours Policy
${fi('Office Start Time','ar_start',r.startTime||'09:00','time')}${fi('Office End Time','ar_end',r.endTime||'18:00','time')}
${fi('Late Threshold (minutes)','ar_late',r.lateMin||30,'number')}${fi('Working Days/Month','ar_wdays',r.workingDays||26,'number')}
${fi('Half Day (min hours)','ar_half',r.halfDayHrs||4,'number')}${fi('Full Day (min hours)','ar_full',r.fullDayHrs||8,'number')}
${fi('Week Off','ar_woff',r.weekOff||'Sunday')}${fi('OT Rate (multiplier)','ar_ot',r.otRate||1.5,'number')}
๐Ÿ“ Geo-Fence Location Restriction
${fi('Office Latitude','ar_lat',r.officeLat||'','number','e.g. 19.2183')}${fi('Office Longitude','ar_lng',r.officeLng||'','number','e.g. 72.9781')}
${fi('Allowed Radius (meters)','ar_radius',r.geoRadius||500,'number','500')}
๐Ÿ’ก When enabled, engineer's GPS location must be within the allowed radius of the office coordinates at check-in time. A warning is shown if outside the zone.
`;} function saveAttR(){DB.settings.attRules={startTime:g('s_ar_start')?.value||'09:00',endTime:g('s_ar_end')?.value||'18:00',lateMin:parseInt(g('s_ar_late')?.value)||30,workingDays:parseInt(g('s_ar_wdays')?.value)||26,halfDayHrs:parseFloat(g('s_ar_half')?.value)||4,fullDayHrs:parseFloat(g('s_ar_full')?.value)||8,weekOff:g('s_ar_woff')?.value||'Sunday',otRate:parseFloat(g('s_ar_ot')?.value)||1.5,geoFence:g('s_ar_geo')?.checked||false,officeLat:parseFloat(g('s_ar_lat')?.value)||null,officeLng:parseFloat(g('s_ar_lng')?.value)||null,geoRadius:parseInt(g('s_ar_radius')?.value)||500};saveDB();toast('Attendance rules saved','s');} function rSetSalR(){const sr=DB.settings.salRules||{};g('s-salR').innerHTML=`
๐Ÿ’ฐ Salary Deduction Rules
${fi('PF Employee %','sr_pf',sr.pf||12,'number','12')}${fi('ESI % (for salary โ‰ค โ‚น21,000)','sr_esi',sr.esi||0.75,'number','0.75')}
${fi('Professional Tax (โ‚น/month)','sr_pt',sr.pt||200,'number','200')}${fi('TDS Override (โ‚น/month, 0 = auto)','sr_tds',sr.tds||0,'number','0')}
${fi('Overtime Rate (multiplier)','sr_ot',sr.otRate||1.5,'number','1.5')}${fi('Bonus Month (e.g. October)','sr_bonus',sr.bonusMonth||'')}
๐Ÿ“Š Auto-Calculation Formula
Step 1 โ€” Effective Days: Present Days + (Half Days ร— 0.5)
Step 2 โ€” LOP Deduction: (Working Days โˆ’ Effective Days) ร— (Gross รท Working Days)
Step 3 โ€” Effective Gross: Full Gross CTC โˆ’ LOP Deduction
Step 4 โ€” PF: Employee 12% of Basic Salary
Step 5 โ€” ESI: 0.75% of Effective Gross (only if โ‰ค โ‚น21,000)
Step 6 โ€” TDS: Auto = max(0, (Effective Gross โˆ’ 15000) ร— 10% รท 12). Override if needed.
Step 7 โ€” Net Pay: Effective Gross โˆ’ PF โˆ’ ESI โˆ’ PT โˆ’ TDS
`;} function saveSalR(){DB.settings.salRules={pf:parseFloat(g('s_sr_pf')?.value)||12,esi:parseFloat(g('s_sr_esi')?.value)||0.75,pt:parseFloat(g('s_sr_pt')?.value)||200,tds:parseFloat(g('s_sr_tds')?.value)||0,otRate:parseFloat(g('s_sr_ot')?.value)||1.5,bonusMonth:g('s_sr_bonus')?.value||''};saveDB();toast('Salary rules saved','s');} // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // TODO // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function rTodo(role){const el=role==='admin'?'adminTodoList':'engTodoList';const f=todoF[role];let items=DB.todos[role]||[];if(f==='pending')items=items.filter(t=>!t.done);if(f==='done')items=items.filter(t=>t.done);if(!items.length){g(el).innerHTML=`
๐Ÿ“‹

No tasks

`;return;}g(el).innerHTML=items.map(t=>`
${t.done?'โœ“':''}
${t.title}
${t.priority}${t.due?`๐Ÿ“… ${t.due}`:''}${t.notes?`๐Ÿ“ ${t.notes}`:''}
`).join('');} function fTodo(f,el,role){todoF[role]=f;el.closest('.filters').querySelectorAll('.fb').forEach(b=>b.classList.remove('active'));el.classList.add('active');rTodo(role);} function openAddTodo(role){g('tdRole').value=role;['tdTitle','tdDue','tdNotes'].forEach(x=>g(x).value='');g('tdPri').value='Medium';openM('mTodo');} function saveTodo(){const title=v('tdTitle');if(!title){toast('Title required','e');return;}const role=v('tdRole');DB.todos[role].push({id:'t'+Date.now(),title,priority:v('tdPri'),due:v('tdDue'),notes:v('tdNotes'),done:false});saveDB();closeM('mTodo');rTodo(role);updBadges();toast('Added','s');} function toggleTodo(tid,role){const t=DB.todos[role].find(x=>x.id===tid);if(t){t.done=!t.done;saveDB();rTodo(role);updBadges();}} function delTodo(tid,role){DB.todos[role]=DB.todos[role].filter(x=>x.id!==tid);saveDB();rTodo(role);updBadges();toast('Removed','a');} // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // ENG VIEWS // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function rEngDash(){ if(!CU)return;const mine=DB.tickets.filter(t=>t.assignedId===CU.id);const open=mine.filter(t=>t.status==='Open').length,inp=mine.filter(t=>t.status==='In Progress').length,done=mine.filter(t=>t.status==='Done').length; g('engDashStats').innerHTML=[{i:'๐Ÿ“‹',v:mine.length,l:'Assigned',c:'teal'},{i:'๐Ÿ“‚',v:open,l:'Open',c:'blue'},{i:'โš™๏ธ',v:inp,l:'In Progress',c:'amber'},{i:'โœ…',v:done,l:'Done',c:'green'}].map(s=>`
${s.i}
${s.v}
${s.l}
`).join(''); const active=mine.filter(t=>t.status!=='Done'&&t.status!=='Closed'); if(!active.length){g('engActiveList').innerHTML=`
๐ŸŽ‰

No active tickets. All clear!

`;return;} g('engActiveList').innerHTML=`
ACTIVE TICKETS
`+ active.map(t=>`
${t.id}
${t.subject}
${t.priority}
๐Ÿข ${t.customer}๐Ÿ“ ${t.location}๐Ÿ“… ${t.date}
${t.status==='Open'?``:''}${t.status==='In Progress'?``:''}
`).join(''); } function rMyTickets(){if(!CU)return;let list=DB.tickets.filter(t=>t.assignedId===CU.id);if(myTktF!=='all')list=list.filter(t=>t.status===myTktF);if(!list.length){g('myTktList').innerHTML=`
๐Ÿ“‹

No tickets

`;return;}g('myTktList').innerHTML=list.map(t=>`
${t.id}
${t.subject}
${t.priority}${t.status}
๐Ÿข ${t.customer}๐Ÿ“ ${t.location}๐Ÿ“… ${t.date}
${t.status==='Open'?``:''}${t.status==='In Progress'?``:''}
`).join('');} function fMyTkt(f,el){myTktF=f;el.closest('.filters').querySelectorAll('.fb').forEach(b=>b.classList.remove('active'));el.classList.add('active');rMyTickets();} // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // CRM ADD-ON โ€” SHARED HELPERS // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• const INR=n=>`โ‚น${(parseFloat(n)||0).toLocaleString('en-IN')}`; const fmtD=d=>d?new Date(d).toLocaleDateString('en-IN',{day:'2-digit',month:'short',year:'numeric'}):'โ€”'; const nextId=(pfx,list)=>{const nums=list.map(x=>parseInt((x.id||'').split('-')[1])||0);return `${pfx}-${String(Math.max(0,...nums)+1).padStart(3,'0')}`;}; const custName=id=>DB.customers.find(c=>c.id===id)?.name||id||'โ€”'; const siteName=id=>DB.sites.find(s=>s.id===id)?.siteName||id||'โ€”'; const engName=id=>DB.users.find(u=>u.id===id)?.name||'โ€”'; const isAdmin=()=>CU?.role==='admin'; const isSales=()=>CU?.role==='sales'; const isEngineer=()=>CU?.role==='engineer'; function setCustSelect(selId,selectedId=''){const s=g(selId);if(!s)return;s.innerHTML=''+DB.customers.map(c=>``).join('');} function setSiteSelect(selId,custId='',selectedId=''){const s=g(selId);if(!s)return;const list=DB.sites.filter(x=>!custId||x.customerId===custId);s.innerHTML=''+list.map(x=>``).join('');} function setLeadSelect(selId,selectedId=''){const s=g(selId);if(!s)return;s.innerHTML=''+DB.leads.map(l=>``).join('');} function setEngSelect(selId,selectedId=''){const s=g(selId);if(!s)return;const list=DB.users.filter(u=>u.role==='engineer'&&u.status!==0);s.innerHTML=list.map(u=>``).join('');} function setSalesSelect(selId,selectedId=''){const s=g(selId);if(!s)return;const list=DB.users.filter(u=>(u.role==='sales'||u.role==='admin')&&u.status!==0);s.innerHTML=list.map(u=>``).join('');} function setTktSelect(selId,selectedId=''){const s=g(selId);if(!s)return;let list=DB.tickets;if(isEngineer())list=list.filter(t=>t.assignedId===CU.id);s.innerHTML=''+list.map(t=>``).join('');} // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // SALES DASHBOARD // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function rSalesDash(){ if(!CU)return; const myLeads=DB.leads.filter(l=>isAdmin()||l.salesUserId===CU.id); const myQuot=DB.quotations.filter(q=>{const l=DB.leads.find(x=>x.id===q.leadId);return isAdmin()||(l&&l.salesUserId===CU.id);}); const myFu=DB.followups.filter(f=>isAdmin()||f.assignedUserId===CU.id); const myMtg=DB.meetings.filter(m=>isAdmin()||m.createdByUserId===CU.id); const pipeVal=myLeads.filter(l=>l.stage!=='Closed').reduce((s,l)=>s+(parseFloat(l.value)||0),0); const wonVal=myLeads.filter(l=>l.stage==='Closed').reduce((s,l)=>s+(parseFloat(l.value)||0),0); g('salesStats').innerHTML=[ {i:'๐Ÿ’ฐ',v:myLeads.filter(l=>l.stage!=='Closed').length,l:'Open Leads',c:'purple'}, {i:'๐Ÿ“‘',v:myQuot.filter(q=>q.status==='Pending').length,l:'Pending Quotes',c:'amber'}, {i:'๐Ÿ”',v:myFu.filter(f=>f.followUpDate===today()&&f.status==='Pending').length,l:'Today F/U',c:'orange'}, {i:'๐Ÿ“†',v:myMtg.filter(m=>m.date===today()&&m.status==='Scheduled').length,l:'Today Mtgs',c:'blue'}, {i:'๐Ÿ“ˆ',v:INR(pipeVal),l:'Pipeline Value',c:'teal'}, {i:'โœ…',v:INR(wonVal),l:'Closed Won',c:'green'}, ].map(s=>`
${s.i}
${s.v}
${s.l}
`).join(''); const todayM=myMtg.filter(m=>m.date===today()); const todayF=myFu.filter(f=>f.followUpDate===today()&&f.status==='Pending'); const pendQ=myQuot.filter(q=>q.status==='Pending').slice(0,5); g('salesPanels').innerHTML=`
๐Ÿ“† Today's Meetings
${todayM.length?todayM.map(m=>`
${m.title}
๐Ÿ• ${m.time} ยท ${m.duration}m ยท ๐Ÿ“ ${m.location}
${m.status}
`).join(''):`
๐Ÿ“†

No meetings today

`}
๐Ÿ” Today's Follow-ups
${todayF.length?todayF.map(f=>`
${f.name}
${f.remarks||'โ€”'}
${f.type}
`).join(''):`
๐Ÿ”

No follow-ups today

`}
๐Ÿ“‘ Pending Quotations
${pendQ.length?pendQ.map(q=>`
${q.customerName}
${q.id} ยท Sent: ${fmtD(q.dateSent)}
${INR(q.amount)}
`).join(''):`
๐Ÿ“‘

No pending quotes

`}
๐Ÿš€ Lead Pipeline
${LEAD_STAGES.map(st=>{const ls=myLeads.filter(l=>l.stage===st);const sv=ls.reduce((s,l)=>s+(parseFloat(l.value)||0),0);const pct=myLeads.length?Math.round(ls.length/myLeads.length*100):0;return `
${st}${ls.length} ยท ${INR(sv)}
`}).join('')}
`; } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // LEADS // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• let leadView='kanban',leadDragId=null; function toggleLeadView(){leadView=leadView==='kanban'?'table':'kanban';g('leadViewBtn').textContent=leadView==='kanban'?'๐Ÿ“‹ Table View':'๐Ÿš€ Kanban View';rLeads();} function rLeads(){ const myLeads=isSales()?DB.leads.filter(l=>l.salesUserId===CU.id):DB.leads; const totV=myLeads.reduce((s,l)=>s+(parseFloat(l.value)||0),0);const openV=myLeads.filter(l=>l.stage!=='Closed').reduce((s,l)=>s+(parseFloat(l.value)||0),0);const wonV=myLeads.filter(l=>l.stage==='Closed').reduce((s,l)=>s+(parseFloat(l.value)||0),0); g('leadTotals').innerHTML=`
Total Leads
${myLeads.length}
Open Pipeline
${INR(openV)}
Closed Won
${INR(wonV)}
Total Value
${INR(totV)}
`; if(leadView==='kanban'){ g('leadKanban').style.display='flex';g('leadTable').style.display='none'; g('leadKanban').innerHTML=LEAD_STAGES.map(st=>{ const ls=myLeads.filter(l=>l.stage===st);const sv=ls.reduce((s,l)=>s+(parseFloat(l.value)||0),0);const col=LEAD_STG_COL[st]; return `
${st}
${ls.length} ยท ${INR(sv)}
${ls.map(l=>`
${l.customerName}
${(l.requirement||'').slice(0,34)}${(l.requirement||'').length>34?'โ€ฆ':''}
${INR(l.value)}${l.probability||0}%
๐Ÿ‘ค ${l.salesName}
${LEAD_STAGES.filter(s=>s!==st).map(s=>``).join('')}
`).join('')||`
No leads
`}
`; }).join(''); }else{ g('leadKanban').style.display='none';g('leadTable').style.display='block'; g('leadTbody').innerHTML=myLeads.map(l=>` ${l.id} ${fmtD(l.date)} ${l.customerName} ${l.contact||'โ€”'} ${(l.requirement||'').slice(0,50)} ${l.salesName} ${l.stage} ${INR(l.value)} ${l.probability||0}% `).join('')||eRow(10,'No leads'); } } function leadDrop(e,st,el){e.preventDefault();el.classList.remove('lead-drop');if(leadDragId)moveLead(leadDragId,st);leadDragId=null;} function moveLead(id,st){const l=DB.leads.find(x=>x.id===id);if(!l)return;l.stage=st;if(st==='Closed'&&!l.probability)l.probability=100;saveDB();addLog(`Lead ${id} โ†’ ${st}`,'๐Ÿ’ฐ');toast(`Moved to ${st}`,'s');rLeads();updBadges();} function openLeadModal(id){g('mLeadT').textContent=id?'Edit Lead':'Add Lead';g('lId').value=id||'';setSalesSelect('lSales',CU.id);if(id){const l=DB.leads.find(x=>x.id===id);if(!l)return;g('lCust').value=l.customerName;g('lContact').value=l.contact||'';g('lDate').value=l.date;g('lSrc').value=l.source;setSalesSelect('lSales',l.salesUserId);g('lStage').value=l.stage;g('lValue').value=l.value||'';g('lProb').value=l.probability||50;g('lReq').value=l.requirement||'';g('lRem').value=l.remarks||'';}else{['lCust','lContact','lReq','lRem'].forEach(x=>g(x).value='');g('lValue').value='';g('lProb').value='50';g('lSrc').value='Reference';g('lStage').value='New';g('lDate').value=today();}openM('mLead');} function openLeadModalStage(st){openLeadModal();g('lStage').value=st;} function saveLead(){const cust=v('lCust');if(!cust){toast('Customer name required','e');return;}const id=v('lId');const sales=DB.users.find(u=>u.id===v('lSales'));const d={customerName:cust,contact:v('lContact'),date:v('lDate'),source:v('lSrc'),salesUserId:v('lSales'),salesName:sales?.name||CU.name,stage:v('lStage'),value:parseFloat(v('lValue'))||0,probability:parseInt(v('lProb'))||0,requirement:v('lReq'),remarks:v('lRem')};if(id){Object.assign(DB.leads.find(x=>x.id===id),d);toast('Lead updated','s');}else{const nid=nextId('L',DB.leads);DB.leads.push({id:nid,...d,customerId:'',createdAt:today()});addLog(`Lead ${nid} created`,'๐Ÿ’ฐ');toast('Lead added','s');}saveDB();closeM('mLead');rLeads();updBadges();} function delLead(id){if(!confirm('Delete lead '+id+'?'))return;DB.leads=DB.leads.filter(x=>x.id!==id);saveDB();toast('Deleted','a');rLeads();updBadges();} // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // QUOTATIONS (TRACKER) // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• let quotF='all'; function fQuot(f,el){quotF=f;g('quotF').querySelectorAll('.fb').forEach(b=>b.classList.remove('active'));el.classList.add('active');rQuotations();} function rQuotations(){ let list=DB.quotations;if(quotF!=='all')list=list.filter(q=>q.status===quotF); g('quotTbody').innerHTML=list.map(q=>{const amt=parseFloat(q.amount)||0;const gst=amt*0.18;const tot=amt+gst;const sb=q.status==='Approved'?'bapp':q.status==='Rejected'?'brej':'bpin';return` ${q.id} ${q.leadId||'โ€”'} ${q.customerName} ${fmtD(q.dateSent)} ${INR(amt)} ${INR(gst)} ${INR(tot)} ${q.status} ${fmtD(q.followUpDate)} `}).join('')||eRow(10,'No quotations'); } function qtCalcTot(){const amt=parseFloat(v('qtAmt'))||0;if(amt<=0){g('qtTotPreview').style.display='none';return;}const gst=amt*0.18;const tot=amt+gst;g('qtTotPreview').style.display='block';g('qtTotPreview').innerHTML=`Base: ${INR(amt)} ยท GST 18%: ${INR(gst)} ยท Total: ${INR(tot)}`;} function openQuotModal(id){g('mQuotTT').textContent=id?'Edit Quotation':'New Quotation';g('qtId').value=id||'';setLeadSelect('qtLead',id?DB.quotations.find(q=>q.id===id)?.leadId:'');if(id){const q=DB.quotations.find(x=>x.id===id);if(!q)return;g('qtCust').value=q.customerName;g('qtDate').value=q.dateSent;g('qtAmt').value=q.amount;g('qtStatus').value=q.status;g('qtFu').value=q.followUpDate||'';g('qtRem').value=q.remarks||'';}else{['qtCust','qtRem'].forEach(x=>g(x).value='');g('qtAmt').value='';g('qtFu').value='';g('qtDate').value=today();g('qtStatus').value='Pending';}qtCalcTot();openM('mQuotT');} function saveQuot(){const cust=v('qtCust');if(!cust){toast('Customer required','e');return;}const amt=parseFloat(v('qtAmt'));if(!amt||amt<=0){toast('Valid amount required','e');return;}const id=v('qtId');const d={leadId:v('qtLead'),customerName:cust,dateSent:v('qtDate'),amount:amt,status:v('qtStatus'),followUpDate:v('qtFu'),remarks:v('qtRem')};if(id){Object.assign(DB.quotations.find(x=>x.id===id),d);toast('Updated','s');}else{const nid=nextId('QT',DB.quotations);DB.quotations.push({id:nid,...d,createdAt:today()});addLog(`Quote ${nid} created for ${cust}`,'๐Ÿ“‘');toast('Quote saved','s');}saveDB();closeM('mQuotT');rQuotations();updBadges();} function delQuot(id){if(!confirm('Delete quote '+id+'?'))return;DB.quotations=DB.quotations.filter(x=>x.id!==id);saveDB();toast('Deleted','a');rQuotations();updBadges();} // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // SITES / INSTALLATIONS // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function rSites(){ g('siteTbody').innerHTML=DB.sites.map(s=>{const ae=daysLeft(s.amcEnd);const aeCls=ae!==null&&ae<=30?'color:var(--red);font-weight:600':'';return` ${s.id} ${custName(s.customerId)} ${s.siteName} ${s.installedProducts||'โ€”'} ${fmtD(s.installDate)} ${fmtD(s.warrantyExpiry)} ${fmtD(s.amcStart)} ${fmtD(s.amcEnd)}${ae!==null&&ae<=30&&ae>=0?` (${ae}d)`:''}${ae!==null&&ae<0?' EXP':''} `}).join('')||eRow(9,'No sites'); } function openSiteModal(id){g('mSiteT').textContent=id?'Edit Site':'Add Site';g('siId').value=id||'';setCustSelect('siCust');if(id){const s=DB.sites.find(x=>x.id===id);if(!s)return;g('siCust').value=s.customerId;g('siName').value=s.siteName;g('siProducts').value=s.installedProducts||'';g('siInstall').value=s.installDate||'';g('siWarranty').value=s.warrantyExpiry||'';g('siAmcS').value=s.amcStart||'';g('siAmcE').value=s.amcEnd||'';g('siCount').value=s.count||'';g('siAddr').value=s.siteAddress||'';g('siNotes').value=s.notes||'';}else{['siName','siProducts','siAddr','siNotes'].forEach(x=>g(x).value='');['siInstall','siWarranty','siAmcS','siAmcE'].forEach(x=>g(x).value='');g('siCount').value='';}openM('mSite');} function saveSite(){const cust=v('siCust'),name=v('siName');if(!cust){toast('Customer required','e');return;}if(!name){toast('Site name required','e');return;}const id=v('siId');const d={customerId:cust,siteName:name,installedProducts:v('siProducts'),installDate:v('siInstall'),warrantyExpiry:v('siWarranty'),amcStart:v('siAmcS'),amcEnd:v('siAmcE'),count:parseInt(v('siCount'))||0,siteAddress:v('siAddr'),notes:v('siNotes')};if(id){Object.assign(DB.sites.find(x=>x.id===id),d);toast('Updated','s');}else{const nid=nextId('S',DB.sites);DB.sites.push({id:nid,...d,createdAt:today()});addLog(`Site ${nid} added`,'๐Ÿ“');toast('Site saved','s');}saveDB();closeM('mSite');rSites();} function delSite(id){if(!confirm('Delete site '+id+'?'))return;DB.sites=DB.sites.filter(x=>x.id!==id);saveDB();toast('Deleted','a');rSites();} // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // AMC TRACKER // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function rAmcTrack(){ const list=DB.amcTrack;const activ=list.filter(a=>a.renewalStatus==='Active').length;const exp=list.filter(a=>a.renewalStatus==='Expired').length;const expSoon=list.filter(a=>{const d=daysLeft(a.endDate);return d!==null&&d<=30&&d>=0&&a.renewalStatus==='Active';}).length;const totV=list.filter(a=>a.renewalStatus==='Active').reduce((s,a)=>s+(parseFloat(a.amount)||0),0); g('amcStats').innerHTML=[{i:'โœ…',v:activ,l:'Active',c:'green'},{i:'โš ๏ธ',v:expSoon,l:'Expiring โ‰ค30d',c:'amber'},{i:'โŒ',v:exp,l:'Expired',c:'red'},{i:'๐Ÿ’ฐ',v:INR(totV),l:'Active Revenue',c:'teal'}].map(s=>`
${s.i}
${s.v}
${s.l}
`).join(''); g('amcTrackTbody').innerHTML=list.map(a=>{const dl=daysLeft(a.endDate);const sb=a.renewalStatus==='Active'?'bac':a.renewalStatus==='Expired'?'bexp':a.renewalStatus==='Renewed'?'bren':'bc';const dCls=dl!==null&&dl<=30?'color:var(--red);font-weight:600':'';return` ${a.id} ${custName(a.customerId)} ${siteName(a.siteId)} ${fmtD(a.startDate)} ${fmtD(a.endDate)} ${dl===null?'โ€”':dl<0?'EXPIRED':dl<=30?`${dl}d`:`${dl}d`} ${INR(a.amount)} ${a.renewalStatus} ${a.renewalStatus==='Active'?``:''} `}).join('')||eRow(9,'No AMC records'); } function openAMCTrackModal(id){g('mAMCTrackT').textContent=id?'Edit AMC':'Add AMC Contract';g('atId').value=id||'';setCustSelect('atCust');if(id){const a=DB.amcTrack.find(x=>x.id===id);if(!a)return;g('atCust').value=a.customerId;setSiteSelect('atSite',a.customerId,a.siteId);g('atStart').value=a.startDate||'';g('atEnd').value=a.endDate||'';g('atAmt').value=a.amount||'';g('atStatus').value=a.renewalStatus;g('atReminder').value=a.reminderDate||'';g('atRem').value=a.remarks||'';}else{setSiteSelect('atSite');['atStart','atEnd','atReminder','atRem'].forEach(x=>g(x).value='');g('atAmt').value='';g('atStatus').value='Active';}g('atCust').onchange=()=>setSiteSelect('atSite',v('atCust'));openM('mAMCTrack');} function saveAMCTrack(){const cust=v('atCust');if(!cust){toast('Customer required','e');return;}const id=v('atId');const d={customerId:cust,siteId:v('atSite'),startDate:v('atStart'),endDate:v('atEnd'),amount:parseFloat(v('atAmt'))||0,renewalStatus:v('atStatus'),reminderDate:v('atReminder'),remarks:v('atRem')};if(id){Object.assign(DB.amcTrack.find(x=>x.id===id),d);toast('Updated','s');}else{const nid=nextId('AMC',DB.amcTrack);DB.amcTrack.push({id:nid,...d,createdAt:today()});addLog(`AMC ${nid} added`,'๐Ÿ“…');toast('AMC saved','s');}saveDB();closeM('mAMCTrack');rAmcTrack();updBadges();} function delAMCTrack(id){if(!confirm('Delete AMC '+id+'?'))return;DB.amcTrack=DB.amcTrack.filter(x=>x.id!==id);saveDB();toast('Deleted','a');rAmcTrack();updBadges();} function renewAMC(id){const a=DB.amcTrack.find(x=>x.id===id);if(!a||!confirm('Renew for 1 year?'))return;const nd=new Date(a.endDate||today());nd.setFullYear(nd.getFullYear()+1);a.startDate=a.endDate||today();a.endDate=nd.toISOString().slice(0,10);a.renewalStatus='Renewed';saveDB();addLog(`AMC ${id} renewed`,'๐Ÿ”');toast('AMC renewed','s');rAmcTrack();updBadges();} // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // FOLLOW-UPS // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• let fuF2='all'; function fFu(f,el){fuF2=f;g('fuF').querySelectorAll('.fb').forEach(b=>b.classList.remove('active'));el.classList.add('active');rFollowups();} function rFollowups(){ let list=isSales()?DB.followups.filter(f=>f.assignedUserId===CU.id):DB.followups; const t=today(); if(fuF2==='today')list=list.filter(f=>f.followUpDate===t&&f.status==='Pending'); else if(fuF2==='overdue')list=list.filter(f=>f.followUpDatef.status===fuF2); if(!list.length){g('fuList').innerHTML=`
๐Ÿ”

No follow-ups

`;return;} g('fuList').innerHTML=list.map(f=>{const dl=daysLeft(f.followUpDate);const isOver=dl!==null&&dl<0&&f.status==='Pending';return`
${f.id}
${f.name}
${f.contact||'โ€”'} ยท ${f.remarks||'No notes'}
${f.type} ๐Ÿ“… ${fmtD(f.followUpDate)}${isOver?' โš ๏ธ OVERDUE':''} ${f.status}
${f.status==='Pending'?``:''}
`}).join(''); } function openFuModal(id){g('mFuT').textContent=id?'Edit Follow-up':'Add Follow-up';g('fuId').value=id||'';setLeadSelect('fuLead');if(id){const f=DB.followups.find(x=>x.id===id);if(!f)return;g('fuName').value=f.name;g('fuContact').value=f.contact||'';g('fuLead').value=f.leadId||'';g('fuType').value=f.type;g('fuDate').value=f.followUpDate;g('fuStatus').value=f.status;g('fuNext').value=f.nextFollowUp||'';g('fuRem').value=f.remarks||'';}else{['fuName','fuContact','fuRem'].forEach(x=>g(x).value='');g('fuNext').value='';g('fuType').value='Call';g('fuStatus').value='Pending';g('fuDate').value=today();}openM('mFu');} function saveFu(){const name=v('fuName');if(!name){toast('Name required','e');return;}const id=v('fuId');const d={name,contact:v('fuContact'),leadId:v('fuLead'),type:v('fuType'),followUpDate:v('fuDate'),status:v('fuStatus'),nextFollowUp:v('fuNext'),remarks:v('fuRem'),assignedUserId:CU.id};if(id){Object.assign(DB.followups.find(x=>x.id===id),d);toast('Updated','s');}else{const nid=nextId('FU',DB.followups);DB.followups.push({id:nid,...d,createdAt:today()});addLog(`Follow-up ${nid} scheduled`,'๐Ÿ”');toast('Follow-up added','s');}saveDB();closeM('mFu');rFollowups();updBadges();} function fuMarkDone(id){const f=DB.followups.find(x=>x.id===id);if(!f)return;f.status='Done';saveDB();toast('Marked done','s');rFollowups();updBadges();} function delFu(id){if(!confirm('Delete follow-up '+id+'?'))return;DB.followups=DB.followups.filter(x=>x.id!==id);saveDB();toast('Deleted','a');rFollowups();updBadges();} // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // MEETINGS // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function rMeetings(){ const list=isSales()?DB.meetings.filter(m=>m.createdByUserId===CU.id):DB.meetings; const t=today();const tm=list.filter(m=>m.date===t); g('mtgToday').innerHTML=tm.length?`
๐Ÿ“ Today โ€” ${new Date().toLocaleDateString('en-IN',{weekday:'long',day:'numeric',month:'long'})}
${tm.map(m=>{const lead=DB.leads.find(l=>l.id===m.leadId);const cls=m.status==='Completed'?'completed':m.status==='Cancelled'?'cancelled':'';return`
${m.title}
${m.status}
๐Ÿ• ${m.time} ยท ${m.duration}m
๐Ÿ“ ${m.location}
๐Ÿ‘ฅ ${m.attendees}${lead?`
๐Ÿ”— ${lead.customerName}`:''}
${m.notes?`
${m.notes}
`:''}
${m.status==='Scheduled'?``:''}
`}).join('')}
`:''; g('mtgTbody').innerHTML=list.map(m=>` ${m.id} ${fmtD(m.date)} ${m.time} (${m.duration}m) ${m.title} ${m.type} ${m.location} ${m.attendees||'โ€”'} ${m.status} ${m.status==='Scheduled'?``:''} `).join('')||eRow(9,'No meetings'); } function openMtgModal(id){g('mMtgT').textContent=id?'Edit Meeting':'Schedule Meeting';g('mtId').value=id||'';setLeadSelect('mtLead');if(id){const m=DB.meetings.find(x=>x.id===id);if(!m)return;g('mtTitle').value=m.title;g('mtType').value=m.type;g('mtStatus').value=m.status;g('mtDate').value=m.date;g('mtTime').value=m.time;g('mtDur').value=m.duration;g('mtLoc').value=m.location;g('mtLead').value=m.leadId||'';g('mtAttendees').value=m.attendees;g('mtNotes').value=m.notes||'';}else{['mtTitle','mtLoc','mtAttendees','mtNotes'].forEach(x=>g(x).value='');g('mtDate').value=today();g('mtTime').value='10:00';g('mtDur').value=60;g('mtType').value='Client Meeting';g('mtStatus').value='Scheduled';}openM('mMtg');} function saveMtg(){const title=v('mtTitle');if(!title){toast('Title required','e');return;}const id=v('mtId');const d={title,type:v('mtType'),status:v('mtStatus'),date:v('mtDate'),time:v('mtTime'),duration:parseInt(v('mtDur'))||60,location:v('mtLoc'),leadId:v('mtLead'),attendees:v('mtAttendees'),notes:v('mtNotes'),createdByUserId:CU.id};if(id){Object.assign(DB.meetings.find(x=>x.id===id),d);toast('Updated','s');}else{const nid=nextId('MT',DB.meetings);DB.meetings.push({id:nid,...d,createdAt:today()});addLog(`Meeting ${nid} scheduled`,'๐Ÿ“†');toast('Meeting scheduled','s');}saveDB();closeM('mMtg');rMeetings();updBadges();} function mtgMarkDone(id){const m=DB.meetings.find(x=>x.id===id);if(!m)return;m.status='Completed';saveDB();toast('Marked complete','s');rMeetings();updBadges();} function delMtg(id){if(!confirm('Delete meeting '+id+'?'))return;DB.meetings=DB.meetings.filter(x=>x.id!==id);saveDB();toast('Deleted','a');rMeetings();updBadges();} // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // FIELD REPORTS (STANDALONE + PRINTABLE) // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function rFieldReports(){ const list=isEngineer()?DB.fieldReports.filter(r=>r.engineerUserId===CU.id):DB.fieldReports; // For engineers โ€” assigned tickets card if(isEngineer()){ const myTkts=DB.tickets.filter(t=>t.assignedId===CU.id&&t.status!=='Closed'&&t.status!=='Done'); g('frAssignedCards').innerHTML=myTkts.length?`
โšก Assigned to You
${myTkts.map(t=>{const cu=DB.customers.find(c=>c.name===t.customer||c.id===t.customer);return`
${t.id}${t.priority}
${t.subject}
${t.customer} ยท ${t.location}
`}).join('')}
`:''; }else{g('frAssignedCards').innerHTML='';} g('frTbody').innerHTML=list.map(r=>{const cu=DB.customers.find(c=>c.id===r.customerId);const exp=(parseInt(r.partsCost)||0)+(parseInt(r.travelExpense)||0);return` ${r.id} ${fmtD(r.date)} ${r.engineerName} ${r.ticketId||'โ€”'} ${cu?.name||'โ€”'} ${r.checkIn||'โ€”'} ${r.checkOut||'โ€”'} ${r.workStatus} ${INR(exp)} ${isAdmin()?``:''} `}).join('')||eRow(10,'No reports yet'); } function frCalcTot(){const p=parseInt(v('frPCost'))||0;const t=parseInt(v('frTravel'))||0;const tot=p+t;if(tot<=0){g('frTotPreview').style.display='none';}else{g('frTotPreview').style.display='block';g('frTotPreview').innerHTML=`Parts: ${INR(p)} ยท Travel: ${INR(t)} ยท Total: ${INR(tot)}`;}g('frAutoClose').style.display=(v('frStatus')==='Completed'&&v('frTkt'))?'block':'none';} function openFRModal(id){g('mFRT').textContent=id?'Edit Field Report':'Submit Field Report';g('frId').value=id||'';setCustSelect('frCust');setSiteSelect('frSite');setTktSelect('frTkt');setEngSelect('frEng',CU.id);if(isEngineer())g('frEng').disabled=true;else g('frEng').disabled=false;if(id){const r=DB.fieldReports.find(x=>x.id===id);if(!r)return;g('frDate').value=r.date;g('frIn').value=r.checkIn;g('frOut').value=r.checkOut||'';setEngSelect('frEng',r.engineerUserId);g('frTkt').value=r.ticketId||'';g('frStatus').value=r.workStatus;g('frCust').value=r.customerId||'';setSiteSelect('frSite',r.customerId,r.siteId);g('frDesc').value=r.jobDescription;g('frParts').value=r.partsUsed||'';g('frPCost').value=r.partsCost||'';g('frTravel').value=r.travelExpense||'';g('frSig').value=r.customerSignature||'';g('frRem').value=r.remarks||'';}else{['frDesc','frParts','frSig','frRem'].forEach(x=>g(x).value='');g('frPCost').value='';g('frTravel').value='';g('frDate').value=today();g('frIn').value=new Date().toTimeString().slice(0,5);g('frOut').value='';g('frStatus').value='Completed';}g('frCust').onchange=()=>setSiteSelect('frSite',v('frCust'));g('frTkt').onchange=()=>{const t=DB.tickets.find(x=>x.id===v('frTkt'));if(t){const cu=DB.customers.find(c=>c.name===t.customer);if(cu)g('frCust').value=cu.id;}frCalcTot();};['frPCost','frTravel','frStatus'].forEach(id=>g(id).oninput=frCalcTot);g('frStatus').onchange=frCalcTot;frCalcTot();openM('mFR');} function openFRForTicket(tid){openFRModal();g('frTkt').value=tid;const t=DB.tickets.find(x=>x.id===tid);if(t){const cu=DB.customers.find(c=>c.name===t.customer);if(cu){g('frCust').value=cu.id;setSiteSelect('frSite',cu.id);}}frCalcTot();} function saveFR(){const desc=v('frDesc');if(!desc){toast('Job description required','e');return;}const id=v('frId');const eng=DB.users.find(u=>u.id===v('frEng'));const d={engineerUserId:v('frEng'),engineerName:eng?.name||CU.name,date:v('frDate'),checkIn:v('frIn'),checkOut:v('frOut'),ticketId:v('frTkt'),customerId:v('frCust'),siteId:v('frSite'),jobDescription:desc,partsUsed:v('frParts'),partsCost:parseFloat(v('frPCost'))||0,travelExpense:parseFloat(v('frTravel'))||0,workStatus:v('frStatus'),customerSignature:v('frSig'),remarks:v('frRem'),submittedAt:new Date().toISOString()};if(id){Object.assign(DB.fieldReports.find(x=>x.id===id),d);toast('Report updated','s');}else{const nid=nextId('FR',DB.fieldReports);DB.fieldReports.push({id:nid,...d,createdAt:today()});addLog(`Field Report ${nid} submitted`,'๐Ÿ“‹');toast('Report submitted','s');} // Auto-close linked ticket if status = Completed if(d.ticketId&&d.workStatus==='Completed'){const t=DB.tickets.find(x=>x.id===d.ticketId);if(t&&t.status!=='Closed'&&t.status!=='Done'){t.status='Done';t.history=t.history||[];t.history.push({time:nowStr(),text:`Auto-closed via Field Report by ${CU.name}`,type:'done'});addLog(`${d.ticketId} auto-closed via FR`,'โœ…');}} saveDB();closeM('mFR');rFieldReports();updBadges();} function delFR(id){if(!confirm('Delete report '+id+'?'))return;DB.fieldReports=DB.fieldReports.filter(x=>x.id!==id);saveDB();toast('Deleted','a');rFieldReports();} function printFieldReport(rid){const r=DB.fieldReports.find(x=>x.id===rid);if(!r)return;const cu=DB.customers.find(c=>c.id===r.customerId);const si=DB.sites.find(s=>s.id===r.siteId);const co=DB.settings.company;const exp=(parseInt(r.partsCost)||0)+(parseInt(r.travelExpense)||0);const html=`

${co.logo} ${co.name}

${co.tagline}
${co.address}
${co.phone} ยท ${co.email}

FIELD SERVICE REPORT

${r.id} ยท ${new Date().toLocaleDateString('en-IN')}

JOB INFORMATION
Report ID${r.id}
Date${fmtD(r.date)}
Ticket${r.ticketId||'โ€”'}
Work Status${r.workStatus}
ENGINEER DETAILS
Engineer${r.engineerName}
Check-in / out${r.checkIn||'โ€”'} โ€” ${r.checkOut||'โ€”'}
CUSTOMER & SITE
Company${cu?.name||'โ€”'}
Contact${cu?.contact||'โ€”'} ยท ${cu?.phone||'โ€”'}
Site${si?.siteName||'โ€”'}
Site Address${si?.siteAddress||cu?.location||'โ€”'}
WORK DETAILS
Job Description${r.jobDescription}
Parts Used${r.partsUsed||'โ€”'}
Parts Cost${INR(r.partsCost||0)}
Travel Expense${INR(r.travelExpense||0)}
Total Expense${INR(exp)}
Remarks${r.remarks||'โ€”'}
Engineer: ${r.engineerName}
Received By: ${r.customerSignature||'___________________'}

Powered by FieldOPS ยท ${co.name}

`;const w=window.open('','_blank');w.document.write(`Field Report ${r.id}${html}`);w.document.close();} // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // ENGINEER LOG // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function rEngLog(){ g('elTbody').innerHTML=DB.engineerLogs.map(e=>` ${e.id} ${fmtD(e.date)} ${e.engineerName} ${e.ticketId||'โ€”'} ${siteName(e.siteId)} ${e.checkIn||'โ€”'} ${e.checkOut||'โ€”'} ${e.workDone||'โ€”'} ${INR(e.expenses||0)} `).join('')||eRow(10,'No log entries'); } function openELModal(id){g('mELT').textContent=id?'Edit Entry':'Log Entry';g('elId').value=id||'';setEngSelect('elEng',CU?.role==='engineer'?CU.id:'');setTktSelect('elTkt');setSiteSelect('elSite');if(id){const e=DB.engineerLogs.find(x=>x.id===id);if(!e)return;g('elDate').value=e.date;setEngSelect('elEng',e.engineerUserId);g('elTkt').value=e.ticketId||'';g('elSite').value=e.siteId||'';g('elIn').value=e.checkIn||'';g('elOut').value=e.checkOut||'';g('elLoc').value=e.location||'';g('elExp').value=e.expenses||'';g('elWork').value=e.workDone||'';}else{['elLoc','elWork'].forEach(x=>g(x).value='');g('elExp').value='';['elIn','elOut'].forEach(x=>g(x).value='');g('elDate').value=today();}openM('mEL');} function saveEL(){const eng=DB.users.find(u=>u.id===v('elEng'));if(!eng){toast('Engineer required','e');return;}const id=v('elId');const d={date:v('elDate'),engineerUserId:eng.id,engineerName:eng.name,ticketId:v('elTkt'),siteId:v('elSite'),checkIn:v('elIn'),checkOut:v('elOut'),workDone:v('elWork'),location:v('elLoc'),expenses:parseFloat(v('elExp'))||0};if(id){Object.assign(DB.engineerLogs.find(x=>x.id===id),d);toast('Updated','s');}else{const nid=nextId('EL',DB.engineerLogs);DB.engineerLogs.push({id:nid,...d,createdAt:today()});addLog(`Engineer log ${nid}`,'๐Ÿ‘ท');toast('Saved','s');}saveDB();closeM('mEL');rEngLog();} function delEL(id){if(!confirm('Delete log '+id+'?'))return;DB.engineerLogs=DB.engineerLogs.filter(x=>x.id!==id);saveDB();toast('Deleted','a');rEngLog();} // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // USER MANAGEMENT // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function rUsers(){ const byRole={admin:DB.users.filter(u=>u.role==='admin').length,sales:DB.users.filter(u=>u.role==='sales').length,engineer:DB.users.filter(u=>u.role==='engineer').length}; const active=DB.users.filter(u=>u.status!==0).length; g('userStats').innerHTML=[{i:'๐Ÿ‘ฅ',v:DB.users.length,l:'Total Users',c:'teal'},{i:'โœ…',v:active,l:'Active',c:'green'},{i:'๐Ÿ”ง',v:byRole.admin,l:'Admins',c:'amber'},{i:'๐Ÿ’ผ',v:byRole.sales,l:'Sales',c:'purple'},{i:'๐Ÿ‘ท',v:byRole.engineer,l:'Engineers',c:'blue'}].map(s=>`
${s.i}
${s.v}
${s.l}
`).join(''); g('userTbody').innerHTML=DB.users.map(u=>{const rb=u.role==='admin'?'ra':u.role==='sales'?'rs':'re';return` @${u.username} ${u.avatar}${u.name} ${u.role} ${u.email||'โ€”'} ${u.phone||'โ€”'} ${u.status!==0?'Active':'Inactive'} ${fmtD(u.createdAt)} ${u.id!==CU?.id&&u.id!=='u1'?``:''} `}).join(''); } function uRoleChange(){g('uEngFields').style.display=v('uRole')==='engineer'?'block':'none';} function openUserModal(id){g('mUserT').textContent=id?'Edit User':'Add User';g('uId').value=id||'';if(id){const u=DB.users.find(x=>x.id===id);if(!u)return;g('uName').value=u.name;g('uUser').value=u.username;g('uUser').readOnly=true;g('uPass').value=u.password;g('uRole').value=u.role;g('uRole').disabled=(u.id==='u1');g('uEmail').value=u.email||'';g('uPhone').value=u.phone||'';g('uStatus').value=u.status!==0?'1':'0';g('uAva').value=u.avatar||'';if(u.sal){g('uSalB').value=u.sal.basic||'';g('uSalH').value=u.sal.hra||'';g('uSalT').value=u.sal.ta||'';g('uSalS').value=u.sal.spl||'';}else{['uSalB','uSalH','uSalT','uSalS'].forEach(x=>g(x).value='');}g('uDesig').value=u.designation||'';g('uZone').value=u.zone||'';g('uSkills').value=(u.skills||[]).join(', ');}else{['uName','uUser','uPass','uEmail','uPhone','uAva','uDesig','uZone','uSkills'].forEach(x=>g(x).value='');['uSalB','uSalH','uSalT','uSalS'].forEach(x=>g(x).value='');g('uUser').readOnly=false;g('uRole').disabled=false;g('uRole').value='sales';g('uStatus').value='1';}uRoleChange();openM('mUser');} function saveUser(){const name=v('uName'),user=v('uUser'),pass=v('uPass');if(!name||!user||!pass){toast('Name, username, password required','e');return;}const id=v('uId');const role=v('uRole');const COLS=['#00d4aa','#3b82f6','#8b5cf6','#ec4899','#f97316','#f59e0b','#22c55e','#06b6d4'];const ava=v('uAva')||name.split(' ').map(w=>w[0]).join('').toUpperCase().slice(0,2);const d={name,username:user,password:pass,role,email:v('uEmail'),phone:v('uPhone'),status:parseInt(v('uStatus')),avatar:ava};if(role==='engineer'){d.designation=v('uDesig')||'Service Engineer';d.zone=v('uZone');d.skills=v('uSkills')?v('uSkills').split(',').map(s=>s.trim()).filter(Boolean):[];d.sal={basic:parseFloat(v('uSalB'))||0,hra:parseFloat(v('uSalH'))||0,ta:parseFloat(v('uSalT'))||0,spl:parseFloat(v('uSalS'))||0};d.available=1;}if(id){const u=DB.users.find(x=>x.id===id);Object.assign(u,d);addLog(`User ${name} updated`,'๐Ÿ‘ฅ');toast('Updated','s');}else{if(DB.users.find(x=>x.username===user)){toast('Username exists','e');return;}d.id='u'+(DB.users.length+1+Date.now()%1000);d.color=COLS[DB.users.length%COLS.length];d.createdAt=today();DB.users.push(d);addLog(`User ${name} (${role}) created`,'๐Ÿ‘ฅ');toast('User created','s');}saveDB();closeM('mUser');rUsers();} function toggleUserStatus(id){const u=DB.users.find(x=>x.id===id);if(!u)return;if(u.id===CU?.id){toast('Cannot deactivate yourself','e');return;}u.status=u.status!==0?0:1;saveDB();addLog(`${u.name} ${u.status?'activated':'deactivated'}`,'๐Ÿ‘ฅ');toast(`${u.name} ${u.status?'activated':'deactivated'}`,'s');rUsers();} function delUser(id){const u=DB.users.find(x=>x.id===id);if(!u||u.id==='u1'){toast('Cannot delete primary admin','e');return;}if(u.id===CU?.id){toast('Cannot delete yourself','e');return;}if(!confirm('Delete user '+u.name+'?'))return;DB.users=DB.users.filter(x=>x.id!==id);saveDB();toast('Deleted','a');rUsers();} // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // LOG // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function rLog(){if(!DB.log.length){g('logList').innerHTML=`
๐Ÿ“œ

No activity

`;return;}g('logList').innerHTML=DB.log.map(l=>`
${l.icon}${l.text}${l.time}
`).join('');} // Modal close on overlay click document.querySelectorAll('.mo').forEach(o=>o.addEventListener('click',e=>{if(e.target===o)o.classList.remove('open')})); // INIT updBadges();