Phase 3a: shadcn init + Home/Case List view
Initialize shadcn/ui (radix-nova preset) and wire its semantic tokens to the editorial navy/cream/gold palette so primitives inherit the judicial voice without per-component overrides. Replace the Phase 2 live-probe with a real dashboard: KPI tiles, conic-gradient status donut (ported from the vanilla renderHero), and a TanStack Table cases list with search + sort. Add useCase(n) hook with 5s staleTime/refetchInterval to replace the old manual polling loop when Case Detail ships next. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
65
web-ui/src/components/cases/kpi-cards.tsx
Normal file
65
web-ui/src/components/cases/kpi-cards.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import type { Case } from "@/lib/api/cases";
|
||||
|
||||
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 ?? [];
|
||||
const inProgress = c.filter((x) =>
|
||||
["processing", "documents_ready", "outcome_set", "brainstorming", "direction_approved"].includes(x.status),
|
||||
).length;
|
||||
const drafting = c.filter((x) =>
|
||||
["drafting", "qa_review", "drafted"].includes(x.status),
|
||||
).length;
|
||||
const done = c.filter((x) =>
|
||||
["exported", "reviewed", "final"].includes(x.status),
|
||||
).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-4 flex flex-col gap-1">
|
||||
<span className="text-[0.72rem] uppercase tracking-[0.08em] text-ink-muted">
|
||||
{b.label}
|
||||
</span>
|
||||
<span className="font-display text-[2.3rem] font-black leading-none">
|
||||
{loading ? "—" : b.value}
|
||||
</span>
|
||||
<span className="text-[0.78rem] text-ink-muted mt-0.5">{b.caption}</span>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user