Files
legal-ai/web-ui/src/components/cases/status-changer.tsx
Chaim f1e9103723
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 9s
feat(ui): WS6 — party accordion, full-width compose, status-hero overview (#206)
שלושת שינויי-ה-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>
2026-06-30 12:22:07 +00:00

92 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState } from "react";
import { toast } from "sonner";
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from "@/components/ui/select";
import { Button } from "@/components/ui/button";
import { STATUS_LABELS, STATUS_ICONS } from "@/components/cases/status-badge";
import { useUpdateCase } from "@/lib/api/cases";
import { CASE_STATUSES, type CaseStatus } from "@/lib/api/case-status";
/*
* Dropdown for manually overriding the case status — skip a step
* or revert to an earlier stage. Calls PUT /api/cases/:caseNumber
* with { status: newValue }. The option list is the SSoT lifecycle.
*/
const ALL_STATUSES: readonly CaseStatus[] = CASE_STATUSES;
export function StatusChanger({
caseNumber,
currentStatus,
inline = false,
}: {
caseNumber: string;
currentStatus?: CaseStatus;
/** Compact horizontal layout for the status hero (mockup 18b hero-controls);
* default is the stacked rail layout. */
inline?: boolean;
}) {
// `null` = untouched → the dropdown tracks the live `currentStatus` (which
// arrives async and changes on the 5s poll / external updates). Only an
// explicit pick overrides it, until save resets back to tracking.
const [picked, setPicked] = useState<CaseStatus | null>(null);
const mutate = useUpdateCase(caseNumber);
const effective: CaseStatus | "" = picked ?? currentStatus ?? "";
const canSave = Boolean(effective) && effective !== currentStatus;
const handleSave = async () => {
if (!canSave || !effective) return;
try {
await mutate.mutateAsync({ status: effective });
toast.success(`הסטטוס עודכן ל${STATUS_LABELS[effective]}`);
setPicked(null);
} catch (e) {
toast.error(e instanceof Error ? e.message : "שגיאה בעדכון הסטטוס");
}
};
return (
<div className={inline ? "space-y-0" : "mt-4 border-t border-rule pt-3 space-y-2"}>
{!inline && (
<label className="text-[0.72rem] text-ink-muted block">שינוי סטטוס ידני</label>
)}
<div className="flex items-center gap-2">
<Select
value={effective || "__current__"}
onValueChange={(v) => setPicked(v === "__current__" ? null : v as CaseStatus)}
dir="rtl"
>
<SelectTrigger className="text-[0.75rem] h-8">
<SelectValue />
</SelectTrigger>
<SelectContent>
{ALL_STATUSES.map((s) => {
const Icon = STATUS_ICONS[s];
return (
<SelectItem key={s} value={s} className="text-[0.75rem]">
<span className="inline-flex items-center gap-1.5">
<Icon className="w-3 h-3 shrink-0" />
{STATUS_LABELS[s]}
</span>
</SelectItem>
);
})}
</SelectContent>
</Select>
<Button
size="sm"
variant="outline"
className="h-8 text-[0.72rem] px-3 shrink-0"
disabled={!canSave || mutate.isPending}
onClick={handleSave}
>
{mutate.isPending ? "שומר…" : inline ? "עדכן סטטוס" : "עדכן"}
</Button>
</div>
</div>
);
}