Implements the approved X17 case-detail redesign foundation (mockups 18c "A" + 18e "V2"): - New StatusRail component — the integrated status strip (current-phase chip, expected-outcome semantic tag, hearing/updated/sync meta inline, agents Popover, manual-changer Popover) over the 5-phase pipeline stepper. Lives INSIDE the parchment band (CaseHeader), so status is now visible on EVERY tab (supersedes the overview-only StatusHero / reverses the prior INV-IA1 overview-only decision, deliberately). - CaseHeader absorbs the rail and drops its separate hearing/updated/sync dl (those moved into the rail — no duplication). - Tabs restyled to the V2 "segmented" look (each tab a pill, active = raised white card) with per-tab icons. - Overview tab body is now just documents + agent-activity preview (status left the body). - StatusHero deleted — its content moved wholesale into StatusRail, so no parallel status surface remains (G2). Reuses the shared StatusChanger / StatusGuide / AgentStatusWidget verbatim (G2). Design-gate: mockups 18c + 18e approved in Claude Design X17. tsc --noEmit + eslint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
196 lines
6.7 KiB
TypeScript
196 lines
6.7 KiB
TypeScript
"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<string, string> = Object.fromEntries(
|
||
expectedOutcomes.map((o) => [o.value, o.label]),
|
||
);
|
||
|
||
/** Semantic colour per expected-outcome value (rejection/partial/full/none). */
|
||
const OUTCOME_TONE: Record<string, string> = {
|
||
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 (
|
||
<ol className="flex items-start gap-0">
|
||
{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 (
|
||
<li key={phase.key} className="relative flex-1 pt-6 text-center">
|
||
{!isLast && (
|
||
<span
|
||
className={`absolute top-[7px] h-0.5 w-full ${segTone}`}
|
||
style={{ insetInlineStart: "50%" }}
|
||
aria-hidden
|
||
/>
|
||
)}
|
||
<span
|
||
className={`absolute top-[2px] h-3.5 w-3.5 rounded-full border-[2.5px] border-surface ${knobTone}`}
|
||
style={{ insetInlineStart: "50%", transform: "translateX(50%)" }}
|
||
aria-hidden
|
||
/>
|
||
<span className={`block text-[0.74rem] ${labelTone}`}>
|
||
{phase.label}
|
||
</span>
|
||
</li>
|
||
);
|
||
})}
|
||
</ol>
|
||
);
|
||
}
|
||
|
||
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 (
|
||
<div className="mt-3 border-t border-rule-soft pt-3">
|
||
{/* chip row */}
|
||
<div className="flex items-center gap-x-3 gap-y-2 flex-wrap mb-3">
|
||
<span className="text-navy text-[0.8rem] font-bold me-1">סטטוס</span>
|
||
{phaseLabel && (
|
||
<span className="rounded-full bg-warn-bg text-warn text-[0.78rem] font-bold px-3 py-1">
|
||
כעת: {phaseLabel}
|
||
</span>
|
||
)}
|
||
<span className="inline-flex items-center gap-1.5">
|
||
<span className="text-[0.72rem] text-ink-muted">תוצאה צפויה:</span>
|
||
<span
|
||
className={`inline-block rounded-full text-[0.74rem] font-semibold px-2.5 py-0.5 ${outcomeTone}`}
|
||
>
|
||
{outcomeLabel ?? "טרם נקבעה"}
|
||
</span>
|
||
</span>
|
||
|
||
<span className="flex-1" />
|
||
|
||
<span className="flex items-center gap-3 text-[0.72rem] text-ink-muted">
|
||
<span>
|
||
דיון{" "}
|
||
<b className="text-ink-soft font-semibold tabular-nums">
|
||
{formatDate(data?.hearing_date)}
|
||
</b>
|
||
</span>
|
||
<span>
|
||
עודכן{" "}
|
||
<b className="text-ink-soft font-semibold tabular-nums">
|
||
{formatDate(data?.updated_at)}
|
||
</b>
|
||
</span>
|
||
<span className="inline-flex items-center gap-1">
|
||
סנכרון <SyncIndicator caseNumber={data?.case_number} />
|
||
</span>
|
||
</span>
|
||
|
||
{/* agents popover */}
|
||
<Popover>
|
||
<PopoverTrigger asChild>
|
||
<button type="button" className={PILL} title="סוכני התיק">
|
||
<span className="h-1.5 w-1.5 rounded-full bg-success" />
|
||
<Users className="w-3.5 h-3.5" />
|
||
סוכנים
|
||
</button>
|
||
</PopoverTrigger>
|
||
<PopoverContent align="end" className="w-80 p-3">
|
||
<AgentStatusWidget caseNumber={caseNumber} />
|
||
</PopoverContent>
|
||
</Popover>
|
||
|
||
{/* manual status changer + status guide popover */}
|
||
<Popover>
|
||
<PopoverTrigger asChild>
|
||
<button type="button" className={PILL} title="שינוי סטטוס ידני">
|
||
<Settings2 className="w-3.5 h-3.5" />
|
||
שינוי
|
||
</button>
|
||
</PopoverTrigger>
|
||
<PopoverContent align="end" className="w-72 p-3 space-y-3">
|
||
<div className="text-[0.72rem] font-semibold text-ink-muted">
|
||
שינוי סטטוס ידני
|
||
</div>
|
||
<StatusChanger caseNumber={caseNumber} currentStatus={status} inline />
|
||
<StatusGuide />
|
||
</PopoverContent>
|
||
</Popover>
|
||
</div>
|
||
|
||
{/* pipeline */}
|
||
<HorizontalStepper status={status} />
|
||
</div>
|
||
);
|
||
}
|