1/* 2 * Anahuac RV Park Management System 3 * Copyright © 2026 Anahuac RV Park LLC. All Rights Reserved. 4 * Proprietary and Confidential. 5 * Unauthorized copying, distribution, or use is strictly prohibited. 6 */ 7let _electricMonths = 6; 8 9async function loadElectric() { 10 const data = await API.get(`/electric/analytics?months=${_electricMonths}`); 11 if (!data) return; 12 13 const trend = data.lastMonth.totalKwh > 0 14 ? ((data.currentMonth.totalKwh - data.lastMonth.totalKwh) / data.lastMonth.totalKwh * 100).toFixed(0) 15 : 0; 16 const trendIcon = trend >= 0 ? 'â' : 'â'; 17 const trendColor = trend <= 0 ? '#16a34a' : '#dc2626'; // Lower usage = green 18 19 document.getElementById('page-content').innerHTML = ` 20 ${helpPanel('electric')} 21 <div class="page-header"> 22 <h2>â¡ Electric Analytics</h2> 23 <div class="btn-group"> 24 <button class="btn btn-sm ${_electricMonths===3?'btn-primary':'btn-outline'}" onclick="_electricMonths=3;loadElectric()">3 Mo</button> 25 <button class="btn btn-sm ${_electricMonths===6?'btn-primary':'btn-outline'}" onclick="_electricMonths=6;loadElectric()">6 Mo</button> 26 <button class="btn btn-sm ${_electricMonths===12?'btn-primary':'btn-outline'}" onclick="_electricMonths=12;loadElectric()">12 Mo</button> 27 </div> 28 </div> 29 30 <div class="dash-top-bar"> 31 <div class="dash-top-item dash-border-blue"> 32 <div class="dash-top-icon">ð</div> 33 <span class="dash-top-val">${data.avgKwh.toLocaleString()}</span> 34 <span class="dash-top-label">Avg KWH/Lot</span> 35 </div> 36 <div class="dash-top-item dash-border-red"> 37 <div class="dash-top-icon">
37ð´</div> 38 <span class="dash-top-val">${data.highest ? data.highest.kwh.toLocaleString() : 'â'}</span> 39 <span class="dash-top-label">Highest: ${data.highest ? data.highest.lot_id + ' ' + (data.highest.first_name || '') : 'â'}</span> 40 </div> 41 <div class="dash-top-item dash-border-green"> 42 <div class="dash-top-icon">ð¢</div> 43 <span class="dash-top-val">${data.lowest ? data.lowest.kwh.toLocaleString() : 'â'}</span> 44 <span class="dash-top-label">Lowest: ${data.lowest ? data.lowest.lot_id + ' ' + (data.lowest.first_name || '') : 'â'}</span> 45 </div> 46 <div class="dash-top-item dash-border-purple"> 47 <div class="dash-top-icon">ð°</div> 48 <span class="dash-top-val">$${data.avgCharge.toFixed(2)}</span> 49 <span class="dash-top-label">Avg Bill/Lot</span> 50 </div> 51 </div> 52 53 ${data.vacantUsage && data.vacantUsage.length > 0 ? ` 54 <div class="card" style="margin-bottom:1rem;border:2px solid #dc2626;background:#fef2f2"> 55 <h3 style="color:#dc2626;margin-bottom:0.5rem">â Vacant Lot Usage Detected</h3> 56 <p style="font-size:0.85rem;color:#78716c;margin-bottom:0.5rem">These lots have no active tenant but show electric consumption this month. Possible unauthorized tap or miswired pedestal.</p> 57 <table class="billing-table" style="font-size:0.85rem"> 58 <thead><tr><th>Lot</th><th>kWh</th><th>Charge</th><th>Reading</th></tr></thead> 59 <tbody> 60 ${data.vacantUsage.map(v => `<tr style="background:#fef2f2"> 61 <td><strong>${v.lot_id}</strong></td> 62 <td style="color:#dc2626;font-weight:700">${v.kwh} kWh</td> 63 <td style="color:#dc2626;font-weight:700">$${Number(v.charge).toFixed(2)}</td> 64 <td>${v.previous_reading.toLocaleString()} â ${v.current_reading.toLocaleString()}</td> 65 </tr>`).join('')} 66 </tbody> 67 </table> 68 </div>` : ''} 69 70 <div class="card" style="margin-bottom:1rem"> 71 <h3>Usage by Lot (Last ${_electricMonths} Months)</h3> 72 <div style="height:350px"><canvas id="electricChart"></canvas></div> 73 </div> 74 75 <div class="card" style="margin-bottom:1rem"> 76 <h3>Per-Lot Detail</h3> 77 <div class="form-row"> 78 <div class="form-group"> 79 <label>Select Lot</label> 80 <select id="electric-lot-select" onchange="loadLotDetail(this.value)"> 81 <option value="">Choose a lot...</option> 82 ${data.allLots.map(l => `<option value="${l}">${l}</option>`).join('')} 83 </select> 84 </div> 85 </div> 86 <div id="lot-detail-content"></div> 87 </div> 88 `; 89 90 window._electricData = data; 91 setTimeout(() => renderElectricChart(data), 100); 92} 93 94function renderElectricChart(data) { 95 const ctx = document.getElementById('electricChart')?.getContext('2d'); 96 if (!ctx || typeof Chart === 'undefined') return; 97 98 const colors = ['#1a5c32','#dc2626','#f59e0b','#0284c7','#7c3aed','#06b6d4','#f97316','#ec4899','#84cc16','#6366f1', 99 '#14b8a6','#e11d48','#a855f7','#0ea5e9','#facc15','#fb923c','#4ade80','#c084fc','#38bdf8','#fbbf24']; 100 const labels = data.history.map(h => h.label); 101 const datasets = data.allLots.slice(0, 20).map((lot, i) => ({ 102 label: lot, 103 data: data.history.map(h => { const r = h.readings.find(x => x.lot_id === lot); return r ? r.kwh : 0; }), 104 borderColor: colors[i % colors.length], 105 backgroundColor: colors[i % colors.length] + '20', 106 borderWidth: 2, 107 tension: 0.3, 108 pointRadius: 3, 109 fill: false, 110 })); 111 112 new Chart(ctx, { 113 type: 'line', 114 data: { labels, datasets }, 115 options: { 116 responsive: true, maintainAspectRatio: false, 117 animation: { duration: 800 }, 118 plugins: { 119 legend: { position: 'bottom', labels: { boxWidth: 10, font: { size: 10 }, padding: 8 } }, 120 tooltip: { callbacks: { label: ctx => `${ctx.dataset.label}: ${ctx.raw.toLocaleString()} kWh` } }, 121 }, 122 scales: { y: { beginAtZero: true, ticks: { callback: v => v.toLocaleString() + ' kWh' } } }, 123 interaction: { mode: 'index', intersect: false }, 124 }, 125 }); 126} 127 128async function loadLotDetail(lotId) { 129 const container = document.getElementById('lot-detail-content'); 130 if (!lotId) { container.innerHTML = ''; return; } 131 132 const data = await API.get(`/electric/lot/${lotId}`); 133 if (!data) return; 134 135 container.innerHTML = ` 136 <div class="stats-grid" style="margin:1rem 0"> 137 <div class="stat-card"><div class="stat-value">
137${data.avgKwh.toLocaleString()}</div><div class="stat-label">Avg KWH/Month</div></div> 138 <div class="stat-card success"><div class="stat-value">$${data.avgCharge.toFixed(2)}</div><div class="stat-label">Avg Bill/Month</div></div> 139 <div class="stat-card danger"><div class="stat-value">${data.highestMonth ? data.highestMonth.kwh.toLocaleString() : 'â'}</div><div class="stat-label">Highest: ${data.highestMonth?.ym || 'â'}</div></div> 140 <div class="stat-card"><div class="stat-value">${data.lowestMonth ? data.lowestMonth.kwh.toLocaleString() : 'â'}</div><div class="stat-label">Lowest: ${data.lowestMonth?.ym || 'â'}</div></div> 141 </div> 142 <div style="height:250px;margin-bottom:1rem"><canvas id="lotChart"></canvas></div> 143 <div class="btn-group" style="margin:0.75rem 0" id="lot-export-buttons"> 144 <button class="btn btn-primary" onclick="downloadLotPdf('${lotId}')">ð Download PDF</button> 145 <button class="btn btn-success" onclick="textLotToTenant('${lotId}')">ð± Text to Guest</button> 146 </div> 147 <div class="table-container" style="margin-top:1rem;max-height:250px" id="lot-readings-table"> 148 <table> 149 <thead><tr><th>Date</th><th>Previous</th><th>Current</th><th>KWH</th><th>Rate</th><th>Charge</th></tr></thead> 150 <tbody> 151 ${data.readings.slice(0, 12).map(r => ` 152 <tr><td>${formatDate(r.reading_date)}</td><td>${r.previous_reading.toLocaleString()}</td><td>${r.current_reading.toLocaleString()}</td><td><strong>${r.kwh_used.toLocaleString()}</strong></td><td>$${Number(r.rate_per_kwh).toFixed(2)}</td><td><strong>${formatMoney(r.electric_charge)}</strong></td></tr> 153 `).join('')} 154 </tbody> 155 </table> 156 </div> 157 `; 158 159 window._lotDetailData = data; 160 window._lotDetailLotId = lotId; 161 162 // Render lot bar chart 163 setTimeout(() => { 164 const ctx = document.getElementById('lotChart')?.getContext('2d'); 165 if (!ctx || typeof Chart === 'undefined') return; 166 new Chart(ctx, { 167 type: 'bar', 168 data: { 169 labels: data.monthly.map(m => { const d = new Date(m.ym + '-15'); return d.toLocaleString('default', { month: 'short', year: '2-digit' }); }), 170 datasets: [{ label: 'KWH Used', data: data.monthly.map(m => m.kwh), backgroundColor: '#1a5c3280', borderColor: '#1a5c32', borderWidth: 1, borderRadius: 4 }], 171 }, 172 options: { 173 responsive: true, maintainAspectRatio: false, 174 plugins: { legend: { display: false } }, 175 scales: { y: { beginAtZero: true, ticks: { callback: v => v.toLocaleString() } } }, 176 }, 177 }); 178 }, 100); 179} 180 181async function downloadLotPdf(lotId) { 182 const data = window._lotDetailData; 183 if (!data) { alert('No data loaded. Select a lot first.'); return; } 184 185 const tenant = data.readings[0] ? `${data.readings[0].first_name || ''} ${data.readings[0].last_name || ''}`.trim() : 'Unknown'; 186 const now = new Date(); 187 const monthName = now.toLocaleString('default', { month: 'long', year: 'numeric' }); 188 const dateRange = data.readings.length 189 ? `${formatDate(data.readings[data.readings.length - 1].reading_date)} â ${formatDate(data.readings[0].reading_date)}` 190 : monthName; 191 192 // Capture the chart as an image 193 const chartCanvas = document.getElementById('lotChart'); 194 const chartImg = chartCanvas ? chartCanvas.toDataURL('image/png') : ''; 195 196 const readingsRows = data.readings.slice(0, 12).map(r => ` 197 <tr> 198 <td>${formatDate(r.reading_date)}</td> 199 <td style="text-align:right">${r.previous_reading.toLocaleString()}</td> 200 <td style="text-align:right">${r.current_reading.toLocaleString()}</td> 201 <td style="text-align:right"><strong>${r.kwh_used.toLocaleString()}</strong></td> 202 <td style="text-align:right">$${Number(r.rate_per_kwh).toFixed(2)}</td> 203 <td style="text-align:right"><strong>${formatMoney(r.electric_charge)}</strong></td> 204 </tr> 205 `).join(''); 206 207 const html = ` 208 <div id="electric-pdf-content" style="font-family:'Inter',sans-serif;color:#1c1917;padding:0.5rem;max-width:700px"> 209 <div style="display:flex;justify-content:space-between;align-items:center;border-bottom:3px solid #1a5c32;padding-bottom:0.75rem;margin-bottom:1rem"> 210 <div> 211 <h1 style="font-size:1.25rem;
211color:#1a5c32;margin:0">Anahuac RV Park</h1> 212 <p style="font-size:0.75rem;color:#78716c;margin:0.15rem 0 0">Electric Usage Report</p> 213 </div> 214 <div style="text-align:right;font-size:0.75rem;color:#78716c"> 215 <div>1003 Davis Ave, Anahuac, TX 77514</div> 216 <div>409-267-6603</div> 217 </div> 218 </div> 219 220 <div style="display:flex;justify-content:space-between;margin-bottom:1rem"> 221 <div> 222 <p style="margin:0;font-size:0.85rem"><strong>Guest:</strong> ${escapeHtml(tenant)}</p> 223 <p style="margin:0.15rem 0 0;font-size:0.85rem"><strong>Lot:</strong> ${escapeHtml(lotId)}</p> 224 </div> 225 <div style="text-align:right"> 226 <p style="margin:0;font-size:0.85rem"><strong>Period:</strong> ${escapeHtml(dateRange)}</p> 227 <p style="margin:0.15rem 0 0;font-size:0.85rem"><strong>Generated:</strong> ${now.toLocaleDateString()}</p> 228 </div> 229 </div> 230 231 <div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:0.6rem;margin-bottom:1.25rem"> 232 <div style="background:#f0fdf4;border:1px solid #dcfce7;border-radius:8px;padding:0.6rem;text-align:center"> 233 <div style="font-size:1.1rem;font-weight:800;color:#1a5c32">${data.avgKwh.toLocaleString()}</div> 234 <div style="font-size:0.65rem;color:#78716c;text-transform:uppercase;letter-spacing:0.05em">Avg KWH/Mo</div> 235 </div> 236 <div style="background:#f0fdf4;border:1px solid #dcfce7;border-radius:8px;padding:0.6rem;text-align:center"> 237 <div style="font-size:1.1rem;font-weight:800;color:#1a5c32">$${data.avgCharge.toFixed(2)}</div> 238 <div style="font-size:0.65rem;color:#78716c;text-transform:uppercase;letter-spacing:0.05em">Avg Bill/Mo</div> 239 </div> 240 <div style="background:#fef3c7;border:1px solid #fde68a;border-radius:8px;padding:0.6rem;text-align:center"> 241 <div style="font-size:1.1rem;font-weight:800;color:#92400e">${data.highestMonth ? data.highestMonth.kwh.toLocaleString() : 'â'}</div> 242 <div style="font-size:0.65rem;color:#78716c;text-transform:uppercase;letter-spacing:0.05em">Highest (${data.highestMonth?.ym || 'â'})</div> 243 </div> 244 <div style="background:#e0f2fe;border:1px solid #bae6fd;border-radius:8px;padding:0.6rem;text-align:center"> 245 <div style="font-size:1.1rem;font-weight:800;color:#0284c7">${data.lowestMonth ? data.lowestMonth.kwh.toLocaleString() : 'â'}</div> 246 <div style="font-size:0.65rem;color:#78716c;text-transform:uppercase;letter-spacing:0.05em">Lowest (${data.lowestMonth?.ym || 'â'})</div> 247 </div> 248 </div> 249 250 ${chartImg ? `<div style="margin-bottom:1.25rem"><img src="${chartImg}" style="width:100%;border-radius:8px;border:1px solid #e7e5e4"></div>` : ''} 251 252 <h3 style="font-size:0.85rem;color:#1a5c32;margin-bottom:0.5rem;border-left:3px solid #f59e0b;padding-left:0.5rem">Meter Readings</h3> 253 <table style="width:100%;border-collapse:collapse;font-size:0.78rem;margin-bottom:1.25rem"> 254 <thead> 255 <tr style="background:#fafaf9;border-bottom:2px solid #e7e5e4"> 256 <th style="padding:0.4rem 0.35rem;text-align:left;font-size:0.7rem;text-transform:uppercase;letter-spacing:0.05em;color:#57534e">Date</th> 257 <th style="padding:0.4rem 0.35rem;text-align:right;font-size:0.7rem;text-transform:uppercase;letter-spacing:0.05em;color:#57534e">Previous</th> 258 <th style="padding:0.4rem 0.35rem;text-align:right;font-size:0.7rem;text-transform:uppercase;letter-spacing:0.05em;color:#57534e">Current</th> 259 <th style="padding:0.4rem 0.35rem;text-align:right;font-size:0.7rem;text-transform:uppercase;letter-spacing:0.05em;color:#57534e">KWH</th> 260 <th style="padding:0.4rem 0.35rem;text-align:right;font-size:0.7rem;text-transform:uppercase;letter-spacing:0.05em;color:#57534e">Rate</th> 261 <th style="padding:0.4rem 0.35rem;text-align:right;font-size:0.7rem;text-transform:uppercase;letter-spacing:0.05em;color:#57534e">Charge</th> 262 </tr> 263 </thead> 264 <tbody>${readingsRows}</tbody> 265 </table> 266 267 <div style="border-top:2px solid #1a5c32;padding-top:0.75rem;display:flex;justify-content:space-between;font-size:0.7rem;color:#78716c">
268 <span>Questions? Call 409-267-6603</span> 269 <span>© 2026 Anahuac RV Park LLC</span> 270 </div> 271 </div> 272 `; 273 274 // Create a temporary element for html2pdf 275 const wrapper = document.createElement('div'); 276 wrapper.innerHTML = html; 277 document.body.appendChild(wrapper); 278 279 const lotClean = lotId.replace(/[^a-zA-Z0-9]/g, ''); 280 const monthClean = now.toLocaleString('default', { month: 'long', year: 'numeric' }).replace(/\s/g, ''); 281 const filename = `Electric_Report_Lot${lotClean}_${monthClean}.pdf`; 282 283 try { 284 await html2pdf().set({ 285 margin: [0.4, 0.5, 0.4, 0.5], 286 filename, 287 image: { type: 'jpeg', quality: 0.95 }, 288 html2canvas: { scale: 2, useCORS: true }, 289 jsPDF: { unit: 'in', format: 'letter', orientation: 'portrait' }, 290 }).from(wrapper.firstElementChild).save(); 291 showStatusToast('â ', 'PDF downloaded!'); 292 } catch (err) { 293 alert('PDF generation failed: ' + (err.message || 'unknown')); 294 } finally { 295 document.body.removeChild(wrapper); 296 } 297} 298 299async function textLotToTenant(lotId) { 300 const data = window._lotDetailData; 301 if (!data) { alert('No data loaded. Select a lot first.'); return; } 302 303 // Find the tenant for this lot 304 let tenants; 305 try { tenants = await API.get('/tenants'); } catch { tenants = []; } 306 const tenant = (tenants || []).find(t => t.lot_id === lotId); 307 const tenantName = tenant ? `${tenant.first_name} ${tenant.last_name}` : 'Guest'; 308 const tenantPhone = tenant?.phone || '(no phone on file)'; 309 310 const lastReading = data.readings[0]; 311 const lastKwh = lastReading ? lastReading.kwh_used.toLocaleString() : 'â'; 312 const lastCharge = lastReading ? '$' + Number(lastReading.electric_charge).toFixed(2) : 'â'; 313 314 const highLabel = data.highestMonth ? `${data.highestMonth.ym} - ${data.highestMonth.kwh.toLocaleString()} KWH ($${data.highestMonth.charge.toFixed(2)})` : 'N/A'; 315 const lowLabel = data.lowestMonth ? `${data.lowestMonth.ym} - ${data.lowestMonth.kwh.toLocaleString()} KWH ($${data.lowestMonth.charge.toFixed(2)})` : 'N/A'; 316 317 const message = `Hi ${tenant ? tenant.first_name : 'there'}! Here is your electric usage summary for Lot ${lotId}:\n` + 318 `⢠Average usage: ${data.avgKwh.toLocaleString()} KWH/month ($${data.avgCharge.toFixed(2)})\n` + 319 `⢠Last month: ${lastKwh} KWH (${lastCharge})\n` + 320 `⢠Highest month: ${highLabel}\n` + 321 `⢠Lowest month: ${lowLabel}\n` + 322 `Questions? Call us at 409-267-6603\n` + 323 `â Anahuac RV Park`; 324 325 showModal('ð± Text Electric Summary', ` 326 <div style="margin-bottom:1rem"> 327 <div style="display:flex;align-items:center;gap:0.75rem;padding:0.75rem;background:var(--gray-50);border-radius:var(--radius);border:1px solid var(--gray-200)"> 328 <div style="width:40px;height:40px;border-radius:50%;background:linear-gradient(135deg,#1a5c32,#2d8a52);display:flex;align-items:center;justify-content:center;color:#fff;font-weight:800;font-size:0.85rem;flex-shrink:0">${escapeHtml(tenantName.split(' ').map(w => w[0] || '').join('').slice(0, 2))}</div> 329 <div> 330 <div style="font-weight:600;color:var(--gray-900)">${escapeHtml(tenantName)}</div> 331 <div style="font-size:0.82rem;color:var(--gray-500)">ð ${escapeHtml(tenantPhone)} · Lot ${escapeHtml(lotId)}</div> 332 </div> 333 </div> 334 </div> 335 <div class="form-group"> 336 <label>Message</label> 337 <textarea id="sms-electric-text" rows="10" style="font-size:0.85rem">${escapeHtml(message)}</textarea> 338 </div> 339 ${!tenant?.phone ? '<p style="color:var(--danger);font-size:0.85rem;margin-bottom:0.75rem">â ï¸ No phone number on file for this tenant. SMS cannot be sent.</p>' : ''} 340 <button class="btn btn-success btn-full" onclick="sendElectricSms('${lotId}', ${tenant ? tenant.id : 'null'})" ${!tenant?.phone ? 'disabled' : ''}>ð± Send SMS</button> 341 `); 342} 343 344async function sendElectricSms(lotId, tenantId) { 345 if (!tenantId) { alert('No tenant found for this lot.'); return; } 346 const text = document.getElementById('sms-electric-text')?.value; 347 if (!text) return; 348 try {
349 const btn = document.querySelector('#modal-body .btn-success'); 350 if (btn) { btn.disabled = true; btn.textContent = 'Sending...'; } 351 await API.post('/messages', { 352 tenant_id: tenantId, 353 subject: 'Electric Usage Summary', 354 body: text, 355 message_type: 'notice', 356 delivery_method: 'sms', 357 is_broadcast: false, 358 }); 359 closeModal(); 360 showStatusToast('â ', 'Electric summary texted to tenant!'); 361 } catch (err) { 362 alert('SMS failed: ' + (err.message || 'unknown')); 363 const btn = document.querySelector('#modal-body .btn-success'); 364 if (btn) { btn.disabled = false; btn.textContent = 'ð± Send SMS'; } 365 } 366}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.