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:
312
server/prisma/schema.prisma
Normal file
312
server/prisma/schema.prisma
Normal 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")
|
||||
}
|
||||
Reference in New Issue
Block a user