From 60aa5c86482de261492fdf2a318ce8813eae9d30 Mon Sep 17 00:00:00 2001 From: iamasx Date: Fri, 24 Apr 2026 15:24:53 +0530 Subject: [PATCH 1/3] feat: add handoff journal route with carry-forward notes --- .../_components/action-bullet-list.tsx | 58 ++ .../_components/carry-forward-panel.tsx | 54 ++ .../_components/handoff-entry-card.tsx | 50 ++ .../_data/handoff-journal-data.ts | 233 ++++++++ .../handoff-journal.module.css | 514 ++++++++++++++++++ src/app/handoff-journal/page.test.tsx | 116 ++++ src/app/handoff-journal/page.tsx | 130 +++++ 7 files changed, 1155 insertions(+) create mode 100644 src/app/handoff-journal/_components/action-bullet-list.tsx create mode 100644 src/app/handoff-journal/_components/carry-forward-panel.tsx create mode 100644 src/app/handoff-journal/_components/handoff-entry-card.tsx create mode 100644 src/app/handoff-journal/_data/handoff-journal-data.ts create mode 100644 src/app/handoff-journal/handoff-journal.module.css create mode 100644 src/app/handoff-journal/page.test.tsx create mode 100644 src/app/handoff-journal/page.tsx diff --git a/src/app/handoff-journal/_components/action-bullet-list.tsx b/src/app/handoff-journal/_components/action-bullet-list.tsx new file mode 100644 index 0000000..e0dbf8e --- /dev/null +++ b/src/app/handoff-journal/_components/action-bullet-list.tsx @@ -0,0 +1,58 @@ +import type { ActionBullet } from "../_data/handoff-journal-data"; +import styles from "../handoff-journal.module.css"; + +type ActionBulletListProps = { + entryTitle: string; + actions: ActionBullet[]; +}; + +const toneLabelMap = { + now: "Immediate", + next: "Queued next", + watch: "Watch closely", +} as const; + +export function ActionBulletList({ + entryTitle, + actions, +}: ActionBulletListProps) { + return ( +
+
+

Action bullets

+

+ Compact, owner-tagged follow-through for the incoming shift. +

+
+ + +
+ ); +} diff --git a/src/app/handoff-journal/_components/carry-forward-panel.tsx b/src/app/handoff-journal/_components/carry-forward-panel.tsx new file mode 100644 index 0000000..ebd70ea --- /dev/null +++ b/src/app/handoff-journal/_components/carry-forward-panel.tsx @@ -0,0 +1,54 @@ +import type { CarryForwardNote } from "../_data/handoff-journal-data"; +import styles from "../handoff-journal.module.css"; + +type CarryForwardPanelProps = { + notes: CarryForwardNote[]; +}; + +export function CarryForwardPanel({ notes }: CarryForwardPanelProps) { + return ( + + ); +} diff --git a/src/app/handoff-journal/_components/handoff-entry-card.tsx b/src/app/handoff-journal/_components/handoff-entry-card.tsx new file mode 100644 index 0000000..86d09dd --- /dev/null +++ b/src/app/handoff-journal/_components/handoff-entry-card.tsx @@ -0,0 +1,50 @@ +import { ActionBulletList } from "./action-bullet-list"; +import type { HandoffEntry } from "../_data/handoff-journal-data"; +import styles from "../handoff-journal.module.css"; + +type HandoffEntryCardProps = { + entry: HandoffEntry; +}; + +export function HandoffEntryCard({ entry }: HandoffEntryCardProps) { + return ( +
+
+
+ {entry.shiftLabel} +

{entry.title}

+
+
+ {entry.window} + {entry.operator} + {entry.channel} +
+
+ +

{entry.summary}

+ + + + +
+ ); +} diff --git a/src/app/handoff-journal/_data/handoff-journal-data.ts b/src/app/handoff-journal/_data/handoff-journal-data.ts new file mode 100644 index 0000000..97b740f --- /dev/null +++ b/src/app/handoff-journal/_data/handoff-journal-data.ts @@ -0,0 +1,233 @@ +export type HandoffEntryTone = "steady" | "watch" | "urgent"; +export type ActionBulletTone = "now" | "next" | "watch"; +export type CarryForwardTone = "open" | "scheduled" | "monitor"; + +export type ActionBullet = { + id: string; + label: string; + owner: string; + timing: string; + tone: ActionBulletTone; +}; + +export type HandoffEntry = { + id: string; + shiftLabel: string; + window: string; + operator: string; + channel: string; + title: string; + summary: string; + highlights: string[]; + tone: HandoffEntryTone; + actions: ActionBullet[]; +}; + +export type CarryForwardNote = { + id: string; + lane: string; + note: string; + owner: string; + nextCheck: string; + tone: CarryForwardTone; +}; + +export type JournalStat = { + id: string; + label: string; + value: string; + detail: string; +}; + +export const handoffJournalOverview = { + eyebrow: "Shift continuity journal", + title: "Capture the handoff before the next shift has to guess what matters.", + description: + "The handoff journal keeps shift-by-shift summaries, action bullets, and carry-forward notes in one route so operators can pass context without reopening active issue boards.", + activeWindow: "Fri 24 Apr ยท 17:30 handoff", + nextSync: "Next continuity sync at 18:00", + actions: [ + { label: "Review handoff entries", href: "#handoff-entries" }, + { label: "Jump to carry-forward", href: "#carry-forward" }, + ], +} as const; + +export const handoffEntries: HandoffEntry[] = [ + { + id: "dawn-shift", + shiftLabel: "Dawn shift", + window: "05:30 - 09:00", + operator: "Mina Alvarez", + channel: "Ops relay + dock board", + title: "Inbound staging reopened after the backup generator swap.", + summary: + "The overnight weather hold cleared at 06:12 and staging lanes were reopened in sequence, which let the team drain the cold queue before the morning dispatch wave.", + highlights: [ + "Cleared 14 queued containers before 07:40.", + "Moved one rover team from sweep support to dock 3 staging.", + "Kept scan accuracy above the 98% target during the restart window.", + ], + tone: "steady", + actions: [ + { + id: "dock-3-fuel", + label: "Confirm the dock 3 rover fuel top-off before the 09:20 departures.", + owner: "Rover lead", + timing: "Before 09:10", + tone: "now", + }, + { + id: "label-audit", + label: "Audit relabeled cold-chain pallets against the revised lane tags.", + owner: "Dock coordinator", + timing: "First half of day shift", + tone: "watch", + }, + { + id: "hold-log", + label: "Append the generator restart times to the continuity log for finance recovery.", + owner: "Shift supervisor", + timing: "Before handoff close", + tone: "next", + }, + ], + }, + { + id: "day-shift", + shiftLabel: "Day shift", + window: "09:00 - 13:30", + operator: "Jordan Pike", + channel: "Control lane briefing", + title: "Dispatch cadence recovered, but parcel imaging still needs active watching.", + summary: + "Outbound departures returned to the planned tempo by 11:05, although the parcel imaging patch introduced intermittent thumbnail lag on two sorting lanes.", + highlights: [ + "Recovered the noon route set without dropping any export windows.", + "Redirected image retries away from lanes 7 and 8 to keep throughput stable.", + "Validated that downstream manifests stayed in sync during the patch rollout.", + ], + tone: "watch", + actions: [ + { + id: "lane-imaging", + label: "Recheck thumbnail latency on lanes 7 and 8 after the next scanner reboot.", + owner: "Imaging support", + timing: "13:45 checkpoint", + tone: "watch", + }, + { + id: "manifest-retry", + label: "Leave the manifest retry ceiling at six attempts until the patch is rolled back or cleared.", + owner: "Control operator", + timing: "Carry through swing shift", + tone: "now", + }, + { + id: "escalation-note", + label: "Escalate to platform support if lag exceeds 45 seconds on three consecutive scans.", + owner: "Shift lead", + timing: "If triggered", + tone: "next", + }, + ], + }, + { + id: "swing-shift", + shiftLabel: "Swing shift", + window: "13:30 - 17:30", + operator: "Rae Okafor", + channel: "Floor sync + support desk", + title: "The floor is stable for close, with two notes explicitly carried into the evening crew.", + summary: + "No new blockers opened during the afternoon run, and the crew is handing over a short, explicit list of follow-ups instead of a broad watchlist.", + highlights: [ + "Held the exception queue below the five-item alert threshold.", + "Closed the late manifest mismatch without delaying the 16:50 convoy.", + "Prepared the evening crew with lane-by-lane staffing notes for the export surge.", + ], + tone: "urgent", + actions: [ + { + id: "staffing-brief", + label: "Walk the evening lead through the lane staffing swaps before the 18:00 surge.", + owner: "Outgoing supervisor", + timing: "At handoff", + tone: "now", + }, + { + id: "printer-ribbon", + label: "Replace the dock 5 thermal ribbon before the export label batch begins.", + owner: "Warehouse tech", + timing: "17:45", + tone: "now", + }, + { + id: "truck-seal", + label: "Verify truck 14 seal photos land in the archive after the delayed upload finishes.", + owner: "Security desk", + timing: "18:10 follow-up", + tone: "watch", + }, + ], + }, +]; + +export const carryForwardNotes: CarryForwardNote[] = [ + { + id: "dock-5-labeling", + lane: "Dock 5 labeling", + note: "The ribbon is fading on the thermal printer. Swap it before the evening export batch so the crew does not have to reprint labels mid-wave.", + owner: "Warehouse tech", + nextCheck: "17:45", + tone: "open", + }, + { + id: "parcel-imaging", + lane: "Parcel imaging", + note: "Scanner thumbnails are occasionally late after the patch. Keep the retry ceiling in place and monitor for three slow scans in a row.", + owner: "Imaging support", + nextCheck: "18:15", + tone: "monitor", + }, + { + id: "seal-archive", + lane: "Security archive", + note: "Truck 14 seal photos are queued for upload. Confirm they land in the archive before the end-of-day seal report is published.", + owner: "Security desk", + nextCheck: "18:10", + tone: "scheduled", + }, +]; + +const actionCount = handoffEntries.reduce( + (count, entry) => count + entry.actions.length, + 0, +); +const watchCount = handoffEntries.filter((entry) => entry.tone !== "steady").length; + +export const handoffJournalStats: JournalStat[] = [ + { + id: "entries", + label: "Shift entries", + value: String(handoffEntries.length), + detail: "Morning, day, and swing notes stay visible in one uninterrupted read.", + }, + { + id: "actions", + label: "Action bullets", + value: String(actionCount), + detail: "Each handoff closes with explicit owner and timing language.", + }, + { + id: "carry-forward", + label: "Carry-forward notes", + value: String(carryForwardNotes.length), + detail: "Only the unresolved notes survive the handoff into the next crew.", + }, + { + id: "watch", + label: "Watch shifts", + value: String(watchCount), + detail: "Two entries still need active monitoring before the route can fully settle.", + }, +]; diff --git a/src/app/handoff-journal/handoff-journal.module.css b/src/app/handoff-journal/handoff-journal.module.css new file mode 100644 index 0000000..c0d4771 --- /dev/null +++ b/src/app/handoff-journal/handoff-journal.module.css @@ -0,0 +1,514 @@ +.shell { + min-height: calc(100vh - var(--nav-height)); + background: + radial-gradient(circle at top right, rgba(45, 212, 191, 0.16), transparent 28%), + radial-gradient(circle at bottom left, rgba(245, 158, 11, 0.18), transparent 30%), + linear-gradient(180deg, #0f172a 0%, #111827 48%, #1f2937 100%); +} + +.shellFrame { + position: relative; + isolation: isolate; +} + +.surfacePanel { + border: 1px solid rgba(226, 232, 240, 0.12); + background: linear-gradient(180deg, rgba(15, 23, 42, 0.84), rgba(15, 23, 42, 0.72)); + box-shadow: 0 32px 110px rgba(2, 6, 23, 0.32); + backdrop-filter: blur(20px); +} + +.heroPanel { + position: relative; + overflow: hidden; +} + +.heroPanel::after { + content: ""; + position: absolute; + inset: auto -5rem -7rem auto; + width: 18rem; + height: 18rem; + border-radius: 9999px; + background: radial-gradient(circle, rgba(250, 204, 21, 0.18), transparent 68%); + pointer-events: none; +} + +.heroGrid { + display: grid; + gap: 2rem; +} + +.heroCopy { + display: grid; + gap: 1.5rem; +} + +.heroEyebrow { + width: fit-content; + border-radius: 9999px; + border: 1px solid rgba(94, 234, 212, 0.22); + background: rgba(45, 212, 191, 0.12); + padding: 0.4rem 0.85rem; + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.18em; + text-transform: uppercase; + color: rgba(204, 251, 241, 0.92); +} + +.heroTitle { + max-width: 52rem; + font-size: clamp(2.5rem, 4vw, 4.75rem); + line-height: 1.02; + letter-spacing: -0.04em; + font-weight: 650; + color: #f8fafc; + text-wrap: balance; +} + +.heroDescription { + max-width: 44rem; + font-size: 1rem; + line-height: 1.9; + color: rgba(226, 232, 240, 0.82); +} + +.heroActionRow { + display: flex; + flex-wrap: wrap; + gap: 0.875rem; +} + +.primaryAction, +.secondaryAction, +.homeLink { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 2.9rem; + border-radius: 9999px; + padding: 0.75rem 1.1rem; + font-size: 0.875rem; + font-weight: 650; + transition: + transform 0.18s ease, + border-color 0.18s ease, + background-color 0.18s ease; +} + +.primaryAction { + background: #f59e0b; + color: #111827; +} + +.secondaryAction { + border: 1px solid rgba(226, 232, 240, 0.18); + background: rgba(255, 255, 255, 0.06); + color: #f8fafc; +} + +.homeLink { + width: fit-content; + border: 1px solid rgba(94, 234, 212, 0.2); + background: rgba(15, 23, 42, 0.44); + color: rgba(240, 253, 250, 0.9); +} + +.primaryAction:hover, +.secondaryAction:hover, +.homeLink:hover { + transform: translateY(-1px); +} + +.heroAside { + display: grid; + gap: 0.9rem; +} + +.heroAsideCard { + border-radius: 1.5rem; + border: 1px solid rgba(226, 232, 240, 0.1); + background: rgba(255, 255, 255, 0.08); + padding: 1rem; +} + +.heroAsideLabel { + font-size: 0.7rem; + font-weight: 700; + letter-spacing: 0.18em; + text-transform: uppercase; + color: rgba(191, 219, 254, 0.72); +} + +.heroAsideValue { + margin-top: 0.55rem; + font-size: 1.5rem; + line-height: 1.25; + font-weight: 650; + color: #f8fafc; +} + +.heroAsideDetail { + margin-top: 0.45rem; + font-size: 0.925rem; + line-height: 1.65; + color: rgba(226, 232, 240, 0.76); +} + +.statGrid { + display: grid; + gap: 1rem; +} + +.statCard { + border-radius: 1.5rem; + border: 1px solid rgba(226, 232, 240, 0.1); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(15, 23, 42, 0.22)); + padding: 1.1rem; +} + +.statLabel { + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.18em; + text-transform: uppercase; + color: rgba(148, 163, 184, 0.88); +} + +.statValue { + margin-top: 0.55rem; + font-size: 2rem; + font-weight: 650; + letter-spacing: -0.03em; + color: #f8fafc; +} + +.statDetail { + margin-top: 0.4rem; + font-size: 0.925rem; + line-height: 1.65; + color: rgba(226, 232, 240, 0.72); +} + +.mainGrid { + display: grid; + gap: 1.5rem; + align-items: start; +} + +.entriesSection { + display: grid; + gap: 1rem; +} + +.panelHeading { + display: grid; + gap: 0.55rem; +} + +.panelEyebrow, +.sectionEyebrow { + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.18em; + text-transform: uppercase; + color: rgba(148, 163, 184, 0.88); +} + +.panelTitle { + font-size: clamp(1.9rem, 3vw, 2.75rem); + line-height: 1.08; + letter-spacing: -0.03em; + font-weight: 650; + color: #f8fafc; +} + +.panelDescription, +.sectionCaption { + max-width: 46rem; + font-size: 0.95rem; + line-height: 1.75; + color: rgba(226, 232, 240, 0.74); +} + +.entryGrid { + display: grid; + gap: 1rem; +} + +.entryCard { + border-radius: 1.85rem; + border: 1px solid rgba(226, 232, 240, 0.1); + padding: 1.25rem; + background: linear-gradient(180deg, rgba(15, 23, 42, 0.92), rgba(15, 23, 42, 0.82)); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04); +} + +.entrySteady { + border-color: rgba(45, 212, 191, 0.2); +} + +.entryWatch { + border-color: rgba(250, 204, 21, 0.26); +} + +.entryUrgent { + border-color: rgba(251, 146, 60, 0.28); +} + +.entryHeader { + display: grid; + gap: 1rem; +} + +.entryHeaderCopy { + display: grid; + gap: 0.75rem; +} + +.entryShiftBadge { + width: fit-content; + border-radius: 9999px; + border: 1px solid rgba(148, 163, 184, 0.2); + background: rgba(255, 255, 255, 0.06); + padding: 0.32rem 0.72rem; + font-size: 0.72rem; + font-weight: 700; + letter-spacing: 0.14em; + text-transform: uppercase; + color: rgba(226, 232, 240, 0.88); +} + +.entryTitle { + font-size: clamp(1.35rem, 2vw, 1.75rem); + line-height: 1.22; + letter-spacing: -0.025em; + font-weight: 650; + color: #f8fafc; +} + +.entryMeta { + display: flex; + flex-wrap: wrap; + gap: 0.6rem; +} + +.entryMetaPill, +.entryMetaText, +.actionMetaPill, +.actionMetaText, +.carryForwardCheck { + border-radius: 9999px; + padding: 0.35rem 0.7rem; + font-size: 0.78rem; + line-height: 1.2; +} + +.entryMetaPill { + background: rgba(94, 234, 212, 0.12); + color: rgba(204, 251, 241, 0.95); +} + +.entryMetaText { + background: rgba(255, 255, 255, 0.06); + color: rgba(226, 232, 240, 0.78); +} + +.entrySummary { + font-size: 0.99rem; + line-height: 1.8; + color: rgba(226, 232, 240, 0.82); +} + +.highlightList { + display: grid; + gap: 0.65rem; + margin: 0; + padding: 0; + list-style: none; +} + +.highlightItem { + border-radius: 1rem; + border: 1px solid rgba(226, 232, 240, 0.08); + background: rgba(255, 255, 255, 0.05); + padding: 0.75rem 0.85rem; + font-size: 0.88rem; + line-height: 1.55; + color: rgba(241, 245, 249, 0.84); +} + +.actionSection { + display: grid; + gap: 0.85rem; + border-top: 1px solid rgba(226, 232, 240, 0.08); + padding-top: 1rem; +} + +.sectionHeader { + display: grid; + gap: 0.35rem; +} + +.actionList { + display: grid; + gap: 0.85rem; + margin: 0; + padding: 0; + list-style: none; +} + +.actionItem { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + gap: 0.8rem; + align-items: start; +} + +.actionDot { + width: 0.65rem; + height: 0.65rem; + margin-top: 0.55rem; + border-radius: 9999px; +} + +.actionDotNow { + background: #34d399; + box-shadow: 0 0 0 0.3rem rgba(52, 211, 153, 0.14); +} + +.actionDotWatch { + background: #fbbf24; + box-shadow: 0 0 0 0.3rem rgba(251, 191, 36, 0.14); +} + +.actionDotNext { + background: #60a5fa; + box-shadow: 0 0 0 0.3rem rgba(96, 165, 250, 0.14); +} + +.actionBody { + display: grid; + gap: 0.45rem; +} + +.actionLabel { + font-size: 0.94rem; + line-height: 1.65; + color: #f8fafc; +} + +.actionMeta { + display: flex; + flex-wrap: wrap; + gap: 0.45rem; +} + +.actionMetaPill { + background: rgba(15, 118, 110, 0.22); + color: rgba(204, 251, 241, 0.95); +} + +.actionMetaText { + background: rgba(255, 255, 255, 0.06); + color: rgba(226, 232, 240, 0.76); +} + +.carryForwardPanel { + display: grid; + gap: 1rem; + padding: 1.25rem; +} + +.carryForwardList { + display: grid; + gap: 0.75rem; +} + +.carryForwardCard { + border-radius: 1.25rem; + border: 1px solid rgba(226, 232, 240, 0.1); + background: rgba(255, 255, 255, 0.05); + padding: 0.95rem; +} + +.carryForwardOpen { + border-color: rgba(251, 146, 60, 0.24); +} + +.carryForwardMonitor { + border-color: rgba(250, 204, 21, 0.24); +} + +.carryForwardScheduled { + border-color: rgba(96, 165, 250, 0.24); +} + +.carryForwardTop { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 0.5rem; +} + +.carryForwardLane { + font-size: 0.93rem; + font-weight: 650; + color: #f8fafc; +} + +.carryForwardCheck { + background: rgba(15, 23, 42, 0.44); + color: rgba(191, 219, 254, 0.84); +} + +.carryForwardNote { + margin-top: 0.7rem; + font-size: 0.87rem; + line-height: 1.65; + color: rgba(226, 232, 240, 0.8); +} + +.carryForwardOwner { + margin-top: 0.65rem; + font-size: 0.78rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: rgba(148, 163, 184, 0.88); +} + +@media (min-width: 640px) { + .entryCard, + .carryForwardPanel { + padding: 1.5rem; + } + + .statGrid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (min-width: 960px) { + .heroGrid { + grid-template-columns: minmax(0, 1.2fr) minmax(18rem, 0.8fr); + align-items: end; + } + + .mainGrid { + grid-template-columns: minmax(0, 1.25fr) minmax(18rem, 0.75fr); + } + + .carryForwardPanel { + position: sticky; + top: calc(var(--nav-height) + 1rem); + } +} + +@media (min-width: 1180px) { + .statGrid { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } +} diff --git a/src/app/handoff-journal/page.test.tsx b/src/app/handoff-journal/page.test.tsx new file mode 100644 index 0000000..276fc45 --- /dev/null +++ b/src/app/handoff-journal/page.test.tsx @@ -0,0 +1,116 @@ +import { cleanup, render, screen, within } from "@testing-library/react"; +import { afterEach, describe, expect, it } from "vitest"; + +import { + carryForwardNotes, + handoffEntries, + handoffJournalOverview, + handoffJournalStats, +} from "./_data/handoff-journal-data"; +import HandoffJournalPage from "./page"; + +afterEach(() => { + cleanup(); +}); + +describe("HandoffJournalPage", () => { + it("renders the journal hero copy, anchor actions, and route escape hatch", () => { + render(); + + expect( + screen.getByRole("heading", { name: handoffJournalOverview.title }), + ).toBeInTheDocument(); + expect(screen.getByText(handoffJournalOverview.description)).toBeInTheDocument(); + expect( + screen.getByRole("link", { name: /review handoff entries/i }), + ).toHaveAttribute("href", "#handoff-entries"); + expect( + screen.getByRole("link", { name: /jump to carry-forward/i }), + ).toHaveAttribute("href", "#carry-forward"); + expect( + screen.getByRole("link", { name: /back to route index/i }), + ).toHaveAttribute("href", "/"); + }); + + it("renders every stat card from the route-local summary data", () => { + render(); + + const statList = screen.getByRole("list", { + name: /handoff journal continuity stats/i, + }); + + expect(statList.querySelectorAll(':scope > [role="listitem"]')).toHaveLength( + handoffJournalStats.length, + ); + + for (const stat of handoffJournalStats) { + const card = within(statList).getByText(stat.label).closest('[role="listitem"]'); + + expect(card).toBeTruthy(); + expect(card?.textContent).toContain(stat.value); + expect(card?.textContent).toContain(stat.detail); + } + }); + + it("renders multiple handoff entries with summaries, highlights, and action bullets", () => { + render(); + + const entryList = screen.getByRole("list", { name: /shift handoff entries/i }); + + expect(entryList.querySelectorAll(':scope > [role="listitem"]')).toHaveLength( + handoffEntries.length, + ); + + for (const entry of handoffEntries) { + const card = within(entryList) + .getByRole("heading", { name: entry.title }) + .closest('[role="listitem"]'); + + expect(card).toBeTruthy(); + expect(card?.textContent).toContain(entry.summary); + expect(card?.textContent).toContain(entry.operator); + expect(card?.textContent).toContain(entry.channel); + + for (const highlight of entry.highlights) { + expect(within(card as HTMLElement).getByText(highlight)).toBeInTheDocument(); + } + + const actionList = within(card as HTMLElement).getByRole("list", { + name: `${entry.title} action bullets`, + }); + + expect( + actionList.querySelectorAll(':scope > li'), + ).toHaveLength(entry.actions.length); + + for (const action of entry.actions) { + expect(within(actionList).getByText(action.label)).toBeInTheDocument(); + expect(within(actionList).getByText(action.owner)).toBeInTheDocument(); + expect(within(actionList).getByText(action.timing)).toBeInTheDocument(); + } + } + }); + + it("renders the compact carry-forward panel with every unresolved note", () => { + render(); + + const carryForwardList = screen.getByRole("list", { + name: /carry-forward notes/i, + }); + + expect( + carryForwardList.querySelectorAll(':scope > [role="listitem"]'), + ).toHaveLength(carryForwardNotes.length); + + for (const note of carryForwardNotes) { + const card = within(carryForwardList) + .getByText(note.lane) + .closest('[role="listitem"]'); + + expect(card).toBeTruthy(); + expect(card?.textContent).toContain(note.note); + expect(card?.textContent).toContain(note.owner); + expect(card?.textContent).toContain(note.nextCheck); + } + }); +}); diff --git a/src/app/handoff-journal/page.tsx b/src/app/handoff-journal/page.tsx new file mode 100644 index 0000000..0aa0c0b --- /dev/null +++ b/src/app/handoff-journal/page.tsx @@ -0,0 +1,130 @@ +import type { Metadata } from "next"; +import Link from "next/link"; + +import { CarryForwardPanel } from "./_components/carry-forward-panel"; +import { HandoffEntryCard } from "./_components/handoff-entry-card"; +import { + carryForwardNotes, + handoffEntries, + handoffJournalOverview, + handoffJournalStats, +} from "./_data/handoff-journal-data"; +import styles from "./handoff-journal.module.css"; + +export const metadata: Metadata = { + title: "Handoff Journal", + description: + "Standalone route for shift handoff summaries, action bullets, and compact carry-forward notes.", +}; + +export default function HandoffJournalPage() { + return ( +
+
+
+
+
+

{handoffJournalOverview.eyebrow}

+
+

{handoffJournalOverview.title}

+

+ {handoffJournalOverview.description} +

+
+ +
+ {handoffJournalOverview.actions.map((action) => ( + + {action.label} + + ))} + + Back to route index + +
+
+ + +
+
+ +
+ {handoffJournalStats.map((stat) => ( +
+

{stat.label}

+

{stat.value}

+

{stat.detail}

+
+ ))} +
+ +
+
+
+

Shift handoffs

+

+ Multiple entries, each with the summary and next actions intact +

+

+ Every card records what changed, why it matters, and what the + incoming shift should do next without opening another route. +

+
+ +
+ {handoffEntries.map((entry) => ( + + ))} +
+
+ + +
+
+
+ ); +} From 0992911b5689572d492ea70ede549486aa166308 Mon Sep 17 00:00:00 2001 From: iamasx Date: Fri, 24 Apr 2026 16:17:08 +0530 Subject: [PATCH 2/3] feat: add handoff journal index entry --- src/app/page.tsx | 60 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/app/page.tsx b/src/app/page.tsx index 6b96cfd..333efb5 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -25,6 +25,12 @@ const fieldGuideHighlights = [ "Reference-ready procedure detail for active response handoffs", ]; +const handoffJournalHighlights = [ + "Multiple shift entries with summaries, highlights, and explicit action bullets", + "Compact carry-forward notes that keep unresolved items visible for the next crew", + "Responsive journal layout tuned for quick reads on floor tablets and desktop stations", +]; + const opsCenterHighlights = [ "Live KPI cards with progress bars for on-time departures, fleet use, and exception backlog", "Prioritized alert queue with severity badges, owner attribution, and playbook guidance", @@ -627,6 +633,60 @@ export default function Home() { + +
+
+
+

+ Issue 174 / Handoff Journal +

+
+

+ Open the handoff journal for shift summaries, action bullets, + and carry-forward notes. +

+

+ The handoff journal stages a standalone operational log with + multiple shift entries, compact next-step bullets, and a short + carry-forward rail so incoming crews can scan what remains + unresolved without reopening active issue boards. +

+
+
+ + Open handoff journal + + + Review issue scope + +
+
+ +
+

+ Handoff journal staging +

+
    + {handoffJournalHighlights.map((item) => ( +
  • + {item} +
  • + ))} +
+
+
+
); } From af6b3f12758be08392d1886921c09579cae269f0 Mon Sep 17 00:00:00 2001 From: iamasx Date: Fri, 24 Apr 2026 16:21:37 +0530 Subject: [PATCH 3/3] chore: add action bullet render log --- src/app/handoff-journal/_components/action-bullet-list.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/handoff-journal/_components/action-bullet-list.tsx b/src/app/handoff-journal/_components/action-bullet-list.tsx index e0dbf8e..2e6f4ff 100644 --- a/src/app/handoff-journal/_components/action-bullet-list.tsx +++ b/src/app/handoff-journal/_components/action-bullet-list.tsx @@ -16,6 +16,8 @@ export function ActionBulletList({ entryTitle, actions, }: ActionBulletListProps) { + console.log(`Rendering ${actions.length} action bullets for ${entryTitle}`); + return (