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>
This commit is contained in:
Daniel Kovalevich
2026-03-30 15:43:27 -04:00
parent 260f7c4928
commit f917fb8014
35 changed files with 4964 additions and 193 deletions

346
server/src/seeds/seed.js Normal file
View File

@@ -0,0 +1,346 @@
// =====================================================================
// Database Seed Script
// Reads mock data from the existing frontend JS files and inserts
// into PostgreSQL via Prisma.
// Run with: npm run db:seed
// =====================================================================
const fs = require('fs');
const path = require('path');
const prisma = require('../config/db');
// Load the mock data files by evaluating them in a controlled context
function loadMockData(filename) {
const filepath = path.join(__dirname, '..', '..', '..', filename);
const code = fs.readFileSync(filepath, 'utf-8');
const context = {};
// Execute the JS to populate the const variables
const fn = new Function(code + '\nreturn { ' +
'MOCK_STATE_REGULATIONS: typeof MOCK_STATE_REGULATIONS !== "undefined" ? MOCK_STATE_REGULATIONS : undefined,' +
'MOCK_LOAD_BOARD: typeof MOCK_LOAD_BOARD !== "undefined" ? MOCK_LOAD_BOARD : undefined,' +
'MOCK_ESCORT_OPERATORS: typeof MOCK_ESCORT_OPERATORS !== "undefined" ? MOCK_ESCORT_OPERATORS : undefined,' +
'MOCK_STATE_CONTACTS: typeof MOCK_STATE_CONTACTS !== "undefined" ? MOCK_STATE_CONTACTS : undefined,' +
'MOCK_STATE_EQUIPMENT: typeof MOCK_STATE_EQUIPMENT !== "undefined" ? MOCK_STATE_EQUIPMENT : undefined,' +
'MOCK_TRUCK_STOPS: typeof MOCK_TRUCK_STOPS !== "undefined" ? MOCK_TRUCK_STOPS : undefined,' +
'MOCK_BRIDGE_CLEARANCES: typeof MOCK_BRIDGE_CLEARANCES !== "undefined" ? MOCK_BRIDGE_CLEARANCES : undefined,' +
'MOCK_WEIGH_STATIONS: typeof MOCK_WEIGH_STATIONS !== "undefined" ? MOCK_WEIGH_STATIONS : undefined,' +
'MOCK_ROUTE_CONDITIONS: typeof MOCK_ROUTE_CONDITIONS !== "undefined" ? MOCK_ROUTE_CONDITIONS : undefined,' +
'MOCK_WEATHER_ALERTS: typeof MOCK_WEATHER_ALERTS !== "undefined" ? MOCK_WEATHER_ALERTS : undefined,' +
'MOCK_SEASONAL_RESTRICTIONS: typeof MOCK_SEASONAL_RESTRICTIONS !== "undefined" ? MOCK_SEASONAL_RESTRICTIONS : undefined,' +
'MOCK_DOCUMENTS: typeof MOCK_DOCUMENTS !== "undefined" ? MOCK_DOCUMENTS : undefined' +
' };');
return fn();
}
async function seed() {
console.log('🌱 Seeding database...\n');
// Load mock data from both files
const data1 = loadMockData('mock-data.js');
const data2 = loadMockData('mock-data-extended.js');
// Clear existing data in reverse dependency order
console.log(' 🗑️ Clearing existing data...');
await prisma.contribution.deleteMany();
await prisma.document.deleteMany();
await prisma.order.deleteMany();
await prisma.load.deleteMany();
await prisma.escortProfile.deleteMany();
await prisma.user.deleteMany();
await prisma.alert.deleteMany();
await prisma.weighStation.deleteMany();
await prisma.bridge.deleteMany();
await prisma.truckStop.deleteMany();
await prisma.seasonalRestriction.deleteMany();
await prisma.contact.deleteMany();
await prisma.equipmentRequirement.deleteMany();
await prisma.regulation.deleteMany();
await prisma.state.deleteMany();
// ---------------------------------------------------------------
// 1. States + Regulations (from MOCK_STATE_REGULATIONS)
// ---------------------------------------------------------------
console.log(' 📍 Seeding states and regulations...');
const stateMap = {}; // abbr -> state.id
for (const reg of data1.MOCK_STATE_REGULATIONS) {
const state = await prisma.state.create({
data: {
name: reg.name,
abbr: reg.abbr,
lat: reg.lat,
lng: reg.lng,
regulation: {
create: {
permitWidth: reg.permitWidth,
permitHeight: reg.permitHeight,
permitLength: reg.permitLength,
permitWeight: reg.permitWeight,
escortWidth: reg.escortWidth,
escortHeight: reg.escortHeight,
escortLength: reg.escortLength,
escortWeight: reg.escortWeight,
travelRestrictions: reg.travel,
holidays: reg.holidays,
agency: reg.agency,
url: reg.url,
notes: reg.notes,
},
},
},
});
stateMap[reg.abbr] = state.id;
}
console.log(`${Object.keys(stateMap).length} states with regulations`);
// ---------------------------------------------------------------
// 2. Contacts (from MOCK_STATE_CONTACTS)
// ---------------------------------------------------------------
console.log(' 📞 Seeding contacts...');
let contactCount = 0;
if (data2.MOCK_STATE_CONTACTS) {
for (const [abbr, contact] of Object.entries(data2.MOCK_STATE_CONTACTS)) {
if (!stateMap[abbr]) continue;
await prisma.contact.create({
data: {
stateId: stateMap[abbr],
permitPhone: contact.permit,
policePhone: contact.police,
email: contact.email,
hours: contact.hours,
portalUrl: contact.portal,
},
});
contactCount++;
}
}
console.log(`${contactCount} state contacts`);
// ---------------------------------------------------------------
// 3. Equipment Requirements (from MOCK_STATE_EQUIPMENT)
// ---------------------------------------------------------------
console.log(' 🔧 Seeding equipment requirements...');
let equipCount = 0;
if (data2.MOCK_STATE_EQUIPMENT) {
for (const [abbr, equip] of Object.entries(data2.MOCK_STATE_EQUIPMENT)) {
if (!stateMap[abbr]) continue;
if (equip.escort) {
await prisma.equipmentRequirement.create({
data: {
stateId: stateMap[abbr],
type: 'escort',
certification: equip.escort.certification || '',
vehicle: equip.escort.vehicle || '',
signs: equip.escort.signs || '',
lights: equip.escort.lights || '',
heightPole: equip.escort.heightPole || '',
flags: equip.escort.flags || '',
safetyGear: equip.escort.safety || '',
communication: equip.escort.communication || '',
},
});
equipCount++;
}
if (equip.carrier) {
await prisma.equipmentRequirement.create({
data: {
stateId: stateMap[abbr],
type: 'carrier',
signs: equip.carrier.signs || '',
flags: equip.carrier.flags || '',
lights: equip.carrier.lights || '',
safetyGear: [
equip.carrier.cones ? `Cones: ${equip.carrier.cones}` : '',
equip.carrier.fireExtinguisher ? `Fire ext: ${equip.carrier.fireExtinguisher}` : '',
equip.carrier.triangles ? `Triangles: ${equip.carrier.triangles}` : '',
equip.carrier.flares ? `Flares: ${equip.carrier.flares}` : '',
equip.carrier.firstAid ? `First aid: ${equip.carrier.firstAid}` : '',
].filter(Boolean).join('; '),
},
});
equipCount++;
}
}
}
console.log(`${equipCount} equipment requirements`);
// ---------------------------------------------------------------
// 4. Truck Stops (from MOCK_TRUCK_STOPS)
// ---------------------------------------------------------------
console.log(' ⛽ Seeding truck stops...');
let tsCount = 0;
if (data2.MOCK_TRUCK_STOPS) {
for (const ts of data2.MOCK_TRUCK_STOPS) {
const stateAbbr = ts.location?.state;
const stateId = stateMap[stateAbbr];
if (!stateId) continue;
await prisma.truckStop.create({
data: {
stateId,
name: ts.name,
lat: ts.location.lat,
lng: ts.location.lng,
address: `${ts.location.city}, ${ts.location.state}`,
hasOversizeParking: ts.oversizeFriendly || false,
entranceHeight: ts.entranceHeight || '',
entranceWidth: ts.entranceWidth || '',
lotSqFt: ts.lotSize ? parseInt(String(ts.lotSize).replace(/[^0-9]/g, '')) || null : null,
facilities: ts.facilities || [],
phone: '',
},
});
tsCount++;
}
}
console.log(`${tsCount} truck stops`);
// ---------------------------------------------------------------
// 5. Bridge Clearances (from MOCK_BRIDGE_CLEARANCES)
// ---------------------------------------------------------------
console.log(' 🌉 Seeding bridges...');
let bridgeCount = 0;
if (data2.MOCK_BRIDGE_CLEARANCES) {
for (const b of data2.MOCK_BRIDGE_CLEARANCES) {
const stateAbbr = b.location?.state;
const stateId = stateMap[stateAbbr];
if (!stateId) continue;
await prisma.bridge.create({
data: {
stateId,
name: `${b.type || 'Bridge'} at ${b.location.desc || b.route}`,
lat: b.location.lat,
lng: b.location.lng,
route: b.route,
heightClearance: parseFloat(String(b.clearanceHeight).replace(/[^0-9.]/g, '')) || 0,
widthClearance: b.clearanceWidth ? parseFloat(String(b.clearanceWidth).replace(/[^0-9.]/g, '')) : null,
weightLimit: b.weightLimit ? parseFloat(String(b.weightLimit).replace(/[^0-9.]/g, '')) : null,
},
});
bridgeCount++;
}
}
console.log(`${bridgeCount} bridges`);
// ---------------------------------------------------------------
// 6. Weigh Stations (from MOCK_WEIGH_STATIONS)
// ---------------------------------------------------------------
console.log(' ⚖️ Seeding weigh stations...');
let wsCount = 0;
if (data2.MOCK_WEIGH_STATIONS) {
for (const ws of data2.MOCK_WEIGH_STATIONS) {
const stateAbbr = ws.location?.state;
const stateId = stateMap[stateAbbr];
if (!stateId) continue;
await prisma.weighStation.create({
data: {
stateId,
name: ws.name,
lat: ws.location.lat,
lng: ws.location.lng,
direction: ws.direction || '',
route: ws.route || '',
prePass: ws.prePass || false,
hours: ws.hours || '',
currentStatus: ws.currentStatus || 'unknown',
},
});
wsCount++;
}
}
console.log(`${wsCount} weigh stations`);
// ---------------------------------------------------------------
// 7. Alerts — Route Conditions + Weather (from MOCK_ROUTE_CONDITIONS + MOCK_WEATHER_ALERTS)
// ---------------------------------------------------------------
console.log(' 🚨 Seeding alerts...');
let alertCount = 0;
if (data2.MOCK_ROUTE_CONDITIONS) {
for (const rc of data2.MOCK_ROUTE_CONDITIONS) {
const stateAbbr = rc.location?.state;
const stateId = stateMap[stateAbbr];
if (!stateId) continue;
await prisma.alert.create({
data: {
stateId,
type: rc.type || 'construction',
route: rc.route || '',
description: rc.description || '',
severity: rc.severity || 'info',
startsAt: rc.startDate ? new Date(rc.startDate) : new Date(),
endsAt: rc.endDate ? new Date(rc.endDate) : null,
},
});
alertCount++;
}
}
if (data2.MOCK_WEATHER_ALERTS) {
for (const wa of data2.MOCK_WEATHER_ALERTS) {
// Weather alerts may not have a state directly, try to match from region
const stateAbbr = wa.state || (wa.routes?.[0] ? 'TX' : null); // fallback
const stateId = stateAbbr ? stateMap[stateAbbr] : Object.values(stateMap)[0];
if (!stateId) continue;
await prisma.alert.create({
data: {
stateId,
type: wa.type || 'weather',
route: (wa.routes || []).join(', '),
description: wa.description || '',
severity: wa.severity || 'info',
startsAt: wa.validFrom ? new Date(wa.validFrom) : new Date(),
endsAt: wa.validTo ? new Date(wa.validTo) : null,
},
});
alertCount++;
}
}
console.log(`${alertCount} alerts`);
// ---------------------------------------------------------------
// 8. Seasonal Restrictions (from MOCK_SEASONAL_RESTRICTIONS)
// ---------------------------------------------------------------
console.log(' 📅 Seeding seasonal restrictions...');
let seasonCount = 0;
if (data2.MOCK_SEASONAL_RESTRICTIONS) {
for (const sr of data2.MOCK_SEASONAL_RESTRICTIONS) {
const stateAbbr = sr.state;
const stateId = stateMap[stateAbbr];
if (!stateId) continue;
await prisma.seasonalRestriction.create({
data: {
stateId,
name: sr.title || sr.type,
type: sr.type || 'other',
startMonth: sr.startMonth || 1,
endMonth: sr.endMonth || 12,
description: sr.description || '',
},
});
seasonCount++;
}
}
console.log(`${seasonCount} seasonal restrictions`);
// ---------------------------------------------------------------
// Summary
// ---------------------------------------------------------------
console.log('\n🎉 Seed complete!\n');
}
seed()
.catch((err) => {
console.error('❌ Seed failed:', err);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});