Files
legal-ai/web-ui/src/app/approvals/page.tsx
Chaim a1245b6b41
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(approvals): רענון מרכז-אישורים — פס-חומרה במקום פיל, תור-נקי מעומעם (#2/#3)
עיצוב-מחדש בכיוון "כרטיסים מרועננים" (מאושר ע"י חיים, Claude Design 01-approvals):
- פס-חומרה דק (3px) בקצה-התחלה של כל כרטיס (border-s לפי severity) במקום
  תג-מספר קטן. הספירה הראשית (המספר הגדול) נשמרת.
- כרטיס תור-נקי מעומעם: רקע parchment, ללא צל, מספר ב-ink-muted, שבב "✓ תור נקי".
- מבנה 2×2 והלוגיקה (usePendingApprovals, samples, CTA) ללא שינוי.

מאושר דרך שער-העיצוב.

Invariant: INV-IA1 — מרכז-אישורים נשאר השער-האנושי-היחיד; שינוי ויזואלי בלבד.

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

221 lines
8.3 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 Link from "next/link";
import { AppShell } from "@/components/app-shell";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import {
usePendingApprovals,
type ApprovalCategory,
type ApprovalSeverity,
} from "@/lib/api/chair";
/**
* מרכז אישורים — דפנה (INV-G10).
*
* עמוד אחד שמרכז את כל השערים האנושיים הממתינים להכרעת היו"ר: אישור הלכות,
* פסיקה חסרה, הערות שטרם יושמו, ותיקים שנכשלו ב-QA. המטרה:
* שאף פריט הדורש את אישורך לא יישכח. הנתונים נשלפים חי מ-/api/chair/pending.
*/
// Severity expressed as a colored dot next to the title (matches the approved
// IA-redesign mockup): high=danger, medium=warn, low=info, ok=success.
const SEVERITY_DOT: Record<ApprovalSeverity, string> = {
high: "bg-danger",
medium: "bg-warn",
low: "bg-info",
ok: "bg-success",
};
// #2 refresh — a thin severity rail on the card's inline-start edge replaces the
// old small count-pill; the big count number stays. high=danger…ok=success.
const SEVERITY_RAIL: Record<ApprovalSeverity, string> = {
high: "border-s-danger",
medium: "border-s-warn",
low: "border-s-info",
ok: "border-s-success",
};
function formatDate(iso?: string | null): string {
if (!iso) return "";
try {
return new Date(iso).toLocaleDateString("he-IL", {
day: "numeric",
month: "long",
year: "numeric",
});
} catch {
return "";
}
}
function ApprovalCard({ cat }: { cat: ApprovalCategory }) {
const cleared = cat.count === 0;
return (
<Card
className={`flex flex-col overflow-hidden rounded-[10px] border-rule border-s-[3px] ${SEVERITY_RAIL[cat.severity]} ${
cleared ? "bg-parchment shadow-none" : "bg-surface shadow-sm"
}`}
>
<CardContent className="p-0 flex flex-col grow">
{/* top row — severity dot · title+age · big count number (mockup 01) */}
<div className="flex items-start gap-3 px-5 pt-5 pb-2">
<span
className={`mt-2 h-2.5 w-2.5 shrink-0 rounded-full ${SEVERITY_DOT[cat.severity]}`}
aria-hidden
/>
<div className="grow min-w-0">
<h2 className="text-navy text-base font-semibold mb-0 leading-snug">{cat.label}</h2>
<div className="text-[0.78rem] text-ink-muted mt-0.5">
{cleared ? (
<span className="inline-block rounded-full bg-success-bg text-success text-[0.72rem] px-2.5 py-0.5 font-semibold">
תור נקי
</span>
) : cat.oldest_at ? (
<>הוותיק ביותר {formatDate(cat.oldest_at)}</>
) : (
cat.description
)}
</div>
</div>
<span
className={`text-3xl font-bold leading-none tabular-nums shrink-0 ${
cleared ? "text-ink-muted" : "text-navy"
}`}
aria-label={`${cat.count} פריטים ממתינים`}
>
{cat.count}
</span>
</div>
{/* description kept (subtle) when the age line took the title slot */}
{!cleared && cat.oldest_at ? (
<p className="px-5 text-[0.8rem] text-ink-muted leading-relaxed mb-0">{cat.description}</p>
) : null}
{cat.extra ? (
<p className="px-5 mt-1 text-[0.78rem] text-ink-muted mb-0">
סך {cat.extra.total} שאילתות · {cat.extra.reviewed} אושרו על־ידך
</p>
) : null}
{!cleared && cat.sample && cat.sample.length > 0 ? (
<ul className="mt-3 px-5 border-t border-rule-soft">
{cat.sample.map((s, i) => {
const row = (
<div className="flex items-start gap-2">
<span className="line-clamp-2 text-ink-soft">{s.text || "—"}</span>
{s.source ? (
<span className="text-ink-muted text-[0.72rem] ms-auto shrink-0 whitespace-nowrap">
{s.source}
</span>
) : null}
</div>
);
return (
<li
key={i}
className="text-[0.82rem] py-2.5 border-b border-rule-soft last:border-b-0"
>
{s.href ? (
<Link href={s.href} className="block hover:text-gold-deep">
{row}
</Link>
) : (
row
)}
</li>
);
})}
</ul>
) : cleared ? (
<p className="px-5 mt-2 text-[0.85rem] text-ink-muted mb-0">
אין פריטים הממתינים להתייחסות. כל התיקים עברו בדיקת-איכות.
</p>
) : null}
{/* foot — gold CTA when actionable, quiet outline when cleared */}
<div className="mt-auto px-5 pt-4 pb-5">
{cat.href ? (
cleared ? (
<Button asChild variant="outline" size="sm" className="border-rule text-ink-muted">
<Link href={cat.href}>צפייה </Link>
</Button>
) : (
<Button
asChild
size="sm"
className="bg-gold text-white hover:bg-gold-deep border-transparent"
>
<Link href={cat.href}>לטיפול </Link>
</Button>
)
) : (
<span className="text-[0.75rem] text-ink-muted">סקירה ידנית (ראה דוח FU-5)</span>
)}
</div>
</CardContent>
</Card>
);
}
export default function ApprovalsPage() {
const { data, isPending, error } = usePendingApprovals();
return (
<AppShell>
<section className="space-y-6">
<header className="space-y-1.5">
<nav className="text-[0.78rem] text-ink-muted mb-1">
<Link href="/" className="hover:text-gold-deep">בית</Link>
<span aria-hidden> · </span>
<span className="text-navy">מרכז אישורים</span>
</nav>
<div className="flex items-end justify-between gap-4 flex-wrap">
<div className="space-y-1">
<div className="text-[0.75rem] uppercase tracking-[0.12em] text-gold-deep">
שערים אנושיים · יו״ר הוועדה
</div>
<h1 className="text-navy mb-0">מרכז אישורים</h1>
<p className="text-ink-muted text-sm mt-1 max-w-2xl leading-relaxed">
כל מה שממתין להכרעתך במקום אחד כדי שאף פריט לא יישכח. מתעדכן חי.
</p>
</div>
{typeof data?.total_pending === "number" ? (
<div className="inline-flex items-baseline gap-2 rounded-lg border border-rule bg-gold-wash px-4 py-2.5">
<span className="text-2xl font-semibold text-gold-deep leading-none tabular-nums">
{data.total_pending}
</span>
<span className="text-[0.85rem] text-ink-soft">פריטים ממתינים</span>
</div>
) : null}
</div>
</header>
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
{error ? (
<Card className="bg-surface border-rule">
<CardContent className="px-6 py-5 text-ink-muted text-sm">
שגיאה בטעינת מרכז האישורים. נסה לרענן.
</CardContent>
</Card>
) : isPending ? (
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
{[0, 1, 2, 3, 4].map((i) => (
<Card key={i} className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5 h-44 animate-pulse" />
</Card>
))}
</div>
) : (
<div className="grid gap-5 md:grid-cols-2 xl:grid-cols-3">
{(data?.categories ?? []).map((cat) => (
<ApprovalCard key={cat.key} cat={cat} />
))}
</div>
)}
</section>
</AppShell>
);
}