תפריט הסטטוס-הידני הכיל 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>
66 lines
2.5 KiB
TypeScript
66 lines
2.5 KiB
TypeScript
import { Card, CardContent } from "@/components/ui/card";
|
||
import type { Case } from "@/lib/api/cases";
|
||
import { phaseOf } from "@/lib/api/case-status";
|
||
|
||
type Bucket = {
|
||
label: string;
|
||
caption: string;
|
||
value: number;
|
||
tone: "navy" | "gold" | "warn" | "success";
|
||
};
|
||
|
||
const TONE_STYLES: Record<Bucket["tone"], string> = {
|
||
navy: "before:bg-navy text-navy",
|
||
gold: "before:bg-gold text-gold-deep",
|
||
warn: "before:bg-warn text-warn",
|
||
success: "before:bg-success text-success",
|
||
};
|
||
|
||
function bucketize(cases: Case[] | undefined): Bucket[] {
|
||
const c = cases ?? [];
|
||
/* Buckets derive from the SSoT phases: "בהכנה" = everything from processing
|
||
* through direction-approval (intake-after-new + prep + thinking). */
|
||
const inProgress = c.filter((x) => {
|
||
const p = phaseOf(x.status);
|
||
return x.status === "processing" || p === "prep" || p === "thinking";
|
||
}).length;
|
||
const drafting = c.filter((x) => phaseOf(x.status) === "writing").length;
|
||
const done = c.filter((x) => phaseOf(x.status) === "done").length;
|
||
|
||
return [
|
||
{ label: "סה״כ תיקי ערר", caption: "בכל הסטטוסים", value: c.length, tone: "navy" },
|
||
{ label: "בהכנה", caption: "מסמכים וניתוח", value: inProgress, tone: "gold" },
|
||
{ label: "בכתיבה", caption: "טיוטות ו-QA", value: drafting, tone: "warn" },
|
||
{ label: "מוכנים", caption: "יוצאו או סופיים", value: done, tone: "success" },
|
||
];
|
||
}
|
||
|
||
export function KPICards({ cases, loading }: { cases?: Case[]; loading?: boolean }) {
|
||
const buckets = bucketize(cases);
|
||
|
||
return (
|
||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||
{buckets.map((b) => (
|
||
<Card
|
||
key={b.label}
|
||
className={`
|
||
relative overflow-hidden bg-surface shadow-sm border-rule
|
||
before:content-[''] before:absolute before:top-0 before:right-0 before:h-full before:w-[3px]
|
||
${TONE_STYLES[b.tone]}
|
||
`}
|
||
>
|
||
<CardContent className="px-5 py-3 flex flex-col gap-0.5">
|
||
<span className="text-[0.72rem] uppercase tracking-[0.08em] text-ink-muted">
|
||
{b.label}
|
||
</span>
|
||
<span className="font-display text-[1.7rem] font-black leading-none">
|
||
{loading ? "—" : b.value}
|
||
</span>
|
||
<span className="text-[0.74rem] text-ink-muted">{b.caption}</span>
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|