Files
PilotEdge/server/prisma/schema.prisma
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

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")
}