PageSourceSearch

https://web-production-89794.up.railway.app/js/pages/electric.js

js web-production-89794.up.railway.app collected 2026-10-03 12:16:12 UTC 19,371 bytes, 366 lines download raw bytes

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>&copy; 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)} &nbsp;·&nbsp; 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.