Edit file File name : index.php Content :<?php require_once '_helpers.php'; $conn_footer = get_db_connection(); // Safer query check for business settings initialization $settings_res = $conn_footer->query("SELECT businessName, primary_color FROM business_settings LIMIT 1"); $business_name_footer = 'OmniFlow'; $initial_primary_color = '#2563eb'; if ($settings_res && $row = $settings_res->fetch_assoc()) { $business_name_footer = $row['businessName'] ?: $business_name_footer; $initial_primary_color = $row['primary_color'] ?: $initial_primary_color; } $conn_footer->close(); ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <title>OmniFlow - Utility Billing SaaS</title> <link rel="manifest" href="manifest.json"> <meta name="theme-color" content="<?php echo $initial_primary_color; ?>"> <script src="https://cdn.tailwindcss.com"></script> <script src="https://unpkg.com/html5-qrcode"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf-autotable/3.5.23/jspdf.plugin.autotable.min.js"></script> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <style> body { font-family: 'Inter', sans-serif; -webkit-tap-highlight-color: transparent; } .tab-active { color: var(--primary-color); border-bottom: 2px solid var(--primary-color); font-weight: 600; } .modal-enter { animation: modal-in 0.3s ease-out forwards; } @keyframes modal-in { from { transform: translateY(100%); opacity: 0; } to { transform: translateY(0); opacity: 1; } } :root { --primary-color: <?php echo $initial_primary_color; ?>; } .bg-primary { background-color: var(--primary-color); } .text-primary { color: var(--primary-color); } </style> </head> <body class="bg-slate-50 text-slate-900 selection:bg-blue-100 min-h-screen flex flex-col"> <div id="splash" class="fixed inset-0 z-50 bg-blue-600 flex flex-col items-center justify-center text-white transition-opacity duration-500"> <div class="animate-bounce mb-4"><i class="fas fa-faucet-drip text-6xl"></i></div> <h1 class="text-3xl font-bold tracking-tighter">OmniFlow</h1> <p class="text-blue-100 mt-2">Professional Utility Management</p> </div> <div id="app" class="hidden flex-grow flex flex-col"> <header class="bg-white border-b sticky top-0 z-40 px-4 h-16 flex items-center justify-between"> <div class="flex items-center gap-2"> <div class="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center text-white shadow-lg"><i class="fas fa-faucet"></i></div> <span class="font-bold text-lg hidden sm:inline">OmniFlow</span> </div> <nav class="hidden md:flex items-center gap-6"> <button onclick="App.loadPage('dashboard')" class="text-sm font-medium hover:text-blue-600">Dashboard</button> <button onclick="App.loadPage('customers')" class="admin-only text-sm font-medium hover:text-blue-600">Customers</button> <button onclick="App.loadPage('expenses')" class="admin-only text-sm font-medium hover:text-blue-600">Expenses</button> <button onclick="App.loadPage('billing')" class="customer-only text-sm font-medium hover:text-blue-600">Billing</button> <button onclick="App.loadPage('howtopay')" class="customer-only text-sm font-medium hover:text-blue-600">How to Pay</button> <button onclick="App.loadPage('reportissue')" class="customer-only text-sm font-medium hover:text-blue-600">Report Issue</button> <button onclick="App.loadPage('reports')" class="admin-only text-sm font-medium hover:text-blue-600">Reports</button> </nav> <div class="flex items-center gap-3"> <button id="nav-scan" class="admin-only w-10 h-10 bg-slate-100 rounded-full flex items-center justify-center text-slate-600"><i class="fas fa-qrcode"></i></button> <div class="flex items-center gap-2"> <button onclick="App.logout()" title="Logout" class="w-10 h-10 bg-slate-100 text-slate-600 rounded-full flex items-center justify-center hover:bg-red-50 hover:text-red-600 transition-colors"> <i class="fas fa-sign-out-alt"></i> </button> <button id="user-initials-btn" class="w-10 h-10 bg-blue-50 text-blue-600 rounded-full flex items-center justify-center font-bold shadow-sm border border-blue-100 uppercase">U</button> </div> </div> </header> <main id="main-content" class="flex-grow p-4 md:p-8 container mx-auto max-w-6xl"></main> <footer class="md:hidden bg-white border-t sticky bottom-0 z-40 h-16 flex justify-around items-center px-2 text-slate-400"> <button onclick="App.loadPage('dashboard')" class="flex flex-col items-center gap-1"><i class="fas fa-home text-lg"></i><span class="text-[10px]">Home</span></button> <button onclick="App.loadPage('customers')" class="admin-only flex flex-col items-center gap-1"><i class="fas fa-users text-lg"></i><span class="text-[10px]">Users</span></button> <button onclick="App.loadPage('expenses')" class="admin-only flex flex-col items-center gap-1"><i class="fas fa-receipt text-lg"></i><span class="text-[10px]">Expenses</span></button> <button onclick="App.loadPage('billing')" class="customer-only flex flex-col items-center gap-1"><i class="fas fa-file-invoice-dollar text-lg"></i><span class="text-[10px]">Billing</span></button> <button onclick="App.loadPage('howtopay')" class="customer-only flex flex-col items-center gap-1"><i class="fas fa-hand-holding-dollar text-lg"></i><span class="text-[10px]">Pay</span></button> <button onclick="App.loadPage('reportissue')" class="customer-only flex flex-col items-center gap-1"><i class="fas fa-circle-exclamation text-lg"></i><span class="text-[10px]">Issue</span></button> <button id="mobile-scan-nav" onclick="App.openActionSelection()" class="admin-only flex flex-col items-center gap-1"><i class="fas fa-qrcode text-lg"></i><span class="text-[10px]">Scan</span></button> <button id="mobile-settings-nav" onclick="App.openSettingsModal()" class="admin-only flex flex-col items-center gap-1"><i class="fas fa-cog text-lg"></i><span class="text-[10px]">Settings</span></button> </footer> </div> <div id="auth" class="hidden flex-grow bg-slate-100 flex items-center justify-center p-6"> <div class="w-full max-w-md bg-white rounded-3xl shadow-xl overflow-hidden"> <div class="bg-blue-600 p-8 text-center text-white"> <i class="fas fa-faucet-drip text-4xl mb-4"></i> <h2 class="text-2xl font-bold">Sign In to OmniFlow</h2> </div> <div class="p-8"> <div class="flex bg-slate-100 p-1 rounded-xl mb-6"> <button id="btn-login-cust" onclick="App.setLoginType('customer')" class="flex-1 py-2 text-sm rounded-lg font-bold bg-white shadow-sm">Customer</button> <button id="btn-login-agent" onclick="App.setLoginType('agent')" class="flex-1 py-2 text-sm rounded-lg font-bold text-slate-500">Agent</button> <button id="btn-login-admin" onclick="App.setLoginType('admin')" class="flex-1 py-2 text-sm rounded-lg font-bold text-slate-500">Business</button> </div> <form onsubmit="App.handleLogin(event)" class="space-y-4"> <input type="hidden" id="login-type" value="customer"> <input type="text" id="login-user" placeholder="Phone or Username" required class="w-full h-12 px-4 rounded-xl border-2 border-slate-100 focus:border-blue-500 outline-none transition-all"> <input type="password" id="login-pass" placeholder="Password" required class="w-full h-12 px-4 rounded-xl border-2 border-slate-100 focus:border-blue-500 outline-none transition-all"> <button type="submit" class="w-full h-14 bg-blue-600 text-white rounded-2xl font-bold text-lg shadow-lg shadow-blue-200 hover:bg-blue-700 active:scale-95 transition-all">Login</button> </form> </div> </div> </div> <!-- Global Footer --> <footer id="app-footer" class="py-4 px-2 text-center text-slate-500 text-xs border-t bg-white/50 backdrop-blur-sm"> <div class="flex flex-col sm:flex-row sm:gap-2 justify-center items-center"> <span>Copyright © <span id="current-year"><?php echo date('Y'); ?></span> <span id="business-name-footer"><?php echo htmlspecialchars($business_name_footer); ?></span>. All Rights Reserved</span> <span class="hidden sm:inline">|</span> <span>Developed by Omni Studio</span> </div> </footer> <div id="modal-backdrop" class="hidden fixed inset-0 z-50 bg-black/60 flex items-end sm:items-center justify-center p-0 sm:p-4 transition-all duration-300 opacity-0"> <div id="modal-view" class="bg-white w-full max-w-lg rounded-t-3xl sm:rounded-2xl shadow-2xl p-6 modal-enter max-h-[90vh] overflow-y-auto relative"> <button onclick="App.closeModal()" class="absolute top-4 right-4 w-10 h-10 flex items-center justify-center bg-slate-100 rounded-full text-slate-500 hover:bg-slate-200 z-50"><i class="fas fa-times"></i></button> <div id="modal-inner"></div> </div> </div> <script> const App = { user: null, userType: null, scanner: null, scanAction: null, dashboardChart: null, usageChart: null, dashboardData: null, activeModal: false, reportLocation: null, reportPhoto: null, reportPhotoFile: null, paymentSlipPhoto: null, paymentSlipFile: null, sessionData: { pollingInterval: null }, async init() { const yearEl = document.getElementById('current-year'); if (yearEl) yearEl.textContent = new Date().getFullYear(); try { const res = await fetch('api.php?action=check_session').then(r => r.json()); if (res.success) { this.user = res.user; this.userType = res.userType; this.setupUI(); } else { document.getElementById('auth').classList.remove('hidden'); } } catch(e) { console.error("Initialization failed", e); document.getElementById('auth').classList.remove('hidden'); } finally { const splash = document.getElementById('splash'); if (splash) { splash.classList.add('opacity-0'); setTimeout(() => splash.remove(), 500); } } document.getElementById('nav-scan').onclick = () => this.openActionSelection(); window.addEventListener('keydown', (e) => { if (e.key === 'Escape' && this.activeModal) { this.closeModal(); } }); }, setupUI() { document.getElementById('auth').classList.add('hidden'); document.getElementById('app').classList.remove('hidden'); const initialsBtn = document.getElementById('user-initials-btn'); initialsBtn.textContent = this.user.name[0]; initialsBtn.onclick = () => { if (this.userType === 'customer') this.openCustomerProfileModal(); else if (this.userType === 'admin') this.openSettingsModal(); else if (this.userType === 'agent') this.openAgentProfileModal(); }; if (this.userType !== 'admin') { document.querySelectorAll('.admin-only').forEach(el => el.style.display = 'none'); } else { document.querySelectorAll('.admin-only').forEach(el => el.style.display = ''); } if (this.userType !== 'customer') { document.querySelectorAll('.customer-only').forEach(el => el.style.display = 'none'); } else { document.querySelectorAll('.customer-only').forEach(el => el.style.display = ''); } this.loadPage('dashboard'); }, setLoginType(type) { document.getElementById('login-type').value = type; const custBtn = document.getElementById('btn-login-cust'); const agentBtn = document.getElementById('btn-login-agent'); const adminBtn = document.getElementById('btn-login-admin'); const loginUser = document.getElementById('login-user'); [custBtn, agentBtn, adminBtn].forEach(btn => btn.className = 'flex-1 py-2 text-sm rounded-lg font-bold text-slate-500'); if (type === 'customer') { custBtn.className = 'flex-1 py-2 text-sm rounded-lg font-bold bg-white shadow-sm'; loginUser.placeholder = 'Phone or Username'; } else if (type === 'agent') { agentBtn.className = 'flex-1 py-2 text-sm rounded-lg font-bold bg-white shadow-sm'; loginUser.placeholder = 'Agent Username'; } else { adminBtn.className = 'flex-1 py-2 text-sm rounded-lg font-bold bg-white shadow-sm'; loginUser.placeholder = 'Admin Username'; } }, async handleLogin(event) { event.preventDefault(); const fd = new FormData(event.target.closest('form')); fd.append('action', 'login'); fd.append('type', document.getElementById('login-type').value); fd.append('username', document.getElementById('login-user').value); fd.append('password', document.getElementById('login-pass').value); const res = await fetch('api.php', { method: 'POST', body: fd }).then(r => r.json()); if (res.success) { this.user = res.user; this.userType = res.userType; this.setupUI(); } else alert(res.message); }, async loadPage(page) { if (this.userType === 'agent' && page !== 'dashboard') { return this.loadPage('dashboard'); } if (this.sessionData.pollingInterval) { clearInterval(this.sessionData.pollingInterval); this.sessionData.pollingInterval = null; } const container = document.getElementById('main-content'); container.innerHTML = '<div class="flex justify-center p-12"><i class="fas fa-circle-notch fa-spin text-3xl text-blue-600"></i></div>'; document.querySelectorAll('header nav button').forEach(btn => { const label = btn.textContent.trim().toLowerCase().replace(/\s+/g, ''); const isMatch = label === page.toLowerCase() || (page === 'dashboard' && label === 'dashboard'); btn.className = isMatch ? 'text-sm font-bold text-blue-600' : 'text-sm font-medium hover:text-blue-600 text-slate-600'; }); document.querySelectorAll('footer button').forEach(btn => { const span = btn.querySelector('span'); if (!span) return; const label = span.textContent.trim().toLowerCase(); const pageMap = { 'home': 'dashboard', 'billing': 'billing', 'pay': 'howtopay', 'issue': 'reportissue', 'expenses': 'expenses' }; const isMatch = (pageMap[label] || label) === page.toLowerCase(); btn.className = isMatch ? 'flex flex-col items-center gap-1 text-blue-600' : 'flex flex-col items-center gap-1 text-slate-400'; }); if (page === 'dashboard') { if (this.userType === 'admin') this.renderAdminDashboard(); else if (this.userType === 'agent') this.renderAgentDashboard(); else this.renderCustomerDashboard(); } else if (page === 'customers') { this.renderCustomers(); } else if (page === 'billing') { this.renderBilling(); } else if (page === 'reports') { this.renderReports(); } else if (page === 'howtopay') { this.renderHowToPay(); } else if (page === 'reportissue') { this.renderReportIssue(); } else if (page === 'expenses') { this.renderExpenses(); } }, async renderAdminDashboard() { const res = await fetch('api.php?action=get_dashboard_data').then(r => r.json()); this.dashboardData = res.data; const s = this.dashboardData; if(s.settings && s.settings.primary_color) { document.documentElement.style.setProperty('--primary-color', s.settings.primary_color); } const footerNameEl = document.getElementById('business-name-footer'); if (footerNameEl && s.settings && s.settings.businessName) { footerNameEl.textContent = s.settings.businessName; } document.getElementById('main-content').innerHTML = ` <div class="space-y-8"> <div class="grid grid-cols-2 md:grid-cols-4 gap-4"> <div class="bg-white p-4 rounded-3xl border shadow-sm flex flex-col justify-center"><p class="text-[10px] font-bold text-slate-400 uppercase">Total Customers</p><p class="text-2xl font-black text-primary">${s.totalCustomers}</p></div> <div class="bg-primary p-4 rounded-3xl text-white shadow-lg flex flex-col justify-center"><p class="text-[10px] font-bold text-blue-200 uppercase">Monthly Rev</p><p class="text-2xl font-black">$${parseFloat(s.monthlyRevenue).toFixed(2)}</p></div> <div class="bg-white p-4 rounded-3xl border shadow-sm flex flex-col justify-center"><p class="text-[10px] font-bold text-slate-400 uppercase">Pending Bills</p><p class="text-2xl font-black text-red-500">${s.pendingPayments}</p></div> <div class="bg-white p-4 rounded-3xl border shadow-sm flex flex-col justify-center"><p class="text-[10px] font-bold text-slate-400 uppercase">Usage</p><p class="text-2xl font-black text-primary">${s.totalUsage} gal</p></div> </div> <div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div class="bg-white p-4 rounded-3xl border shadow-sm flex flex-col justify-center border-l-4 border-l-orange-500"><p class="text-[10px] font-bold text-slate-400 uppercase">Pending Disconnections</p><p class="text-2xl font-black text-orange-500">${s.pendingDisconnections}</p></div> <div class="bg-white p-4 rounded-3xl border shadow-sm flex flex-col justify-center border-l-4 border-l-red-600"><p class="text-[10px] font-bold text-slate-400 uppercase">Disconnected Accounts</p><p class="text-2xl font-black text-red-600">${s.disconnectedUsers}</p></div> </div> <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <button onclick="App.openScanner('reading')" class="bg-white p-6 rounded-3xl border shadow-sm flex items-center gap-4 hover:border-blue-500 transition-all"> <div class="w-12 h-12 bg-blue-100 text-blue-600 rounded-2xl flex items-center justify-center text-xl"><i class="fas fa-gauge-high"></i></div> <div class="text-left"><p class="font-black text-lg">Meter Reader</p><p class="text-xs text-slate-400">Scan QR to enter reading</p></div> </button> <button onclick="App.openScanner('payment')" class="bg-white p-6 rounded-3xl border shadow-sm flex items-center gap-4 hover:border-emerald-500 transition-all"> <div class="w-12 h-12 bg-emerald-100 text-emerald-600 rounded-2xl flex items-center justify-center text-xl"><i class="fas fa-file-invoice-dollar"></i></div> <div class="text-left"><p class="font-black text-lg">Easy Bill Pay</p><p class="text-xs text-slate-400">Scan QR to record payment</p></div> </button> </div> <div class="bg-white rounded-3xl border shadow-sm overflow-hidden"> <div class="p-6 border-b flex justify-between items-center bg-slate-50/50"> <div class="flex gap-4"> <button onclick="App.switchDashboardTab('trend')" id="tab-trend" class="text-sm font-bold pb-1 border-b-2 border-blue-600 text-blue-600">Performance Trend</button> <button onclick="App.switchDashboardTab('comparison')" id="tab-comparison" class="text-sm font-bold pb-1 border-b-2 border-transparent text-slate-400">Usage Comparison</button> </div> <button onclick="App.seedDemoData()" class="text-[10px] bg-slate-100 px-2 py-1 rounded-full font-bold text-slate-400 hover:bg-slate-200">Seed Demo Data</button> </div> <div id="dashboard-trend-view" class="p-6"> <div class="h-[300px] w-full"> <canvas id="dashboardChart"></canvas> </div> </div> <div id="dashboard-comparison-view" class="p-6 hidden"> <div class="h-[300px] w-full flex items-center justify-center"> <canvas id="comparisonChart"></canvas> </div> </div> </div> <div class="bg-white rounded-3xl border shadow-sm overflow-hidden"> <div class="p-6 border-b flex justify-between items-center bg-slate-50/50"> <h3 class="font-bold text-lg">Recent Activities</h3> <button onclick="App.loadPage('reports')" class="text-sm text-blue-600 font-bold">View All</button> </div> <div class="divide-y"> ${s.recentActivity.map(a => ` <div class="p-4 flex items-center justify-between hover:bg-slate-50 transition-colors"> <div class="flex items-center gap-3"> <div class="w-10 h-10 ${a.type==='payment' ? 'bg-emerald-100 text-emerald-600' : 'bg-blue-100 text-blue-600'} rounded-full flex items-center justify-center"><i class="fas ${a.type==='payment' ? 'fa-receipt' : 'fa-gauge-high'}"></i></div> <div><p class="font-bold text-sm">${a.name}</p><p class="text-[10px] text-slate-400">${a.date}</p></div> </div> <div class="text-right"><p class="font-bold text-sm ${a.type==='payment' ? 'text-emerald-600' : 'text-blue-600'}">${a.type==='payment' ? '+' : ''}$${parseFloat(a.amount).toFixed(2)}</p></div> </div> `).join('')} </div> </div> </div>`; this.initDashboardChart(s.historicalData); this.initComparisonChart(s.comparisonData); }, async renderAgentDashboard() { document.getElementById('main-content').innerHTML = ` <div class="flex flex-col items-center justify-center space-y-8 py-12"> <div class="w-24 h-24 bg-blue-100 text-blue-600 rounded-3xl flex items-center justify-center text-4xl shadow-lg"> <i class="fas fa-qrcode"></i> </div> <div class="text-center space-y-2"> <h2 class="text-3xl font-black text-slate-800">Meter Reader Mode</h2> <p class="text-slate-500 font-medium px-4">Identify customers by scanning their QR code to record new meter readings.</p> </div> <button onclick="App.openScanner('reading')" class="w-full max-sm:px-4 h-20 bg-blue-600 text-white rounded-[2rem] font-black text-xl shadow-xl shadow-blue-100 hover:bg-blue-700 active:scale-95 transition-all"> <i class="fas fa-camera mr-3"></i> Start Scanning </button> <div class="pt-8 text-center"> <p class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Logged in as Agent</p> <p class="text-sm font-bold text-slate-600">${this.user.name}</p> </div> </div>`; }, async renderCustomerDashboard() { if (!this.sessionData.pollingInterval) { this.sessionData.pollingInterval = setInterval(async () => { if (!this.user || this.userType !== 'customer') { clearInterval(this.sessionData.pollingInterval); this.sessionData.pollingInterval = null; return; } const r = await fetch(`api.php?action=get_customer_details&id=${this.user.id}`).then(res => res.json()); if (r.success) { const newB = parseFloat(r.data.balance); const oldB = parseFloat(document.getElementById('display-balance')?.textContent.replace('$', '') || 0); if (newB !== oldB) this.renderCustomerDashboard(); } }, 3000); } const res = await fetch(`api.php?action=get_customer_details&id=${this.user.id}`).then(r => r.json()); if (!res.success) return alert(res.message); const c = res.data; const statusColors = { 'Active': 'bg-emerald-100 text-emerald-700 border-emerald-200', 'Overdue': 'bg-amber-100 text-amber-700 border-amber-200', 'Pending Disconnection': 'bg-orange-100 text-orange-700 border-orange-200', 'Disconnected': 'bg-red-100 text-red-700 border-red-200' }; const statusColor = statusColors[c.status] || 'bg-slate-100 text-slate-700 border-slate-200'; const goldenIndicator = c.is_golden_citizen == 1 ? '<div class="absolute left-4 top-4 bg-white/20 p-2 rounded-xl text-white backdrop-blur-sm" title="Golden Citizen Status"><i class="fas fa-crown text-xl text-yellow-300"></i></div>' : ''; document.getElementById('main-content').innerHTML = ` <div class="space-y-6 max-w-md mx-auto pb-20 text-center flex flex-col items-center"> <div class="bg-primary p-10 rounded-[2.5rem] text-white shadow-2xl relative overflow-hidden text-center flex flex-col items-center w-full"> ${goldenIndicator} <div class="absolute -right-4 -top-4 w-32 h-32 bg-white/10 rounded-full blur-3xl"></div> <p class="text-sm font-bold text-blue-200 uppercase tracking-widest">Amount Due</p> <h2 id="display-balance" class="text-7xl font-black mt-2 tracking-tighter">$${parseFloat(c.balance).toFixed(2)}</h2> <div class="mt-10 flex items-center justify-center gap-4"> <span class="px-4 py-1.5 rounded-full text-xs font-black uppercase tracking-tighter border ${statusColor}">${c.status}</span> <p class="text-xs font-bold text-blue-200">Account: ${c.id}</p> </div> </div> <div class="bg-white p-8 rounded-[2.5rem] border shadow-sm flex flex-col items-center gap-6 w-full"> <div class="w-full flex justify-center items-center"> <span class="bg-slate-100 text-slate-500 font-black uppercase text-[10px] tracking-widest px-3 py-1.5 rounded-lg">My Digital ID</span> </div> <div class="p-6 bg-white rounded-[2rem] border-2 border-slate-50 shadow-inner flex items-center justify-center"> <img src="https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=${c.id}" class="w-64 h-64 object-contain"> </div> <div class="text-center space-y-1"> <p class="text-2xl font-black text-slate-800">${c.name}</p> <p class="text-base text-slate-400 font-bold">Meter: ${c.meter_number || 'N/A'}</p> </div> </div> </div>`; }, async renderCustomers() { const res = await fetch('api.php?action=get_customers').then(r => r.json()); const list = res.data.map(c => { const statusColors = { 'Active': 'bg-emerald-100 text-emerald-700', 'Overdue': 'bg-amber-100 text-amber-700', 'Pending Disconnection': 'bg-orange-100 text-orange-700', 'Disconnected': 'bg-red-100 text-red-700' }; const statusColor = statusColors[c.status] || 'bg-slate-100 text-slate-700'; const goldenIndicator = c.is_golden_citizen == 1 ? '<i class="fas fa-crown text-amber-500 ml-2" title="Golden Citizen"></i>' : ''; return ` <div onclick="App.viewCustomer('${c.id}')" class="bg-white p-4 rounded-2xl border shadow-sm flex items-center justify-between mb-3 cursor-pointer hover:border-blue-300 transition-all"> <div class="flex items-center gap-3"> <div class="w-12 h-12 bg-blue-100 text-blue-600 rounded-2xl flex items-center justify-center font-black text-lg uppercase">${c.name[0]}</div> <div><p class="font-bold text-slate-800">${c.name}${goldenIndicator}</p><p class="text-[10px] text-slate-400 font-medium">Meter: ${c.meter_number || 'N/A'} • ${c.phone}</p></div> </div> <div class="text-right flex flex-col items-end gap-1"> <p class="font-bold text-sm ${c.balance > 0 ? 'text-red-600' : 'text-emerald-600'}">$${parseFloat(c.balance).toFixed(2)}</p> <span class="text-[8px] uppercase px-2 py-0.5 rounded-full ${statusColor} font-black">${c.status}</span> </div> </div>`; }).join(''); document.getElementById('main-content').innerHTML = ` <div class="space-y-6"> <div class="flex justify-between items-center"> <h2 class="text-2xl font-black">Customers</h2> <button onclick="App.openAddCustomer()" class="w-10 h-10 bg-blue-600 text-white rounded-full flex items-center justify-center shadow-lg"><i class="fas fa-plus"></i></button> </div> <div class="relative"><i class="fas fa-search absolute left-4 top-1/2 -translate-y-1/2 text-slate-400"></i><input type="text" placeholder="Search customers..." onkeyup="App.filterCustomers(this.value)" class="w-full h-12 pl-12 pr-4 rounded-2xl border-none shadow-sm outline-none"></div> <div id="customer-list-container" class="customer-list">${list}</div> </div>`; }, filterCustomers(val) { const container = document.getElementById('customer-list-container'); if (!container) return; const items = container.children; const q = val.toLowerCase(); for(let item of items) { const text = item.textContent.toLowerCase(); item.style.display = text.includes(q) ? 'flex' : 'none'; } }, async renderBilling() { const container = document.getElementById('main-content'); container.innerHTML = '<div class="flex justify-center p-12"><i class="fas fa-circle-notch fa-spin text-blue-600"></i></div>'; const res = await fetch(`api.php?action=get_customer_details&id=${this.user.id}`).then(r => r.json()); if (!res.success) return alert(res.message); const c = res.data; const currM = new Date().toISOString().slice(0, 7); const lastDate = new Date(); lastDate.setMonth(lastDate.getMonth() - 1); const lastM = lastDate.toISOString().slice(0, 7); let currUsage = 0, lastUsage = 0; c.readings.forEach(r => { const rM = r.reading_date.slice(0, 7); if (rM === currM) currUsage += parseFloat(r.consumption); if (rM === lastM) lastUsage += parseFloat(r.consumption); }); container.innerHTML = ` <div class="space-y-6 max-w-2xl mx-auto pb-20 text-center"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div class="bg-primary p-8 rounded-3xl text-white shadow-lg text-center relative flex flex-col items-center gap-2"> <p class="text-[10px] font-black text-blue-200 uppercase tracking-widest">Current Balance</p> <p class="text-5xl font-black">$${parseFloat(c.balance).toFixed(2)}</p> <p class="text-xs font-bold text-blue-200 mt-2">Due Date: ${this.getEndOfMonth()}</p> </div> <div class="bg-white p-8 rounded-3xl border shadow-sm flex flex-col justify-center gap-4"> <div class="flex justify-between items-center"> <p class="text-[10px] font-black text-slate-400 uppercase tracking-widest text-left">Previous Reading</p> <p class="font-black text-lg text-slate-800">${parseInt(c.prev_meter_reading)} gal</p> </div> <div class="h-px bg-slate-100"></div> <div class="flex justify-between items-center"> <p class="text-[10px] font-black text-slate-400 uppercase tracking-widest text-left">Current Reading</p> <p class="font-black text-lg text-primary">${parseInt(c.curr_meter_reading)} gal</p> </div> </div> </div> <div class="bg-white p-6 rounded-3xl border shadow-sm"> <h3 class="font-bold text-sm uppercase tracking-widest text-slate-400 mb-4">Water Usage Comparison</h3> <div class="h-[250px] w-full"> <canvas id="usageComparisonChart"></canvas> </div> </div> <div class="bg-white rounded-3xl border shadow-sm overflow-hidden text-left"> <div class="p-6 border-b bg-slate-50/50 flex justify-between items-center"> <h3 class="font-bold text-sm uppercase tracking-widest text-slate-400">Statement History</h3> <button onclick="App.downloadBill()" class="text-blue-600 text-xs font-bold bg-blue-50 px-3 py-1 rounded-full"><i class="fas fa-download mr-1"></i> Download Latest Bill</button> </div> <div class="divide-y"> ${[...c.readings, ...c.payments].sort((a,b) => new Date(b.reading_date || b.payment_date) - new Date(a.reading_date || a.payment_date)).map(item => { const isPayment = !!item.payment_date; return ` <div class="p-4 flex items-center justify-between"> <div class="flex items-center gap-3"> <div class="w-10 h-10 ${isPayment ? 'bg-emerald-50 text-emerald-600' : 'bg-blue-50 text-blue-600'} rounded-full flex items-center justify-center"><i class="fas ${isPayment ? 'fa-receipt' : 'fa-gauge-high'}"></i></div> <div> <p class="font-bold text-sm">${isPayment ? 'Bill Payment' : 'Meter Reading'}</p> <p class="text-[10px] text-slate-400">${isPayment ? item.payment_date : item.reading_date}</p> </div> </div> <div class="text-right"> <p class="font-black text-sm ${isPayment ? 'text-emerald-600' : 'text-blue-600'}">${isPayment ? '-' : '+'}$${parseFloat(item.amount).toFixed(2)}</p> </div> </div>`; }).join('') || '<div class="p-12 text-center text-xs text-slate-400 font-bold uppercase tracking-widest">No history</div>'} </div> </div> </div>`; this.initUsageChart(lastUsage, currUsage); }, async renderHowToPay() { const container = document.getElementById('main-content'); const dashRes = await fetch('api.php?action=get_dashboard_data').then(r => r.json()); const s = dashRes.data.settings || {}; this.paymentSlipPhoto = null; this.paymentSlipFile = null; container.innerHTML = ` <div class="space-y-6 max-w-2xl mx-auto pb-20 text-center"> <div class="bg-white p-8 rounded-[2.5rem] border shadow-sm flex flex-col items-center gap-6"> <div class="w-16 h-16 bg-blue-100 text-blue-600 rounded-2xl flex items-center justify-center text-3xl shadow-sm"><i class="fas fa-hand-holding-dollar"></i></div> <div class="space-y-2"> <h2 class="text-2xl font-black text-slate-800">How to Pay Your Bill</h2> <p class="text-slate-500 font-medium">Choose your preferred payment method below.</p> </div> </div> <div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div class="bg-white p-6 rounded-3xl border shadow-sm text-left space-y-4"> <div class="flex items-center gap-3"> <div class="w-10 h-10 bg-emerald-50 text-emerald-600 rounded-xl flex items-center justify-center"><i class="fas fa-university"></i></div> <h3 class="font-black text-lg">Bank Deposit</h3> </div> <div class="space-y-2 p-4 bg-slate-50 rounded-2xl"> <p class="text-[10px] font-black text-slate-400 uppercase">Bank Name</p> <p class="text-sm font-bold text-slate-700">${s.bank_name || 'Not Configured'}</p> <p class="text-[10px] font-black text-slate-400 uppercase mt-2">Account Name</p> <p class="text-sm font-bold text-slate-700">${s.bank_account_name || 'Not Configured'}</p> <p class="text-[10px] font-black text-slate-400 uppercase mt-2">Account Number</p> <p class="text-sm font-bold text-slate-700">${s.bank_number || 'Not Configured'}</p> </div> <p class="text-xs text-slate-400 leading-relaxed">Please use your <b>Account ID</b> as the payment reference when making a transfer.</p> </div> <div class="bg-white p-6 rounded-3xl border shadow-sm text-left space-y-4"> <div class="flex items-center gap-3"> <div class="w-10 h-10 bg-blue-50 text-blue-600 rounded-xl flex items-center justify-center"><i class="fas fa-credit-card"></i></div> <h3 class="font-black text-lg">Online Payment</h3> </div> <p class="text-sm text-slate-600 leading-relaxed">Simply screenshot your transfer confirmation and upload it below to verify your payment.</p> <div class="space-y-4"> <input type="file" id="payment-slip-input" accept="image/*" capture="environment" class="hidden" onchange="App.handlePaymentSlipPhoto(event)"> <button onclick="document.getElementById('payment-slip-input').click()" id="btn-upload-slip" class="w-full h-14 bg-slate-50 border-2 border-slate-100 rounded-2xl font-bold flex items-center justify-center gap-2 hover:bg-slate-100 transition-all text-slate-600"> <i class="fas fa-camera"></i> <span>Capture Payment File</span> </button> <div id="payment-preview" class="relative hidden"> <img id="payment-preview-img" class="w-full h-32 object-cover rounded-2xl border shadow-inner"> <button onclick="App.clearPaymentSlipPhoto()" class="absolute -top-2 -right-2 w-8 h-8 bg-red-500 text-white rounded-full flex items-center justify-center shadow-lg"><i class="fas fa-times"></i></button> </div> <button id="btn-send-payment" onclick="App.sendPaymentConfirmation('${s.support_phone || '5016115121'}')" class="w-full h-16 bg-emerald-600 text-white rounded-2xl font-black text-lg shadow-lg shadow-emerald-100 hover:bg-emerald-700 active:scale-95 transition-all hidden"> <i class="fab fa-whatsapp mr-2 text-xl"></i> <span>Send Confirmation</span> </button> </div> <div class="p-4 bg-blue-50 rounded-2xl border border-blue-100 flex items-center gap-3"> <i class="fas fa-info-circle text-blue-600"></i> <p class="text-[10px] font-bold text-blue-700 uppercase tracking-tight">System updates hourly</p> </div> </div> </div> </div>`; }, handlePaymentSlipPhoto(event) { const file = event.target.files[0]; if (!file) return; this.paymentSlipFile = file; const reader = new FileReader(); reader.onload = (e) => { this.paymentSlipPhoto = e.target.result; const preview = document.getElementById('payment-preview'); const previewImg = document.getElementById('payment-preview-img'); const sendBtn = document.getElementById('btn-send-payment'); const uploadBtn = document.getElementById('btn-upload-slip'); if(previewImg) previewImg.src = this.paymentSlipPhoto; if(preview) preview.classList.remove('hidden'); if(sendBtn) sendBtn.classList.remove('hidden'); if(uploadBtn) { uploadBtn.innerHTML = '<i class="fas fa-check-circle"></i> File Selected'; uploadBtn.className = "w-full h-14 bg-emerald-50 border-2 border-emerald-200 rounded-2xl font-bold flex items-center justify-center gap-2 text-emerald-600"; } }; reader.readAsDataURL(file); }, clearPaymentSlipPhoto() { this.paymentSlipPhoto = null; this.paymentSlipFile = null; const preview = document.getElementById('payment-preview'); const sendBtn = document.getElementById('btn-send-payment'); const uploadBtn = document.getElementById('btn-upload-slip'); if(preview) preview.classList.add('hidden'); if(sendBtn) sendBtn.classList.add('hidden'); if(uploadBtn) { uploadBtn.innerHTML = '<i class="fas fa-camera"></i> Capture Payment File'; uploadBtn.className = "w-full h-14 bg-slate-50 border-2 border-slate-100 rounded-2xl font-bold flex items-center justify-center gap-2 text-slate-600"; } }, async sendPaymentConfirmation(supportPhone) { const btn = document.getElementById('btn-send-payment'); if (!this.paymentSlipFile) return alert("Please capture your payment file first."); const originalText = btn.innerHTML; btn.disabled = true; btn.innerHTML = '<i class="fas fa-circle-notch fa-spin"></i> Processing...'; try { let photoLink = ""; let tempFilename = ""; const fd = new FormData(); fd.append('action', 'upload_temp_image'); fd.append('image', this.paymentSlipFile); fd.append('type', 'payments'); const res = await fetch('api.php', { method: 'POST', body: fd }).then(r => r.json()); if (res && res.success) { photoLink = res.url; tempFilename = res.filename; } else { throw new Error("Failed to upload image file."); } const msg = `*OmniFlow Payment Confirmation*\n` + `*From:* ${this.user.name}\n` + `*Account ID:* ${this.user.id}\n` + `*File:* ${photoLink}\n\nPlease verify this payment. Thank you!`; const targetNumber = String(supportPhone).replace(/[^0-9]/g, ''); const waUrl = `https://wa.me/${targetNumber}?text=${encodeURIComponent(msg)}`; window.location.href = waUrl; if (tempFilename) { setTimeout(() => { const cleanupFd = new FormData(); cleanupFd.append('action', 'cleanup_temp_image'); cleanupFd.append('filename', tempFilename); cleanupFd.append('type', 'payments'); fetch('api.php', { method: 'POST', body: cleanupFd }); }, 30000); } } catch (e) { console.error("Failed to send payment verification", e); alert("An error occurred while preparing the confirmation. Please try again."); } finally { btn.disabled = false; btn.innerHTML = originalText; } }, async renderReportIssue() { const container = document.getElementById('main-content'); const dashRes = await fetch('api.php?action=get_dashboard_data').then(r => r.json()); const s = dashRes.data.settings || {}; this.reportLocation = null; this.reportPhoto = null; this.reportPhotoFile = null; container.innerHTML = ` <div class="space-y-6 max-w-2xl mx-auto pb-20 text-center"> <div class="bg-white p-8 rounded-[2.5rem] border shadow-sm flex flex-col items-center gap-6"> <div class="w-16 h-16 bg-red-100 text-red-600 rounded-2xl flex items-center justify-center text-3xl shadow-sm"><i class="fas fa-circle-exclamation"></i></div> <div class="space-y-2"> <h2 class="text-2xl font-black text-slate-800">Report an Issue</h2> <p class="text-slate-500 font-medium">Experiencing a leak or billing error? Let us know.</p> </div> </div> <div class="bg-white p-8 rounded-[2.5rem] border shadow-sm space-y-6 text-left"> <div class="space-y-4"> <label class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Problem Description</label> <textarea id="issue-desc" rows="4" placeholder="Describe the issue here..." class="w-full p-4 rounded-2xl border-2 border-slate-100 focus:border-red-500 outline-none transition-all"></textarea> </div> <div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div class="space-y-2"> <label class="text-[10px] font-black text-slate-400 uppercase tracking-widest">Location File</label> <button onclick="App.captureLocation()" id="btn-location" class="w-full h-14 bg-slate-50 border-2 border-slate-100 rounded-2xl font-bold flex items-center justify-center gap-2 hover:bg-slate-100 transition-all text-slate-600"> <i class="fas fa-location-dot"></i> <span>Pin My Location</span> </button> </div> <div class="space-y-2"> <label class="text-[10px] font-black text-slate-400 uppercase tracking-widest">File</label> <input type="file" id="issue-photo-input" accept="image/*" capture="environment" class="hidden" onchange="App.handleReportPhoto(event)"> <button onclick="document.getElementById('issue-photo-input').click()" id="btn-photo" class="w-full h-14 bg-slate-50 border-2 border-slate-100 rounded-2xl font-bold flex items-center justify-center gap-2 hover:bg-slate-100 transition-all text-slate-600"> <i class="fas fa-camera"></i> <span>Take Photo</span> </button> </div> </div> <div id="report-preview" class="grid grid-cols-2 gap-4 hidden"> <div id="location-preview" class="p-3 bg-blue-50 rounded-xl text-[10px] font-bold text-blue-600 flex items-center gap-2 truncate"><i class="fas fa-check-circle"></i> Location Pinned</div> <div id="photo-preview-container" class="relative group"> <img id="photo-preview-img" class="w-full h-20 object-cover rounded-xl border"> <button onclick="App.clearReportPhoto()" class="absolute -top-2 -right-2 w-6 h-6 bg-red-500 text-white rounded-full flex items-center justify-center text-[10px] shadow-sm"><i class="fas fa-times"></i></button> </div> </div> <button id="btn-send-report" onclick="App.sendReport('${s.support_phone || '5016115121'}')" class="w-full h-16 bg-blue-600 text-white rounded-2xl font-black text-lg shadow-lg shadow-blue-100 hover:bg-blue-700 active:scale-95 transition-all mt-4"> <i class="fab fa-whatsapp mr-2 text-xl"></i> <span>Send Report via WhatsApp</span> </button> </div> </div>`; }, async renderExpenses() { const res = await fetch('api.php?action=get_expenses').then(r => r.json()); const list = res.data.map(e => ` <div class="bg-white p-4 rounded-2xl border shadow-sm flex items-center justify-between mb-3"> <div class="flex items-center gap-3"> <div class="w-10 h-10 bg-orange-100 text-orange-600 rounded-full flex items-center justify-center"><i class="fas fa-receipt"></i></div> <div> <p class="font-bold text-sm text-slate-800">${e.category}</p> <p class="text-[10px] text-slate-400 font-medium">${e.description || 'No description'} • ${e.date}</p> </div> </div> <div class="text-right"> <p class="font-black text-red-600">-$${parseFloat(e.amount).toFixed(2)}</p> </div> </div>`).join('') || '<div class="p-12 text-center text-slate-400 font-bold uppercase tracking-widest">No expenses recorded</div>'; document.getElementById('main-content').innerHTML = ` <div class="space-y-6"> <div class="flex justify-between items-center"> <h2 class="text-2xl font-black">Business Expenses</h2> <button onclick="App.openAddExpenseModal()" class="w-10 h-10 bg-orange-600 text-white rounded-full flex items-center justify-center shadow-lg shadow-orange-100"><i class="fas fa-plus"></i></button> </div> <div class="expense-list">${list}</div> </div>`; }, openAddExpenseModal() { this.openModal(` <h3 class="text-2xl font-black mb-6">Record Expense</h3> <form id="add-expense-form" class="space-y-4"> <div> <label class="text-[10px] font-bold text-slate-400 uppercase tracking-widest block mb-1 ml-1">Amount</label> <div class="relative"> <span class="absolute left-4 top-1/2 -translate-y-1/2 font-bold text-slate-400">$</span> <input type="number" step="0.01" name="amount" required placeholder="0.00" class="w-full h-14 pl-8 pr-4 rounded-2xl border-2 border-slate-100 focus:border-orange-500 outline-none font-bold"> </div> </div> <div> <label class="text-[10px] font-bold text-slate-400 uppercase tracking-widest block mb-1 ml-1">Category</label> <select name="category" required class="w-full h-14 px-4 rounded-2xl border-2 border-slate-100 focus:border-orange-500 outline-none font-bold bg-white"> <option>Maintenance</option> <option>Electricity</option> <option>Supplies</option> <option>Fuel</option> <option>Other</option> </select> </div> <div> <label class="text-[10px] font-bold text-slate-400 uppercase tracking-widest block mb-1 ml-1">Description</label> <textarea name="description" placeholder="Optional details..." rows="3" class="w-full p-4 rounded-2xl border-2 border-slate-100 focus:border-orange-500 outline-none transition-all font-bold"></textarea> </div> <button type="submit" class="w-full h-16 bg-orange-600 text-white rounded-2xl font-black text-lg shadow-lg shadow-orange-100 hover:bg-orange-700 active:scale-95 transition-all mt-4">Save Expense</button> </form>`); document.getElementById('add-expense-form').onsubmit = async (e) => { e.preventDefault(); const fd = new FormData(e.target); fd.append('action', 'record_expense'); const res = await fetch('api.php', { method: 'POST', body: fd }).then(r => r.json()); if (res.success) { alert(res.message); this.closeModal(); this.loadPage('expenses'); } else alert(res.message); }; }, captureLocation() { const btn = document.getElementById('btn-location'); btn.innerHTML = '<i class="fas fa-circle-notch fa-spin"></i> Locating...'; navigator.geolocation.getCurrentPosition((pos) => { this.reportLocation = `https://www.google.com/maps?q=${pos.coords.latitude},${pos.coords.longitude}`; btn.className = "w-full h-14 bg-blue-50 border-2 border-blue-200 rounded-2xl font-bold flex items-center justify-center gap-2 text-blue-600"; btn.innerHTML = '<i class="fas fa-check-circle"></i> Location Pinned'; document.getElementById('report-preview').classList.remove('hidden'); document.getElementById('location-preview').classList.remove('hidden'); }, (err) => { alert("Please enable location services to pin your location."); btn.innerHTML = '<i class="fas fa-location-dot"></i> Pin My Location'; }); }, handleReportPhoto(event) { const file = event.target.files[0]; if (!file) return; this.reportPhotoFile = file; const reader = new FileReader(); reader.onload = (e) => { this.reportPhoto = e.target.result; const previewImg = document.getElementById('photo-preview-img'); const previewContainer = document.getElementById('report-preview'); const previewPhotoContainer = document.getElementById('photo-preview-container'); const photoBtn = document.getElementById('btn-photo'); if(previewImg) previewImg.src = this.reportPhoto; if(previewContainer) previewContainer.classList.remove('hidden'); if(previewPhotoContainer) previewPhotoContainer.classList.remove('hidden'); if(photoBtn) { photoBtn.innerHTML = '<i class="fas fa-check-circle"></i> File Selected'; photoBtn.className = "w-full h-14 bg-emerald-50 border-2 border-emerald-200 rounded-2xl font-bold flex items-center justify-center gap-2 text-emerald-600"; } }; reader.readAsDataURL(file); }, clearReportPhoto() { this.reportPhoto = null; this.reportPhotoFile = null; const previewPhotoContainer = document.getElementById('photo-preview-container'); const photoBtn = document.getElementById('btn-photo'); if(previewPhotoContainer) previewPhotoContainer.classList.add('hidden'); if(photoBtn) { photoBtn.innerHTML = '<i class="fas fa-camera"></i> Take Photo'; photoBtn.className = "w-full h-14 bg-slate-50 border-2 border-slate-100 rounded-2xl font-bold flex items-center justify-center gap-2 text-slate-600"; } }, async sendReport(supportPhone) { const desc = document.getElementById('issue-desc').value; const btn = document.getElementById('btn-send-report'); if (!desc) return alert("Please provide a description of the issue."); const originalText = btn.innerHTML; btn.disabled = true; btn.innerHTML = '<i class="fas fa-circle-notch fa-spin"></i> Preparing Report...'; try { let photoLink = ""; let tempFilename = ""; if (this.reportPhotoFile) { const fd = new FormData(); fd.append('action', 'upload_temp_image'); fd.append('image', this.reportPhotoFile); fd.append('type', 'reports'); const res = await fetch('api.php', { method: 'POST', body: fd }).then(r => r.json()); if (res && res.success) { photoLink = res.url; tempFilename = res.filename; } else { console.warn("Photo upload failed, proceeding without link"); } } const msg = `*OmniFlow Issue Report*\n` + `*From:* ${this.user.name} (${this.user.id})\n` + `*Issue:* ${desc}\n` + (this.reportLocation ? `*Location:* ${this.reportLocation}\n` : '') + (photoLink ? `*File:* ${photoLink}` : ''); const targetNumber = String(supportPhone).replace(/[^0-9]/g, ''); const waUrl = `https://wa.me/${targetNumber}?text=${encodeURIComponent(msg)}`; window.location.href = waUrl; if (tempFilename) { setTimeout(() => { const cleanupFd = new FormData(); cleanupFd.append('action', 'cleanup_temp_image'); cleanupFd.append('filename', tempFilename); cleanupFd.append('type', 'reports'); fetch('api.php', { method: 'POST', body: cleanupFd }); }, 30000); } } catch (e) { console.error("Failed to send report", e); alert("An error occurred while preparing the report. Please try again."); } finally { btn.disabled = false; btn.innerHTML = originalText; } }, getEndOfMonth() { const d = new Date(); const lastDay = new Date(d.getFullYear(), d.getMonth() + 1, 0); return lastDay.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); }, initUsageChart(last, curr) { const canvas = document.getElementById('usageComparisonChart'); if(!canvas) return; const ctx = canvas.getContext('2d'); if (this.usageChart) this.usageChart.destroy(); this.usageChart = new Chart(ctx, { type: 'bar', data: { labels: ['Last Month', 'Current Month'], datasets: [{ label: 'Gallons Used', data: [last, curr], backgroundColor: ['#94a3b8', '#2563eb'], borderRadius: 12 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, grid: { display: false } }, x: { grid: { display: false } } } } }); }, async downloadBill() { const res = await fetch(`api.php?action=get_customer_details&id=${this.user.id}`).then(r => r.json()); const c = res.data; const dashRes = await fetch('api.php?action=get_dashboard_data').then(r => r.json()); const business = dashRes.data.settings; const { jsPDF } = window.jspdf; const doc = new jsPDF(); let startY = 20; if (business && (business.logoUrl || business.logo_url)) { const logo = business.logoUrl || business.logo_url; try { const img = new Image(); img.src = logo; img.crossOrigin = "Anonymous"; await new Promise((resolve) => { img.onload = resolve; img.onerror = resolve; }); doc.addImage(img, 'PNG', 105 - 15, 10, 30, 30); startY = 50; } catch (e) { console.error("Logo failed to load", e); } } const latestReading = c.readings[0] || { current_reading: 0, prev_reading: 0, consumption: 0, amount: 0, reading_date: 'N/A' }; const arrears = parseFloat(c.balance) - parseFloat(latestReading.amount); doc.setFontSize(22); doc.text("Utility Bill", 105, startY, { align: "center" }); doc.setFontSize(12); doc.text(business.businessName, 105, startY + 10, { align: "center" }); doc.line(20, startY + 20, 190, startY + 20); doc.text(`Customer: ${c.name}`, 20, startY + 30); doc.text(`Account ID: ${c.id}`, 20, startY + 37); doc.text(`Meter Number: ${c.meter_number || 'N/A'}`, 20, startY + 44); doc.text(`Billing Date: ${new Date().toLocaleDateString()}`, 140, startY + 30); doc.line(20, startY + 52, 190, startY + 52); doc.setFont("helvetica", "bold"); doc.text("Reading Period Summary", 20, startY + 62); doc.setFont("helvetica", "normal"); doc.text("Previous Meter Reading", 20, startY + 72); doc.text(`${parseInt(latestReading.prev_reading)} gal`, 180, startY + 72, { align: "right" }); doc.text("Current Meter Reading", 20, startY + 79); doc.text(`${parseInt(latestReading.current_reading)} gal`, 180, startY + 79, { align: "right" }); doc.setFont("helvetica", "bold"); doc.text("Total Usage (Consumption)", 20, startY + 86); doc.text(`${parseInt(latestReading.consumption)} gal`, 180, startY + 86, { align: "right" }); doc.setFont("helvetica", "normal"); doc.line(20, startY + 92, 190, startY + 92); doc.text("Description", 20, startY + 102); doc.text("Amount (BZD)", 180, startY + 102, { align: "right" }); doc.text("Current Usage Charges", 20, startY + 112); doc.text(`$${parseFloat(latestReading.amount).toFixed(2)}`, 180, startY + 112, { align: "right" }); doc.text("Arrears / Previous Balance", 20, startY + 119); doc.text(`$${Math.max(0, arrears).toFixed(2)}`, 180, startY + 119, { align: "right" }); doc.line(140, startY + 127, 190, startY + 127); doc.setFontSize(14); doc.setFont("helvetica", "bold"); doc.text("TOTAL AMOUNT DUE", 20, startY + 137); doc.text(`$${parseFloat(c.balance).toFixed(2)}`, 180, startY + 137, { align: "right" }); doc.setFont("helvetica", "normal"); doc.setFontSize(10); doc.setTextColor(100); doc.text("Please pay by the 5th of next month to avoid disconnection.", 105, startY + 160, { align: "center" }); doc.save(`bill_${c.id}.pdf`); }, async renderReports() { const now = new Date(); const currentMonth = now.toISOString().slice(0, 7); document.getElementById('main-content').innerHTML = ` <div class="space-y-6"> <div class="bg-white p-6 rounded-3xl border shadow-sm"> <h3 class="font-bold text-xl mb-6">Financial Reports</h3> <div class="grid grid-cols-1 md:grid-cols-3 gap-4 items-end"> <div><label class="text-[10px] font-bold text-slate-400 uppercase mb-1 block">Report Type</label> <select id="rep-type" class="w-full h-12 px-4 rounded-xl border bg-slate-50 font-bold"> <option value="revenue">Payments (Inflow)</option> <option value="readings">Usage (Readings)</option> <option value="expenses">Expenses (Outflow)</option> </select></div> <div><label class="text-[10px] font-bold text-slate-400 uppercase mb-1 block">Month</label><input type="month" id="rep-month" value="${currentMonth}" class="w-full h-12 px-4 rounded-xl border bg-slate-50 font-bold"></div> <button onclick="App.generateReport()" class="h-12 bg-blue-600 text-white rounded-xl font-bold">Generate Report</button> </div> </div> <div id="report-results" class="bg-white rounded-3xl border shadow-sm min-h-[200px]"></div> </div>`; }, async generateReport() { const type = document.getElementById('rep-type').value; const month = document.getElementById('rep-month').value; const results = document.getElementById('report-results'); results.innerHTML = '<div class="p-12 text-center"><i class="fas fa-circle-notch fa-spin text-blue-600"></i></div>'; const res = await fetch(`api.php?action=get_reports&type=${type}&period=${month}`).then(r => r.json()); if (res.success) { if (res.data.length === 0) { results.innerHTML = '<div class="p-12 text-center text-slate-400 font-bold">No records found for this period.</div>'; return; } let headers = ''; let rows = ''; let footer = ''; let reportTitle = type === 'readings' ? 'Water Usage Records' : `${type.charAt(0).toUpperCase() + type.slice(1)} Summary`; if (type === 'expenses') { headers = '<th class="p-4">Category</th><th class="p-4">Description</th><th class="p-4">Date</th><th class="p-4 text-right">Amount</th>'; rows = res.data.map(r => ` <tr class="border-b text-sm"> <td class="p-4 font-bold">${r.category}</td> <td class="p-4 text-slate-500">${r.description || '-'}</td> <td class="p-4">${r.date}</td> <td class="p-4 text-right font-black text-red-600">$${parseFloat(r.amount).toFixed(2)}</td> </tr>`).join(''); const totalExp = res.data.reduce((acc, r) => acc + parseFloat(r.amount || 0), 0); footer = ` <tfoot class="bg-slate-50 font-black"> <tr> <td colspan="3" class="p-4 text-right text-[10px] uppercase text-slate-400 tracking-widest">Total Expenses</td> <td class="p-4 text-right text-red-600">$${totalExp.toFixed(2)}</td> </tr> </tfoot>`; } else if (type === 'readings') { const totalUsage = res.data.reduce((acc, r) => acc + parseInt(r.consumption || 0), 0); const totalAmount = res.data.reduce((acc, r) => acc + parseFloat(r.amount || 0), 0); headers = '<th class="p-4">Customer</th><th class="p-4">Date</th><th class="p-4 text-right">Consumption (gal)</th><th class="p-4 text-right">Amount</th>'; rows = res.data.map(r => ` <tr class="border-b text-sm"> <td class="p-4 font-bold">${r.name}</td> <td class="p-4">${r.reading_date}</td> <td class="p-4 text-right font-medium">${parseInt(r.consumption)}</td> <td class="p-4 text-right font-black text-blue-600">$${parseFloat(r.amount).toFixed(2)}</td> </tr>`).join(''); footer = ` <tfoot class="bg-slate-50 font-black"> <tr> <td colspan="2" class="p-4 text-right text-[10px] uppercase text-slate-400 tracking-widest">Total Overall</td> <td class="p-4 text-right text-blue-600">${totalUsage} gal</td> <td class="p-4 text-right text-blue-600">$${totalAmount.toFixed(2)}</td> </tr> </tfoot>`; } else { headers = '<th class="p-4">Customer</th><th class="p-4">Date</th><th class="p-4 text-right">Amount</th>'; rows = res.data.map(r => ` <tr class="border-b text-sm"> <td class="p-4 font-bold">${r.name}</td> <td class="p-4">${r.payment_date}</td> <td class="p-4 text-right font-black text-emerald-600">$${parseFloat(r.amount).toFixed(2)}</td> </tr>`).join(''); const totalRev = res.data.reduce((acc, r) => acc + parseFloat(r.amount || 0), 0); footer = ` <tfoot class="bg-slate-50 font-black"> <tr> <td colspan="2" class="p-4 text-right text-[10px] uppercase text-slate-400 tracking-widest">Total Revenue</td> <td class="p-4 text-right text-emerald-600">$${totalRev.toFixed(2)}</td> </tr> </tfoot>`; } results.innerHTML = ` <div class="p-6 border-b flex justify-between items-center"><h4 class="font-black uppercase text-xs tracking-widest">${reportTitle}</h4><button onclick="App.exportReportToPDF('${type}', '${month}')" class="text-xs bg-slate-100 px-4 py-2 rounded-full font-bold">Export PDF</button></div> <div class="overflow-x-auto"> <table id="report-table-export" class="w-full text-left"> <thead><tr class="text-[10px] uppercase text-slate-400 font-black tracking-widest">${headers}</tr></thead> <tbody>${rows}</tbody> ${footer} </table> </div>`; } }, async exportReportToPDF(type, month) { const { jsPDF } = window.jspdf; const doc = new jsPDF(); let yPos = 20; const settings = this.dashboardData ? this.dashboardData.settings : null; if (settings && (settings.logoUrl || settings.logo_url)) { const logo = settings.logoUrl || settings.logo_url; try { const img = new Image(); img.src = logo; img.crossOrigin = "Anonymous"; await new Promise((resolve) => { img.onload = resolve; img.onerror = resolve; }); doc.addImage(img, 'PNG', 14, 10, 20, 20); yPos = 40; } catch (e) { console.error("Logo failed to load", e); } } doc.setFontSize(18); const reportTitle = type === 'readings' ? 'Water Usage Records' : `${type.charAt(0).toUpperCase() + type.slice(1)} Report`; doc.text(`${reportTitle} - ${month}`, 14, yPos); doc.autoTable({ html: '#report-table-export', startY: yPos + 10, theme: 'striped', headStyles: { fillColor: [37, 99, 235] }, footStyles: { fillColor: [241, 245, 249], textColor: [37, 99, 235], fontStyle: 'bold' }, styles: { fontSize: 10 } }); doc.save(`${type}_report_${month}.pdf`); }, async openSettingsModal() { const res = await fetch('api.php?action=get_dashboard_data').then(r => r.json()); const s = res.data.settings || {}; const admins = res.data.admins || []; const agents = res.data.agents || []; this.openModal(` <div class="space-y-8"> <div class="flex items-center gap-3"> <div class="w-10 h-10 bg-blue-100 text-blue-600 rounded-xl flex items-center justify-center text-lg"><i class="fas fa-cog"></i></div> <h3 class="text-2xl font-black">System Configuration</h3> </div> <div class="flex bg-slate-100 p-1 rounded-xl"> <button onclick="App.switchSettingsTab('general')" id="tab-settings-general" class="flex-1 py-2 text-sm rounded-lg font-bold bg-white shadow-sm">General</button> <button onclick="App.switchSettingsTab('security')" id="tab-settings-security" class="flex-1 py-2 text-sm rounded-lg font-bold text-slate-500">Security</button> <button onclick="App.switchSettingsTab('agents')" id="tab-settings-agents" class="flex-1 py-2 text-sm rounded-lg font-bold text-slate-500">Agents</button> <button onclick="App.switchSettingsTab('billing')" id="tab-settings-billing" class="flex-1 py-2 text-sm rounded-lg font-bold text-slate-500">Billing</button> </div> <form id="settings-form" onsubmit="App.handleSaveSettings(event)" class="space-y-6"> <div id="settings-general-view" class="space-y-4"> <div> <label class="text-[10px] font-bold text-slate-400 uppercase mb-1 block tracking-widest">Business Name</label> <input type="text" name="businessName" value="${s.businessName || ''}" required class="w-full h-12 px-4 rounded-xl border bg-slate-50 font-bold"> </div> <div> <label class="text-[10px] font-bold text-slate-400 uppercase mb-1 block tracking-widest">Support Phone (WhatsApp)</label> <input type="tel" name="support_phone" value="${s.support_phone || '5016115121'}" class="w-full h-12 px-4 rounded-xl border bg-slate-50 font-bold"> </div> <div> <label class="text-[10px] font-bold text-slate-400 uppercase mb-1 block tracking-widest">General Inquiries Phone</label> <input type="tel" name="phone" value="${s.phone || ''}" class="w-full h-12 px-4 rounded-xl border bg-slate-50 font-bold"> </div> <div> <label class="text-[10px] font-bold text-slate-400 uppercase mb-1 block tracking-widest">Primary Theme Color</label> <input type="color" name="primaryColor" value="${s.primary_color || '#2563eb'}" class="w-full h-12 p-1 rounded-xl border bg-slate-50"> </div> <div> <label class="text-[10px] font-bold text-slate-400 uppercase mb-1 block tracking-widest">Utility Rate (per gal)</label> <div class="relative"> <span class="absolute left-4 top-1/2 -translate-y-1/2 font-bold text-slate-400">$</span> <input type="number" step="0.01" name="ratePerUnit" value="${s.ratePerUnit || '0.00'}" required class="w-full h-12 pl-8 pr-4 rounded-xl border bg-slate-50 font-bold"> </div> </div> </div> <div id="settings-security-view" class="space-y-6 hidden"> <div class="p-4 bg-blue-50 rounded-2xl border border-blue-100 space-y-4"> <h4 class="text-[10px] font-black uppercase text-blue-400 tracking-widest">Your Account Profile</h4> <div> <label class="text-[10px] font-bold text-blue-400 uppercase mb-1 block tracking-widest">Your Phone Number</label> <input type="tel" name="adminPhone" value="${this.user.phone || ''}" placeholder="Enter your phone number" class="w-full h-12 px-4 rounded-xl border-none font-bold"> </div> <div> <label class="text-[10px] font-bold text-blue-400 uppercase mb-1 block tracking-widest">Change Your Password</label> <input type="password" name="adminPassword" placeholder="Enter new password" class="w-full h-12 px-4 rounded-xl border-none font-bold"> </div> </div> <div class="space-y-4"> <div class="flex justify-between items-center"><h4 class="text-xs font-black uppercase text-slate-400 tracking-widest">Other Admins</h4><button type="button" onclick="App.openAddAdmin()" class="text-xs text-blue-600 font-bold">+ Add Admin</button></div> <div class="divide-y border rounded-2xl overflow-hidden bg-slate-50"> ${admins.map(a => ` <div class="p-3 flex justify-between items-center text-sm font-bold"> <span>${a.name} (@${a.username})</span> <div class="flex gap-1"> <button type="button" onclick="App.openResetUserPasswordModal('${a.id}', '${a.name}', 'admin')" title="Reset Password" class="text-blue-600 p-2 hover:bg-blue-50 rounded-full"><i class="fas fa-key"></i></button> <button type="button" onclick="App.deleteAdmin('${a.id}')" title="Delete" class="text-red-500 p-2 hover:bg-red-50 rounded-full"><i class="fas fa-trash"></i></button> </div> </div> `).join('')} </div> </div> </div> <div id="settings-agents-view" class="space-y-4 hidden"> <div class="flex justify-between items-center"><h4 class="text-xs font-black uppercase text-slate-400 tracking-widest">Meter Reader Agents</h4><button type="button" onclick="App.openAddAgent()" class="text-xs text-blue-600 font-bold">+ Add Agent</button></div> <div class="divide-y border rounded-2xl overflow-hidden bg-slate-50"> ${agents.map(a => ` <div class="p-3 flex justify-between items-center text-sm font-bold"> <span>${a.name} (@${a.username})</span> <div class="flex gap-1"> <button type="button" onclick="App.openResetUserPasswordModal('${a.id}', '${a.name}', 'agent')" title="Reset Password" class="text-blue-600 p-2 hover:bg-blue-50 rounded-full"><i class="fas fa-key"></i></button> <button type="button" onclick="App.deleteAgent('${a.id}')" title="Delete" class="text-red-500 p-2 hover:bg-red-50 rounded-full"><i class="fas fa-trash"></i></button> </div> </div> `).join('') || '<p class="p-4 text-center text-xs text-slate-400">No agents registered</p>'} </div> </div> <div id="settings-billing-view" class="space-y-4 hidden"> <div class="p-4 bg-emerald-50 rounded-2xl border border-emerald-100"> <label class="text-[10px] font-bold text-emerald-600 uppercase mb-1 block tracking-widest">Banking Information</label> <p class="text-[10px] text-emerald-500 mb-4 uppercase font-bold">For Direct Deposit payments</p> <div class="space-y-3"> <input type="text" name="bankName" value="${s.bank_name || ''}" placeholder="Bank Name" class="w-full h-12 px-4 rounded-xl border-none font-bold"> <input type="text" name="bankAccountName" value="${s.bank_account_name || ''}" placeholder="Account Name" class="w-full h-12 px-4 rounded-xl border-none font-bold"> <input type="text" name="bankNumber" value="${s.bank_number || ''}" placeholder="Account Number" class="w-full h-12 px-4 rounded-xl border-none font-bold"> </div> </div> </div> <button type="submit" class="w-full h-14 bg-blue-600 text-white rounded-2xl font-bold text-lg shadow-lg shadow-blue-100 hover:bg-blue-700 active:scale-95 transition-all mt-4">Save Configuration</button> </form> </div>`); }, switchSettingsTab(tab) { ['general', 'security', 'agents', 'billing'].forEach(t => { const el = document.getElementById(`settings-${t}-view`); const btn = document.getElementById(`tab-settings-${t}`); if (el) el.classList.add('hidden'); if (btn) btn.className = 'flex-1 py-2 text-sm rounded-lg font-bold text-slate-500'; }); const activeEl = document.getElementById(`settings-${tab}-view`); const activeBtn = document.getElementById(`tab-settings-${tab}`); if (activeEl) activeEl.classList.remove('hidden'); if (activeBtn) activeBtn.className = 'flex-1 py-2 text-sm rounded-lg font-bold bg-white shadow-sm'; }, async handleSaveSettings(event) { event.preventDefault(); const fd = new FormData(event.target); fd.append('action', 'update_settings'); const res = await fetch('api.php', { method: 'POST', body: fd }).then(r => r.json()); if (res.success) { alert('Settings updated successfully'); if (fd.get('adminPhone')) this.user.phone = fd.get('adminPhone'); this.closeModal(); this.loadPage('dashboard'); } else alert(res.message); }, openAddAdmin() { this.openModal(` <h3 class="text-2xl font-black mb-6">Add New Admin</h3> <form id="add-admin-form" class="space-y-4"> <div> <label class="text-[10px] font-bold text-slate-400 uppercase tracking-widest block mb-1 ml-1">Full Name</label> <input type="text" name="name" required placeholder="John Doe" class="w-full h-14 px-5 rounded-2xl border-2 border-slate-100 focus:border-blue-500 outline-none font-bold"> </div> <div> <label class="text-[10px] font-bold text-slate-400 uppercase tracking-widest block mb-1 ml-1">Username</label> <input type="text" name="username" required placeholder="admin_user" class="w-full h-14 px-5 rounded-2xl border-2 border-slate-100 focus:border-blue-500 outline-none font-bold"> </div> <div> <label class="text-[10px] font-bold text-slate-400 uppercase tracking-widest block mb-1 ml-1">Password</label> <input type="password" name="password" required placeholder="••••••••" class="w-full h-14 px-5 rounded-2xl border-2 border-slate-100 focus:border-blue-500 outline-none font-bold"> </div> <button type="submit" class="w-full h-16 bg-blue-600 text-white rounded-2xl font-black text-lg shadow-lg shadow-blue-100 hover:bg-blue-700 active:scale-95 transition-all mt-4">Create Admin Account</button> </form>`); document.getElementById('add-admin-form').onsubmit = async (e) => { e.preventDefault(); const fd = new FormData(e.target); fd.append('action', 'add_admin'); const res = await fetch('api.php', { method: 'POST', body: fd }).then(r => r.json()); if (res.success) { this.closeModal(); this.openSettingsModal(); } else alert(res.message); }; }, async deleteAdmin(id) { if(!confirm("Permanently delete this administrator?")) return; const fd = new FormData(); fd.append('action', 'delete_admin'); fd.append('id', id); const res = await fetch('api.php', { method: 'POST', body: fd }).then(r => r.json()); if(res.success) { this.openSettingsModal(); } else alert(res.message); }, openAddAgent() { this.openModal(` <h3 class="text-2xl font-black mb-6">Add New Agent</h3> <form id="add-agent-form" class="space-y-4"> <div> <label class="text-[10px] font-bold text-slate-400 uppercase tracking-widest block mb-1 ml-1">Full Name</label> <input type="text" name="name" required placeholder="Jane Doe" class="w-full h-14 px-5 rounded-2xl border-2 border-slate-100 focus:border-blue-500 outline-none font-bold"> </div> <div> <label class="text-[10px] font-bold text-slate-400 uppercase tracking-widest block mb-1 ml-1">Username</label> <input type="text" name="username" required placeholder="agent_user" class="w-full h-14 px-5 rounded-2xl border-2 border-slate-100 focus:border-blue-500 outline-none font-bold"> </div> <div> <label class="text-[10px] font-bold text-slate-400 uppercase tracking-widest block mb-1 ml-1">Password</label> <input type="password" name="password" required placeholder="••••••••" class="w-full h-14 px-5 rounded-2xl border-2 border-slate-100 focus:border-blue-500 outline-none font-bold"> </div> <button type="submit" class="w-full h-16 bg-blue-600 text-white rounded-2xl font-black text-lg shadow-lg shadow-blue-100 hover:bg-blue-700 active:scale-95 transition-all mt-4">Create Agent Account</button> </form>`); document.getElementById('add-agent-form').onsubmit = async (e) => { e.preventDefault(); const fd = new FormData(e.target); fd.append('action', 'add_agent'); const res = await fetch('api.php', { method: 'POST', body: fd }).then(r => r.json()); if (res.success) { this.closeModal(); this.openSettingsModal(); } else alert(res.message); }; }, async deleteAgent(id) { if(!confirm("Permanently delete this agent account?")) return; const fd = new FormData(); fd.append('action', 'delete_agent'); fd.append('id', id); const res = await fetch('api.php', { method: 'POST', body: fd }).then(r => r.json()); if(res.success) { this.openSettingsModal(); } else alert(res.message); }, openResetUserPasswordModal(id, name, type) { this.openModal(` <h3 class="text-2xl font-black mb-6">Reset Password</h3> <p class="text-sm text-slate-500 mb-4 text-center font-bold uppercase tracking-widest">User: ${name}</p> <form id="reset-user-pass-form" class="space-y-4"> <input type="hidden" name="id" value="${id}"> <input type="hidden" name="type" value="${type}"> <div> <label class="text-[10px] font-bold text-slate-400 uppercase tracking-widest block mb-1 ml-1">New Password</label> <input type="password" name="password" required minlength="6" class="w-full h-14 px-5 rounded-2xl border-2 border-slate-100 focus:border-blue-500 outline-none font-bold"> </div> <button type="submit" class="w-full h-16 bg-blue-600 text-white rounded-2xl font-bold text-lg shadow-lg hover:bg-blue-700 active:scale-95 transition-all mt-4">Update Password</button> </form>`); document.getElementById('reset-user-pass-form').onsubmit = async (e) => { e.preventDefault(); const fd = new FormData(e.target); const action = type === 'admin' ? 'reset_admin_password' : 'reset_agent_password'; fd.append('action', action); const res = await fetch('api.php', { method: 'POST', body: fd }).then(r => r.json()); if (res.success) { alert(res.message); this.closeModal(); this.openSettingsModal(); } else alert(res.message); }; }, async openCustomerProfileModal() { const res = await fetch(`api.php?action=get_customer_details&id=${this.user.id}`).then(r => r.json()); if (!res.success) return alert(res.message); const c = res.data; this.openModal(` <div class="relative text-center"> <h3 class="text-2xl font-black mb-1">Edit Profile</h3> <p class="text-slate-400 font-bold mb-8 uppercase text-[10px] tracking-[0.2em]">Personal Information</p> <div class="space-y-8 text-left"> <form id="cust-phone-form" class="space-y-2"> <label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">Mobile Number</label> <div class="flex gap-2"> <input type="tel" name="phone" value="${c.phone}" required class="flex-grow h-14 px-5 rounded-2xl border-2 border-slate-100 focus:border-blue-500 outline-none font-bold text-slate-700 transition-all"> <button type="submit" class="w-14 h-14 bg-blue-600 text-white rounded-2xl shadow-lg shadow-blue-100 flex items-center justify-center active:scale-90 transition-all"><i class="fas fa-check"></i></button> </div> </form> <div class="h-px bg-slate-100 w-full"></div> <form id="cust-pass-form" class="space-y-4"> <div class="space-y-2"> <label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1">New Password</label> <input type="password" name="new_password" placeholder="6+ characters" required minlength="6" class="w-full h-14 px-5 rounded-2xl border-2 border-slate-100 focus:border-blue-500 outline-none font-bold text-slate-700 transition-all"> </div> <button type="submit" class="w-full h-14 bg-slate-800 text-white rounded-2xl font-black mt-2 hover:bg-slate-900 shadow-lg active:scale-95 transition-all uppercase tracking-widest text-sm">Change Password</button> </form> </div> </div>`); document.getElementById('cust-phone-form').onsubmit = async (e) => { e.preventDefault(); const fd = new FormData(e.target); fd.append('action', 'update_customer'); const res = await fetch('api.php', { method: 'POST', body: fd }).then(r => r.json()); if (res.success) { alert('Phone number updated successfully'); this.user.phone = fd.get('phone'); this.closeModal(); this.loadPage('dashboard'); } else alert(res.message); }; document.getElementById('cust-pass-form').onsubmit = async (e) => { e.preventDefault(); const fd = new FormData(e.target); fd.append('action', 'reset_customer_password'); const res = await fetch('api.php', { method: 'POST', body: fd }).then(r => r.json()); if (res.success) { alert('Password updated successfully'); this.closeModal(); } else alert(res.message); }; }, openAgentProfileModal() { this.openModal(` <h3 class="text-2xl font-black mb-6">My Profile</h3> <form id="agent-profile-form" class="space-y-4"> <div> <label class="text-[10px] font-bold text-slate-400 uppercase tracking-widest block mb-1">Phone Number</label> <input type="tel" name="phone" value="${this.user.phone || ''}" required class="w-full h-12 px-4 rounded-xl border bg-slate-50 font-bold"> </div> <div> <label class="text-[10px] font-bold text-slate-400 uppercase tracking-widest block mb-1">New Password (optional)</label> <input type="password" name="password" placeholder="Leave blank to keep current" minlength="6" class="w-full h-12 px-4 rounded-xl border bg-slate-50 font-bold"> </div> <button type="submit" class="w-full h-14 bg-blue-600 text-white rounded-2xl font-bold shadow-lg hover:bg-blue-700 active:scale-95 transition-all mt-4">Save Profile</button> </form>`); document.getElementById('agent-profile-form').onsubmit = async (e) => { e.preventDefault(); const fd = new FormData(e.target); fd.append('action', 'update_self_profile'); const res = await fetch('api.php', { method: 'POST', body: fd }).then(r => r.json()); if (res.success) { alert('Profile updated'); if (fd.get('phone')) this.user.phone = fd.get('phone'); this.closeModal(); } else alert(res.message); }; }, async viewCustomer(id) { const res = await fetch(`api.php?action=get_customer_details&id=${id}`).then(r => r.json()); if(!res.success) return alert(res.message); const c = res.data; const goldenBadge = c.is_golden_citizen == 1 ? '<span class="bg-amber-100 text-amber-700 text-[10px] font-black uppercase px-2 py-0.5 rounded-md flex items-center gap-1"><i class="fas fa-crown"></i> Golden Citizen</span>' : ''; this.openModal(` <div class="space-y-6"> <div class="flex items-center justify-between"> <div class="flex items-center gap-4"> <div class="w-16 h-16 bg-blue-600 text-white rounded-2xl flex items-center justify-center font-black text-3xl shadow-lg uppercase">${c.name[0]}</div> <div> <div class="flex items-center gap-2"> <h3 class="font-black text-2xl">${c.name}</h3> ${goldenBadge} </div> <p class="text-slate-400 font-bold">${c.phone} • Meter: ${c.meter_number || 'N/A'}</p> </div> </div> <div class="flex gap-2"> <button onclick="App.openEditCustomer('${c.id}')" class="w-10 h-10 bg-slate-100 rounded-full flex items-center justify-center text-blue-600"><i class="fas fa-edit"></i></button> <button onclick="App.confirmDeleteCustomer('${c.id}', '${c.name}')" class="w-10 h-10 bg-red-50 rounded-full flex items-center justify-center text-red-600"><i class="fas fa-trash"></i></button> </div> </div> <div class="bg-slate-50 p-6 rounded-3xl flex justify-around"> <div class="text-center"><p class="text-[10px] font-bold text-slate-400 uppercase">Balance</p><p class="text-xl font-black text-red-600">$${parseFloat(c.balance).toFixed(2)}</p></div> <div class="text-center"><p class="text-[10px] font-bold text-slate-400 uppercase">Status</p><p class="text-xl font-black text-blue-600">${c.status}</p></div> </div> <div class="grid grid-cols-2 gap-3"> <button onclick="App.openReadingForm('${c.id}')" class="h-14 bg-blue-600 text-white rounded-2xl font-bold shadow-lg shadow-blue-100">Enter Reading</button> <button onclick="App.openPaymentForm('${c.id}')" class="h-14 bg-emerald-600 text-white rounded-2xl font-bold shadow-lg shadow-emerald-100 admin-only">Add Payment</button> </div> <div class="grid grid-cols-2 gap-3"> <button onclick="App.openResetPassword('${c.id}', '${c.name}', '${c.phone}')" class="h-12 bg-white border-2 border-slate-100 rounded-2xl font-bold text-sm text-slate-600 admin-only">Reset Password</button> <button onclick="App.viewQRModal('${c.id}')" class="h-12 bg-white border-2 border-slate-100 rounded-2xl font-bold text-sm text-slate-600">View QR Code</button> </div> <div class="space-y-4"> <h4 class="text-sm font-black uppercase tracking-widest text-slate-400">History Records</h4> <div class="space-y-2 max-h-40 overflow-y-auto"> ${c.readings.map(r => `<div class="p-3 bg-blue-50 rounded-xl flex justify-between text-xs font-bold"><span>Reading: ${parseInt(r.current_reading)} gal</span><span class="text-blue-600">$${parseFloat(r.amount).toFixed(2)}</span></div>`).join('')} ${c.payments.map(p => `<div class="p-3 bg-emerald-50 rounded-xl flex justify-between text-xs font-bold"><span>Payment: ${p.method}</span><span class="text-emerald-600">-$${parseFloat(p.amount).toFixed(2)}</span></div>`).join('')} </div> </div> </div>`); if(this.userType !== 'admin') document.querySelectorAll('.admin-only').forEach(el => el.style.display = 'none'); }, openEditCustomer(id) { fetch(`api.php?action=get_customer_details&id=${id}`).then(r => r.json()).then(res => { const c = res.data; this.openModal(` <h3 class="text-2xl font-black mb-6">Edit Profile</h3> <form id="edit-cust-form" class="space-y-4"> <input type="hidden" name="id" value="${c.id}"> <div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div><label class="text-[10px] font-bold text-slate-400 uppercase">Full Name</label><input type="text" name="name" value="${c.name}" required class="w-full h-12 px-4 rounded-xl border bg-slate-50 mt-1"></div> <div><label class="text-[10px] font-bold text-slate-400 uppercase">Phone Number</label><input type="tel" name="phone" value="${c.phone}" required class="w-full h-12 px-4 rounded-xl border bg-slate-50 mt-1"></div> </div> <div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div><label class="text-[10px] font-bold text-slate-400 uppercase">Meter Number</label><input type="text" name="meter_number" value="${c.meter_number || ''}" class="w-full h-12 px-4 rounded-xl border bg-slate-50 mt-1"></div> <div><label class="text-[10px] font-bold text-slate-400 uppercase">Account Status</label> <select name="status" class="w-full h-12 px-4 rounded-xl border bg-slate-50 mt-1 font-bold"> <option ${c.status==='Active'?'selected':''}>Active</option> <option ${c.status==='Overdue'?'selected':''}>Overdue</option> <option ${c.status==='Pending Disconnection'?'selected':''}>Pending Disconnection</option> <option ${c.status==='Disconnected'?'selected':''}>Disconnected</option> </select> </div> </div> <div class="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div><label class="text-[10px] font-bold text-slate-400 uppercase">Prev. Meter Reading</label><input type="number" step="1" name="prev_meter_reading" value="${parseInt(c.prev_meter_reading)}" class="w-full h-12 px-4 rounded-xl border bg-slate-50 mt-1"></div> <div><label class="text-[10px] font-bold text-slate-400 uppercase">Curr. Meter Reading</label><input type="number" step="1" name="curr_meter_reading" value="${parseInt(c.curr_meter_reading)}" class="w-full h-12 px-4 rounded-xl border bg-slate-50 mt-1"></div> </div> <div class="flex items-center gap-3 p-1"> <input type="checkbox" name="is_golden_citizen" id="edit_is_golden_citizen" value="1" class="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500" ${c.is_golden_citizen == 1 ? 'checked' : ''}> <label for="edit_is_golden_citizen" class="text-sm font-bold text-slate-700">Golden Citizen Status</label> </div> <button type="submit" class="w-full h-14 bg-blue-600 text-white rounded-2xl font-bold mt-4 shadow-lg active:scale-95 transition-all">Save Changes</button> </form>`); document.getElementById('edit-cust-form').onsubmit = async (e) => { e.preventDefault(); const fd = new FormData(e.target); fd.append('action', 'update_customer'); if (!e.target.is_golden_citizen.checked) fd.set('is_golden_citizen', '0'); const res = await fetch('api.php', { method: 'POST', body: fd }).then(r => r.json()); if (res.success) { this.closeModal(); this.viewCustomer(id); } else alert(res.message); }; }); }, async openResetPassword(id, name, phone) { if(!confirm(`Reset password for ${name}?`)) return; const fd = new FormData(); fd.append('action', 'reset_customer_password'); fd.append('id', id); const res = await fetch('api.php', { method: 'POST', body: fd }).then(r => r.json()); if(res.success) { const cleanPhone = phone.replace(/[^0-9]/g, ''); const msg = encodeURIComponent(`Hello ${name}, your new OmniFlow password is: ${res.new_password}`); const waLink = `https://wa.me/${cleanPhone}?text=${msg}`; this.openModal(` <div class="text-center space-y-6 py-4"> <div class="w-20 h-20 bg-emerald-100 text-emerald-600 rounded-full flex items-center justify-center text-3xl mx-auto"><i class="fas fa-key"></i></div> <h3 class="text-2xl font-black">Password Generated</h3> <div class="bg-slate-100 p-4 rounded-2xl border-2 border-dashed border-slate-200"> <p class="text-3xl font-mono font-black tracking-widest text-blue-600">${res.new_password}</p> </div> <div class="grid grid-cols-1 gap-3"> <button onclick="App.copyToClipboard('${res.new_password}')" class="h-12 bg-slate-800 text-white rounded-xl font-bold"><i class="fas fa-copy mr-2"></i> Copy Password</button> <a href="${waLink}" target="_blank" class="h-12 bg-[#25D366] text-white rounded-xl font-bold flex items-center justify-center gap-2"><i class="fab fa-whatsapp text-xl"></i> Send via WhatsApp</a> </div> </div>`); } }, copyToClipboard(text) { navigator.clipboard.writeText(text).then(() => alert("Password copied to clipboard")); }, viewQRModal(id) { this.openModal(` <div class="text-center space-y-6"> <h3 class="text-xl font-black">Customer QR Code</h3> <div class="bg-white p-6 rounded-3xl border shadow-inner inline-block mx-auto"> <img src="https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${id}" class="w-64 h-64"> </div> <p class="text-sm text-slate-400 font-bold">${id}</p> <div class="grid grid-cols-2 gap-3"> <button onclick="window.print()" class="h-12 bg-slate-100 rounded-xl font-bold">Print ID</button> <a href="https://api.qrserver.com/v1/create-qr-code/?size=500x500&data=${id}" download="qr_${id}.png" target="_blank" class="h-12 bg-blue-600 text-white rounded-xl font-bold flex items-center justify-center">Download</a> </div> </div>`); }, confirmDeleteCustomer(id, name) { if(confirm(`WARNING: This will permanently delete ${name} and all their history. This cannot be undone. Proceed?`)) { const fd = new FormData(); fd.append('action', 'delete_customer'); fd.append('id', id); fetch('api.php', { method: 'POST', body: fd }).then(r => r.json()).then(res => { if(res.success) { this.closeModal(); this.loadPage('customers'); } else alert(res.message); }); } }, openActionSelection() { if (this.userType === 'agent') { return this.openScanner('reading'); } this.openModal(` <div class="space-y-6 text-center py-4"> <h3 class="text-2xl font-black text-slate-800">Quick Scan Actions</h3> <p class="text-sm text-slate-500">What would you like to do after scanning?</p> <div class="grid grid-cols-1 gap-4"> <button onclick="App.openScanner('reading')" class="h-24 bg-blue-600 text-white rounded-[2rem] flex items-center gap-6 px-8 shadow-lg shadow-blue-100 hover:bg-blue-700 transition-all"> <div class="w-12 h-12 bg-white/20 rounded-2xl flex items-center justify-center text-2xl"><i class="fas fa-gauge-high"></i></div> <div class="text-left"> <p class="font-black text-lg leading-tight">Meter Reader</p> <p class="text-blue-100 text-xs">Scan & Enter Usage</p> </div> </button> <button onclick="App.openScanner('payment')" class="h-24 bg-emerald-600 text-white rounded-[2rem] flex items-center gap-6 px-8 shadow-lg shadow-emerald-100 hover:bg-emerald-700 transition-all"> <div class="w-12 h-12 bg-white/20 rounded-2xl flex items-center justify-center text-2xl"><i class="fas fa-file-invoice-dollar"></i></div> <div class="text-left"> <p class="font-black text-lg leading-tight">Easy Bill Pay</p> <p class="text-emerald-100 text-xs">Scan & Collect Payment</p> </div> </button> </div> </div> `); }, openScanner(action = null) { if (this.userType === 'agent' && action !== 'reading') { action = 'reading'; } this.scanAction = action; this.openModal(` <div class="space-y-4"> <h3 class="text-xl font-black text-center">${action === 'reading' ? 'Meter Reader' : action === 'payment' ? 'Easy Bill Pay' : 'Scanner'}</h3> <div id="scanner-reader" class="rounded-3xl overflow-hidden aspect-square bg-slate-100 border-4 border-slate-50"></div> <p class="text-center text-xs text-slate-400 font-bold">Align the customer QR code inside the box</p> </div>`); this.scanner = new Html5QrcodeScanner("scanner-reader", { fps: 15, qrbox: 250 }); this.scanner.render(async (text) => { try { await this.scanner.clear(); } catch(e) { console.warn("Scanner clear failed", e); } this.openModal('<div class="p-12 text-center flex flex-col items-center gap-4"><i class="fas fa-circle-notch fa-spin text-3xl text-blue-600"></i><p class="font-black text-[10px] text-slate-400 uppercase tracking-widest">Identifying Account...</p></div>'); const res = await fetch(`api.php?action=get_customer_details&id=${text}`).then(r => r.json()); if (res.success) { const c = res.data; const goldenBadge = c.is_golden_citizen == 1 ? '<span class="bg-amber-100 text-amber-700 text-[10px] font-black uppercase px-2 py-0.5 rounded-md flex items-center gap-1"><i class="fas fa-crown"></i> Golden Citizen</span>' : ''; this.openModal(` <div class="text-center space-y-6 py-4"> <div class="w-20 h-20 bg-blue-100 text-blue-600 rounded-3xl flex items-center justify-center text-3xl mx-auto shadow-sm"> <i class="fas fa-user-check"></i> </div> <div class="space-y-2"> <div class="flex items-center justify-center gap-2"> <h3 class="text-2xl font-black text-slate-800">${c.name}</h3> ${goldenBadge} </div> <p class="text-sm text-slate-400 font-bold uppercase tracking-widest">Meter: ${c.meter_number || 'N/A'}</p> </div> <div class="bg-slate-50 p-4 rounded-2xl border border-slate-100 text-left"> <p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">Account ID</p> <p class="font-mono font-bold text-slate-600 truncate">${c.id}</p> </div> <div class="pt-4"> <button id="btn-proceed-scan" class="w-full h-16 bg-blue-600 text-white rounded-2xl font-black text-lg shadow-lg hover:bg-blue-700 active:scale-95 transition-all"> Proceed to ${this.scanAction === 'payment' ? 'Payment' : (this.scanAction === 'reading' ? 'Usage Entry' : 'View Profile')} </button> </div> </div> `); document.getElementById('btn-proceed-scan').onclick = () => { if (this.scanAction === 'reading') this.openReadingForm(c.id); else if (this.scanAction === 'payment') this.openPaymentForm(c.id); else this.viewCustomer(c.id); }; } else { this.closeModal(); alert(res.message); } }); }, openAddCustomer() { this.openModal(` <h3 class="text-2xl font-black mb-6">New User</h3> <form id="add-cust-form" class="space-y-4"> <div><label class="text-[10px] font-bold text-slate-400 uppercase">Full Name</label><input type="text" name="name" required class="w-full h-12 px-4 rounded-xl border bg-slate-50 mt-1"></div> <div><label class="text-[10px] font-bold text-slate-400 uppercase">System Username</label><input type="text" name="username" required class="w-full h-12 px-4 rounded-xl border bg-slate-50 mt-1"></div> <div><label class="text-[10px] font-bold text-slate-400 uppercase">Phone Number</label><input type="tel" name="phone" required class="w-full h-12 px-4 rounded-xl border bg-slate-50 mt-1"></div> <div class="grid grid-cols-2 gap-4"> <div><label class="text-[10px] font-bold text-slate-400 uppercase">Meter Number</label><input type="text" name="meter_number" placeholder="#" class="w-full h-12 px-4 rounded-xl border bg-slate-50 mt-1"></div> <div><label class="text-[10px] font-bold text-slate-400 uppercase">Init. Reading</label><input type="number" step="1" name="curr_meter_reading" value="0" class="w-full h-12 px-4 rounded-xl border bg-slate-50 mt-1"></div> </div> <div class="flex items-center gap-3 p-1"> <input type="checkbox" name="is_golden_citizen" id="is_golden_citizen" value="1" class="w-5 h-5 rounded border-gray-300 text-blue-600 focus:ring-blue-500"> <label for="is_golden_citizen" class="text-sm font-bold text-slate-700">Golden Citizen Status</label> </div> <div><label class="text-[10px] font-bold text-slate-400 uppercase">Password</label><input type="password" name="password" required class="w-full h-12 px-4 rounded-xl border bg-slate-50 mt-1"></div> <button type="submit" class="w-full h-14 bg-blue-600 text-white rounded-2xl font-bold mt-4 shadow-lg active:scale-95 transition-all">Register Customer</button> </form>`); document.getElementById('add-cust-form').onsubmit = async (e) => { e.preventDefault(); const fd = new FormData(e.target); fd.append('action', 'add_customer'); const res = await fetch('api.php', { method: 'POST', body: fd }).then(r => r.json()); if (res.success) { this.closeModal(); this.loadPage('customers'); } else alert(res.message); }; }, openReadingForm(id) { this.openModal(` <h3 class="text-xl font-bold mb-6 text-center">Meter Entry</h3> <form id="reading-form" class="space-y-6"> <div class="text-center p-8 bg-blue-50 rounded-3xl border-2 border-blue-100"> <input type="number" step="1" name="reading" placeholder="0000" autofocus required class="w-full bg-transparent text-center text-5xl font-black text-blue-600 outline-none placeholder:text-blue-200"> <p class="text-[10px] font-bold text-blue-400 uppercase mt-2 tracking-widest">Enter Current Reading</p> </div> <button type="submit" class="w-full h-14 bg-blue-600 text-white rounded-2xl font-bold shadow-lg">Save Reading</button> </form>`); document.getElementById('reading-form').onsubmit = async (e) => { e.preventDefault(); const fd = new FormData(e.target); fd.append('action', 'record_reading'); fd.append('customerId', id); const res = await fetch('api.php', { method: 'POST', body: fd }).then(r => r.json()); if (res.success) { this.closeModal(); this.loadPage('dashboard'); } else alert(res.message); }; }, openPaymentForm(id) { this.openModal(` <h3 class="text-xl font-bold mb-6 text-center">Collect Payment</h3> <form id="payment-form" class="space-y-6"> <div class="text-center p-8 bg-emerald-50 rounded-3xl border-2 border-emerald-100"> <input type="number" step="0.01" name="amount" placeholder="0.00" autofocus required class="w-full bg-transparent text-center text-5xl font-black text-emerald-600 outline-none placeholder:text-emerald-200"> <p class="text-[10px] font-bold text-emerald-400 uppercase mt-2 tracking-widest">Payment Amount</p> </div> <select name="method" class="w-full h-12 px-4 rounded-xl border bg-slate-50 font-bold"><option>Cash</option><option>Bank Transfer</option><option>Cheque</option></select> <button type="submit" class="w-full h-14 bg-emerald-600 text-white rounded-2xl font-bold shadow-lg">Process Payment</button> </form>`); document.getElementById('payment-form').onsubmit = async (e) => { e.preventDefault(); const fd = new FormData(e.target); fd.append('action', 'record_payment'); fd.append('customerId', id); const res = await fetch('api.php', { method: 'POST', body: fd }).then(r => r.json()); if (res.success) { this.closeModal(); this.loadPage('dashboard'); } else alert(res.message); }; }, openModal(html) { document.getElementById('modal-inner').innerHTML = html; const overlay = document.getElementById('modal-backdrop'); overlay.classList.remove('hidden'); this.activeModal = true; setTimeout(() => { overlay.classList.replace('opacity-0', 'opacity-100'); }, 10); }, async closeModal() { const overlay = document.getElementById('modal-backdrop'); overlay.classList.replace('opacity-100', 'opacity-0'); this.activeModal = false; setTimeout(() => overlay.classList.add('hidden'), 300); if (this.scanner) { try { await this.scanner.clear(); } catch (e) { console.warn("Scanner clear failed", e); } this.scanner = null; } }, async logout() { await fetch('api.php?action=logout'); location.reload(); }, handleLogout() { this.logout(); }, switchDashboardTab(tab) { const trendBtn = document.getElementById('tab-trend'); const compBtn = document.getElementById('tab-comparison'); const trendView = document.getElementById('dashboard-trend-view'); const compView = document.getElementById('dashboard-comparison-view'); if (tab === 'trend') { trendBtn.className = 'text-sm font-bold pb-1 border-b-2 border-blue-600 text-blue-600'; compBtn.className = 'text-sm font-bold pb-1 border-b-2 border-transparent text-slate-400'; trendView.classList.remove('hidden'); compView.classList.add('hidden'); } else { compBtn.className = 'text-sm font-bold pb-1 border-b-2 border-blue-600 text-blue-600'; trendBtn.className = 'text-sm font-bold pb-1 border-b-2 border-transparent text-slate-400'; compView.classList.remove('hidden'); trendView.classList.add('hidden'); } }, initDashboardChart(data) { const ctx = document.getElementById('dashboardChart').getContext('2d'); if (this.dashboardChart) this.dashboardChart.destroy(); this.dashboardChart = new Chart(ctx, { type: 'line', data: { labels: data.map(d => d.month), datasets: [ { label: 'Revenue ($)', data: data.map(d => d.revenue), borderColor: '#2563eb', backgroundColor: 'rgba(37, 99, 235, 0.1)', fill: true, tension: 0.4 }, { label: 'Usage (gal)', data: data.map(d => d.usage), borderColor: '#94a3b8', borderDash: [5, 5], tension: 0.4 } ] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom' } }, scales: { y: { beginAtZero: true } } } }); }, initComparisonChart(data) { const ctx = document.getElementById('comparisonChart').getContext('2d'); if (window.comparisonChartInstance) window.comparisonChartInstance.destroy(); window.comparisonChartInstance = new Chart(ctx, { type: 'bar', data: { labels: [data.last.label, data.current.label], datasets: [ { label: 'Revenue ($)', data: [data.last.revenue, data.current.revenue], backgroundColor: '#2563eb', borderRadius: 8 }, { label: 'Usage (gal)', data: [data.last.usage, data.current.usage], backgroundColor: '#94a3b8', borderRadius: 8 } ] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom' } }, scales: { y: { beginAtZero: true } } } }); }, async seedDemoData() { if(!confirm("Seed database with demo usage and payment data for testing?")) return; alert("This feature would trigger a backend script to generate dummy records."); } }; window.App = App; App.init(); if ('serviceWorker' in navigator) navigator.serviceWorker.register('sw.js'); </script> </body> </html>Save