diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b21cbda..99c4d46 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -35,6 +35,7 @@ const navLinks = [ { href: "/parcel-hub", label: "Parcel Hub" }, { href: "/status-board", label: "Status Board" }, { href: "/capacity-planner", label: "Capacity Planner" }, + { href: "/transit-board", label: "Transit Board" }, ] as const; export default function RootLayout({ diff --git a/src/app/page.tsx b/src/app/page.tsx index 6c37aa1..e9b004e 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -53,6 +53,12 @@ const queueParcelHighlights = [ "Inter-hub transfer table tracking volume and priority between regions", ]; +const transitBoardHighlights = [ + "Corridor summary cards with status indicators, on-time rates, and active movement counts", + "Movement cards tracking origin-destination pairs, cargo types, and real-time status", + "Compact delay highlights panel with severity badges and duration breakdowns", +]; + const statusBoardHighlights = [ "Real-time service health grid with uptime percentages and status indicators", "Incident timeline with severity badges and open/resolved state tracking", @@ -569,6 +575,59 @@ export default function Home() { +
+
+
+

+ Issue 213 / Transit Board +

+
+

+ Track corridor movements, schedules, and delay highlights + from a unified transit board. +

+

+ The transit board consolidates corridor summaries, movement + cards with origin-destination tracking, and a compact delay + highlights panel for rapid operational triage. +

+
+
+ + Open transit board + + + Review issue scope + +
+
+ +
+

+ Transit board features +

+
    + {transitBoardHighlights.map((item) => ( +
  • + {item} +
  • + ))} +
+
+
+
+
diff --git a/src/app/transit-board/page.test.tsx b/src/app/transit-board/page.test.tsx new file mode 100644 index 0000000..bcafe65 --- /dev/null +++ b/src/app/transit-board/page.test.tsx @@ -0,0 +1,99 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import TransitBoardPage from "./page"; + +describe("TransitBoardPage", () => { + it("renders the page heading and description", () => { + render(); + + expect( + screen.getByRole("heading", { + level: 1, + name: /corridor movements, schedules, and delay tracking at a glance/i, + }), + ).toBeInTheDocument(); + expect( + screen.getByText(/monitor active corridors/i), + ).toBeInTheDocument(); + }); + + it("renders all corridor summary cards", () => { + render(); + + expect(screen.getAllByText("North Arterial").length).toBeGreaterThan(0); + expect(screen.getAllByText("East Connector").length).toBeGreaterThan(0); + expect(screen.getAllByText("South Bypass").length).toBeGreaterThan(0); + expect(screen.getAllByText("West Loop").length).toBeGreaterThan(0); + expect(screen.getAllByText("Central Trunk").length).toBeGreaterThan(0); + }); + + it("displays corridor status and metrics", () => { + render(); + + expect(screen.getByText(/Flowing · Northbound/)).toBeInTheDocument(); + expect(screen.getByText(/Congested · Eastbound/)).toBeInTheDocument(); + expect(screen.getByText(/Blocked · Westbound/)).toBeInTheDocument(); + expect(screen.getByText(/97% on-time/)).toBeInTheDocument(); + }); + + it("renders summary bar with correct counts", () => { + render(); + + expect(screen.getByText("5")).toBeInTheDocument(); + expect(screen.getByText("3/5")).toBeInTheDocument(); + }); + + it("renders all movement cards with status badges", () => { + render(); + + expect(screen.getAllByText("MV-1001").length).toBeGreaterThan(0); + expect(screen.getByText("Express Freight Alpha")).toBeInTheDocument(); + expect(screen.getAllByText("MV-1004").length).toBeGreaterThan(0); + expect(screen.getByText("Relief Convoy Delta")).toBeInTheDocument(); + expect(screen.getByText(/Depot A → Terminal 7/)).toBeInTheDocument(); + }); + + it("renders movement origin-destination and cargo info", () => { + render(); + + expect(screen.getByText(/Yard 3 → Hub East · Bulk materials/)).toBeInTheDocument(); + expect(screen.getByText(/Base West → Junction 9 · Emergency supplies/)).toBeInTheDocument(); + }); + + it("renders delay highlights with severity and delay duration", () => { + render(); + + expect(screen.getByText("+32 min")).toBeInTheDocument(); + expect(screen.getByText("+67 min")).toBeInTheDocument(); + expect(screen.getByText("+8 min")).toBeInTheDocument(); + expect(screen.getByText("+14 min")).toBeInTheDocument(); + }); + + it("renders delay reasons", () => { + render(); + + expect( + screen.getByText(/signal failure at junction EC-4/i), + ).toBeInTheDocument(); + expect( + screen.getByText(/track obstruction reported between WL-2 and WL-3/i), + ).toBeInTheDocument(); + }); + + it("shows the tracked movements count", () => { + render(); + + expect( + screen.getByText(/6 tracked movements across all corridors/), + ).toBeInTheDocument(); + }); + + it("shows the active delay reports count", () => { + render(); + + expect( + screen.getByText(/4 active delay reports/), + ).toBeInTheDocument(); + }); +}); diff --git a/src/app/transit-board/page.tsx b/src/app/transit-board/page.tsx new file mode 100644 index 0000000..543da06 --- /dev/null +++ b/src/app/transit-board/page.tsx @@ -0,0 +1,219 @@ +import type { Metadata } from "next"; +import Link from "next/link"; + +export const metadata: Metadata = { + title: "Transit Board", + description: + "Corridor summaries, movement cards, and delay highlights for transit operations.", +}; + +interface Corridor { + id: string; + name: string; + direction: string; + status: "flowing" | "congested" | "blocked"; + activeMovements: number; + avgTransitMin: number; + onTimeRate: string; +} + +interface Movement { + id: string; + label: string; + corridor: string; + origin: string; + destination: string; + status: "in-transit" | "arrived" | "delayed" | "scheduled"; + departedAt: string; + eta: string; + cargoType: string; +} + +interface DelayHighlight { + id: string; + movementId: string; + corridor: string; + reason: string; + delayMin: number; + severity: "low" | "moderate" | "high"; + reportedAt: string; +} + +const corridors: Corridor[] = [ + { id: "COR-N1", name: "North Arterial", direction: "Northbound", status: "flowing", activeMovements: 12, avgTransitMin: 34, onTimeRate: "94%" }, + { id: "COR-E2", name: "East Connector", direction: "Eastbound", status: "congested", activeMovements: 8, avgTransitMin: 52, onTimeRate: "78%" }, + { id: "COR-S3", name: "South Bypass", direction: "Southbound", status: "flowing", activeMovements: 15, avgTransitMin: 28, onTimeRate: "91%" }, + { id: "COR-W4", name: "West Loop", direction: "Westbound", status: "blocked", activeMovements: 3, avgTransitMin: 87, onTimeRate: "42%" }, + { id: "COR-C5", name: "Central Trunk", direction: "Bidirectional", status: "flowing", activeMovements: 20, avgTransitMin: 19, onTimeRate: "97%" }, +]; + +const movements: Movement[] = [ + { id: "MV-1001", label: "Express Freight Alpha", corridor: "North Arterial", origin: "Depot A", destination: "Terminal 7", status: "in-transit", departedAt: "2026-04-25T06:15:00Z", eta: "2026-04-25T06:49:00Z", cargoType: "Priority parcels" }, + { id: "MV-1002", label: "Bulk Transfer Bravo", corridor: "East Connector", origin: "Yard 3", destination: "Hub East", status: "delayed", departedAt: "2026-04-25T05:40:00Z", eta: "2026-04-25T07:12:00Z", cargoType: "Bulk materials" }, + { id: "MV-1003", label: "Shuttle Run Charlie", corridor: "South Bypass", origin: "Station 12", destination: "Depot C", status: "arrived", departedAt: "2026-04-25T04:50:00Z", eta: "2026-04-25T05:18:00Z", cargoType: "Equipment" }, + { id: "MV-1004", label: "Relief Convoy Delta", corridor: "West Loop", origin: "Base West", destination: "Junction 9", status: "delayed", departedAt: "2026-04-25T05:00:00Z", eta: "2026-04-25T07:47:00Z", cargoType: "Emergency supplies" }, + { id: "MV-1005", label: "Commuter Link Echo", corridor: "Central Trunk", origin: "Central Hub", destination: "Terminal 2", status: "in-transit", departedAt: "2026-04-25T06:30:00Z", eta: "2026-04-25T06:49:00Z", cargoType: "Passenger transfer" }, + { id: "MV-1006", label: "Night Haul Foxtrot", corridor: "North Arterial", origin: "Depot A", destination: "Warehouse N", status: "scheduled", departedAt: "2026-04-25T22:00:00Z", eta: "2026-04-25T22:38:00Z", cargoType: "Refrigerated goods" }, +]; + +const delayHighlights: DelayHighlight[] = [ + { id: "DH-01", movementId: "MV-1002", corridor: "East Connector", reason: "Signal failure at junction EC-4 causing single-track operation", delayMin: 32, severity: "high", reportedAt: "2026-04-25T06:05:00Z" }, + { id: "DH-02", movementId: "MV-1004", corridor: "West Loop", reason: "Track obstruction reported between WL-2 and WL-3 segments", delayMin: 67, severity: "high", reportedAt: "2026-04-25T05:30:00Z" }, + { id: "DH-03", movementId: "MV-1001", corridor: "North Arterial", reason: "Minor schedule adjustment due to platform congestion at Terminal 7", delayMin: 8, severity: "low", reportedAt: "2026-04-25T06:40:00Z" }, + { id: "DH-04", movementId: "MV-1005", corridor: "Central Trunk", reason: "Holding pattern for cross-traffic clearance at Central Hub", delayMin: 14, severity: "moderate", reportedAt: "2026-04-25T06:35:00Z" }, +]; + +const corridorStatusColor: Record = { + flowing: "bg-emerald-500", + congested: "bg-amber-500", + blocked: "bg-red-500", +}; + +const corridorStatusLabel: Record = { + flowing: "Flowing", + congested: "Congested", + blocked: "Blocked", +}; + +const movementStatusStyle: Record = { + "in-transit": "border-sky-200 bg-sky-50 text-sky-700", + arrived: "border-emerald-200 bg-emerald-50 text-emerald-700", + delayed: "border-red-200 bg-red-50 text-red-700", + scheduled: "border-slate-200 bg-slate-50 text-slate-600", +}; + +const delaySeverityStyle: Record = { + low: "border-slate-200 bg-slate-50 text-slate-600", + moderate: "border-amber-200 bg-amber-50 text-amber-700", + high: "border-red-200 bg-red-50 text-red-700", +}; + +export default function TransitBoardPage() { + const totalMovements = movements.length; + const inTransit = movements.filter((m) => m.status === "in-transit").length; + const delayed = movements.filter((m) => m.status === "delayed").length; + const flowingCorridors = corridors.filter((c) => c.status === "flowing").length; + + return ( +
+
+
+

+ Transit Board +

+ + Back to overview + +
+

+ Corridor movements, schedules, and delay tracking at a glance. +

+

+ Monitor active corridors, track individual movements across the + network, and review delay highlights for operational triage. +

+
+ + {/* Summary bar */} +
+
+

Corridors

+

{corridors.length}

+
+
+

Flowing

+

{flowingCorridors}/{corridors.length}

+
+
+

In Transit

+

{inTransit}

+
+
+

Delayed

+

{delayed}

+
+
+ + {/* Corridor summaries */} +
+

Corridor Summaries

+
+ {corridors.map((cor) => ( +
+ +
+

{cor.name}

+

+ {corridorStatusLabel[cor.status]} · {cor.direction} +

+

+ {cor.activeMovements} active · avg {cor.avgTransitMin}m · {cor.onTimeRate} on-time +

+
+
+ ))} +
+
+ + {/* Movement cards */} +
+

Movements

+

{totalMovements} tracked movements across all corridors

+
    + {movements.map((mv) => ( +
  • +
    + {mv.id} + + {mv.status} + + {mv.corridor} +
    +

    {mv.label}

    +

    + {mv.origin} → {mv.destination} · {mv.cargoType} +

    +
  • + ))} +
+
+ + {/* Delay highlights */} +
+

Delay Highlights

+

{delayHighlights.length} active delay reports

+
    + {delayHighlights.map((dh) => ( +
  • +
    + {dh.movementId} + + {dh.severity} + + +{dh.delayMin} min +
    +

    {dh.reason}

    +

    + {dh.corridor} · Reported {dh.reportedAt.slice(11, 16)} UTC +

    +
  • + ))} +
+
+
+ ); +}