POWER WORLD

Service Entry Sheet (SeS) – Secure Login

First time?
1. Owner email poweroffice23@gmail.com से Register करें – auto Admin बनेगा
2. Firebase Console में Authentication > Sign-in method > Email/Password Enabled होना चाहिए
3. Realtime Database Rules नीचे Settings में दिए हैं – उन्हें Firebase Console में paste करें
`); win.document.close(); }; // ===== SES LOGIC ===== function normBill(s){return (s||'').toString().trim().toUpperCase().replace(/\s*\/\s*/g,'/').replace(/\s+/g,'').replace(/[^A-Z0-9\/\-]/g,'').replace(/\/+/g,'/')} function parseQty(raw){raw=(raw||'').toString().trim();if(!raw)return{display:'',num:0,unit:''};const m=raw.match(/(\d+(?:\.\d+)?)\s*([A-Za-z\.]+)?/);if(!m)return{display:raw,num:0,unit:''};const num=parseFloat(m[1])||0;const unit=(m[2]||'No').replace('.','');const pad=String(Math.floor(num)).padStart(2,'0');return{display:raw.includes(' ')?raw:`${pad} ${unit}`,num,unit,pad:`${pad} ${unit}`}} function parseDate(v){if(!v)return'';v=v.toString().trim();if(!v)return'';if(!isNaN(v)&&Number(v)>20000){const b=new Date(1899,11,30);b.setDate(b.getDate()+Number(v));return b.toISOString().slice(0,10);}let m;if(m=v.match(/^(\d{4})-(\d{2})-(\d{2})$/))return`${m[1]}-${m[2]}-${m[3]}`;if(m=v.match(/^(\d{2})[\/\-\.](\d{2})[\/\-\.](\d{4})$/))return`${m[3]}-${m[2]}-${m[1]}`;if(m=v.match(/^(\d{2})[\/\-\.](\d{2})[\/\-\.](\d{2})$/)){let y=+m[3];y=y<50?2000+y:1900+y;return`${y}-${m[2]}-${m[1]}`}const d=new Date(v);if(!isNaN(d))return d.toISOString().slice(0,10);return''} function dDisp(iso){if(!iso)return'';const p=iso.split('-');if(p.length!==3)return iso;return`${p[2]}.${p[1]}.${p[0].slice(-2)}`} function fyFrom(iso){if(!iso)return'Unknown';const d=new Date(iso);if(isNaN(d))return'Unknown';const y=d.getFullYear(),m=d.getMonth()+1;return m>=4?`FY ${y}-${String(y+1).slice(-2)}`:`FY ${y-1}-${String(y).slice(-2)}`} function classify(p){p=(p||'').toLowerCase();if(p.includes('installation of battery')&&p.includes('old'))return'Battery Installation (OLD)';if(p.includes('installation of battery'))return'Battery Installation';if(p.includes('dismantling of battery')||(p.includes('dismantle')&&p.includes('battery')))return'Battery Dismantle';if(p.includes('installation of power plant')&&p.includes('old'))return'Power Plant Installation (OLD)';if(p.includes('installation of power plant'))return'Power Plant Installation';if(p.includes('dismantling of power plant')||(p.includes('dismantle')&&p.includes('power')))return'Power Plant Dismantle';if(p.includes('supply of cable'))return'Cable Supply';if(p.includes('supply of'))return'Supply Other';if(p.includes('cable laying')||p.includes('lasing'))return'Cable Laying';return'Other'} function isCurBat(r){return classify(r.particular)==='Battery Installation'}function isCurPP(r){return classify(r.particular)==='Power Plant Installation'} const VIEWS=[{id:'dashboard',label:'Dashboard',icon:'📊',need:'view'},{id:'reports',label:'Reports',icon:'📈',need:'view'},{id:'records',label:'Records',icon:'📋',need:'view'},{id:'bills',label:'Bills',icon:'🧾',need:'view'},{id:'import',label:'Import',icon:'📤',need:'import'},{id:'settings',label:'Settings',icon:'⚙️',need:'settings'}]; let curView='dashboard', curFilter=null, editRecId=null, editBillId=null, Sstack=[{id:'dashboard',label:'Home'}]; function renderMenu(){const el=document.getElementById('menu');const per=normalize(currentProfile);el.innerHTML=VIEWS.filter(v=>{if(v.need==='settings'&&!per.settings&&!isAdmin())return false;if(v.need==='import'&&!per.import&&!isAdmin())return false;if(v.need==='view'&&!per.view&&!isAdmin())return false;return true}).map(v=>``).join('');el.querySelectorAll('.mbtn').forEach(b=>b.addEventListener('click',()=>nav(b.dataset.v)))} function nav(id,opts={}){const per=normalize(currentProfile);if(id==='settings'&&!per.settings&&!isAdmin()){alert('Settings access is not allowed for this user');return}if(id==='import'&&!per.import&&!isAdmin()){alert('Import access is not allowed');return}if(!per.view&&!isAdmin()){alert('View access denied');return}if(id!==curView){Sstack.push({id,label:opts.label||VIEWS.find(x=>x.id===id)?.label||id,filter:opts.filter||null})}curView=id;curFilter=opts.filter||curFilter;document.querySelectorAll('.view').forEach(x=>x.classList.remove('on'));const t=document.getElementById('v-'+id);if(t)t.classList.add('on');renderMenu();renderBC();if(id==='dashboard')renderDash();if(id==='reports')renderReports();if(id==='records')renderRecs(curFilter);if(id==='bills')renderBills();if(id==='settings')renderUsers();} function renderBC(){const el=document.getElementById('bc');el.innerHTML=Sstack.map((s,i)=>``+(i→':'')).join('')} window.jumpNav=(i)=>{Sstack=Sstack.slice(0,i+1);const s=Sstack[Sstack.length-1];curView=s.id;curFilter=s.filter||null;document.querySelectorAll('.view').forEach(x=>x.classList.remove('on'));document.getElementById('v-'+s.id).classList.add('on');renderMenu();renderBC();if(s.id==='dashboard')renderDash();if(s.id==='reports')renderReports();if(s.id==='records')renderRecs(curFilter);if(s.id==='bills')renderBills();if(s.id==='settings')renderUsers();} window.backNav=()=>{if(Sstack.length>1){Sstack.pop();window.jumpNav(Sstack.length-1)}}; window.goHome=()=>{Sstack=[{id:'dashboard',label:'Home'}];nav('dashboard')} function getFys(){const set=new Set(entries.map(r=>r.fy).filter(Boolean));return['All Time',...Array.from(set).sort().reverse()]} function filtered(){const fy=document.getElementById('fySel').value||'All Time';let recs=entries;if(fy!=='All Time')recs=recs.filter(r=>r.fy===fy);const q=document.getElementById('gSearch').value.trim().toLowerCase();if(q){recs=recs.filter(r=>[r.site,r.sde,r.particular,r.make,r.capacity,r.source,r.remark,r.qtyDisplay].join(' ').toLowerCase().includes(q))}return applyVisibility(recs)} function applyVisibility(recs){ const per=normalize(currentProfile); if(isAdmin()||per.viewAllWork) return recs; let out=recs; if(per.allowedSections && per.allowedSections.length>0){ out=out.filter(r=>per.allowedSections.includes(r.sde)); } if(per.allowedItems && per.allowedItems.length>0){ out=out.filter(r=>per.allowedItems.some(it=> (r.particular||'').toLowerCase().includes(it.toLowerCase()) || classify(r.particular).toLowerCase().includes(it.toLowerCase()))); } else { if(!per.visibilityConfigured) out=out.filter(r=>{ const c=classify(r.particular); return c.includes('Installation')||c.includes('Dismantle'); }); } return out; } function renderFY(){const sel=document.getElementById('fySel');const fys=getFys();const cur=sel.value||'All Time';sel.innerHTML=fys.map(f=>``).join('');sel.onchange=()=>renderAll()} function renderDash(){ const recs=filtered(); const cnt=f=>recs.filter(f).length; const sites=new Set(recs.map(r=>r.site).filter(Boolean)).size; const bat=cnt(r=>classify(r.particular)==='Battery Installation'), batOLD=cnt(r=>classify(r.particular)==='Battery Installation (OLD)'), batD=cnt(r=>classify(r.particular)==='Battery Dismantle'); const pp=cnt(r=>classify(r.particular)==='Power Plant Installation'), ppOLD=cnt(r=>classify(r.particular)==='Power Plant Installation (OLD)'), ppD=cnt(r=>classify(r.particular)==='Power Plant Dismantle'); const cableMtr=recs.filter(r=>classify(r.particular)==='Cable Supply').reduce((s,r)=>s+(r.qtyUnit&&r.qtyUnit.toLowerCase().startsWith('m')?r.qtyNum:0),0); const batComp=cnt(r=>isCurBat(r)&&r.atCompletion), batPend=cnt(r=>isCurBat(r)&&!r.atCompletion); const ppComp=cnt(r=>isCurPP(r)&&r.atCompletion), ppPend=cnt(r=>isCurPP(r)&&!r.atCompletion); const cards=[{k:'Total Entries',v:recs.length,s:'All records'},{k:'Sites',v:sites,s:'Unique'},{k:'Battery Installation',v:bat,s:'Current'},{k:'Battery Installation (OLD)',v:batOLD,s:'OLD'},{k:'Battery Dismantle',v:batD,s:'Dismantled'},{k:'Power Plant Installation',v:pp,s:'Current'},{k:'Power Plant Installation (OLD)',v:ppOLD,s:'OLD'},{k:'Power Plant Dismantle',v:ppD,s:'Dismantled'},{k:'Cable Usage',v:cableMtr+' Mtr.',s:'Supply only'},{k:'Battery A/T',v:`${batComp}/${bat}`,s:`Pending ${batPend}`},{k:'Power Plant A/T',v:`${ppComp}/${pp}`,s:`Pending ${ppPend}`},{k:'Pending A/T',v:batPend+ppPend,s:'Battery+PP'}]; window._dash=cards; document.getElementById('cards').innerHTML=cards.map((c,i)=>`

${c.k}

${c.v}
${c.s} • Tap
`).join(''); document.querySelectorAll('#cards .card').forEach(el=>el.addEventListener('click',()=>{const c=window._dash[+el.dataset.ci];nav('records',{label:c.k,filter:{type:'dash',name:c.k}})})); const map={}; recs.forEach(r=>{const sde=r.sde||'Unknown';if(!map[sde])map[sde]={cable:0,inst:0,dis:0,total:0};const cl=classify(r.particular);if(cl==='Cable Supply')map[sde].cable++;else if(cl.includes('Installation'))map[sde].inst++;else if(cl.includes('Dismantle'))map[sde].dis++;else map[sde].inst++;map[sde].total++}); const entriesList=Object.entries(map).sort((a,b)=>b[1].total-a[1].total); document.getElementById('secAct').innerHTML=entriesList.length?entriesList.map(([sde,v])=>{const tot=v.cable+v.inst+v.dis||1;const pct=n=>Math.round((n/tot)*100);return`
${sde}
${v.total}
`}).join(''):`
No data – Import करें
`; document.querySelectorAll('#secAct [data-sde]').forEach(el=>el.addEventListener('click',()=>nav('records',{label:`SDE: ${el.dataset.sde}`,filter:{type:'sde',sde:el.dataset.sde,seg:el.dataset.seg}}))); const wc={}; recs.forEach(r=>{const c=classify(r.particular);wc[c]=(wc[c]||0)+1}); document.getElementById('wcVis').innerHTML=Object.entries(wc).map(([k,v])=>`
${k}${v}
`).join('')||'
No data
'; document.querySelectorAll('#wcVis [data-cat]').forEach(el=>el.addEventListener('click',()=>nav('records',{label:el.dataset.cat,filter:{type:'cat',cat:el.dataset.cat}}))); const per=normalize(currentProfile); let atHtml=`
`; if(per.showATDetails||isAdmin()){ atHtml+=`
BATTERY A/T
Done ${batComp}Pending ${batPend}
POWER PLANT A/T
Done ${ppComp}Pending ${ppPend}
`; }else{ atHtml+=`
A/T details hidden by admin
`; } atHtml+=`
`; document.getElementById('atVis').innerHTML=atHtml; const rev=calcRevenue(recs); const revEntries=Object.entries(rev).sort((a,b)=>b[1]-a[1]); document.getElementById('revVis').innerHTML=revEntries.length?revEntries.map(([sde,amt])=>{const max=Math.max(...revEntries.map(e=>e[1]))||1;const w=Math.round((amt/max)*100);return`
${sde}
${typeof amt==='number'?`₹${amt.toLocaleString('en-IN')}`:amt}
`}).join(''):`
No revenue
`; document.getElementById('btnNewEntry').style.display=canDo('add')?'inline-flex':'none'; document.getElementById('btnNewBill').style.display=canDo('add')?'inline-flex':'none'; document.getElementById('btnExportSvc').style.display=canDo('export')?'inline-flex':'none'; document.getElementById('btnExportBill').style.display=canDo('export')?'inline-flex':'none'; document.getElementById('btnAddBill2').style.display=canDo('add')?'inline-flex':'none'; } function calcRevenue(recs){const perB=normalize(currentProfile); if(!perB.viewBillAmount && !isAdmin()) return {'Revenue Hidden (No Amount Permission)':'Hidden'}; const bMap={};bills.forEach(b=>bMap[normBill(b.billNumber)]=b);const bToRec={};recs.forEach(r=>{const nb=normBill(r.source);if(!nb)return;if(!bToRec[nb])bToRec[nb]=[];bToRec[nb].push(r)});const rev={};Object.entries(bToRec).forEach(([nb,arr])=>{const b=bMap[nb];if(!b)return;const sdes=[...new Set(arr.map(x=>x.sde).filter(Boolean))];const key=sdes.length===1?sdes[0]:'Multi-SDE / Unallocated';rev[key]=(rev[key]||0)+(+b.amount||0)});return rev} function renderRecs(filter){ curFilter=filter||curFilter; let recs=filtered(); if(curFilter){ if(curFilter.type==='dash'){const m={'Total Entries':()=>true,'Sites':()=>true,'Battery Installation':r=>classify(r.particular)==='Battery Installation','Battery Installation (OLD)':r=>classify(r.particular)==='Battery Installation (OLD)','Battery Dismantle':r=>classify(r.particular)==='Battery Dismantle','Power Plant Installation':r=>classify(r.particular)==='Power Plant Installation','Power Plant Installation (OLD)':r=>classify(r.particular)==='Power Plant Installation (OLD)','Power Plant Dismantle':r=>classify(r.particular)==='Power Plant Dismantle','Cable Usage':r=>classify(r.particular)==='Cable Supply','Battery A/T':r=>isCurBat(r),'Power Plant A/T':r=>isCurPP(r),'Pending A/T':r=>(isCurBat(r)||isCurPP(r))&&!r.atCompletion};const fn=m[curFilter.name]; if(fn) recs=recs.filter(fn);} else if(curFilter.type==='sde'){recs=recs.filter(r=>r.sde===curFilter.sde); if(curFilter.seg==='cable')recs=recs.filter(r=>classify(r.particular)==='Cable Supply'); if(curFilter.seg==='inst')recs=recs.filter(r=>classify(r.particular).includes('Installation')); if(curFilter.seg==='dis')recs=recs.filter(r=>classify(r.particular).includes('Dismantle'))} else if(curFilter.type==='cat'){recs=recs.filter(r=>classify(r.particular)===curFilter.cat)} else if(curFilter.type==='bill'){recs=recs.filter(r=>normBill(r.source)===normBill(curFilter.bill))} } document.getElementById('recTitle').textContent=curFilter?`Records – ${curFilter.name||curFilter.sde||curFilter.cat||curFilter.bill}`:'Service Records'; document.getElementById('recCount').textContent=`${recs.length} records`; const tbl=document.getElementById('recTable'); if(!recs.length){tbl.innerHTML='
No data available – Import करें
';return} tbl.innerHTML=`SiteSDEParticularMakeCapacityQtyDateBill NoA/TAction`+recs.map(r=>{const per=normalize(currentProfile);const atShow=per.showATDetails||isAdmin();const showBillNo=per.viewBillNo||isAdmin();return `${r.site||'-'}${r.sde||'-'}${r.particular||'-'}${r.make||'-'}${r.capacity||'-'}${r.qtyDisplay||'-'}${dDisp(r.serviceDate)}${showBillNo?(r.source||'-'):'••••'}${atShow?(r.atCompletion?'Done':(isCurBat(r)||isCurPP(r)?'Pending':'-')):'Hidden'}`}).join('')+``; tbl.querySelectorAll('[data-open]').forEach(el=>el.addEventListener('click',()=>openRec(el.dataset.open))); tbl.querySelectorAll('[data-bill]').forEach(el=>el.addEventListener('click',()=>openBillDet(el.dataset.bill))); } function renderBills(){ let recs=filtered(); const billsList=bills; const perB=normalize(currentProfile); const total=billsList.length, totVal=billsList.reduce((s,b)=>s+(+b.amount||0),0); const billNorms=new Set(billsList.map(b=>normBill(b.billNumber))); const linked=new Set(); recs.forEach(r=>{const nb=normBill(r.source); if(billNorms.has(nb))linked.add(nb)}); const dupMap={}; billsList.forEach(b=>{const nb=normBill(b.billNumber); dupMap[nb]=(dupMap[nb]||0)+1}); const dup=Object.values(dupMap).filter(v=>v>1).length; document.getElementById('billSum').innerHTML=[{l:'Total Bills',v:total},{l:'Total Value',v:perB.viewBillAmount||isAdmin()?'₹'+totVal.toLocaleString('en-IN'):'₹ ••••'},{l:'Linked',v:linked.size},{l:'Unlinked',v:total-linked.size},{l:'Duplicate',v:dup}].map(x=>`
${x.l}
${x.v}
`).join(''); const tbl=document.getElementById('billTable'); if(!billsList.length){tbl.innerHTML='
No bills – Import करें
';return} tbl.innerHTML=`Bill No.DateAmountStatusLinkedAction`+billsList.map(b=>{ const nb=normBill(b.billNumber); const cnt=recs.filter(r=>normBill(r.source)===nb).length; const showNo=perB.viewBillNo||isAdmin(); const showAmt=perB.viewBillAmount||isAdmin(); const showDet=perB.viewBillDetails||isAdmin(); const showSt=perB.viewBillStatus||isAdmin(); return `${showNo?b.billNumber:'••••'}${dDisp(b.billDate)}${showAmt?`₹${(+b.amount||0).toLocaleString('en-IN')}`:'₹ ••••'}${showSt?`${b.status||'-'}`:'Hidden'}${cnt} records${showDet?``: 'No Detail Access'} ${canDo('edit')?``:''} ${canDo('delete')?``:''}`}).join('')+``; tbl.querySelectorAll('[data-bd]').forEach(el=>el.addEventListener('click',()=>openBillDet(el.dataset.bd))); tbl.querySelectorAll('[data-be]').forEach(el=>el.addEventListener('click',()=>editBill(el.dataset.be))); tbl.querySelectorAll('[data-bdel]').forEach(el=>el.addEventListener('click',async()=>{ if(!canDo('delete')){alert('Delete not allowed');return} if(confirm('Delete bill?')){ await remove(ref(db,'bills/'+el.dataset.bdel)); }})); } function openBillDet(billNo){ if(!billNo||billNo==='-')return; const perB=normalize(currentProfile); if(!perB.viewBillDetails && !isAdmin()){ alert('Bill Details access is not allowed for this user'); return; } const bill=bills.find(b=>normBill(b.billNumber)===normBill(billNo))||{billNumber:billNo,amount:0,billDate:''}; const recs=entries.filter(r=>normBill(r.source)===normBill(billNo)); const filteredRecs=applyVisibility(recs); const pan=document.getElementById('billDetailPan'); const body=document.getElementById('billDetailBody'); document.getElementById('billDetailTit').textContent=`Bill Detail – ${bill.billNumber}`; const showNo=perB.viewBillNo||isAdmin(); const showAmt=perB.viewBillAmount||isAdmin(); body.innerHTML=`
Bill No.
${showNo?bill.billNumber:'••••'}
Date
${dDisp(bill.billDate)}
Total Amount
${showAmt?`₹${(+bill.amount||0).toLocaleString('en-IN')}`:'₹ •••• (Hidden)'}
${filteredRecs.length?`
${filteredRecs.map((r,i)=>``).join('')}
S.N.PARTICULARNAME OF SITETYPEQTY.
${i+1}${r.particular}${r.site}${r.capacity||r.make||''}${r.qtyDisplay}
`:`
No linked service reports (or hidden by permissions)
`}`; pan.style.display='block'; pan.scrollIntoView({behavior:'smooth'}); body.querySelectorAll('[data-open]').forEach(el=>el.addEventListener('click',()=>openRec(el.dataset.open))); nav('bills',{label:`Bill ${billNo}`,filter:{type:'bill',bill:billNo}}); } function renderReports(){ const recs=filtered(); const el=document.getElementById('reportsBody'); const fyG={}; recs.forEach(r=>{fyG[r.fy]=(fyG[r.fy]||0)+1}); el.innerHTML=`

FY-wise

${Object.entries(fyG).map(([fy,c])=>{const max=Math.max(...Object.values(fyG))||1;return `
${fy}
${c}
`}).join('')||'
No data
'}

Revenue by SDE

`; const rev=calcRevenue(recs); const revEntries=Object.entries(rev).sort((a,b)=>{if(typeof a[1]!=='number')return 1; if(typeof b[1]!=='number')return -1; return b[1]-a[1]}); document.getElementById('repRev').innerHTML=revEntries.map(([s,a])=>`
${s}${typeof a==='number'?`₹${a.toLocaleString('en-IN')}`:a}
`).join('')||'
No revenue
'; } function buildRecForm(data={}){ const opts=['Installation of Battery','Installation of Battery (OLD)','Dismantling of Battery','Installation of Power Plant','Installation of Power Plant (OLD)','Dismantling of Power Plant','Supply of Cable','Supply of Lugs','Supply of PVC Channel','Supply of HRC Fuse','Cable Laying Lasing','Other']; const fields=[{k:'site',l:'Site',w:'w6'},{k:'sde',l:'SDE In-Charge',w:'w6'},{k:'particular',l:'Particular',type:'sel',opts},{k:'make',l:'Make'},{k:'capacity',l:'Capacity'},{k:'qtyRaw',l:'Qty + Unit (01 Set / 08 Mtr.)'},{k:'serviceDate',l:'Service Report Date',type:'date'},{k:'source',l:'Source / Bill Number'},{k:'remark',l:'Remark',w:'w12'},{k:'photo',l:'Site Photo Attachment (Optional)',type:'file',w:'w12'},{k:'atBy',l:'A/T Conducted By'},{k:'atOffer',l:'A/T Offer Date',type:'date'},{k:'atCompletion',l:'A/T Completion Date',type:'date'}]; const form=document.getElementById('recForm'); form.innerHTML=fields.map(f=>{const v=data[f.k]||data[f.k==='qtyRaw'?'qtyDisplay':'']||'';const cls=`f ${f.w||''}`;if(f.type==='sel')return`
`;if(f.type==='date')return`
`;if(f.type==='file')return`
${data.photoBase64?``:''}
`;return`
`}).join(''); setTimeout(()=>{const inp=document.getElementById('rf_photo'); if(inp){inp.addEventListener('change',e=>{const file=e.target.files[0]; if(!file) return; const rd=new FileReader(); rd.onload=ev=>{document.getElementById('photoPreview').innerHTML=``; document.getElementById('photoPreview').dataset.base64=ev.target.result;}; rd.readAsDataURL(file);})}},100); } window.openAdd=()=>{ if(!canDo('add')){alert('Add not allowed');return} editRecId=null; buildRecForm({}); document.getElementById('recModalTit').textContent='New Service Entry'; document.getElementById('recDetail').innerHTML=''; document.getElementById('recModalBg').classList.add('open'); } window.openRec=(id)=>{ const r=entries.find(x=>x.id===id); if(!r) return; editRecId=id; buildRecForm(r); document.getElementById('recModalTit').textContent='Service Report Detail'; const per=normalize(currentProfile); const atShow=per.showATDetails||isAdmin(); document.getElementById('recDetail').innerHTML=`
Site: ${r.site}
SDE: ${r.sde}
Particular: ${r.particular}
Make: ${r.make}
Capacity: ${r.capacity}
Qty: ${r.qtyDisplay}
Date: ${dDisp(r.serviceDate)} (${r.fy})
Bill: ${r.source}
Remark: ${r.remark||'-'}
${atShow?`
A/T By: ${r.atBy||'-'}
A/T Offer: ${dDisp(r.atOffer)}
A/T Comp: ${dDisp(r.atCompletion)}
`:''}
${r.photoBase64?`
Site Photo:
`:''}
${canDo('delete')?``:''}${(isCurBat(r)||isCurPP(r))&&atShow?``:''}
`; document.getElementById('recModalBg').classList.add('open'); document.getElementById('btnSaveRec').style.display=canDo('edit')?'inline-flex':'none'; } window.closeRecModal=()=>document.getElementById('recModalBg').classList.remove('open'); window.saveRec=async()=>{ const get=k=>document.getElementById('rf_'+k)?.value||''; const q=parseQty(get('qtyRaw')); let photoBase64=null; const preview=document.getElementById('photoPreview'); if(preview && preview.dataset.base64) photoBase64=preview.dataset.base64; else { const existing=entries.find(x=>x.id===editRecId); if(existing) photoBase64=existing.photoBase64||null; } const rec={id:editRecId||push(ref(db,'entries')).key,site:get('site').trim(),sde:get('sde').trim(),particular:get('particular').trim(),make:get('make').trim(),capacity:get('capacity').trim(),qtyRaw:get('qtyRaw').trim(),qtyDisplay:q.display||get('qtyRaw'),qtyNum:q.num,qtyUnit:q.unit,serviceDate:parseDate(get('serviceDate'))||get('serviceDate'),source:get('source').trim(),sourceNorm:normBill(get('source')),remark:get('remark').trim(),atBy:get('atBy').trim(),atOffer:parseDate(get('atOffer'))||get('atOffer'),atCompletion:parseDate(get('atCompletion'))||get('atCompletion'),photoBase64}; rec.fy=fyFrom(rec.serviceDate); if(!rec.site){alert('Site required');return} if(editRecId && !canDo('edit')){alert('Edit not allowed');return} if(!editRecId && !canDo('add')){alert('Add not allowed');return} await set(ref(db,'entries/'+rec.id),rec); closeRecModal(); }; window.deleteRec=async(id)=>{ if(!canDo('delete')){alert('Delete not allowed');return} if(confirm('Delete this record?')){ await remove(ref(db,'entries/'+id)); closeRecModal(); }} function buildBillForm(d={}){const fields=[{k:'billNumber',l:'Bill Number',w:'w6'},{k:'billDate',l:'Bill Date',type:'date',w:'w6'},{k:'amount',l:'Bill Amount',w:'w6'},{k:'status',l:'Status',type:'sel',opts:['Paid','Unpaid','Pending'],w:'w6'},{k:'remark',l:'Remark',w:'w12'}];const form=document.getElementById('billForm');form.innerHTML=fields.map(f=>{const v=d[f.k]||'';const cls=`f ${f.w||''}`;if(f.type==='sel')return`
`;if(f.type==='date')return`
`;return`
`}).join('')} window.openBillAdd=()=>{ if(!canDo('add')){alert('Add not allowed');return} editBillId=null; buildBillForm({}); document.getElementById('billModalBg').classList.add('open');} window.closeBillModal=()=>document.getElementById('billModalBg').classList.remove('open'); window.editBill=(id)=>{ if(!canDo('edit')){alert('Edit not allowed');return} const b=bills.find(x=>x.id===id); if(!b)return; editBillId=id; buildBillForm(b); document.getElementById('billModalBg').classList.add('open'); } window.saveBill=async()=>{ const get=k=>document.getElementById('bf_'+k)?.value||''; const bill={id:editBillId||push(ref(db,'bills')).key,billNumber:get('billNumber').trim(),billDate:parseDate(get('billDate'))||get('billDate'),amount:parseFloat(get('amount'))||0,status:get('status').trim(),remark:get('remark').trim(),norm:normBill(get('billNumber'))}; if(!bill.billNumber){alert('Bill Number required');return} if(editBillId && !canDo('edit')){alert('Edit not allowed');return} if(!editBillId && !canDo('add')){alert('Add not allowed');return} await set(ref(db,'bills/'+bill.id),bill); closeBillModal(); } function renderUsers(){ if(!isAdmin()) return; const users=Object.values(allUsers); const pending=users.filter(u=>u.status==='Pending'||u.pendingApproval); const approved=users.filter(u=>u.status==='Approved'&&u.active); const disabled=users.filter(u=>u.status==='Disabled'||!u.active); document.getElementById('pendingCount').textContent=pending.length; document.getElementById('approvedCount').textContent=approved.length; document.getElementById('disabledCount').textContent=disabled.length; const tbl=document.getElementById('userTable'); tbl.innerHTML=`NameEmailRoleStatusActivePermissionsSections/ItemsActions`+users.map(u=>{const per=normalize(u);return `${u.name||'-'}${u.email}${u.role}${u.status}${u.active?'✅':'❌'}${Object.entries(per).filter(([k])=>['view','add','edit','delete','import','export','settings','viewAllWork','showATDetails','viewBillNo','viewBillAmount','viewBillDetails'].includes(k)).map(([k,v])=>`${k}:${v?'✓':'✗'}`).join(' ')}${(per.allowedSections||[]).join(', ')||'-'} | ${(per.allowedItems||[]).join(', ')||'-'}
`}).join('')+``; const rules=`{ "rules": { "users": { "$uid": { ".read": "auth != null && (auth.uid == $uid || root.child('users').child(auth.uid).child('role').val() == 'Admin' || auth.token.email == 'poweroffice23@gmail.com')", ".write": "auth != null && (auth.uid == $uid || root.child('users').child(auth.uid).child('role').val() == 'Admin' || auth.token.email == 'poweroffice23@gmail.com')" }, ".read": "auth != null && (root.child('users').child(auth.uid).child('role').val() == 'Admin' || auth.token.email == 'poweroffice23@gmail.com')", ".write": "auth != null && (root.child('users').child(auth.uid).child('role').val() == 'Admin' || auth.token.email == 'poweroffice23@gmail.com')" }, "entries": { ".read": "auth != null && root.child('users').child(auth.uid).child('active').val() == true", ".write": "auth != null && root.child('users').child(auth.uid).child('active').val() == true && (root.child('users').child(auth.uid).child('permissions').child('add').val() == true || root.child('users').child(auth.uid).child('role').val() == 'Admin' || auth.token.email == 'poweroffice23@gmail.com')" }, "bills": { ".read": "auth != null && root.child('users').child(auth.uid).child('active').val() == true", ".write": "auth != null && root.child('users').child(auth.uid).child('active').val() == true && (root.child('users').child(auth.uid).child('permissions').child('add').val() == true || root.child('users').child(auth.uid).child('role').val() == 'Admin' || auth.token.email == 'poweroffice23@gmail.com')" }, "settings": { "logo": { ".read": "auth != null && root.child('users').child(auth.uid).child('active').val() == true", ".write": "auth != null && (root.child('users').child(auth.uid).child('role').val() == 'Admin' || auth.token.email == 'poweroffice23@gmail.com')" }, ".read": "auth != null && (root.child('users').child(auth.uid).child('role').val() == 'Admin' || auth.token.email == 'poweroffice23@gmail.com')", ".write": "auth != null && (root.child('users').child(auth.uid).child('role').val() == 'Admin' || auth.token.email == 'poweroffice23@gmail.com')" } } } `; document.getElementById('rulesArea').value=rules; } window.approveUser=async(uid)=>{ if(!isAdmin()){alert('Admin only');return} const u=allUsers[uid]; if(!u)return; if(u.email.toLowerCase()===OWNER_EMAIL.toLowerCase()){alert('Owner cannot be changed');return} await update(ref(db,'users/'+uid),{active:true,status:'Approved',pendingApproval:false}); }; window.disableUser=async(uid)=>{ if(!isAdmin())return; const u=allUsers[uid]; if(!u)return; if(u.email.toLowerCase()===OWNER_EMAIL.toLowerCase()){alert('Cannot disable owner');return} await update(ref(db,'users/'+uid),{active:false,status:'Disabled'}); }; window.removeUser=async(uid)=>{ if(!isAdmin())return; const u=allUsers[uid]; if(!u)return; if(u.email.toLowerCase()===OWNER_EMAIL.toLowerCase()){alert('Cannot remove owner');return} if(confirm(`Remove user ${u.email} ?`)){ await remove(ref(db,'users/'+uid)); } }; let accessUid=null; window.openUserAccess=(uid)=>{ if(!isAdmin())return; accessUid=uid; const u=allUsers[uid]; if(!u)return; const per=normalize(u); const allSDEs=[...new Set(entries.map(e=>e.sde).filter(Boolean))]; const allItems=['Installation of Battery','Installation of Power Plant','Dismantling of Battery','Dismantling of Power Plant','Supply of Cable','Supply of Lugs','Supply of PVC Channel','Supply of HRC Fuse','Cable Laying Lasing']; const body=document.getElementById('userAccessBody'); body.innerHTML=`
${u.name} – ${u.email}
${['view','add','edit','delete','import','export','settings','viewAllWork','showATDetails'].map(k=>``).join('')}
${['viewBillNo','viewBillAmount','viewBillDetails','viewBillStatus'].map(k=>``).join('')}
${allSDEs.map(s=>``).join('')||'No SDE yet'}
${allItems.map(it=>``).join('')}
`; document.getElementById('userAccessBg').classList.add('open'); }; window.closeUserAccess=()=>{ document.getElementById('userAccessBg').classList.remove('open'); accessUid=null; } window.saveUserAccess=async()=>{ if(!accessUid) return; const per={}; ['view','add','edit','delete','import','export','settings','viewAllWork','showATDetails','viewBillNo','viewBillAmount','viewBillDetails','viewBillStatus'].forEach(k=>{ per[k]=document.getElementById('perm_'+k).checked; }); const secs=[...document.querySelectorAll('.secCheck:checked')].map(el=>el.value); const items=[...document.querySelectorAll('.itemCheck:checked')].map(el=>el.value); per.allowedSections=secs; per.allowedItems=items; per.visibilityConfigured=document.getElementById('acc_vis').value==='true'; const role=document.getElementById('acc_role').value; const status=document.getElementById('acc_status').value; const u=allUsers[accessUid]; if(u.email.toLowerCase()===OWNER_EMAIL.toLowerCase()){ alert('Owner always Admin'); return; } let finalPerms=per; if(role==='Admin') finalPerms={...FULL_PERMS}; else if(role==='Viewer') finalPerms={view:true,add:false,edit:false,delete:false,import:false,export:false,settings:false,viewAllWork:false,allowedSections:secs,allowedItems:items,visibilityConfigured:per.visibilityConfigured,showATDetails:per.showATDetails,viewBillNo:per.viewBillNo,viewBillAmount:per.viewBillAmount,viewBillDetails:per.viewBillDetails,viewBillStatus:per.viewBillStatus}; else if(role==='Editor') finalPerms={view:true,add:true,edit:true,delete:false,import:false,export:false,settings:false,viewAllWork:false,allowedSections:secs,allowedItems:items,visibilityConfigured:per.visibilityConfigured,showATDetails:per.showATDetails,viewBillNo:true,viewBillAmount:false,viewBillDetails:true,viewBillStatus:true}; else if(role==='Editor+Import') finalPerms={view:true,add:true,edit:true,delete:false,import:true,export:false,settings:false,viewAllWork:false,allowedSections:secs,allowedItems:items,visibilityConfigured:per.visibilityConfigured,showATDetails:per.showATDetails,viewBillNo:true,viewBillAmount:false,viewBillDetails:true,viewBillStatus:true}; await update(ref(db,'users/'+accessUid),{role,status,active:status==='Approved',pendingApproval:status!=='Approved',permissions:finalPerms}); closeUserAccess(); }; window.doLogout=async()=>{ await signOut(auth); }; window.openChangePass=()=>{ document.getElementById('passBg').classList.add('open'); }; window.closeChangePass=()=>{ document.getElementById('passBg').classList.remove('open'); }; window.doChangePass=async()=>{ const p1=document.getElementById('newPass').value; const p2=document.getElementById('newPass2').value; const st=document.getElementById('passStatus'); if(p1.length<6){ st.textContent='Min 6 chars'; st.className='status show err'; return; } if(p1!==p2){ st.textContent='Passwords mismatch'; st.className='status show err'; return; } try{ await updatePassword(auth.currentUser,p1); st.textContent='Password updated'; st.className='status show ok'; setTimeout(closeChangePass,1200); } catch(e){ if(e.code==='auth/requires-recent-login'){ st.textContent='Please login again before changing password.'; } else st.textContent=e.message; st.className='status show err'; } }; function csvParse(txt){const lines=txt.split(/\r?\n/).filter(l=>l.trim());return lines.map(l=>{const out=[];let cur='',q=false;for(let i=0;is.replace(/^"|"$/g,'').trim())})} window.importSvcPaste=async()=>{ if(!canDo('import')){alert('Import not allowed');return} const txt=document.getElementById('svcPaste').value; const rows=csvParse(txt); await importSvcRows(rows); }; window.importSvcFile=async()=>{ if(!canDo('import')){alert('Import not allowed');return} const f=document.getElementById('svcFile').files[0]; if(!f)return; const txt=await f.text(); const rows=csvParse(txt); await importSvcRows(rows); }; async function importSvcRows(rows){ let add=0,dup=0; for(const cols of rows){ if(cols[0].toLowerCase().includes('site')) continue; if(cols.length<5) continue; const [site,sde,part,make,cap,qty,date,src,rem,atBy,atOff,atComp]=cols; const rec={id:push(ref(db,'entries')).key,site:(site||'').trim(),sde:(sde||'').trim(),particular:(part||'').trim(),make:(make||'').trim(),capacity:(cap||'').trim(),qtyRaw:(qty||'').trim(),serviceDate:parseDate(date),source:(src||'').trim(),sourceNorm:normBill(src),remark:(rem||'').trim(),atBy:(atBy||'').trim(),atOffer:parseDate(atOff),atCompletion:parseDate(atComp)}; const qq=parseQty(rec.qtyRaw); rec.qtyDisplay=qq.display||qq.pad||rec.qtyRaw; rec.qtyNum=qq.num; rec.qtyUnit=qq.unit; rec.fy=fyFrom(rec.serviceDate); if(!rec.site) continue; if(entries.some(r=>r.site===rec.site&&r.particular===rec.particular&&r.serviceDate===rec.serviceDate&&normBill(r.source)===rec.sourceNorm)){dup++;continue} await set(ref(db,'entries/'+rec.id),rec); add++; } document.getElementById('svcLog').textContent=`Added ${add}, Duplicate ${dup}`; } window.importBillPaste=async()=>{ if(!canDo('import')){alert('Import not allowed');return} const rows=csvParse(document.getElementById('billPaste').value); await importBillRows(rows); }; window.importBillFile=async()=>{ if(!canDo('import')){alert('Import not allowed');return} const f=document.getElementById('billFile').files[0]; if(!f)return; const txt=await f.text(); const rows=csvParse(txt); await importBillRows(rows); }; async function importBillRows(rows){ let add=0,dup=0; for(const cols of rows){ if(cols[0].toLowerCase().includes('bill')) continue; const [bn,bd,am,st,rm]=cols; const b={id:push(ref(db,'bills')).key,billNumber:(bn||'').trim(),billDate:parseDate(bd),amount:parseFloat((am||'').toString().replace(/[^0-9.]/g,''))||0,status:(st||'').trim(),remark:(rm||'').trim(),norm:normBill(bn)}; if(!b.billNumber) continue; if(bills.some(x=>normBill(x.billNumber)===b.norm)){dup++;continue} await set(ref(db,'bills/'+b.id),b); add++; } document.getElementById('billLog').textContent=`Added ${add}, Duplicate ${dup}`; } window.loadSample=async()=>{ if(!canDo('import')){alert('Import not allowed');return} const svc=[['Ambika Saw Mill Kondagaon','SDE (T) Kondagaon','Installation of Battery','Exide','400 AH','01 Set','2025-04-09','PWR/25-26/003','New install','R. K. Sahu','2025-04-10','2025-04-12'],['Ambika Saw Mill Kondagaon','SDE (T) Kondagaon','Dismantling of Battery','Exide','400 AH','01 Set','2025-04-09','PWR/25-26/003','Old removed','','',''],['Ambika Saw Mill Kondagaon','SDE (T) Kondagaon','Supply of Cable','Polycab','50 mm','08 Mtr.','2025-04-09','PWR/25-26/003','','','',''],['Bastar Palace Jagdalpur','SDE (T) Jagdalpur','Installation of Power Plant','Delta','200A','01 No.','2025-05-15','PWR/25-26/010','Main','A. Verma','2025-05-16','']]; const billsS=[['PWR/25-26/003','09.04.2025','61380','Paid','Kondagaon'],['PWR/25-26/010','15.05.2025','185000','Unpaid','Jagdalpur']]; await importSvcRows(svc); await importBillRows(billsS); alert('Sample loaded'); } window.exportService=()=>{ if(!canDo('export')&&!isAdmin()){alert('Export not allowed');return} const cols=['Site','SDE In-Charge','Particular','Make','Capacity','Qty','Service Report Date','Source / Bill Number','Remark','A/T Conducted By','A/T Offer Date','A/T Completion Date']; const rows=[cols.join(',')]; filtered().forEach(r=>{rows.push([r.site,r.sde,r.particular,r.make,r.capacity,r.qtyDisplay,r.serviceDate,r.source,r.remark,r.atBy,r.atOffer,r.atCompletion].map(v=>`"${(v||'').toString().replace(/"/g,'""')}"`).join(','))}); dl(rows.join('\n'),'SES_Records.csv');} window.exportBills=()=>{ if(!canDo('export')&&!isAdmin()){alert('Export not allowed');return} const rows=['Bill Number,Bill Date,Bill Amount,Status,Remark']; bills.forEach(b=>rows.push([b.billNumber,b.billDate,b.amount,b.status,b.remark].map(v=>`"${(v||'').toString().replace(/"/g,'""')}"`).join(','))); dl(rows.join('\n'),'SES_Bills.csv');} function dl(txt,name){const blob=new Blob([txt],{type:'text/csv'});const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download=name;a.click();URL.revokeObjectURL(url)} function renderAll(){ renderFY(); renderDash(); renderRecs(curFilter); renderBills(); renderReports(); if(isAdmin()) renderUsers(); } window.copyRules=()=>{ const t=document.getElementById('rulesArea'); t.select(); document.execCommand('copy'); alert('Rules copied – Paste in Firebase Console > Realtime Database > Rules > Publish'); } document.getElementById('gSearch').addEventListener('input',renderAll); document.getElementById('loginEmail').addEventListener('keydown',e=>{ if(e.key==='Enter') document.getElementById('btnLogin').click(); }); document.getElementById('loginPass').addEventListener('keydown',e=>{ if(e.key==='Enter') document.getElementById('btnLogin').click(); });