Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions src/app/forecast-desk/_data/forecast-desk-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
export const forecastDeskOverview = {
eyebrow: "Forecast Desk",
title: "Read the next operating window through trend snapshots instead of a single blended forecast.",
description:
"This route stages short-horizon signals for inbound volume, dwell, exceptions, and reserve coverage. Snapshot cards show what is moving, short notes translate that movement into operating guidance, and the comparison band keeps the current shift anchored to the next two forecast windows.",
primaryAction: "Review trend snapshots",
secondaryAction: "Open forecast notes",
stats: [
{
label: "Trend windows",
value: "3",
detail: "Signals tracked across the next handoff",
},
{
label: "Forecast notes",
value: "3",
detail: "Short reads for desk and floor leads",
},
{
label: "Comparison lenses",
value: "3",
detail: "Shared metrics across current, next 6h, and next day",
},
],
} as const;

export const comparisonMetrics = [
{
label: "Inbound volume",
current: "18.4k",
nextWindow: "20.1k",
nextDay: "17.6k",
note: "The peak lands before midnight, then cools into the morning reset.",
},
{
label: "Dock dwell",
current: "42 min",
nextWindow: "49 min",
nextDay: "38 min",
note: "Weather pressure widens the turn window tonight but clears by the next day.",
},
{
label: "Crew reserve",
current: "14 teams",
nextWindow: "11 teams",
nextDay: "16 teams",
note: "Reserve capacity stays usable, but most of it is already committed to contingencies.",
},
] as const;

export const trendSnapshots = [
{
id: "metro-east",
title: "Metro east inbound lift",
tone: "accelerating",
metricValue: "20.1k parcels",
change: "+9.4% vs. current",
confidence: "High confidence",
window: "18:00-23:00 local",
summary:
"Late pickups compress the first sort block above the usual evening baseline.",
drivers: [
"Retail release cutoffs moved 30 minutes later.",
"Linehaul timing recovered after the noon disruption.",
],
action: "Pre-stage overflow sort support before 19:30.",
},
{
id: "coastal-dwell",
title: "Coastal dwell pressure",
tone: "watch",
metricValue: "49 minutes",
change: "+7 minutes",
confidence: "Moderate confidence",
window: "20:00-01:00 local",
summary:
"A narrow rain band slows trailer turns on the coastal lanes and raises congestion risk.",
drivers: [
"Southbound relay swaps stack inside one 90-minute window.",
"Only one spare dock team is free before midnight rotation.",
],
action: "Protect one flex dock team for the coastal handoff.",
},
{
id: "returns-slack",
title: "Returns slack stays available",
tone: "steady",
metricValue: "11.8%",
change: "-1.6 pts",
confidence: "High confidence",
window: "Current through 06:00",
summary:
"The lighter returns mix leaves usable capacity that can absorb routine variance.",
drivers: [
"Weekend reversals cleared earlier than expected.",
"Secondary inspection backlog is already under threshold.",
],
action: "Use returns capacity before drawing down reserve teams.",
},
] as const;

export const forecastNotes = [
{
id: "east-overflow",
title: "Pre-stage east overflow support",
state: "ready",
owner: "Desk lead",
window: "By 19:30",
summary:
"Move one overflow crew toward the east sort block before the compression wave arrives.",
trigger: "Keep this move if inbound volume stays above 19.6k at 18:45.",
},
{
id: "coastal-flex",
title: "Hold a flex dock team for coastal turns",
state: "watch",
owner: "Floor coordinator",
window: "20:00-23:30",
summary:
"Avoid spending the last spare dock team until the weather-linked dwell trend confirms.",
trigger: "Release the team only if dwell remains under 45 minutes after 20:30.",
},
{
id: "reserve-handoff",
title: "Protect reserve coverage into handoff",
state: "follow-up",
owner: "Shift manager",
window: "Review at 23:15",
summary:
"Tonight's reserve coverage is adequate, but the margin disappears if dwell and exceptions rise together.",
trigger: "Escalate staffing review if exception share crosses 3.2% before handoff.",
},
] as const;
95 changes: 95 additions & 0 deletions src/app/forecast-desk/page.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { cleanup, render, screen, within } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";

import {
comparisonMetrics,
forecastDeskOverview,
forecastNotes,
trendSnapshots,
} from "./_data/forecast-desk-data";
import ForecastDeskPage from "./page";

afterEach(() => {
cleanup();
});

describe("ForecastDeskPage", () => {
it("renders the hero content and route actions", () => {
render(<ForecastDeskPage />);

expect(
screen.getByRole("heading", {
name: /read the next operating window through trend snapshots instead of a single blended forecast\./i,
}),
).toBeInTheDocument();
expect(
screen.getByRole("link", { name: forecastDeskOverview.primaryAction }),
).toHaveAttribute("href", "#trend-snapshots");
expect(
screen.getByRole("link", { name: forecastDeskOverview.secondaryAction }),
).toHaveAttribute("href", "#forecast-notes");
expect(
screen.getByRole("link", { name: /back to route index/i }),
).toHaveAttribute("href", "/");
});

it("renders every metric in the comparison band", () => {
render(<ForecastDeskPage />);

const comparisonBand = screen.getByLabelText(/metric comparison band/i);

for (const metric of comparisonMetrics) {
const card = within(comparisonBand).getByText(metric.label).closest("article");

expect(card).toBeTruthy();
expect(card?.textContent).toContain(metric.current);
expect(card?.textContent).toContain(metric.nextWindow);
expect(card?.textContent).toContain(metric.nextDay);
expect(card?.textContent).toContain(metric.note);
}
});

it("renders each trend snapshot with drivers and action guidance", () => {
render(<ForecastDeskPage />);

const snapshotList = screen.getByRole("list", { name: /trend snapshots/i });

expect(snapshotList.querySelectorAll(':scope > [role="listitem"]')).toHaveLength(
trendSnapshots.length,
);

for (const snapshot of trendSnapshots) {
const card = within(snapshotList)
.getByRole("heading", { name: snapshot.title })
.closest('[role="listitem"]');

expect(card).toBeTruthy();
expect(card?.textContent).toContain(snapshot.metricValue);
expect(card?.textContent).toContain(snapshot.confidence);
expect(card?.textContent).toContain(snapshot.drivers[0]);
expect(card?.textContent).toContain(snapshot.action);
}
});

it("renders the forecast notes with owners, windows, and triggers", () => {
render(<ForecastDeskPage />);

const notesList = screen.getByRole("list", { name: /forecast notes/i });

expect(notesList.querySelectorAll(':scope > [role="listitem"]')).toHaveLength(
forecastNotes.length,
);

for (const note of forecastNotes) {
const card = within(notesList)
.getByRole("heading", { name: note.title })
.closest('[role="listitem"]');

expect(card).toBeTruthy();
expect(card?.textContent).toContain(note.owner);
expect(card?.textContent).toContain(note.window);
expect(card?.textContent).toContain(note.summary);
expect(card?.textContent).toContain(note.trigger);
}
});
});
Loading
Loading