feat(ui): WS6 — party accordion, full-width compose, status-hero overview (#206)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 9s

שלושת שינויי-ה-UI של WS6 לפי המוקאפים המאושרים (25b/03b/18b, X17):

1. טאב "טיעונים" (legal-arguments-panel) — אקורדיון ברמת-הצד, סגור
   כברירת-מחדל (type="multiple" ללא defaultValue). כל trigger מציג
   שם-צד + תת-כותרת + באדג'-מונה. בתוך כל צד נשמר הקיבוץ-לפי-קדימות
   והאקורדיון הפנימי ברמת-הטיעון.

2. טאב "עמדות וטענות" (compose) — רוחב-דף מלא: בוטל ה-grid 1fr/320px,
   ראיל "מסמכי התיק" הוסר (DocumentsPanel בסקירה הוא הבעלים היחיד)
   ועבר לפס-מסמכים מתקפל למעלה (סגור כברירת-מחדל). כרטיסי-הסוגיות
   2-up. פסיקה-מצורפת ושערי-הייצוא/העלאה שומרו (relocate, לא remove).
   מפת-StatusChip המקבילה הוסרה → StatusBadge המשותף (G2).

3. סטטוס רק בסקירה (overview) — StatusHero חדש (stepper אופקי + chip
   שלב-נוכחי + שינוי-סטטוס ידני + meta-strip), הבעלים היחיד של
   הסטטוס. WorkflowTimeline/StatusChanger/AgentStatusWidget ירדו משאר
   הטאבים; הטאבים האחרים רוחב-מלא. StatusBadge read-only נשאר ב-H1.

Invariants: INV-IA1 (בעלים-יחיד לסטטוס=hero), INV-IA3/G10 (כל שער-אנוש
נשמר במקום אחד — שינוי-סטטוס בסקירה, ייצוא/העלאה ב-compose),
INV-IA4 (compose=מרחב-עבודה ממוקד-משימה רוחב-מלא), INV-IA6 (מפת-סטטוסים
progressive-disclosure), INV-UI7/UI8, G2 (StatusBadge משותף, אין
מפת-סטטוס מקבילה).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-30 12:22:06 +00:00
parent 4feac756ba
commit f1e9103723
5 changed files with 485 additions and 300 deletions

View File

@@ -0,0 +1,170 @@
"use client";
import { PHASES, 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 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<string, string> = Object.fromEntries(
expectedOutcomes.map((o) => [o.value, o.label]),
);
function formatDate(iso?: string | null): string {
if (!iso) return "—";
try {
return new Date(iso).toLocaleDateString("he-IL", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
} catch {
return iso ?? "—";
}
}
function phaseIndexOf(status?: CaseStatus): number {
if (!status) return -1;
return PHASES.findIndex((p) => p.statuses.includes(status));
}
/** Horizontal RTL stepper over the 5 lifecycle phases (mockup 18b .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";
// 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 (
<li
key={phase.key}
className="relative flex-1 pt-7 text-center"
>
{!isLast && (
<span
className={`absolute top-[9px] h-0.5 w-full ${segTone}`}
style={{ insetInlineStart: "50%" }}
aria-hidden
/>
)}
<span
className={`absolute top-[3px] h-4 w-4 -translate-x-1/2 rounded-full border-[3px] border-surface ${knobTone}`}
style={{ insetInlineStart: "50%", transform: "translateX(50%)" }}
aria-hidden
/>
<span className={`block text-[0.78rem] ${labelTone}`}>
{phase.label}
<span className="block text-[0.68rem] text-ink-muted mt-0.5 tabular-nums">
{state === "now" ? "כעת" : state === "done" ? "✓" : "—"}
</span>
</span>
</li>
);
})}
</ol>
);
}
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 (
<div className="rounded-xl border border-rule bg-surface shadow-sm px-6 py-5">
{/* hero top — title + current stage chip + manual changer */}
<div className="flex items-start justify-between flex-wrap gap-4 mb-4">
<div className="flex items-center gap-3">
<h2 className="text-navy text-base font-bold">סטטוס התיק</h2>
{status && (
<span className="rounded-full bg-warn-bg text-warn text-[0.8rem] font-bold px-3.5 py-1">
כעת: {statusLabel(status)}
</span>
)}
</div>
{/* manual status changer — the SINGLE manual-status surface (INV-IA1) */}
<StatusChanger caseNumber={caseNumber} currentStatus={status} inline />
</div>
{/* horizontal pipeline */}
<HorizontalStepper status={status} />
{/* meta strip below the stepper (mockup 18b .hero-meta) */}
<div className="flex flex-wrap gap-x-8 gap-y-3 mt-5 pt-4 border-t border-rule-soft">
<div className="flex flex-col">
<span className="text-[0.72rem] text-ink-muted">תוצאה צפויה</span>
<span className="text-sm font-semibold mt-0.5">
{expectedOutcomeLabel ? (
<span className="inline-block rounded-full bg-warn-bg text-warn text-[0.78rem] font-semibold px-2.5 py-0.5">
{expectedOutcomeLabel}
</span>
) : (
<span className="text-ink-soft">לא נקבע</span>
)}
</span>
</div>
<div className="flex flex-col">
<span className="text-[0.72rem] text-ink-muted">תאריך דיון</span>
<span className="text-sm font-semibold text-ink-soft mt-0.5 tabular-nums">
{formatDate(data?.hearing_date)}
</span>
</div>
<div className="flex flex-col">
<span className="text-[0.72rem] text-ink-muted">עודכן לאחרונה</span>
<span className="text-sm font-semibold text-ink-soft mt-0.5 tabular-nums">
{formatDate(data?.updated_at)}
</span>
</div>
</div>
{/* agents activity + status-map progressive disclosure (INV-IA6) */}
<div className="mt-4 pt-4 border-t border-rule-soft space-y-3">
<AgentStatusWidget caseNumber={caseNumber} />
<StatusGuide />
</div>
</div>
);
}