Default login: admin / admin123
Change password in Admin panel after first login.
SUPABASE
Total
Installed
Warehouse
Total Meters
Installed
Warehouse
Defective
Customers
Status Breakdown
Recent Activity
Recently Installed
Recently Removed
Meter #SerialBarcodeManufacturerModelTypeStatusAccountService AddressInstall DateActions
Account #Customer NameService AddressCityTypeMeter #PhoneBilling StatusActions
Scan / Enter Meter #
Install / Remove
Generate Barcode Label
Barcode appears here
Recent Scans
System Administration
Branding, settings, and data management
🎨 Branding
Leave blank to use default icon
👁 Preview
UTILITY METER TRACKING SYSTEM
🔐 Change Credentials
📋 Activity Log Management
Entries
Oldest
Newest
Clear Before Date
Remove older entries, keep recent history
⚠ Clear Entire Log
Permanently deletes ALL log entries
📊 Database Info
DatabaseSupabase / PostgreSQL
Meters
Customers
Log Entries
💾 Data Export
`);w.document.close();w.print();w.close();} // ── Log ─────────────────────────────────────────────────────────────────────── async function loadLog(){ const search=document.getElementById('log-search').value.toLowerCase(); const atype=document.getElementById('log-ftype').value; const from=document.getElementById('log-from').value; const to=document.getElementById('log-to').value; const filters=[]; if(atype) filters.push(`action_type=eq.${atype}`); if(from) filters.push(`created_at=gte.${from}`); if(to) filters.push(`created_at=lte.${to}T23:59:59`); try{ let rows=await dbGet('activity_log',{filters,order:'id.desc',limit:300}); if(search)rows=rows.filter(r=>Object.values(r).join(' ').toLowerCase().includes(search)); document.getElementById('log-container').innerHTML=rows.length?rows.map(fmtLog).join(''):'
No entries found
'; }catch(e){toast('Load failed: '+e.message,'error');} } // ── Reports ─────────────────────────────────────────────────────────────────── function loadReports(){ if(!document.getElementById('rpt-from').value){ const to=new Date().toISOString().split('T')[0]; const from=new Date(Date.now()-30*864e5).toISOString().split('T')[0]; document.getElementById('rpt-from').value=from; document.getElementById('rpt-to').value=to; } runReport(); } function selReport(name,btn){ _rpt=name; document.querySelectorAll('.rpt-tab-btn').forEach(b=>b.classList.remove('active')); btn.classList.add('active'); document.getElementById('rw-status').style.display=name==='meters'?'':'none'; document.getElementById('rw-mtype').style.display=name==='meters'?'':'none'; document.getElementById('rw-tech').style.display=name==='actlog'?'':'none'; document.getElementById('rw-action').style.display=name==='actlog'?'':'none'; runReport(); } const rStat=(v,l,c='var(--text)')=>`
${v}
${l}
`; const rHdr=(t,s)=>{document.getElementById('print-title').textContent=t;document.getElementById('print-sub').textContent=`${s} | Generated: ${new Date().toLocaleString()}`;return '';}; async function runReport(){ const out=document.getElementById('rpt-output'); out.innerHTML='
Loading...
'; const from=document.getElementById('rpt-from').value; const to=document.getElementById('rpt-to').value; const status=document.getElementById('rpt-status').value; const mtype=document.getElementById('rpt-mtype').value; const tech=document.getElementById('rpt-tech').value; const action=document.getElementById('rpt-action').value; try{ if(_rpt==='meters'){ const f=[];if(status)f.push(`status=eq.${encodeURIComponent(status)}`);if(mtype)f.push(`type=eq.${encodeURIComponent(mtype)}`); const rows=await dbGet('meters',{filters:f,order:'status.asc,manufacturer.asc,meter_number.asc'}); const sm={total:rows.length,bs:{},bt:{},bm:{}}; rows.forEach(r=>{sm.bs[r.status]=(sm.bs[r.status]||0)+1;sm.bt[r.type||'?']=(sm.bt[r.type||'?']||0)+1;sm.bm[r.manufacturer||'?']=(sm.bm[r.manufacturer||'?']||0)+1;}); _rptData={rows,summary:sm}; rHdr('Meter Inventory Report','All Meters'); out.innerHTML=`
${rStat(sm.total,'Total')}${rStat(sm.bs.Installed||0,'Installed','var(--success)')}${rStat(sm.bs.Warehouse||0,'Warehouse','var(--accent2)')}${rStat(sm.bs.Defective||0,'Defective','var(--danger)')}${rStat(sm.bs.Removed||0,'Removed','var(--accent)')}
By Manufacturer
${Object.entries(sm.bm).sort((a,b)=>b[1]-a[1]).map(([m,c])=>``).join('')}
ManufacturerCount%
${m}${c}${sm.total?Math.round(c/sm.total*100):0}%
By Type
${Object.entries(sm.bt).sort((a,b)=>b[1]-a[1]).map(([t,c])=>``).join('')}
TypeCount%
${t}${c}${sm.total?Math.round(c/sm.total*100):0}%
Detail — ${rows.length} records
${rows.map(r=>``).join('')}
Meter #ManufacturerModelTypeStatusAccount #Service AddressInstall DateTech
${r.meter_number}${r.manufacturer||'—'}${r.model||'—'}${r.type||'—'}${badge(r.status)}${r.account_number||'—'}${(r.service_address||'').slice(0,32)||'—'}${r.install_date||'—'}${r.technician||'—'}
`; }else if(_rpt==='installs'){ const rows=await dbGet('meters',{filters:[`install_date=gte.${from}`,`install_date=lte.${to}`,`status=not.is.null`],order:'install_date.desc'}); _rptData={rows}; rHdr('Meter Installs Report',`${from} to ${to}`); out.innerHTML=`
${rStat(rows.length,'Total Installs','var(--success)')}${rStat(from,'From')}${rStat(to,'To')}${rStat([...new Set(rows.map(r=>r.technician).filter(Boolean))].length,'Technicians')}
${rows.map(r=>``).join('')}
Install DateMeter #ManufacturerModelTypeAccount #Service AddressTechnicianWork OrderInstall Read
${r.install_date||'—'}${r.meter_number}${r.manufacturer||'—'}${r.model||'—'}${r.type||'—'}${r.account_number||'—'}${(r.service_address||'').slice(0,28)||'—'}${r.technician||'—'}${r.work_order||'—'}${r.read_at_install!=null?r.read_at_install+' kWh':'—'}
`; }else if(_rpt==='removals'){ const rows=await dbGet('meters',{filters:[`remove_date=gte.${from}`,`remove_date=lte.${to}`],order:'remove_date.desc'}); rows.forEach(r=>{r.consumption=r.read_at_install!=null&&r.read_at_remove!=null?Math.round(r.read_at_remove-r.read_at_install):null;}); const totalC=rows.reduce((s,r)=>s+(r.consumption||0),0); _rptData={rows}; rHdr('Meter Removals Report',`${from} to ${to}`); out.innerHTML=`
${rStat(rows.length,'Total Removals','var(--accent)')}${rStat(from,'From')}${rStat(to,'To')}${rStat(Math.round(totalC).toLocaleString()+' kWh','Total Consumption','var(--accent2)')}
${rows.map(r=>``).join('')}
Remove DateMeter #ManufacturerAccount #Service AddressInstall DateInstall ReadFinal ReadConsumptionTechnicianWork Order
${r.remove_date||'—'}${r.meter_number}${r.manufacturer||'—'}${r.account_number||'—'}${(r.service_address||'').slice(0,26)||'—'}${r.install_date||'—'}${r.read_at_install!=null?r.read_at_install+' kWh':'—'}${r.read_at_remove!=null?r.read_at_remove+' kWh':'—'}${r.consumption!=null?r.consumption.toLocaleString()+' kWh':'—'}${r.technician||'—'}${r.work_order||'—'}
`; }else if(_rpt==='technicians'){ const f=[`created_at=gte.${from}`,`created_at=lte.${to}T23:59:59`,`technician=not.is.null`,`technician=neq.`]; const detail=await dbGet('activity_log',{filters:f,order:'created_at.desc'}); const byt={}; detail.forEach(r=>{if(!byt[r.technician])byt[r.technician]={technician:r.technician,total_actions:0,installs:0,removals:0,defectives:0,updates:0,first_activity:r.created_at.slice(0,10),last_activity:r.created_at.slice(0,10)}; const t=byt[r.technician];t.total_actions++;t.last_activity=r.created_at.slice(0,10); if(r.action_type==='INSTALLED')t.installs++;else if(r.action_type==='REMOVED')t.removals++;else if(r.action_type==='DEFECTIVE')t.defectives++;else if(r.action_type==='UPDATED')t.updates++;}); const summary=Object.values(byt).sort((a,b)=>b.total_actions-a.total_actions); _rptData={summary,detail}; rHdr('Field Technician Report',`${from} to ${to}`); const tc={INSTALLED:'l-install',REMOVED:'l-remove',ADDED:'l-new',UPDATED:'l-update',DEFECTIVE:'l-defect'}; const tl={INSTALLED:'INSTALL',REMOVED:'REMOVE',ADDED:'NEW',UPDATED:'UPDATE',DEFECTIVE:'DEFECT'}; out.innerHTML=`
${rStat(summary.length,'Active Techs','var(--accent2)')}${rStat(detail.filter(r=>r.action_type==='INSTALLED').length,'Installs','var(--success)')}${rStat(detail.filter(r=>r.action_type==='REMOVED').length,'Removals','var(--accent)')}${rStat(detail.filter(r=>r.action_type==='DEFECTIVE').length,'Defectives','var(--danger)')}
${summary.map(r=>``).join('')}
TechnicianTotalInstallsRemovalsDefectivesUpdatesFirstLast
${r.technician}${r.total_actions}${r.installs}${r.removals}${r.defectives}${r.updates}${r.first_activity}${r.last_activity}
`+ (()=>{const byt2={};detail.forEach(r=>{if(!byt2[r.technician])byt2[r.technician]=[];byt2[r.technician].push(r);}); return Object.entries(byt2).map(([t,rows])=>`
${t} ${rows.length} actions
${rows.map(r=>``).join('')}
DateActionMeter #Account #Work OrderDetail
${(r.created_at||'').slice(0,10)}${tl[r.action_type]||r.action_type}${r.meter_number||'—'}${r.account_number||'—'}${r.work_order||'—'}${r.action_detail||'—'}
`).join('');})(); }else if(_rpt==='actlog'){ const f=[`created_at=gte.${from}`,`created_at=lte.${to}T23:59:59`]; if(action)f.push(`action_type=eq.${action}`);if(tech)f.push(`technician=eq.${encodeURIComponent(tech)}`); const rows=await dbGet('activity_log',{filters:f,order:'created_at.desc',limit:500}); const tc2={};rows.forEach(r=>{tc2[r.action_type||'OTHER']=(tc2[r.action_type||'OTHER']||0)+1;}); const allTechs=[...new Set(rows.map(r=>r.technician).filter(Boolean))].sort(); const sel=document.getElementById('rpt-tech');const cur=sel.value; sel.innerHTML=''+allTechs.map(t=>`${t}`).join(''); _rptData={rows}; rHdr('Activity Log Report',`${from} to ${to}`); const tc={INSTALLED:'l-install',REMOVED:'l-remove',ADDED:'l-new',UPDATED:'l-update',DEFECTIVE:'l-defect'}; const tl={INSTALLED:'INSTALL',REMOVED:'REMOVE',ADDED:'NEW',UPDATED:'UPDATE',DEFECTIVE:'DEFECT'}; out.innerHTML=`
${rStat(rows.length,'Total')}${rStat(tc2.INSTALLED||0,'Installs','var(--success)')}${rStat(tc2.REMOVED||0,'Removals','var(--accent)')}${rStat(tc2.DEFECTIVE||0,'Defectives','var(--danger)')}${rStat(tc2.ADDED||0,'Added','var(--purple)')}${rStat(tc2.UPDATED||0,'Updates','var(--accent2)')}
${rows.map(r=>``).join('')}
TimestampActionMeter #Account #TechnicianWork OrderDetail
${(r.created_at||'').slice(0,16).replace('T',' ')}${tl[r.action_type]||r.action_type}${r.meter_number||'—'}${r.account_number||'—'}${r.technician||'—'}${r.work_order||'—'}${r.action_detail||'—'}
`; } }catch(e){out.innerHTML=`
Report failed: ${e.message}
`;} } function printReport(){document.getElementById('print-hdr').style.display='block';window.print();document.getElementById('print-hdr').style.display='none';} function exportRptCSV(){if(!_rptData){toast('Run a report first','error');return;}const rows=_rptData.rows||_rptData.detail||_rptData.summary||[];if(!rows.length){toast('No data','error');return;}const keys=Object.keys(rows[0]);const csv=[keys.join(','),...rows.map(r=>keys.map(k=>JSON.stringify(r[k]??'')).join(','))].join('\n');const a=document.createElement('a');a.href=URL.createObjectURL(new Blob([csv],{type:'text/csv'}));a.download=`report_${_rpt}_${new Date().toISOString().split('T')[0]}.csv`;a.click();toast('Report exported');} // ── Admin ───────────────────────────────────────────────────────────────────── async function loadAdmin(){ await applyBranding(); try{ const[m,c,l]=await Promise.all([dbCount('meters'),dbCount('customers'),dbCount('activity_log')]); document.getElementById('adm-cnt-m').textContent=m; document.getElementById('adm-cnt-c').textContent=c; document.getElementById('adm-cnt-l').textContent=l; document.getElementById('adm-log-cnt').textContent=l; const[oldest]=await dbGet('activity_log',{order:'created_at.asc',limit:1}); const[newest]=await dbGet('activity_log',{order:'created_at.desc',limit:1}); document.getElementById('adm-log-old').textContent=oldest?.created_at?.slice(0,10)||'—'; document.getElementById('adm-log-new').textContent=newest?.created_at?.slice(0,10)||'—'; }catch(e){toast('Admin load failed: '+e.message,'error');} } async function clearLogBefore(){ const date=document.getElementById('adm-clear-date').value; if(!date){toast('Select a date','error');return;} if(!confirm(`Delete all log entries before ${date}?`))return; await dbDelete('activity_log',{filters:[`created_at=lt.${date}`]}); toast(`Log entries before ${date} deleted`);loadAdmin(); } async function clearAllLog(){ const cnt=await dbCount('activity_log'); if(!confirm(`Delete ALL ${cnt} log entries? Cannot be undone.`))return; await dbDelete('activity_log',{filters:['id=gt.0']}); toast('All log entries cleared');loadAdmin(); } // ── CSV Export ──────────────────────────────────────────────────────────────── async function exportCSV(table){ try{ const tmap={meters:'meters',customers:'customers',log:'activity_log'}; const rows=await dbGet(tmap[table]||table,{order:'id.asc'}); if(!rows?.length){toast('No data','error');return;} const keys=Object.keys(rows[0]); const csv=[keys.join(','),...rows.map(r=>keys.map(k=>JSON.stringify(r[k]??'')).join(','))].join('\n'); const a=document.createElement('a'); a.href=URL.createObjectURL(new Blob([csv],{type:'text/csv'})); a.download=`${table}_${new Date().toISOString().split('T')[0]}.csv`; a.click();toast('Exported'); }catch(e){toast('Export failed: '+e.message,'error');} }