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.