- 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>
313 lines
8.7 KiB
Plaintext
313 lines
8.7 KiB
Plaintext
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")
|
|
}
|