Files
PilotEdge/weighstations.html
Daniel Kovalevich f917fb8014 Add Node.js/Express backend with PostgreSQL and wire frontend to API
- Server: Express.js with 13 API route files (auth, regulations, contacts,
  calendar, truck stops, bridges, weigh stations, alerts, load board,
  escort locator, orders, documents, contributions)
- Database: PostgreSQL with Prisma ORM, 15 models covering all modules
- Auth: JWT + bcrypt with role-based access control (driver/carrier/escort/admin)
- Geospatial: Haversine distance filtering on truck stops, bridges, escorts
- Seed script: Imports all existing mock data (51 states, contacts, equipment,
  truck stops, bridges, weigh stations, alerts, seasonal restrictions)
- Frontend: All 10 data-driven pages now fetch from /api instead of mock-data.js
- API client (api.js): Compatibility layer that transforms API responses to
  match existing frontend rendering code, minimizing page-level changes

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-30 15:43:27 -04:00

270 lines
12 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Weigh Stations | PilotEdge</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<style>
#map { height: 450px; width: 100%; border-radius: 0.75rem; }
.leaflet-popup-content { margin: 8px 12px; }
.leaflet-popup-content-wrapper { border-radius: 12px; }
.toast-msg {
position: fixed; bottom: 1.5rem; right: 1.5rem; z-index: 9999;
background: #0f172a; color: #fbbf24; padding: 0.75rem 1.25rem;
border-radius: 0.75rem; font-weight: 600; box-shadow: 0 8px 30px rgba(0,0,0,.25);
animation: slideUp .3s ease-out;
}
@keyframes slideUp { from { opacity:0; transform:translateY(12px); } to { opacity:1; transform:translateY(0); } }
</style>
</head>
<body class="bg-slate-50 min-h-screen flex flex-col">
<div id="main-nav"></div>
<div id="poc-banner"></div>
<!-- Page Header -->
<section class="bg-slate-900 text-white pt-24 pb-12 px-4">
<div class="max-w-7xl mx-auto">
<h1 class="text-3xl md:text-4xl font-bold mb-3">Weigh Stations &amp; Inspection Stations</h1>
<p class="text-lg text-gray-400 max-w-3xl">Live crowd-sourced status — see which stations are open or closed right now.</p>
</div>
</section>
<!-- Stats Bar -->
<section class="max-w-7xl mx-auto px-4 -mt-6 w-full relative z-10">
<div id="stats-bar" class="grid grid-cols-3 gap-4"></div>
</section>
<!-- Map Section -->
<section class="max-w-7xl mx-auto px-4 py-8 w-full">
<div class="bg-white rounded-2xl shadow-lg p-4">
<div id="map"></div>
</div>
</section>
<!-- Filter Bar -->
<section class="max-w-7xl mx-auto px-4 pb-6 w-full">
<div class="bg-white rounded-2xl shadow-lg p-6">
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-4">
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Search</label>
<input id="filter-search" type="text" placeholder="Name, state, or route…"
class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">Status</label>
<select id="filter-status"
class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
<option value="all">All Stations</option>
<option value="open">Open Only</option>
<option value="closed">Closed Only</option>
</select>
</div>
<div>
<label class="block text-sm font-semibold text-slate-700 mb-1">PrePass</label>
<select id="filter-prepass"
class="w-full border border-slate-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-amber-400 focus:border-amber-400 outline-none">
<option value="all">Any</option>
<option value="yes">PrePass Accepted</option>
<option value="no">No PrePass</option>
</select>
</div>
<div class="flex items-end">
<button id="btn-clear-filters"
class="w-full bg-slate-200 hover:bg-slate-300 text-slate-700 font-semibold px-4 py-2 rounded-lg text-sm transition-colors">
Clear Filters
</button>
</div>
</div>
</div>
</section>
<!-- Station Cards -->
<section class="max-w-7xl mx-auto px-4 pb-16 w-full">
<div id="station-list" class="grid md:grid-cols-2 xl:grid-cols-3 gap-6"></div>
<p id="no-results" class="hidden text-center text-slate-500 py-12 text-lg">No stations match your filters.</p>
</section>
<div id="main-footer"></div>
<script src="api.js"></script>
<script src="nav.js"></script>
<script>
renderNav('weighstations');
renderBanner();
renderFooter();
(async () => {
const MOCK_WEIGH_STATIONS = await PilotEdge.getWeighStations();
// ── Local mutable copy of station data ──
const stations = JSON.parse(JSON.stringify(MOCK_WEIGH_STATIONS));
// ── Map setup ──
const map = L.map('map').setView([39.5, -98.35], 4);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap contributors', maxZoom: 18
}).addTo(map);
// Store markers keyed by station id so flag buttons can update them
const markerMap = {};
function markerColor(status) {
return status === 'open' ? '#22c55e' : '#ef4444';
}
function buildPopup(s) {
const badge = s.currentStatus === 'open'
? '<span style="background:#22c55e;color:#fff;padding:2px 8px;border-radius:9999px;font-size:12px;font-weight:700;">OPEN</span>'
: '<span style="background:#ef4444;color:#fff;padding:2px 8px;border-radius:9999px;font-size:12px;font-weight:700;">CLOSED</span>';
return `<div style="min-width:200px;">
<strong style="font-size:14px;">${s.name}</strong><br>
<span style="color:#64748b;font-size:12px;">${s.route} · ${s.location.city}, ${s.location.state}</span><br>
<div style="margin:6px 0;">${badge}</div>
<span style="font-size:12px;"><strong>Hours:</strong> ${s.hours}</span><br>
<span style="font-size:11px;color:#94a3b8;">Last flagged: ${fmtTime(s.lastFlagged)} by ${s.flaggedBy}</span>
</div>`;
}
function addMarker(s) {
const m = L.circleMarker([s.location.lat, s.location.lng], {
radius: 9, fillColor: markerColor(s.currentStatus), color: '#fff',
weight: 2, fillOpacity: 0.9
}).addTo(map).bindPopup(buildPopup(s));
markerMap[s.id] = m;
}
stations.forEach(addMarker);
// ── Helpers ──
function fmtTime(iso) {
const d = new Date(iso);
return d.toLocaleString('en-US', { month:'short', day:'numeric', hour:'numeric', minute:'2-digit' });
}
function showToast(msg) {
const el = document.createElement('div');
el.className = 'toast-msg';
el.textContent = msg;
document.body.appendChild(el);
setTimeout(() => el.remove(), 2500);
}
// ── Stats bar ──
function renderStats() {
const open = stations.filter(s => s.currentStatus === 'open').length;
const closed = stations.length - open;
document.getElementById('stats-bar').innerHTML = [
{ label:'Open Stations', value:open, color:'bg-green-500' },
{ label:'Closed Stations', value:closed, color:'bg-red-500' },
{ label:'Total Stations', value:stations.length, color:'bg-amber-500' }
].map(s => `
<div class="bg-white rounded-2xl shadow-lg p-5 flex items-center gap-4">
<div class="${s.color} text-white rounded-xl w-12 h-12 flex items-center justify-center text-xl font-bold">${s.value}</div>
<span class="text-slate-700 font-semibold text-sm">${s.label}</span>
</div>
`).join('');
}
renderStats();
// ── Card rendering ──
function renderCards(list) {
const container = document.getElementById('station-list');
const noResults = document.getElementById('no-results');
if (!list.length) { container.innerHTML = ''; noResults.classList.remove('hidden'); return; }
noResults.classList.add('hidden');
container.innerHTML = list.map(s => {
const isOpen = s.currentStatus === 'open';
const badge = isOpen
? '<span class="inline-block bg-green-500 text-white text-xs font-bold px-3 py-1 rounded-full uppercase tracking-wide">Open</span>'
: '<span class="inline-block bg-red-500 text-white text-xs font-bold px-3 py-1 rounded-full uppercase tracking-wide">Closed</span>';
const prePassBadge = s.prePass
? '<span class="inline-block bg-blue-100 text-blue-700 text-xs font-semibold px-2.5 py-0.5 rounded-full">✓ PrePass</span>'
: '<span class="inline-block bg-slate-100 text-slate-500 text-xs font-semibold px-2.5 py-0.5 rounded-full">No PrePass</span>';
return `
<div class="bg-white rounded-2xl shadow-lg p-6 flex flex-col justify-between" id="card-${s.id}">
<div>
<div class="flex items-start justify-between gap-2 mb-2">
<h3 class="text-lg font-bold text-slate-900 leading-tight">${s.name}</h3>
${badge}
</div>
<p class="text-sm text-slate-500 mb-1">${s.route} · ${s.location.city}, ${s.location.state}</p>
<p class="text-xs text-slate-400 mb-3">Last reported: ${fmtTime(s.lastFlagged)} by <span class="font-medium text-slate-600">${s.flaggedBy}</span></p>
<div class="flex flex-wrap gap-2 mb-3">
<span class="inline-block bg-slate-100 text-slate-600 text-xs font-semibold px-2.5 py-0.5 rounded-full">🕒 ${s.hours}</span>
${prePassBadge}
</div>
<p class="text-sm text-slate-600 leading-relaxed">${s.notes}</p>
</div>
<div class="flex gap-2 mt-5">
<button onclick="flagStation('${s.id}','open')"
class="flex-1 text-sm font-semibold px-3 py-2 rounded-lg transition-colors ${isOpen ? 'bg-green-500 text-white' : 'bg-green-100 text-green-700 hover:bg-green-200'}">
🟢 Flag as Open
</button>
<button onclick="flagStation('${s.id}','closed')"
class="flex-1 text-sm font-semibold px-3 py-2 rounded-lg transition-colors ${!isOpen ? 'bg-red-500 text-white' : 'bg-red-100 text-red-700 hover:bg-red-200'}">
🔴 Flag as Closed
</button>
</div>
</div>`;
}).join('');
}
// ── Flag a station ──
function flagStation(id, newStatus) {
const s = stations.find(st => st.id === id);
if (!s) return;
s.currentStatus = newStatus;
s.lastFlagged = new Date().toISOString();
s.flaggedBy = 'You';
// Update map marker
const marker = markerMap[id];
if (marker) {
marker.setStyle({ fillColor: markerColor(newStatus) });
marker.setPopupContent(buildPopup(s));
}
renderStats();
applyFilters();
showToast('Thanks for reporting! Status updated.');
}
// ── Filtering ──
function applyFilters() {
const q = document.getElementById('filter-search').value.trim().toLowerCase();
const status = document.getElementById('filter-status').value;
const prepass = document.getElementById('filter-prepass').value;
const filtered = stations.filter(s => {
if (q && !(s.name.toLowerCase().includes(q) || s.location.state.toLowerCase().includes(q) || s.route.toLowerCase().includes(q) || s.location.city.toLowerCase().includes(q))) return false;
if (status !== 'all' && s.currentStatus !== status) return false;
if (prepass === 'yes' && !s.prePass) return false;
if (prepass === 'no' && s.prePass) return false;
return true;
});
renderCards(filtered);
}
document.getElementById('filter-search').addEventListener('input', applyFilters);
document.getElementById('filter-status').addEventListener('change', applyFilters);
document.getElementById('filter-prepass').addEventListener('change', applyFilters);
document.getElementById('btn-clear-filters').addEventListener('click', () => {
document.getElementById('filter-search').value = '';
document.getElementById('filter-status').value = 'all';
document.getElementById('filter-prepass').value = 'all';
applyFilters();
});
// Initial render
applyFilters();
})();
</script>
</body>
</html>