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

View File

@@ -0,0 +1,308 @@
-- CreateTable
CREATE TABLE "states" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"abbr" VARCHAR(2) NOT NULL,
"lat" DOUBLE PRECISION NOT NULL,
"lng" DOUBLE PRECISION NOT NULL,
CONSTRAINT "states_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "regulations" (
"id" TEXT NOT NULL,
"stateId" TEXT NOT NULL,
"permitWidth" TEXT NOT NULL,
"permitHeight" TEXT NOT NULL,
"permitLength" TEXT NOT NULL,
"permitWeight" TEXT NOT NULL,
"escortWidth" TEXT NOT NULL,
"escortHeight" TEXT NOT NULL,
"escortLength" TEXT NOT NULL,
"escortWeight" TEXT NOT NULL,
"travelRestrictions" TEXT NOT NULL,
"holidays" TEXT NOT NULL,
"agency" TEXT NOT NULL,
"url" TEXT NOT NULL,
"notes" TEXT NOT NULL,
CONSTRAINT "regulations_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "equipment_requirements" (
"id" TEXT NOT NULL,
"stateId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"certification" TEXT NOT NULL DEFAULT '',
"vehicle" TEXT NOT NULL DEFAULT '',
"signs" TEXT NOT NULL DEFAULT '',
"lights" TEXT NOT NULL DEFAULT '',
"heightPole" TEXT NOT NULL DEFAULT '',
"flags" TEXT NOT NULL DEFAULT '',
"safetyGear" TEXT NOT NULL DEFAULT '',
"communication" TEXT NOT NULL DEFAULT '',
CONSTRAINT "equipment_requirements_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "contacts" (
"id" TEXT NOT NULL,
"stateId" TEXT NOT NULL,
"permitPhone" TEXT NOT NULL,
"policePhone" TEXT NOT NULL,
"email" TEXT NOT NULL,
"hours" TEXT NOT NULL,
"portalUrl" TEXT NOT NULL,
CONSTRAINT "contacts_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "seasonal_restrictions" (
"id" TEXT NOT NULL,
"stateId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"type" TEXT NOT NULL,
"startMonth" INTEGER NOT NULL,
"endMonth" INTEGER NOT NULL,
"description" TEXT NOT NULL,
CONSTRAINT "seasonal_restrictions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "truck_stops" (
"id" TEXT NOT NULL,
"stateId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"lat" DOUBLE PRECISION NOT NULL,
"lng" DOUBLE PRECISION NOT NULL,
"address" TEXT NOT NULL,
"hasOversizeParking" BOOLEAN NOT NULL DEFAULT false,
"entranceHeight" TEXT NOT NULL DEFAULT '',
"entranceWidth" TEXT NOT NULL DEFAULT '',
"lotSqFt" INTEGER,
"facilities" JSONB NOT NULL DEFAULT '[]',
"satelliteUrl" TEXT NOT NULL DEFAULT '',
"phone" TEXT NOT NULL DEFAULT '',
CONSTRAINT "truck_stops_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "bridges" (
"id" TEXT NOT NULL,
"stateId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"lat" DOUBLE PRECISION NOT NULL,
"lng" DOUBLE PRECISION NOT NULL,
"route" TEXT NOT NULL,
"heightClearance" DOUBLE PRECISION NOT NULL,
"widthClearance" DOUBLE PRECISION,
"weightLimit" DOUBLE PRECISION,
"lastVerified" TIMESTAMP(3),
CONSTRAINT "bridges_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "weigh_stations" (
"id" TEXT NOT NULL,
"stateId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"lat" DOUBLE PRECISION NOT NULL,
"lng" DOUBLE PRECISION NOT NULL,
"direction" TEXT NOT NULL,
"route" TEXT NOT NULL,
"prePass" BOOLEAN NOT NULL DEFAULT false,
"hours" TEXT NOT NULL DEFAULT '',
"currentStatus" TEXT NOT NULL DEFAULT 'unknown',
"lastStatusUpdate" TIMESTAMP(3),
"lastStatusUserId" TEXT,
CONSTRAINT "weigh_stations_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"passwordHash" TEXT NOT NULL,
"name" TEXT NOT NULL,
"role" TEXT NOT NULL DEFAULT 'driver',
"tier" TEXT NOT NULL DEFAULT 'free',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "escort_profiles" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"lat" DOUBLE PRECISION NOT NULL,
"lng" DOUBLE PRECISION NOT NULL,
"radiusMiles" INTEGER NOT NULL DEFAULT 100,
"certifications" JSONB NOT NULL DEFAULT '[]',
"vehicleType" TEXT NOT NULL DEFAULT '',
"availability" TEXT NOT NULL DEFAULT 'available',
"rating" DOUBLE PRECISION NOT NULL DEFAULT 0,
"ratingCount" INTEGER NOT NULL DEFAULT 0,
"phone" TEXT NOT NULL DEFAULT '',
"bio" TEXT NOT NULL DEFAULT '',
CONSTRAINT "escort_profiles_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "loads" (
"id" TEXT NOT NULL,
"posterId" TEXT NOT NULL,
"origin" TEXT NOT NULL,
"destination" TEXT NOT NULL,
"pickupDate" TIMESTAMP(3) NOT NULL,
"width" TEXT NOT NULL,
"height" TEXT NOT NULL,
"length" TEXT NOT NULL,
"weight" TEXT NOT NULL,
"description" TEXT NOT NULL DEFAULT '',
"escortsNeeded" INTEGER NOT NULL DEFAULT 1,
"status" TEXT NOT NULL DEFAULT 'open',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "loads_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "orders" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"origin" TEXT NOT NULL,
"destination" TEXT NOT NULL,
"pickupDate" TIMESTAMP(3) NOT NULL,
"width" TEXT NOT NULL DEFAULT '',
"height" TEXT NOT NULL DEFAULT '',
"length" TEXT NOT NULL DEFAULT '',
"weight" TEXT NOT NULL DEFAULT '',
"loadType" TEXT NOT NULL DEFAULT '',
"escortsNeeded" INTEGER NOT NULL DEFAULT 1,
"status" TEXT NOT NULL DEFAULT 'pending',
"notes" TEXT NOT NULL DEFAULT '',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "orders_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "documents" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"filename" TEXT NOT NULL,
"filepath" TEXT NOT NULL,
"mimeType" TEXT NOT NULL DEFAULT '',
"sizeBytes" INTEGER NOT NULL DEFAULT 0,
"expiresAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "documents_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "contributions" (
"id" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"entityType" TEXT NOT NULL,
"entityId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"content" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"truckStopId" TEXT,
"weighStationId" TEXT,
CONSTRAINT "contributions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "alerts" (
"id" TEXT NOT NULL,
"stateId" TEXT NOT NULL,
"type" TEXT NOT NULL,
"route" TEXT NOT NULL,
"description" TEXT NOT NULL,
"severity" TEXT NOT NULL DEFAULT 'info',
"startsAt" TIMESTAMP(3) NOT NULL,
"endsAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "alerts_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "states_name_key" ON "states"("name");
-- CreateIndex
CREATE UNIQUE INDEX "states_abbr_key" ON "states"("abbr");
-- CreateIndex
CREATE UNIQUE INDEX "regulations_stateId_key" ON "regulations"("stateId");
-- CreateIndex
CREATE UNIQUE INDEX "contacts_stateId_key" ON "contacts"("stateId");
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- CreateIndex
CREATE UNIQUE INDEX "escort_profiles_userId_key" ON "escort_profiles"("userId");
-- AddForeignKey
ALTER TABLE "regulations" ADD CONSTRAINT "regulations_stateId_fkey" FOREIGN KEY ("stateId") REFERENCES "states"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "equipment_requirements" ADD CONSTRAINT "equipment_requirements_stateId_fkey" FOREIGN KEY ("stateId") REFERENCES "states"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "contacts" ADD CONSTRAINT "contacts_stateId_fkey" FOREIGN KEY ("stateId") REFERENCES "states"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "seasonal_restrictions" ADD CONSTRAINT "seasonal_restrictions_stateId_fkey" FOREIGN KEY ("stateId") REFERENCES "states"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "truck_stops" ADD CONSTRAINT "truck_stops_stateId_fkey" FOREIGN KEY ("stateId") REFERENCES "states"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "bridges" ADD CONSTRAINT "bridges_stateId_fkey" FOREIGN KEY ("stateId") REFERENCES "states"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "weigh_stations" ADD CONSTRAINT "weigh_stations_stateId_fkey" FOREIGN KEY ("stateId") REFERENCES "states"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "escort_profiles" ADD CONSTRAINT "escort_profiles_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "loads" ADD CONSTRAINT "loads_posterId_fkey" FOREIGN KEY ("posterId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "orders" ADD CONSTRAINT "orders_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "documents" ADD CONSTRAINT "documents_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "contributions" ADD CONSTRAINT "contributions_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "contributions" ADD CONSTRAINT "contributions_truckStopId_fkey" FOREIGN KEY ("truckStopId") REFERENCES "truck_stops"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "contributions" ADD CONSTRAINT "contributions_weighStationId_fkey" FOREIGN KEY ("weighStationId") REFERENCES "weigh_stations"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "alerts" ADD CONSTRAINT "alerts_stateId_fkey" FOREIGN KEY ("stateId") REFERENCES "states"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

312
server/prisma/schema.prisma Normal file
View File

@@ -0,0 +1,312 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// =====================================================================
// Core Reference Data
// =====================================================================
model State {
id String @id @default(cuid())
name String @unique
abbr String @unique @db.VarChar(2)
lat Float
lng Float
regulation Regulation?
equipmentRequirements EquipmentRequirement[]
contact Contact?
seasonalRestrictions SeasonalRestriction[]
truckStops TruckStop[]
bridges Bridge[]
weighStations WeighStation[]
alerts Alert[]
@@map("states")
}
model Regulation {
id String @id @default(cuid())
stateId String @unique
state State @relation(fields: [stateId], references: [id], onDelete: Cascade)
permitWidth String
permitHeight String
permitLength String
permitWeight String
escortWidth String
escortHeight String
escortLength String
escortWeight String
travelRestrictions String
holidays String
agency String
url String
notes String @db.Text
@@map("regulations")
}
model EquipmentRequirement {
id String @id @default(cuid())
stateId String
state State @relation(fields: [stateId], references: [id], onDelete: Cascade)
type String // "escort" or "carrier"
certification String @default("")
vehicle String @default("")
signs String @default("")
lights String @default("")
heightPole String @default("")
flags String @default("")
safetyGear String @default("") @db.Text
communication String @default("")
@@map("equipment_requirements")
}
model Contact {
id String @id @default(cuid())
stateId String @unique
state State @relation(fields: [stateId], references: [id], onDelete: Cascade)
permitPhone String
policePhone String
email String
hours String
portalUrl String
@@map("contacts")
}
model SeasonalRestriction {
id String @id @default(cuid())
stateId String
state State @relation(fields: [stateId], references: [id], onDelete: Cascade)
name String
type String // "spring_weight", "winter_closure", "harvest", "holiday_blackout"
startMonth Int
endMonth Int
description String @db.Text
@@map("seasonal_restrictions")
}
// =====================================================================
// Geospatial Entities
// =====================================================================
model TruckStop {
id String @id @default(cuid())
stateId String
state State @relation(fields: [stateId], references: [id], onDelete: Cascade)
name String
lat Float
lng Float
address String
hasOversizeParking Boolean @default(false)
entranceHeight String @default("")
entranceWidth String @default("")
lotSqFt Int?
facilities Json @default("[]") // ["fuel","food","showers","restrooms"]
satelliteUrl String @default("")
phone String @default("")
contributions Contribution[]
@@map("truck_stops")
}
model Bridge {
id String @id @default(cuid())
stateId String
state State @relation(fields: [stateId], references: [id], onDelete: Cascade)
name String
lat Float
lng Float
route String
heightClearance Float // in feet
widthClearance Float? // in feet, null = unrestricted
weightLimit Float? // in lbs, null = unrestricted
lastVerified DateTime?
@@map("bridges")
}
model WeighStation {
id String @id @default(cuid())
stateId String
state State @relation(fields: [stateId], references: [id], onDelete: Cascade)
name String
lat Float
lng Float
direction String // "NB", "SB", "EB", "WB"
route String
prePass Boolean @default(false)
hours String @default("")
currentStatus String @default("unknown") // "open", "closed", "unknown"
lastStatusUpdate DateTime?
lastStatusUserId String?
contributions Contribution[]
@@map("weigh_stations")
}
// =====================================================================
// Users & Auth
// =====================================================================
model User {
id String @id @default(cuid())
email String @unique
passwordHash String
name String
role String @default("driver") // "driver", "carrier", "escort", "admin"
tier String @default("free") // "free", "subscriber", "premium"
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
escortProfile EscortProfile?
loads Load[]
orders Order[]
documents Document[]
contributions Contribution[]
@@map("users")
}
model EscortProfile {
id String @id @default(cuid())
userId String @unique
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
lat Float
lng Float
radiusMiles Int @default(100)
certifications Json @default("[]") // ["TX","OK","CA"]
vehicleType String @default("")
availability String @default("available") // "available", "on_job", "unavailable"
rating Float @default(0)
ratingCount Int @default(0)
phone String @default("")
bio String @default("") @db.Text
@@map("escort_profiles")
}
// =====================================================================
// Transactional Data
// =====================================================================
model Load {
id String @id @default(cuid())
posterId String
poster User @relation(fields: [posterId], references: [id], onDelete: Cascade)
origin String
destination String
pickupDate DateTime
width String
height String
length String
weight String
description String @default("") @db.Text
escortsNeeded Int @default(1)
status String @default("open") // "open", "assigned", "in_transit", "delivered", "cancelled"
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("loads")
}
model Order {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
// Load details (embedded, since orders may not reference a load board listing)
origin String
destination String
pickupDate DateTime
width String @default("")
height String @default("")
length String @default("")
weight String @default("")
loadType String @default("")
escortsNeeded Int @default(1)
status String @default("pending") // "pending", "confirmed", "in_progress", "completed", "cancelled"
notes String @default("") @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("orders")
}
model Document {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
type String // "permit", "insurance", "certification", "license", "other"
filename String
filepath String
mimeType String @default("")
sizeBytes Int @default(0)
expiresAt DateTime?
createdAt DateTime @default(now())
@@map("documents")
}
model Contribution {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
entityType String // "truck_stop", "weigh_station", "bridge", "regulation"
entityId String
type String // "info", "flag", "confirm"
content String @db.Text
createdAt DateTime @default(now())
// Optional relations (polymorphic — only one will be set)
truckStop TruckStop? @relation(fields: [truckStopId], references: [id])
truckStopId String?
weighStation WeighStation? @relation(fields: [weighStationId], references: [id])
weighStationId String?
@@map("contributions")
}
// =====================================================================
// Alerts
// =====================================================================
model Alert {
id String @id @default(cuid())
stateId String
state State @relation(fields: [stateId], references: [id], onDelete: Cascade)
type String // "construction", "closure", "weather", "wind"
route String
description String @db.Text
severity String @default("info") // "info", "warning", "critical"
startsAt DateTime
endsAt DateTime?
createdAt DateTime @default(now())
@@map("alerts")
}