PageSourceSearch

https://web-production-89794.up.railway.app/js/pages/water-meters.js?v=87

js web-production-89794.up.railway.app collected 2026-10-03 12:15:43 UTC 18,029 bytes, 310 lines download raw bytes

1/*
2 * Anahuac RV Park — Water Meter Tracking
3 */
4
5var _waterTab = 'readings';
6var _waterSettings = {};
7
8async function loadWaterMeters() {
9  if (API.user?.role !== 'admin') {
10    document.getElementById('page-content').innerHTML = '<div class="card"><p>Admin access required.</p></div>';
11    return;
12  }
13
14  _waterSettings = await API.get('/water-meters/settings') || {};
15  var analytics = await API.get('/water-meters/analytics') || {};
16  var modeLabel = _waterSettings.evaluation_mode ? 'Evaluation' : _waterSettings.overage_only_mode ? 'Overage Only' : 'Full Billing';
17  var modeBadge = _waterSettings.evaluation_mode ? 'badge-warning' : 'badge-success';
18
19  document.getElementById('page-content').innerHTML =
20    '<div class="page-header"><h2>💧 Water Meters</h2><div class="btn-group">' +
21      '<button class="btn btn-primary" onclick="showEnterWaterReading()">+ Enter Reading</button>' +
22      '<button class="btn btn-outline" onclick="exportWaterCSV()">📥 Export CSV</button>' +
23    '</div></div>' +
24
25    // Evaluation mode warning
26    (_waterSettings.evaluation_mode ? '<div class="card" style="border-left:4px solid #f59e0b;background:#fffbeb;padding:0.75rem 1rem;margin-bottom:1rem">' +
27      '<strong style="color:#92400e">⚠️ EVALUATION MODE ACTIVE</strong>' +
28      '<span style="font-size:0.85rem;color:#92400e;margin-left:0.5rem">— Water usage is being tracked but tenants are NOT being charged. Switch billing mode in Settings when ready.</span></div>' : '') +
29
30    // Summary cards
31    '<div class="dash-top-bar" style="margin-bottom:1rem">' +
32      '<div class="dash-top-item dash-border-blue"><div class="dash-top-icon">📊</div><span class="dash-top-val">' + (analytics.readingsCount || 0) + '/' + (analytics.totalLots || 0) + '</span><span class="dash-top-label">Lots Read This Month</span></div>' +
33      '<div class="dash-top-item dash-border-blue"><div class="dash-top-icon">💧</div><span class="dash-top-val">' + formatWaterGallons(analytics.totalGallons || 0) + '</span><span class="dash-top-label">Total Gallons (Month)</span></div>' +
34      '<div class="dash-top-item"><div class="dash-top-icon">📈</div><span class="dash-top-val">' + formatWaterGallons(analytics.avgPerLot || 0) + '</span><span class="dash-top-label">Avg per Lot</span></div>' +
35      '<div class="dash-top-item"><div class="dash-top-icon">⚙️</div><span class="dash-top-val badge ' + modeBadge + '" style="font-size:0.7rem;padding:2px 6px">' + modeLabel + '</span><span class="dash-top-label">Billing Mode</span></div>' +
36    '</div>' +
37
38    // Tabs
39    '<div style="display:flex;gap:0;margin-bottom:1rem;border-bottom:2px solid var(--gray-200)">' +
40      '<button class="btn btn-sm" id="water-tab-readings" onclick="switchWaterTab(\'readings\')" style="border-radius:8px 8px 0 0;border:1px solid var(--gray-200);border-bottom:none;font-weight:600">📋 Readings</button>' +
41      '<button class="btn btn-sm" id="water-tab-settings" onclick="switchWaterTab(\'settings\')" style="border-radius:8px 8px 0 0;border:1px solid var(--gray-200);border-bottom:none;font-weight:600;margin-left:-1px">⚙️ Settings</button>' +
42    '</div>' +
43    '<div id="water-tab-content"></div>';
44
45  switchWaterTab(_waterTab);
46}
47
48function formatWaterGallons(g) {
49  if (g >= 1000) return Math.round(g).toLocaleString();
50  return String(Math.round(g));
51}
52
53function switchWaterTab(tab) {
54  _waterTab = tab;
55  document.getElementById('water-tab-readings')?.classList.toggle('btn-primary', tab === 'readings');
56  document.getElementById('water-tab-readings')?.classList.toggle('btn-outline', tab !== 'readings');
57  document.getElementById('water-tab-settings')?.classList.toggle('btn-primary', tab === 'settings');
58  document.getElementById('water-tab-settings')?.classList.toggle('btn-outline', tab !== 'settings');
59  if (tab === 'readings') loadWaterReadingsTab();
60  else loadWaterSettingsTab();
61}
62
63async function loadWaterReadingsTab() {
64  var el = document.getElementById('water-tab-content');
65  if (!el) return;
66  el.innerHTML = '<div style="text-align:center;padding:2rem"><div class="loading-spinner"></div></div>';
67
68  var month = new Date().toISOString().slice(0, 7);
69  try {
70    var readings = await API.get('/water-meters/readings?month=' + month);
71    if (!readings || !readings.length) {
72      el.innerHTML = '<div class="card" style="text-align:center;padding:2rem;color:#78716c">' +
73        '<p style="font-size:1.2rem;margin-bottom:0.5rem">💧</p>' +
74        '<p>No water readings entered this month yet.</p>' +
75        '<button class="btn btn-primary" style="margin-top:0.75rem" onclick="showEnterWaterReading()">+ Enter First Reading</button></div>';
76      return;
77    }
78
79    var total = readings.reduce(function(s, r) { return s + (r.gallons_used || 0); }, 0);
80    el.innerHTML = '<div class="card"><div class="table-container"><table>' +
81      '<thead><tr><th>Lot</th><th>Guest</th><th>Date</th><th>Prev</th><th>Current</th><th>Gallons</th><th>Est. Charge</th><th>Photo</th><th>Actions</th></tr></thead><tbody>' +
82      readings.map(function(r) {
83        var name = ((r.first_name || '') + ' ' + (r.last_name || '')).trim() || '—';
84        var photoLink = r.has_photo ? '<a href="/api/water-meters/readings/' + r.id + '/photo" target="_blank" style="color:var(--brand-primary);font-size:0.75rem">📷 View</a>' : '<span style="color:#a8a29e">—</span>';
85        return '<tr>' +
86          '<td><strong>' + escapeHtml(r.lot_id) + '</strong></td>' +
87          '<td>' + escapeHtml(name) + '</td>' +
88          '<td>' + formatDate(r.reading_date) + '</td>' +
89          '<td>' + Number(r.previous_reading).toLocaleString() + '</td>' +
90          '<td>' + Number(r.current_reading).toLocaleString() + '</td>' +
91          '<td><strong>' + Number(r.gallons_used).toLocaleString() + '</strong></td>' +
92          '<td>' + (_waterSettings.evaluation_mode ? '<span style="color:#a8a29e">N/A</span>' : formatMoney(r.estimated_charge)) + '</td>' +
93          '<td>' + photoLink + '</td>' +
94          '<td class="btn-group"><button class="btn btn-sm btn-outline" onclick="editWaterReading(' + r.id + ')">Edit</button>' +
95          '<button class="btn btn-sm btn-danger" onclick="deleteWaterReading(' + r.id + ')">Del</button></td></tr>';
96      }).join('') +
97      '<tr style="border-top:2px solid #111"><td colspan="5"><strong>Total (' + readings.length + ' readings)</strong></td>' +
98        '<td><strong>' + total.toLocaleString() + '</strong></td><td colspan="3"></td></tr>' +
99      '</tbody></table></div></div>';
100  } catch { el.innerHTML = '<div class="card" style="color:#dc2626">Failed to load readings</div>'; }
101}
102
103async function showEnterWaterReading() {
104  var lots = await API.get('/lots');
105  var occupiedLots = (lots || []).filter(function(l) { return l.status === 'occupied'; });
106  var today = new Date().toISOString().split('T')[0];
107
108  showModal('💧 Enter Water Reading',
109    '<form id="water-reading-form">' +
110    '<div class="form-row"><div class="form-group"><label>Lot *</label>' +
111      '<select name="lot_id" id="water-lot-select" required onchange="onWaterLotChange(this.value)">' +
112        '<option value="">Select lot...</option>' +
113        occupiedLots.map(function(l) {
114          var tName = l.first_name ? l.first_name + ' ' + l.last_name : '';
115          return '<option value="' + l.id + '">' + l.id + (tName ? ' — ' + tName : '') + '</option>';
116        }).join('') +
117      '</select></div>' +
118    '<div class="form-group"><label>Reading Date</label><input name="reading_date" type="date" value="' + today + '" required></div></div>' +
119    '<div class="form-row">' +
120      '<div class="form-group"><label>Previous Reading</label><input name="previous_reading" id="water-prev" type="number" step="1" value="0" readonly style="background:#f5f5f4"></div>' +
121      '<div class="form-group"><label>Current Reading *</label><input name="current_reading" id="water-curr" type="number" step="1" required oninput="calcWaterUsage()"></div>' +
122    '</div>' +
123    '<div id="water-calc" style="background:#f0fdf4;border:1px solid #a7f3d0;border-radius:8px;padding:0.6rem 0.75rem;margin-bottom:0.75rem;font-size:0.88rem;display:none">' +
124      '<div>💧 Gallons used: <strong id="water-calc-gallons">0</strong></div>' +
125      '<div>
125💰 Est. charge: <strong id="water-calc-charge">$0.00</strong></div>' +
126    '</div>' +
127    '<div class="form-group"><label>Notes</label><input name="notes" placeholder="Optional notes"></div>' +
128    '<div class="form-group"><label>Meter Photo <span style="color:#a8a29e">(optional)</span></label>' +
129      '<input type="file" id="water-photo-input" accept="image/*" capture="environment">' +
130      '<div id="water-photo-preview" style="margin-top:0.4rem"></div>' +
131    '</div>' +
132    '<button type="submit" class="btn btn-primary btn-full">Save Reading</button></form>'
133  );
134
135  setTimeout(function() {
136    var form = document.getElementById('water-reading-form');
137    if (form) form.addEventListener('submit', submitWaterReading);
138  }, 50);
139}
140
141async function onWaterLotChange(lotId) {
142  if (!lotId) return;
143  // Get previous reading for this lot
144  try {
145    var readings = await API.get('/water-meters/readings?lot_id=' + lotId);
146    var prev = document.getElementById('water-prev');
147    if (prev && readings && readings.length) {
148      prev.value = readings[0].current_reading || 0;
149    } else if (prev) {
150      prev.value = 0;
151    }
152    calcWaterUsage();
153  } catch {}
154}
155
156function calcWaterUsage() {
157  var prev = Number(document.getElementById('water-prev')?.value) || 0;
158  var curr = Number(document.getElementById('water-curr')?.value) || 0;
159  var gallons = Math.max(0, curr - prev);
160  var calcDiv = document.getElementById('water-calc');
161  if (calcDiv) calcDiv.style.display = gallons > 0 ? '' : 'none';
162  var gEl = document.getElementById('water-calc-gallons');
163  if (gEl) gEl.textContent = gallons.toLocaleString();
164
165  var charge = 0;
166  if (!_waterSettings.evaluation_mode) {
167    var rate = Number(_waterSettings.rate_per_gallon) || 0;
168    var fee = Math.min(Number(_waterSettings.service_fee_percent) || 0, 9);
169    var allowance = _waterSettings.monthly_allowance_gallons ? Number(_waterSettings.monthly_allowance_gallons) : null;
170    var billable = gallons;
171    if (_waterSettings.overage_only_mode && allowance) billable = Math.max(0, gallons - allowance);
172    charge = billable * rate * (1 + fee / 100);
173  }
174  var cEl = document.getElementById('water-calc-charge');
175  if (cEl) cEl.textContent = _waterSettings.evaluation_mode ? 'N/A (Evaluation Mode)' : '$' + charge.toFixed(2);
176}
177
178async function submitWaterReading(e) {
179  e.preventDefault();
180  var fd = new FormData(e.target);
181  var data = {
182    lot_id: fd.get('lot_id'),
183    reading_date: fd.get('reading_date'),
184    previous_reading: Number(fd.get('previous_reading')) || 0,
185    current_reading: Number(fd.get('current_reading')) || 0,
186    notes: fd.get('notes') || '',
187  };
188
189  try {
190    var result = await API.post('/water-meters/readings', data);
191
192    // Upload photo if selected
193    var fileInput = document.getElementById('water-photo-input');
194    if (fileInput && fileInput.files && fileInput.files[0] && result.id) {
195      var b64 = await compressImage(fileInput.files[0]);
196      API.post('/water-meters/readings/' + result.id + '/photo', { data: b64 }).catch(function() {});
197    }
198
199    closeModal();
200    showStatusToast('✅', 'Water reading saved — ' + (result.gallons_used || 0).toLocaleString() + ' gallons');
201    loadWaterMeters();
202  } catch (err) {
203    alert('Failed to save: ' + (err.message || 'unknown'));
204  }
205}
206
207async function editWaterReading(id) {
208  var readings = await API.get('/water-meters/readings');
209  var r = (readings || []).find(function(x) { return x.id === id; });
210  if (!r) return;
211  showModal('Edit Water Reading',
212    '<form id="water-edit-form">' +
213    '<div class="form-group"><label>Lot</label><input value="' + escapeHtml(r.lot_id) + '" readonly style="background:#f5f5f4"></div>' +
214    '<div class="form-row"><div class="form-group"><label>Date</label><input name="reading_date" type="date" value="' + r.reading_date + '"></div>' +
215    '<div class="form-group"><label>Previous</label><input name="previous_reading" type="number" value="' + r.previous_reading + '"></div></div>' +
216    '<div class="form-group"><label>Current Reading</label><input name="current_reading" type="number" value="' + r.current_reading + '"></div>' +
217    '<div class="form-group"><label>Notes</label><input name="notes" value="' + escapeHtml(r.notes || '') + '"></div>' +
218    '<input type="hidden" name="lot_id" value="' + r.lot_id + '">' +
219    '<button type="submit" class="btn btn-primary btn-full">Update</button></form>'
220  );
221  setTimeout(function() {
222    var form = document.getElementById('water-edit-form');
223    if (form) form.addEventListener('submit', async function(ev) {
224      ev.preventDefault();
225      var data = Object.fromEntries(new FormData(ev.target));
226      await API.put('/water-meters/readings/' + id, data);
227      closeModal();
228      showStatusToast('✅', 'Reading updated');
229      loadWaterMeters();
230    });
231  }, 50);
232}
233
234async function deleteWaterReading(id) {
235  if (!confirm('Delete this water reading?')) return;
236  await API.del('/water-meters/readings/' + id);
237  loadWaterReadingsTab();
238}
239
240function loadWaterSettingsTab() {
241  var el = document.getElementById('water-tab-content');
242  if (!el) return;
243  var s = _waterSettings;
244  var evalChecked = s.evaluation_mode ? 'checked' : '';
245  var overageChecked = !s.evaluation_mode && s.overage_only_mode ? 'checked' : '';
246  var fullChecked = !s.evaluation_mode && !s.overage_only_mode ? 'checked' : '';
247
248  el.innerHTML =
249    '<div class="card" style="max-width:640px">' +
250    '<h3 style="margin-bottom:1rem">Water Billing Settings</h3>' +
251    '<form id="water-settings-form">' +
252    '<div class="form-group"><label>Rate per gallon ($)</label>' +
253      '<input name="rate_per_gallon" type="number" step="0.0001" value="' + (s.rate_per_gallon || 0) + '">' +
254      '<small style="color:#78716c">Your current rate from city water bill (per gallon)</small></div>' +
255    '<div class="form-group"><label>Service fee %</label>' +
256      '<input name="service_fee_percent" type="number" step="0.1" max="9" value="' + (s.service_fee_percent || 9) + '">' +
257      '<small style="color:#78716c">Texas law allows maximum 9%</small></div>' +
258    '<div class="form-group"><label>Monthly gallon allowance per lot</label>' +
259      '<input name="monthly_allowance_gallons" type="number" step="1" value="' + (s.monthly_allowance_gallons || '') + '" placeholder="Leave blank for none">' +
260      '<small style="color:#78716c">Leave blank for no allowance</small></div>' +
261
262    '<div class="form-group" style="margin-top:1.25rem"><label><strong>Billing Mode</strong></label>' +
263      '<div style="display:flex;flex-direction:column;gap:0.75rem;margin-top:0.5rem">' +
264        '<label style="display:flex;align-items:flex-start;gap:0.5rem;cursor:pointer;padding:0.6rem;border:1.5px solid ' + (s.evaluation_mode ? '#f59e0b' : '#e5e7eb') + ';border-radius:8px;background:' + (s.evaluation_mode ? '#fffbeb' : '#fff') + '">' +
265          '<input type="radio" name="billing_mode" value="evaluation" ' + evalChecked + ' style="margin-top:3px">' +
266          '<div><strong>Evaluation Mode</strong><br><span style="font-size:0.82rem;color:#78716c">Tracking usage only — tenants are NOT charged anything</span></div></label>' +
267        '<label style="display:flex;align-items:flex-start;gap:0.5rem;cursor:pointer;padding:0.6rem;border:1.5px solid ' + (!s.evaluation_mode && s.overage_only_mode ? '#1a5c32' : '#e5e7eb') + ';border-radius:8px">' +
268          '<input type="radio" name="billing_mode" value="overage" ' + overageChecked + ' style="margin-top:3px">' +
269          '<div><strong>Overage Only Mode</strong><br><span style="font-size:0.82rem;color:#78716c">Only charge tenants who exceed monthly allowance</span></div></label>' +
270        '<label style="display:flex;align-items:flex-start;gap:0.5rem;cursor:pointer;padding:0.6rem;border:1.5px solid ' + (!s.evaluation_mode && !s.overage_only_mode ? '#1a5c32' : '#e5e7eb') + ';border-radius:8px">' +
271          '<input type="radio" name="billing_mode" value="full" ' + fullChecked + ' style="margin-top:3px">' +
272          '<div><strong>Full Billing Mode</strong><br><span style="font-size:0.82rem;color:#78716c">Charge all usage like electric metering</span></div></label>' +
273      '</div></div>' +
274    '<button type="submit" class="btn btn-primary btn-full" style="margin-top:1.25rem">Save Settings</button></form></div>';
275
276  setTimeout(function() {
277    var form = document.getElementById('water-settings-form');
278    if (form) form.addEventListener('submit', async function(ev) {
279      ev.preventDefault();
280      var fd = new FormData(ev.target);
281      var mode = fd.get('billing_mode');
282      await API.put('/water-meters/settings', {
283        rate_per_gallon: fd.get('rate_per_gallon'),
284        service_fee_percent: fd.get('service_fee_percent'),
285        monthly_allowance_gallons: fd.get('monthly_allowance_gallons') || null,
286        billing_enabled: mode !== 'evaluation' ? 1 : 0,
287        evaluation_mode: mode === 'evaluation' ? 1 : 0,
288        overage_only_mode: mode === 'overage' ? 1 : 0,
289      });
290      _waterSettings = await API.get('/water-meters/settings') || {};
291      closeModal();
292      showStatusToast('✅', 'Water settings saved');
293      loadWaterMeters();
294    });
295  }, 50);
296}
297
298async function exportWaterCSV() {
299  var month = new Date().toISOString().slice(0, 7);
300  try {
301    var res = await fetch('/api/water-meters/export/csv?month=' + month, { headers: { 'Authorization': 'Bearer ' + API.token } });
302    if (!res.ok) throw new Error('Export failed');
303    var blob = await res.blob();
304    var url = URL.createObjectURL(blob);
305    var a = document.createElement('a');
306    a.href = url; a.download = 'water-readings-' + month + '.csv';
307    document.body.appendChild(a); a.click(); a.remove();
308    URL.revokeObjectURL(url);
309  } catch (err) { alert('Export failed: ' + (err.message || 'unknown')); }
310}

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.