A case parked on a status the trimmed PHASES list dropped — but the agents still set, e.g. `analyst_verified` (HEARTBEAT.md), `research_complete` (legal-researcher) — rendered with NO active phase: phaseIndexOf returned -1, so the pipeline stepper went fully blank and the chair couldn't see where the case stood (reported on 1043-02-26, currently at analyst_verified). Add LEGACY_STATUS_PHASE in case-status.ts mapping each legacy/intermediate status to its display phase (analyst_verified/research_complete → "ניתוח וכיוון"), and a single phaseIndexOf() exported from the SSoT. status-hero.tsx and workflow-timeline.tsx both dropped their identical local phaseIndexOf (which had the bug) and import the shared one — no parallel path (G2). Display-only data-mapping fix; the stepper design is unchanged. Does NOT address the root drift (agents still set analyst_verified) — that prompt-alignment is a separate, carefully-sequenced follow-up needing cross-company agent sync. Invariants: G2 (single phase-resolution path, legacy handled in the SSoT, no new parallel logic). No backend / status-model change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
104 lines
3.6 KiB
TypeScript
104 lines
3.6 KiB
TypeScript
"use client";
|
|
|
|
import { PHASES, phaseIndexOf, type CaseStatus, type PhaseKey } from "@/lib/api/case-status";
|
|
import { STATUS_LABELS, STATUS_ICONS, STATUS_DESCRIPTIONS } from "@/components/cases/status-badge";
|
|
import {
|
|
FolderInput, ClipboardList, Brain, PenLine, CheckCircle2,
|
|
} from "lucide-react";
|
|
import type { LucideIcon } from "lucide-react";
|
|
|
|
/*
|
|
* Vertical RTL workflow timeline. Phases + their statuses come from the SSoT
|
|
* (@/lib/api/case-status); only the per-phase icon is a view-layer concern and
|
|
* stays here. The user sees "where am I in the process" rather than micro-steps.
|
|
*/
|
|
|
|
const PHASE_ICONS: Record<PhaseKey, LucideIcon> = {
|
|
intake: FolderInput,
|
|
prep: ClipboardList,
|
|
thinking: Brain,
|
|
writing: PenLine,
|
|
done: CheckCircle2,
|
|
};
|
|
|
|
export function WorkflowTimeline({ status }: { status?: CaseStatus }) {
|
|
const currentIdx = phaseIndexOf(status);
|
|
|
|
return (
|
|
<ol className="relative">
|
|
{PHASES.map((phase, i) => {
|
|
const state =
|
|
currentIdx === -1 ? "pending"
|
|
: i < currentIdx ? "done"
|
|
: i === currentIdx ? "current"
|
|
: "pending";
|
|
|
|
const dotTone =
|
|
state === "done" ? "bg-success [box-shadow:0_0_0_1px_var(--color-success)]"
|
|
: state === "current" ? "bg-gold [box-shadow:0_0_0_1px_var(--color-gold)]"
|
|
: "bg-rule";
|
|
|
|
const labelTone =
|
|
state === "done" ? "text-ink-soft"
|
|
: state === "current" ? "text-navy font-semibold"
|
|
: "text-ink-muted";
|
|
|
|
const iconTone =
|
|
state === "done" ? "text-success"
|
|
: state === "current" ? "text-gold-deep"
|
|
: "text-ink-muted/50";
|
|
|
|
const PhaseIcon = PHASE_ICONS[phase.key];
|
|
const isLast = i === PHASES.length - 1;
|
|
|
|
return (
|
|
<li
|
|
key={phase.key}
|
|
className="relative flex items-center gap-3 py-2"
|
|
>
|
|
{/* connector line below the dot (mockup .tl .line) */}
|
|
{!isLast && (
|
|
<span
|
|
className="absolute top-[26px] w-px h-[20px] bg-rule"
|
|
style={{ insetInlineStart: "5px" }}
|
|
aria-hidden
|
|
/>
|
|
)}
|
|
<span
|
|
className={`inline-block w-[11px] h-[11px] rounded-full border-2 border-surface shrink-0 ${dotTone}`}
|
|
aria-hidden
|
|
/>
|
|
<div className="flex items-center gap-2 grow min-w-0">
|
|
<span className={`text-[0.84rem] flex items-center gap-1.5 ${labelTone}`}>
|
|
<PhaseIcon className={`w-3.5 h-3.5 shrink-0 ${iconTone}`} />
|
|
{phase.label}
|
|
</span>
|
|
<span className="ms-auto text-[0.72rem] text-ink-muted tabular-nums shrink-0">
|
|
{state === "current" ? "כעת" : state === "done" ? "✓" : "—"}
|
|
</span>
|
|
</div>
|
|
</li>
|
|
);
|
|
})}
|
|
{/* current micro-status detail under the active phase */}
|
|
{currentIdx !== -1 && status && (
|
|
<li className="ps-[26px] pt-1">
|
|
<span className="text-[0.72rem] text-gold-deep flex items-center gap-1">
|
|
{STATUS_ICONS[status] &&
|
|
(() => {
|
|
const Icon = STATUS_ICONS[status];
|
|
return <Icon className="w-3 h-3 shrink-0" />;
|
|
})()}
|
|
{STATUS_LABELS[status]}
|
|
</span>
|
|
{STATUS_DESCRIPTIONS[status] && (
|
|
<span className="block text-[0.65rem] text-ink-muted leading-snug mt-0.5">
|
|
{STATUS_DESCRIPTIONS[status]}
|
|
</span>
|
|
)}
|
|
</li>
|
|
)}
|
|
</ol>
|
|
);
|
|
}
|