תפריט הסטטוס-הידני הכיל 17 סטטוסים שמתוכם ~9 דקורציה טהורה — שלבי-ביניים שאף קוד בפייפליין לא קבע ושום לוגיקה לא הסתעפה לפיהם, עם רשימות כפולות לא-עקביות ב-6+ קבצים (UI-B1) ו-exported כסטטוס-רפאים (באג agent-audit). הליבה (10): new, processing, documents_ready, outcome_set, direction_approved, qa_review, drafted, exported, reviewed, final. - SSoT חדש web-ui/src/lib/api/case-status.ts (רשימה/שלבים/תוויות/statusLabel); כל הצרכנים (badge/changer/timeline/guide/donut/kpi/compose) מייבאים משם. - statusLabel() מבטיח תווית עברית תמיד — גם לערך-מורשת (נפילה עברית, לא סלאג). - בקאנד: STATUS_ORDER 10, models.CaseStatus מיושר, set_outcome קובע outcome_set/direction_approved (במקום in_progress) כמו endpoint ה-web. - exported מוקשח אחרי export-DOCX מוצלח (forward-only); widget "נכשל ב-QA" עודכן ל-qa_review (הסטטוס שנקבע בפועל בכשל-QA). - scripts/backfill_case_status_trim.py: מיפוי שורות-מורשת לסטטוס-הליבה הקודם. Invariants: UI-B1 (מקור-אמת יחיד) ✅ · G2 (אין מסלול מקביל) ✅ · GAP-42 (חלקי). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
80 lines
2.6 KiB
TypeScript
80 lines
2.6 KiB
TypeScript
"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,
|
||
}: {
|
||
caseNumber: string;
|
||
currentStatus?: CaseStatus;
|
||
}) {
|
||
const [selected, setSelected] = useState<CaseStatus | "">(currentStatus ?? "");
|
||
const mutate = useUpdateCase(caseNumber);
|
||
|
||
const handleSave = async () => {
|
||
if (!selected || selected === currentStatus) return;
|
||
try {
|
||
await mutate.mutateAsync({ status: selected });
|
||
toast.success(`הסטטוס עודכן ל${STATUS_LABELS[selected]}`);
|
||
} catch (e) {
|
||
toast.error(e instanceof Error ? e.message : "שגיאה בעדכון הסטטוס");
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="mt-4 border-t border-rule pt-3 space-y-2">
|
||
<label className="text-[0.72rem] text-ink-muted block">שינוי סטטוס ידני</label>
|
||
<div className="flex items-center gap-2">
|
||
<Select
|
||
value={selected || "__current__"}
|
||
onValueChange={(v) => setSelected(v === "__current__" ? "" : 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={!selected || selected === currentStatus || mutate.isPending}
|
||
onClick={handleSave}
|
||
>
|
||
{mutate.isPending ? "שומר…" : "עדכן"}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|