diff --git a/web-ui/src/app/cases/[caseNumber]/page.tsx b/web-ui/src/app/cases/[caseNumber]/page.tsx index 9436628..229400b 100644 --- a/web-ui/src/app/cases/[caseNumber]/page.tsx +++ b/web-ui/src/app/cases/[caseNumber]/page.tsx @@ -9,7 +9,6 @@ import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { CaseHeader } from "@/components/cases/case-header"; import { CaseEditDialog } from "@/components/cases/case-edit-dialog"; -import { StatusHero } from "@/components/cases/status-hero"; import { DocumentsPanel } from "@/components/cases/documents-panel"; import { DraftsPanel } from "@/components/cases/drafts-panel"; import { DecisionBlocksPanel } from "@/components/cases/decision-blocks-panel"; @@ -19,7 +18,10 @@ import { AgentActivityPreview } from "@/components/cases/agent-activity-preview" import { UploadSheet } from "@/components/documents/upload-sheet"; import { useCase, useStartWorkflow } from "@/lib/api/cases"; import { toast } from "sonner"; -import { Play, Loader2 } from "lucide-react"; +import { + Play, Loader2, LayoutGrid, Scale, FileText, MessageSquare, Users, + type LucideIcon, +} from "lucide-react"; /* * Next 16 breaking change: route params are now a Promise. @@ -60,23 +62,27 @@ export default function CaseDetailPage({ ); } + const tabDefs: [string, string, LucideIcon][] = [ + ["overview", "סקירה", LayoutGrid], + ["arguments", "טיעונים", Scale], + ["decision", "ההחלטה", FileText], + ["drafts", "טיוטות והערות", MessageSquare], + ["agents", "סוכנים", Users], + ]; + + // V2 "segmented" tab strip (X17): each tab a pill, active = raised white card. const tabsList = ( - {[ - ["overview", "סקירה"], - ["arguments", "טיעונים"], - ["decision", "ההחלטה"], - ["drafts", "טיוטות והערות"], - ["agents", "סוכנים"], - ].map(([value, label]) => ( + {tabDefs.map(([value, label, Icon]) => ( + {label} ))} @@ -124,22 +130,18 @@ export default function CaseDetailPage({ ) : ( - + )} - {/* full-width tab content — status lives ONLY in the overview hero - (mockup 18b); the other tabs lose the status rail entirely (INV-IA1). */} + {/* full-width tab content — status now lives in the always-visible band + StatusRail (banner A, X17), so every tab keeps the status context. */}
- - {/* ★ STATUS HERO — the single home for case status (INV-IA1/INV-UI7) */} - - - {/* documents + agents two-column below the hero (mockup 18b) */} + + {/* documents + agents two-column (mockup 18j) */}
- {/* decision-editor CTA moved to the band actions (visible on all tabs) */}
diff --git a/web-ui/src/components/cases/case-header.tsx b/web-ui/src/components/cases/case-header.tsx index 0b8180a..4bdc18c 100644 --- a/web-ui/src/components/cases/case-header.tsx +++ b/web-ui/src/components/cases/case-header.tsx @@ -2,7 +2,7 @@ import type { ReactNode } from "react"; import Link from "next/link"; import { Badge } from "@/components/ui/badge"; import { StatusBadge } from "@/components/cases/status-badge"; -import { SyncIndicator } from "@/components/cases/sync-indicator"; +import { StatusRail } from "@/components/cases/status-rail"; import { CaseArchiveAction } from "@/components/cases/case-archive-action"; import { CreateRepoButton } from "@/components/cases/create-repo-button"; import { @@ -10,7 +10,6 @@ import { APPEAL_SUBTYPE_LABELS, isBlamSubtype, } from "@/lib/practice-area"; -import { formatDate } from "@/lib/format-date"; import type { CaseDetail } from "@/lib/api/cases"; function partiesLine(data?: CaseDetail): string | null { @@ -29,10 +28,12 @@ function partiesLine(data?: CaseDetail): string | null { * the tab strip inside the band, anchored to its bottom edge. */ export function CaseHeader({ + caseNumber, data, actions, tabs, }: { + caseNumber: string; data?: CaseDetail; actions?: ReactNode; tabs?: ReactNode; @@ -123,25 +124,13 @@ export function CaseHeader({
- {/* metadata strip — hearing date / updated / sync */} -
-
- תאריך דיון -
-
{formatDate(data?.hearing_date)}
-
- עודכן -
-
{formatDate(data?.updated_at)}
-
- סנכרון -
-
-
- {/* tab strip anchored to band bottom (mockup .tabs) */} - {tabs ?
{tabs}
: null} + {/* ★ integrated status rail — visible on every tab (banner A, X17) */} + + + {/* tab strip anchored to band bottom */} + {tabs ?
{tabs}
: null} ); } diff --git a/web-ui/src/components/cases/status-hero.tsx b/web-ui/src/components/cases/status-hero.tsx deleted file mode 100644 index 043d822..0000000 --- a/web-ui/src/components/cases/status-hero.tsx +++ /dev/null @@ -1,153 +0,0 @@ -"use client"; - -import { PHASES, phaseIndexOf, type CaseStatus } from "@/lib/api/case-status"; -import { statusLabel } from "@/lib/api/case-status"; -import { StatusChanger } from "@/components/cases/status-changer"; -import { StatusGuide } from "@/components/cases/status-guide"; -import { AgentStatusWidget } from "@/components/cases/agent-status-widget"; -import { expectedOutcomes } from "@/lib/schemas/case"; -import { formatDate } from "@/lib/format-date"; -import type { CaseDetail } from "@/lib/api/cases"; - -/* - * StatusHero — the single, prominent home for case status (mockup 18b, INV-IA1). - * Renders the horizontal pipeline stepper, the current-stage chip, the manual - * status changer, and a meta strip. This is the ONLY surface in the app that - * owns the status changer / pipeline timeline (status was removed from the - * compose editor and the other case tabs). Reuses the shared StatusChanger, - * StatusGuide and AgentStatusWidget so no parallel status path is created (G2). - */ - -const EXPECTED_OUTCOME_LABELS: Record = Object.fromEntries( - expectedOutcomes.map((o) => [o.value, o.label]), -); - -/** Horizontal RTL stepper over the 5 lifecycle phases (mockup 18b .stepper). */ -function HorizontalStepper({ status }: { status?: CaseStatus }) { - const currentIdx = phaseIndexOf(status); - return ( -
    - {PHASES.map((phase, i) => { - const state = - currentIdx === -1 ? "pending" - : i < currentIdx ? "done" - : i === currentIdx ? "now" - : "pending"; - - const isLast = i === PHASES.length - 1; - - const knobTone = - state === "done" - ? "bg-success [box-shadow:0_0_0_1px_var(--color-success)]" - : state === "now" - ? "bg-gold [box-shadow:0_0_0_2px_var(--color-gold-wash),0_0_0_3px_var(--color-gold)]" - : "bg-rule [box-shadow:0_0_0_1px_var(--color-rule)]"; - - const labelTone = - state === "done" - ? "text-ink-soft" - : state === "now" - ? "text-navy font-bold" - : "text-ink-muted"; - - // connector runs toward the next (earlier-completed) phase; in RTL the - // line sits to the start side. Coloured when this phase is done. - const segTone = state === "done" ? "bg-success" : "bg-rule"; - - return ( -
  1. - {!isLast && ( - - )} - - - {phase.label} - - {state === "now" ? "כעת" : state === "done" ? "✓" : "—"} - - -
  2. - ); - })} -
- ); -} - -export function StatusHero({ - caseNumber, - data, -}: { - caseNumber: string; - data?: CaseDetail; -}) { - const status = data?.status; - const expectedOutcomeLabel = data?.expected_outcome - ? EXPECTED_OUTCOME_LABELS[data.expected_outcome] ?? data.expected_outcome - : null; - - return ( -
- {/* hero top — title + current stage chip + manual changer */} -
-
-

סטטוס התיק

- {status && ( - - כעת: {statusLabel(status)} - - )} -
- {/* manual status changer — the SINGLE manual-status surface (INV-IA1) */} - -
- - {/* horizontal pipeline */} - - - {/* meta strip below the stepper (mockup 18b .hero-meta) */} -
-
- תוצאה צפויה - - {expectedOutcomeLabel ? ( - - {expectedOutcomeLabel} - - ) : ( - לא נקבע - )} - -
-
- תאריך דיון - - {formatDate(data?.hearing_date)} - -
-
- עודכן לאחרונה - - {formatDate(data?.updated_at)} - -
-
- - {/* agents activity + status-map progressive disclosure (INV-IA6) */} -
- - -
-
- ); -} diff --git a/web-ui/src/components/cases/status-rail.tsx b/web-ui/src/components/cases/status-rail.tsx new file mode 100644 index 0000000..6d8aaed --- /dev/null +++ b/web-ui/src/components/cases/status-rail.tsx @@ -0,0 +1,195 @@ +"use client"; + +import { + PHASES, + phaseIndexOf, + phaseOf, + type CaseStatus, +} from "@/lib/api/case-status"; +import { StatusChanger } from "@/components/cases/status-changer"; +import { StatusGuide } from "@/components/cases/status-guide"; +import { AgentStatusWidget } from "@/components/cases/agent-status-widget"; +import { SyncIndicator } from "@/components/cases/sync-indicator"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { expectedOutcomes } from "@/lib/schemas/case"; +import { formatDate } from "@/lib/format-date"; +import { Settings2, Users } from "lucide-react"; +import type { CaseDetail } from "@/lib/api/cases"; + +/* + * StatusRail — the integrated case-status strip that lives inside the parchment + * band (CaseHeader), so it is visible on EVERY tab (banner "A", X17 case-detail + * redesign — supersedes the overview-only StatusHero). One thin row of chips + * (current phase · expected outcome · meta · agents popover · manual changer) + * over the 5-phase pipeline stepper. Reuses the shared StatusChanger / StatusGuide + * / AgentStatusWidget so no parallel status path is created (G2). + */ + +const EXPECTED_OUTCOME_LABELS: Record = Object.fromEntries( + expectedOutcomes.map((o) => [o.value, o.label]), +); + +/** Semantic colour per expected-outcome value (rejection/partial/full/none). */ +const OUTCOME_TONE: Record = { + rejection: "bg-danger-bg text-danger", + partial_acceptance: "bg-warn-bg text-warn", + full_acceptance: "bg-success-bg text-success", +}; + +/** Horizontal RTL stepper over the 5 lifecycle phases (mockup A .stepper). */ +function HorizontalStepper({ status }: { status?: CaseStatus }) { + const currentIdx = phaseIndexOf(status); + return ( +
    + {PHASES.map((phase, i) => { + const state = + currentIdx === -1 ? "pending" + : i < currentIdx ? "done" + : i === currentIdx ? "now" + : "pending"; + + const isLast = i === PHASES.length - 1; + + const knobTone = + state === "done" + ? "bg-success [box-shadow:0_0_0_1px_var(--color-success)]" + : state === "now" + ? "bg-gold [box-shadow:0_0_0_2px_var(--color-gold-wash),0_0_0_3px_var(--color-gold)]" + : "bg-rule [box-shadow:0_0_0_1px_var(--color-rule)]"; + + const labelTone = + state === "done" + ? "text-ink-soft" + : state === "now" + ? "text-navy font-bold" + : "text-ink-muted"; + + const segTone = state === "done" ? "bg-success" : "bg-rule"; + + return ( +
  1. + {!isLast && ( + + )} + + + {phase.label} + +
  2. + ); + })} +
+ ); +} + +const PILL = + "inline-flex items-center gap-1.5 rounded-full border border-rule bg-surface px-3 py-1 text-[0.72rem] font-semibold text-ink-soft hover:bg-parchment transition-colors"; + +export function StatusRail({ + caseNumber, + data, +}: { + caseNumber: string; + data?: CaseDetail; +}) { + const status = data?.status; + const phaseKey = phaseOf(status); + const phaseLabel = phaseKey + ? PHASES.find((p) => p.key === phaseKey)?.label + : null; + const outcome = data?.expected_outcome ?? ""; + const outcomeLabel = outcome + ? EXPECTED_OUTCOME_LABELS[outcome] ?? outcome + : null; + const outcomeTone = outcome + ? OUTCOME_TONE[outcome] ?? "bg-warn-bg text-warn" + : "bg-rule-soft text-ink-muted"; + + return ( +
+ {/* chip row */} +
+ סטטוס + {phaseLabel && ( + + כעת: {phaseLabel} + + )} + + תוצאה צפויה: + + {outcomeLabel ?? "טרם נקבעה"} + + + + + + + + דיון{" "} + + {formatDate(data?.hearing_date)} + + + + עודכן{" "} + + {formatDate(data?.updated_at)} + + + + סנכרון + + + + {/* agents popover */} + + + + + + + + + + {/* manual status changer + status guide popover */} + + + + + +
+ שינוי סטטוס ידני +
+ + +
+
+
+ + {/* pipeline */} + +
+ ); +}