`); w.document.close(); w.print(); w.close(); } // ── Activity Log ────────────────────────────────────────────────────────────── async function loadLog() { const search = document.getElementById('log-search').value.toLowerCase(); const atype = document.getElementById('log-filter').value; const from = document.getElementById('log-from').value; const to = document.getElementById('log-to').value; let q = db.from('activity_log').select('*').order('id',{ascending:false}).limit(300); if (atype) q = q.eq('action_type',atype); if (from) q = q.gte('created_at',from); if (to) q = q.lte('created_at',to+'T23:59:59'); const { data } = await q; let rows = data||[]; if (search) rows = rows.filter(r=>Object.values(r).join(' ').toLowerCase().includes(search)); document.getElementById('log-container').innerHTML = rows.length ? rows.map(renderLogEntry).join('') : '
No log entries found
'; } // ── 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 selectReport(name, btn) { _currentReport = name; document.querySelectorAll('#meter-app .rpt-tab-btn').forEach(b=>b.classList.remove('active')); btn.classList.add('active'); document.getElementById('rpt-status-wrap').style.display = name==='meters' ? '' : 'none'; document.getElementById('rpt-type-wrap').style.display = name==='meters' ? '' : 'none'; document.getElementById('rpt-tech-wrap').style.display = name==='actlog' ? '' : 'none'; document.getElementById('rpt-action-wrap').style.display = name==='actlog' ? '' : 'none'; runReport(); } const statBox = (val,label,color='var(--text)')=>`
${val}
${label}
`; const rptHdr = (title,sub)=>``; 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; let data; if (_currentReport==='meters') { let q = db.from('meters').select('*').order('status').order('manufacturer').order('meter_number'); if (status) q=q.eq('status',status); if (mtype) q=q.eq('type',mtype); const {data:rows}=await q; const summary={total:rows.length,by_status:{},by_type:{},by_manufacturer:{}}; (rows||[]).forEach(r=>{ summary.by_status[r.status]=(summary.by_status[r.status]||0)+1; summary.by_type[r.type||'?']=(summary.by_type[r.type||'?']||0)+1; summary.by_manufacturer[r.manufacturer||'?']=(summary.by_manufacturer[r.manufacturer||'?']||0)+1; }); data={rows,summary}; out.innerHTML=renderMeterRpt(data); } else if (_currentReport==='installs') { let q=db.from('meters').select('*').not('install_date','is',null).gte('install_date',from).lte('install_date',to).order('install_date',{ascending:false}); const {data:rows}=await q; data={rows,date_from:from,date_to:to,total:rows.length}; out.innerHTML=renderInstallRpt(data); } else if (_currentReport==='removals') { let q=db.from('meters').select('*').not('remove_date','is',null).gte('remove_date',from).lte('remove_date',to).order('remove_date',{ascending:false}); const {data:rows}=await q; (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; }); data={rows,date_from:from,date_to:to,total:rows.length}; out.innerHTML=renderRemovalRpt(data); } else if (_currentReport==='technicians') { let q=db.from('activity_log').select('*').gte('created_at',from).lte('created_at',to+'T23:59:59').not('technician','is',null).neq('technician','').order('created_at',{ascending:false}); const {data:detail}=await q; const byTech={}; (detail||[]).forEach(r=>{ if(!byTech[r.technician]) byTech[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=byTech[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++; }); data={summary:Object.values(byTech).sort((a,b)=>b.total_actions-a.total_actions),detail,date_from:from,date_to:to}; out.innerHTML=renderTechRpt(data); } else if (_currentReport==='actlog') { let q=db.from('activity_log').select('*').gte('created_at',from).lte('created_at',to+'T23:59:59').order('created_at',{ascending:false}).limit(500); if (action) q=q.eq('action_type',action); if (tech) q=q.eq('technician',tech); const {data:rows}=await q; const type_counts={}; (rows||[]).forEach(r=>{ type_counts[r.action_type||'OTHER']=(type_counts[r.action_type||'OTHER']||0)+1; }); const {data:techs}=await db.from('activity_log').select('technician').not('technician','is',null).neq('technician',''); const uniqueTechs=[...new Set((techs||[]).map(t=>t.technician))].sort(); const sel=document.getElementById('rpt-tech'); const cur=sel.value; sel.innerHTML=''+uniqueTechs.map(t=>`${t}`).join(''); data={rows,date_from:from,date_to:to,total:rows.length,type_counts}; out.innerHTML=renderActLogRpt(data); } _lastReportData = data; } const renderMeterRpt = d => { const s=d.summary||{}; const bs=s.by_status||{}; return rptHdr('Meter Inventory Report','All Meters') + `
${statBox(s.total||0,'Total')}${statBox(bs.Installed||0,'Installed','var(--success)')}${statBox(bs.Warehouse||0,'Warehouse','var(--accent2)')}${statBox(bs.Defective||0,'Defective','var(--danger)')}${statBox(bs.Removed||0,'Removed','var(--accent)')}
` + `
By Manufacturer
${Object.entries(s.by_manufacturer||{}).sort((a,b)=>b[1]-a[1]).map(([m,c])=>``).join('')}
ManufacturerCount%
${m}${c}${s.total?Math.round(c/s.total*100):0}%
By Meter Type
${Object.entries(s.by_type||{}).sort((a,b)=>b[1]-a[1]).map(([t,c])=>``).join('')}
TypeCount%
${t}${c}${s.total?Math.round(c/s.total*100):0}%
` + `
Detail — ${(d.rows||[]).length} records
${(d.rows||[]).map(r=>``).join('')}
Meter #ManufacturerModelTypeStatusAccount #Service AddressInstall DateTech
${r.meter_number}${r.manufacturer||'—'}${r.model||'—'}${r.type||'—'}${sbadge(r.status)}${r.account_number||'—'}${(r.service_address||'').slice(0,32)||'—'}${r.install_date||'—'}${r.technician||'—'}
`; }; const renderInstallRpt = d => rptHdr('Meter Installs Report',`${d.date_from} to ${d.date_to}`) + `
${statBox(d.total||0,'Total Installs','var(--success)')}${statBox(d.date_from,'From')}${statBox(d.date_to,'To')}${statBox([...new Set((d.rows||[]).map(r=>r.technician).filter(Boolean))].length,'Technicians')}
` + `
${(d.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':'—'}
`; const renderRemovalRpt = d => { const totalC=(d.rows||[]).reduce((s,r)=>s+(r.consumption||0),0); return rptHdr('Meter Removals Report',`${d.date_from} to ${d.date_to}`) + `
${statBox(d.total||0,'Total Removals','var(--accent)')}${statBox(d.date_from,'From')}${statBox(d.date_to,'To')}${statBox(Math.round(totalC).toLocaleString()+' kWh','Total Consumption','var(--accent2)')}
` + `
${(d.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||'—'}
`; }; const renderTechRpt = d => rptHdr('Field Technician Report',`${d.date_from} to ${d.date_to}`) + `
${statBox((d.summary||[]).length,'Active Techs','var(--accent2)')}${statBox((d.detail||[]).filter(r=>r.action_type==='INSTALLED').length,'Installs','var(--success)')}${statBox((d.detail||[]).filter(r=>r.action_type==='REMOVED').length,'Removals','var(--accent)')}${statBox((d.detail||[]).filter(r=>r.action_type==='DEFECTIVE').length,'Defectives','var(--danger)')}
` + `
${(d.summary||[]).map(r=>``).join('')}
TechnicianTotalInstallsRemovalsDefectivesUpdatesFirst ActivityLast Activity
${r.technician}${r.total_actions}${r.installs}${r.removals}${r.defectives}${r.updates}${r.first_activity||'—'}${r.last_activity||'—'}
` + (() => { const byTech={}; (d.detail||[]).forEach(r=>{ if(!byTech[r.technician])byTech[r.technician]=[]; byTech[r.technician].push(r); }); const tmap={INSTALLED:'log-install',REMOVED:'log-remove',ADDED:'log-new',UPDATED:'log-update',DEFECTIVE:'log-defect'}; const lmap={INSTALLED:'INSTALL',REMOVED:'REMOVE',ADDED:'NEW',UPDATED:'UPDATE',DEFECTIVE:'DEFECT'}; return Object.entries(byTech).map(([tech,rows])=>`
${tech} ${rows.length} actions
${rows.map(r=>``).join('')}
DateActionMeter #Account #Work OrderDetail
${(r.created_at||'').slice(0,10)}${lmap[r.action_type]||r.action_type}${r.meter_number||'—'}${r.account_number||'—'}${r.work_order||'—'}${r.action_detail||'—'}
`).join(''); })(); const renderActLogRpt = d => { const tc=d.type_counts||{}; const tmap={INSTALLED:'log-install',REMOVED:'log-remove',ADDED:'log-new',UPDATED:'log-update',DEFECTIVE:'log-defect'}; const lmap={INSTALLED:'INSTALL',REMOVED:'REMOVE',ADDED:'NEW',UPDATED:'UPDATE',DEFECTIVE:'DEFECT'}; return rptHdr('Activity Log Report',`${d.date_from} to ${d.date_to}`) + `
${statBox(d.total||0,'Total')}${statBox(tc.INSTALLED||0,'Installs','var(--success)')}${statBox(tc.REMOVED||0,'Removals','var(--accent)')}${statBox(tc.DEFECTIVE||0,'Defectives','var(--danger)')}${statBox(tc.ADDED||0,'Added','var(--purple)')}${statBox(tc.UPDATED||0,'Updates','var(--accent2)')}
` + `
${(d.rows||[]).map(r=>``).join('')}
TimestampActionMeter #Account #TechnicianWork OrderDetail
${(r.created_at||'').slice(0,16).replace('T',' ')}${lmap[r.action_type]||r.action_type}${r.meter_number||'—'}${r.account_number||'—'}${r.technician||'—'}${r.work_order||'—'}${r.action_detail||'—'}
`; }; function printReport() { document.getElementById('print-header').style.display='block'; window.print(); document.getElementById('print-header').style.display='none'; } function exportReportCSV() { if (!_lastReportData) { toast('Run a report first','error'); return; } const rows = _lastReportData.rows || _lastReportData.detail || _lastReportData.summary || []; if (!rows.length) { toast('No data to export','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_${_currentReport}_${new Date().toISOString().split('T')[0]}.csv`; a.click(); toast('Report exported'); } // ── Admin ───────────────────────────────────────────────────────────────────── async function loadAdmin() { await applyBranding(); const [{count:meters},{count:customers},{count:logs}] = await Promise.all([ db.from('meters').select('*',{count:'exact',head:true}), db.from('customers').select('*',{count:'exact',head:true}), db.from('activity_log').select('*',{count:'exact',head:true}), ]); document.getElementById('adm-cnt-meters').textContent = meters||0; document.getElementById('adm-cnt-customers').textContent = customers||0; document.getElementById('adm-cnt-log').textContent = logs||0; document.getElementById('adm-log-total').textContent = logs||0; const {data:oldest} = await db.from('activity_log').select('created_at').order('created_at',{ascending:true}).limit(1); const {data:newest} = await db.from('activity_log').select('created_at').order('created_at',{ascending:false}).limit(1); document.getElementById('adm-log-oldest').textContent = oldest?.[0]?.created_at?.slice(0,10) || '—'; document.getElementById('adm-log-newest').textContent = newest?.[0]?.created_at?.slice(0,10) || '—'; } async function clearLogBefore() { const date = document.getElementById('adm-clear-date').value; if (!date) { toast('Select a date first','error'); return; } if (!confirm(`Delete all log entries before ${date}?`)) return; await db.from('activity_log').delete().lt('created_at', date); toast(`Log entries before ${date} deleted`); loadAdmin(); } async function clearAllLog() { const {count} = await db.from('activity_log').select('*',{count:'exact',head:true}); if (!confirm(`Delete ALL ${count} log entries? This cannot be undone.`)) return; await db.from('activity_log').delete().neq('id',0); toast('All log entries cleared'); loadAdmin(); } // ── CSV Export ──────────────────────────────────────────────────────────────── async function exportCSV(table) { let data; if (table==='meters') { const {data:d}=await db.from('meters').select('*').order('id'); data=d; } else if (table==='customers') { const {data:d}=await db.from('customers').select('*').order('id'); data=d; } else if (table==='log') { const {data:d}=await db.from('activity_log').select('*').order('id'); data=d; } if (!data?.length) { toast('No data to export','error'); return; } const keys=Object.keys(data[0]); const csv=[keys.join(','),...data.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'); }