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>
This commit is contained in:
@@ -4,18 +4,82 @@ import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useLibraryStats } from "@/lib/api/precedent-library";
|
||||
import { practiceAreaLabel } from "./practice-area";
|
||||
|
||||
function StatCard({ label, value, accent }: { label: string; value: number | string; accent?: boolean }) {
|
||||
const fmt = (n: number) => n.toLocaleString("he-IL");
|
||||
|
||||
function StatCard({ label, value }: { label: string; value: number | string }) {
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
rounded-lg border p-5 bg-surface shadow-sm
|
||||
${accent ? "border-gold bg-gold-wash/40" : "border-rule"}
|
||||
`}
|
||||
>
|
||||
<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 ${accent ? "text-gold-deep" : "text-navy"}`}>
|
||||
{value}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -33,56 +97,43 @@ export function LibraryStatsPanel() {
|
||||
|
||||
if (isPending || !data) {
|
||||
return (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
{[...Array(4)].map((_, i) => <Skeleton key={i} className="h-28 w-full" />)}
|
||||
<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-6">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<StatCard label="פסיקה בקורפוס" value={data.precedents_total} />
|
||||
<StatCard label="הלכות בסך הכל" value={data.halachot_total} />
|
||||
<StatCard
|
||||
label="ממתינות לאישור" value={data.halachot_pending}
|
||||
accent={data.halachot_pending > 0}
|
||||
/>
|
||||
<StatCard label="מאושרות (זמינות לסוכנים)" value={data.halachot_approved} />
|
||||
<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">
|
||||
<div className="rounded-lg border border-rule bg-surface p-5">
|
||||
<h3 className="text-navy font-semibold mb-3">פילוח לפי תחום</h3>
|
||||
{data.by_practice_area.length === 0 ? (
|
||||
<p className="text-ink-muted text-sm">אין נתונים</p>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{data.by_practice_area.map((row) => (
|
||||
<li key={row.practice_area || "—"} className="flex items-center gap-2 text-sm">
|
||||
<span className="text-ink">{practiceAreaLabel(row.practice_area || null)}</span>
|
||||
<span className="ms-auto tabular-nums text-navy font-semibold">{row.count}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-rule bg-surface p-5">
|
||||
<h3 className="text-navy font-semibold mb-3">פילוח לפי רמת תקדים</h3>
|
||||
{data.by_precedent_level.length === 0 ? (
|
||||
<p className="text-ink-muted text-sm">אין נתונים</p>
|
||||
) : (
|
||||
<ul className="space-y-2">
|
||||
{data.by_precedent_level.map((row) => (
|
||||
<li key={row.precedent_level || "—"} className="flex items-center gap-2 text-sm">
|
||||
<span className="text-ink">{row.precedent_level || "—"}</span>
|
||||
<span className="ms-auto tabular-nums text-navy font-semibold">{row.count}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user