Files
legal-ai/web-ui/src/components/precedents/library-stats-panel.tsx
Chaim 37cd28eab6
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 10s
feat(precedents): שיפור טאב-הסטטיסטיקה — משפך-סקירה + פסי-פילוח (#5)
נשאר תחת ספריית-פסיקה (לבקשת חיים) אך משופר:
- שני מדדי-על: פסיקות בקורפוס · הלכות בסך-הכל.
- כרטיס חדש "סטטוס סקירת הלכות" — פס-מוערם מאושרות/ממתינות/נדחו, מנצל את
  halachot_rejected שנחשף ב-/api/precedent-library/stats (PR #273).
- "פילוח לפי תחום" ו"לפי רמת-תקדים" כפסים אופקיים במקום רשימות-טקסט.

מאושר דרך שער-העיצוב (Claude Design 07b-precedents-stats).

Invariant: G2 — צרכן read-only של מקור-הספירה היחיד (precedent_library_stats).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 18:58:20 +00:00

141 lines
5.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 { Skeleton } from "@/components/ui/skeleton";
import { useLibraryStats } from "@/lib/api/precedent-library";
import { practiceAreaLabel } from "./practice-area";
const fmt = (n: number) => n.toLocaleString("he-IL");
function StatCard({ label, value }: { label: string; value: number | string }) {
return (
<div className="rounded-[10px] border border-rule bg-surface p-5 shadow-sm">
<div className="text-[0.78rem] text-ink-muted mb-1">{label}</div>
<div className="text-3xl font-bold tabular-nums text-navy">{value}</div>
</div>
);
}
/** #5 — review-status funnel: one stacked bar (approved / pending / rejected)
* + legend with counts. Surfaces the rejected count now exposed by the stats
* endpoint, so the review pipeline is visible at a glance (mockup 07b). */
function ReviewFunnel({
approved, pending, rejected,
}: { approved: number; pending: number; rejected: number }) {
const total = approved + pending + rejected;
const pct = (n: number) => (total > 0 ? `${(n / total) * 100}%` : "0%");
return (
<div className="rounded-[10px] border border-rule bg-surface p-5 shadow-sm">
<h3 className="text-navy font-semibold mb-3.5">סטטוס סקירת הלכות</h3>
<div className="flex h-[26px] overflow-hidden rounded-md border border-rule">
<span className="block h-full bg-success" style={{ width: pct(approved) }} />
<span className="block h-full bg-warn" style={{ width: pct(pending) }} />
<span className="block h-full bg-danger" style={{ width: pct(rejected) }} />
</div>
<div className="flex flex-wrap gap-x-6 gap-y-2 mt-3.5 text-[0.84rem] text-ink-soft">
<span className="flex items-center gap-2">
<span className="h-3 w-3 shrink-0 rounded-[3px] bg-success" />
מאושרות (זמינות לסוכנים)
<span className="font-bold text-navy tabular-nums">{fmt(approved)}</span>
</span>
<span className="flex items-center gap-2">
<span className="h-3 w-3 shrink-0 rounded-[3px] bg-warn" />
ממתינות לאישור
<span className="font-bold text-navy tabular-nums">{fmt(pending)}</span>
</span>
<span className="flex items-center gap-2">
<span className="h-3 w-3 shrink-0 rounded-[3px] bg-danger" />
נדחו
<span className="font-bold text-navy tabular-nums">{fmt(rejected)}</span>
</span>
</div>
</div>
);
}
/** Distribution rendered as horizontal bars (was a plain count list). */
function DistributionBars({
title, rows,
}: { title: string; rows: { label: string; count: number }[] }) {
const max = Math.max(1, ...rows.map((r) => r.count));
return (
<div className="rounded-[10px] border border-rule bg-surface p-5 shadow-sm">
<h3 className="text-navy font-semibold mb-3.5">{title}</h3>
{rows.length === 0 ? (
<p className="text-ink-muted text-sm">אין נתונים</p>
) : (
<ul className="space-y-2.5">
{rows.map((r) => (
<li key={r.label} className="flex items-center gap-3">
<span className="w-[108px] shrink-0 text-[0.82rem] text-ink-soft">{r.label}</span>
<span className="flex-1 h-3 rounded-full bg-rule-soft overflow-hidden">
<span
className="block h-full rounded-full bg-gold transition-[width] duration-500"
style={{ width: `${(r.count / max) * 100}%` }}
/>
</span>
<span className="w-9 shrink-0 text-start text-[0.82rem] font-semibold text-navy tabular-nums">
{r.count}
</span>
</li>
))}
</ul>
)}
</div>
);
}
export function LibraryStatsPanel() {
const { data, isPending, error } = useLibraryStats();
if (error) {
return (
<div className="rounded bg-danger-bg border border-danger/40 px-6 py-5 text-danger text-center">
{error.message}
</div>
);
}
if (isPending || !data) {
return (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
{[...Array(2)].map((_, i) => <Skeleton key={i} className="h-24 w-full" />)}
</div>
<Skeleton className="h-28 w-full" />
</div>
);
}
return (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<StatCard label="פסיקות בקורפוס" value={fmt(data.precedents_total)} />
<StatCard label="הלכות בסך הכל" value={fmt(data.halachot_total)} />
</div>
<ReviewFunnel
approved={data.halachot_approved}
pending={data.halachot_pending}
rejected={data.halachot_rejected}
/>
<div className="grid md:grid-cols-2 gap-4">
<DistributionBars
title="פילוח לפי תחום"
rows={data.by_practice_area.map((r) => ({
label: practiceAreaLabel(r.practice_area || null),
count: r.count,
}))}
/>
<DistributionBars
title="פילוח לפי רמת-תקדים"
rows={data.by_precedent_level.map((r) => ({
label: r.precedent_level || "לא סווג",
count: r.count,
}))}
/>
</div>
</div>
);
}