Files
legal-ai/web-ui/src/components/precedents/halacha-review-panel.tsx
Chaim 1f1a025509 fix(lint): תיקון 10 שגיאות ESLint + ניקוי directives מיותרים
10 שגיאות (כולן קיימות-מראש, לא מהפיצ'רים האחרונים):
- react/no-unescaped-entities (3): legal-arguments-panel, precedent-edit-sheet
  — escaping של מרכאות ב-JSX (“/")
- react-hooks/set-state-in-effect (6): documents-panel, chair-editor,
  content-checklists, discussion-rules, golden-ratios, documents.ts
  — disable-comment לדפוסי sync/reset לגיטימיים (false-positive ידוע)
- React Compiler reassign (1): subject-donut — refactor לחישוב prefix-sums
  ללא mutable accumulator

ניקוי: הסרת 5 eslint-disable directives מיותרים (halacha-review-panel,
precedent-upload-sheet). תוצאה: 0 errors (היה 10), 24→ warnings (היה 29).

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

805 lines
28 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { useEffect, useMemo, useState } from "react";
import { Check, X, Edit2, ChevronDown, ChevronLeft, AlertTriangle, Clock, RotateCcw } from "lucide-react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Skeleton } from "@/components/ui/skeleton";
import { Textarea } from "@/components/ui/textarea";
import { CorroborationBadge } from "./corroboration-badge";
import { practiceAreaLabel } from "./practice-area";
import {
useHalachotPending, useHalachotByStatus, useUpdateHalacha, useBatchReviewHalachot, type Halacha,
} from "@/lib/api/precedent-library";
/** #81 strict-rubric flags — why an item was held back from auto-approval. */
const QUALITY_FLAG_LABELS: Record<string, string> = {
non_decision: "אי-הכרעה",
truncated_quote: "ציטוט קטוע",
thin_restatement: "ניסוח דק",
quote_unverified: "ציטוט לא מאומת",
nli_unsupported: "כלל לא נגזר מהציטוט",
};
function formatDate(iso: string | null | undefined) {
if (!iso) return "—";
try {
return new Date(iso).toLocaleDateString("he-IL");
} catch {
return iso;
}
}
/* Strip Unicode bidi marks that render as zero-width but shift visual position. */
function cleanCitation(s: string | null | undefined): string {
if (!s) return "—";
return s.replace(/[--]/g, "").trim();
}
const RULE_TYPE_LABELS: Record<string, string> = {
binding: "הלכה מחייבת",
interpretive: "פרשני",
procedural: "פרוצדורלי",
obiter: "אמרת אגב",
application: "יישום הלכה",
persuasive: "משכנע",
};
function ruleTypeLabel(t: string): string {
return RULE_TYPE_LABELS[t] ?? t;
}
type EditState = { rule_statement: string; reasoning_summary: string };
// ─── Pending-queue card (full interactions) ───────────────────────────────────
function HalachaCard({
h, focused, onApprove, onReject, onDefer, onSave,
}: {
h: Halacha;
focused: boolean;
onApprove: () => void;
onReject: () => void;
onDefer: () => void;
onSave: (patch: Partial<EditState>) => Promise<void>;
}) {
const [editing, setEditing] = useState(false);
const [draft, setDraft] = useState<EditState>({
rule_statement: h.rule_statement,
reasoning_summary: h.reasoning_summary,
});
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setDraft({
rule_statement: h.rule_statement,
reasoning_summary: h.reasoning_summary,
});
}, [h.id, h.rule_statement, h.reasoning_summary]);
const onSubmitEdit = async () => {
await onSave(draft);
setEditing(false);
};
return (
<div
data-halacha-id={h.id}
className={`
rounded-lg border bg-surface p-4 space-y-3 transition-colors
${focused ? "border-gold ring-2 ring-gold/40 shadow-md" : "border-rule"}
`}
>
<div className="flex items-start gap-2 text-[0.78rem] text-ink-muted flex-wrap">
{h.page_reference && (
<span className="text-[0.7rem]">{h.page_reference}</span>
)}
<span className="ms-auto flex items-center gap-2">
{h.quote_verified ? (
<Badge variant="outline" className="text-[0.65rem] bg-gold-wash text-gold-deep border-gold/40">
<Check className="w-3 h-3 me-1" /> ציטוט מאומת
</Badge>
) : (
<Badge variant="outline" className="text-[0.65rem] bg-danger-bg text-danger border-danger/40">
<AlertTriangle className="w-3 h-3 me-1" /> ציטוט לא מאומת
</Badge>
)}
<Badge variant="outline" className="text-[0.65rem] tabular-nums">
ביטחון {h.confidence.toFixed(2)}
</Badge>
<Badge variant="outline" className="text-[0.65rem]">
{ruleTypeLabel(h.rule_type)}
</Badge>
<CorroborationBadge halacha={h} />
</span>
</div>
{h.quality_flags && h.quality_flags.length > 0 && (
<div className="flex items-center gap-1.5 flex-wrap">
{h.quality_flags.map((f) => (
<Badge key={f} variant="outline"
className="text-[0.65rem] bg-danger-bg text-danger border-danger/40">
<AlertTriangle className="w-3 h-3 me-1" />
{QUALITY_FLAG_LABELS[f] ?? f}
</Badge>
))}
</div>
)}
<div className="grid md:grid-cols-2 gap-3">
<div>
<div className="text-[0.7rem] text-ink-muted mb-1">ניסוח הכלל</div>
{editing ? (
<Textarea
value={draft.rule_statement} rows={4} dir="rtl"
onChange={(e) => setDraft({ ...draft, rule_statement: e.target.value })}
className="bg-gold-wash/50 border-gold/30"
/>
) : (
<p className="text-navy font-medium leading-relaxed" dir="rtl">
{h.rule_statement}
</p>
)}
</div>
<div>
<div className="text-[0.7rem] text-ink-muted mb-1">ציטוט תומך</div>
<blockquote className="text-ink-soft text-sm leading-relaxed border-r-2 border-gold pr-3" dir="rtl">
&ldquo;{h.supporting_quote}&rdquo;
</blockquote>
</div>
</div>
{(editing || h.reasoning_summary) && (
<div>
<div className="text-[0.7rem] text-ink-muted mb-1">תמצית ההיגיון</div>
{editing ? (
<Textarea
value={draft.reasoning_summary} rows={2} dir="rtl"
onChange={(e) => setDraft({ ...draft, reasoning_summary: e.target.value })}
className="bg-gold-wash/50 border-gold/30"
/>
) : (
<p className="text-ink-soft text-sm leading-relaxed" dir="rtl">
{h.reasoning_summary}
</p>
)}
</div>
)}
<div className="flex items-center gap-2 flex-wrap">
{h.practice_areas?.map((p) => (
<Badge key={p} variant="outline" className="text-[0.65rem] bg-navy-soft/30 text-navy">
{practiceAreaLabel(p)}
</Badge>
))}
{h.subject_tags?.map((t) => (
<Badge key={t} variant="outline" className="text-[0.65rem]">
{t}
</Badge>
))}
</div>
<div className="flex items-center gap-2 justify-end pt-1 border-t border-rule-soft">
{editing ? (
<>
<Button size="sm" variant="ghost" onClick={() => setEditing(false)}>
ביטול
</Button>
<Button size="sm" onClick={onSubmitEdit}
className="bg-navy text-parchment hover:bg-navy-soft">
שמור שינויים
</Button>
</>
) : (
<>
<Button size="sm" variant="ghost" onClick={() => setEditing(true)}>
<Edit2 className="w-3.5 h-3.5 me-1" />
ערוך (E)
</Button>
<Button size="sm" variant="ghost" onClick={onDefer}
className="text-ink-muted hover:text-navy">
<Clock className="w-3.5 h-3.5 me-1" />
דחה למועד (D)
</Button>
<Button size="sm" variant="ghost"
onClick={onReject}
className="text-danger hover:text-danger hover:bg-danger-bg">
<X className="w-3.5 h-3.5 me-1" />
דחה (R)
</Button>
<Button size="sm" onClick={onApprove}
className="bg-gold text-navy hover:bg-gold-deep">
<Check className="w-3.5 h-3.5 me-1" />
אשר (A)
</Button>
</>
)}
</div>
</div>
);
}
// ─── Restore card (rejected / approved tabs) ──────────────────────────────────
function HalachaRestoreCard({
h,
primaryLabel,
primaryAction,
secondaryLabel,
secondaryAction,
}: {
h: Halacha;
primaryLabel: string;
primaryAction: () => void;
secondaryLabel: string;
secondaryAction: () => void;
}) {
return (
<div className="rounded-lg border border-rule bg-surface p-4 space-y-3">
<div className="flex items-start gap-2 text-[0.78rem] text-ink-muted flex-wrap">
{h.page_reference && <span className="text-[0.7rem]">{h.page_reference}</span>}
<span className="ms-auto flex items-center gap-2">
<Badge variant="outline" className="text-[0.65rem] tabular-nums">
ביטחון {h.confidence.toFixed(2)}
</Badge>
<Badge variant="outline" className="text-[0.65rem]">
{ruleTypeLabel(h.rule_type)}
</Badge>
<CorroborationBadge halacha={h} />
</span>
</div>
<div className="grid md:grid-cols-2 gap-3">
<div>
<div className="text-[0.7rem] text-ink-muted mb-1">ניסוח הכלל</div>
<p className="text-navy font-medium leading-relaxed" dir="rtl">
{h.rule_statement}
</p>
</div>
<div>
<div className="text-[0.7rem] text-ink-muted mb-1">ציטוט תומך</div>
<blockquote className="text-ink-soft text-sm leading-relaxed border-r-2 border-gold pr-3" dir="rtl">
&ldquo;{h.supporting_quote}&rdquo;
</blockquote>
</div>
</div>
<div className="flex items-center gap-2 flex-wrap">
{h.practice_areas?.map((p) => (
<Badge key={p} variant="outline" className="text-[0.65rem] bg-navy-soft/30 text-navy">
{practiceAreaLabel(p)}
</Badge>
))}
</div>
<div className="flex items-center gap-2 justify-end pt-1 border-t border-rule-soft">
<Button size="sm" variant="ghost"
onClick={secondaryAction}
className="text-ink-muted hover:text-navy">
<RotateCcw className="w-3.5 h-3.5 me-1" />
{secondaryLabel}
</Button>
<Button size="sm" onClick={primaryAction}
className="bg-gold text-navy hover:bg-gold-deep">
<Check className="w-3.5 h-3.5 me-1" />
{primaryLabel}
</Button>
</div>
</div>
);
}
// ─── Shared group type ────────────────────────────────────────────────────────
type Group = {
caseLawId: string;
caseNumber: string;
court: string;
decisionDate: string | null;
precedentLevel: string;
items: Halacha[];
};
function buildGroups(items: Halacha[]): Group[] {
const map = new Map<string, Group>();
for (const h of items) {
const k = h.case_law_id;
let g = map.get(k);
if (!g) {
g = {
caseLawId: k,
caseNumber: h.case_number ?? "",
court: h.court ?? "",
decisionDate: h.decision_date ?? null,
precedentLevel: h.precedent_level ?? "",
items: [],
};
map.set(k, g);
}
g.items.push(h);
}
for (const g of map.values()) {
g.items.sort((a, b) => a.confidence - b.confidence);
}
return Array.from(map.values()).sort((a, b) => b.items.length - a.items.length);
}
// ─── Restore panel (used for "rejected" and "approved" tabs) ──────────────────
function RestorePanel({
status,
primaryLabel,
getPrimaryStatus,
secondaryLabel,
getSecondaryStatus,
}: {
status: string;
primaryLabel: string;
getPrimaryStatus: () => "approved" | "rejected" | "pending_review";
secondaryLabel: string;
getSecondaryStatus: () => "approved" | "rejected" | "pending_review";
}) {
const { data, isPending, error } = useHalachotByStatus(status);
const update = useUpdateHalacha();
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
const groups = useMemo<Group[]>(
() => buildGroups(data?.items ?? []),
[data],
);
const restore = async (h: Halacha, newStatus: "approved" | "rejected" | "pending_review") => {
try {
await update.mutateAsync({ id: h.id, patch: { review_status: newStatus } });
toast.success("עודכן");
} catch (e) {
toast.error(e instanceof Error ? e.message : "שגיאה");
}
};
const toggleGroup = (caseLawId: string) => {
setExpandedIds((prev) => {
const next = new Set(prev);
if (next.has(caseLawId)) next.delete(caseLawId);
else next.add(caseLawId);
return next;
});
};
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) {
return (
<div className="space-y-3">
{[...Array(3)].map((_, i) => <Skeleton key={i} className="h-24 w-full" />)}
</div>
);
}
if (!groups.length) {
return (
<div className="text-center text-ink-muted py-16">
<p className="text-lg">אין הלכות בסטטוס זה.</p>
</div>
);
}
return (
<div className="space-y-3">
{groups.map((g) => {
const isOpen = expandedIds.has(g.caseLawId);
return (
<div key={g.caseLawId} className="rounded-lg border border-rule bg-surface overflow-hidden">
<button
type="button"
onClick={() => toggleGroup(g.caseLawId)}
className={`
w-full flex items-center gap-3 px-4 py-3 text-right
hover:bg-gold-wash/30 transition-colors
${isOpen ? "bg-gold-wash/40 border-b border-rule" : ""}
`}
aria-expanded={isOpen}
>
{isOpen ? (
<ChevronDown className="w-4 h-4 text-ink-muted shrink-0" />
) : (
<ChevronLeft className="w-4 h-4 text-ink-muted shrink-0" />
)}
<div className="flex-1 min-w-0 text-right">
<div className="font-semibold text-navy truncate">
{cleanCitation(g.caseNumber)}
</div>
<div className="text-[0.72rem] text-ink-muted flex items-center gap-2 flex-wrap mt-0.5">
{g.court && <span>{g.court}</span>}
{g.decisionDate && <span>· {formatDate(g.decisionDate)}</span>}
</div>
</div>
<Badge variant="outline" className="tabular-nums">
{g.items.length}
</Badge>
</button>
{isOpen && (
<div className="p-4 space-y-3 bg-rule-soft/20">
{g.items.map((h) => (
<HalachaRestoreCard
key={h.id}
h={h}
primaryLabel={primaryLabel}
primaryAction={() => restore(h, getPrimaryStatus())}
secondaryLabel={secondaryLabel}
secondaryAction={() => restore(h, getSecondaryStatus())}
/>
))}
</div>
)}
</div>
);
})}
</div>
);
}
// ─── Pending queue panel (main review flow) ───────────────────────────────────
function PendingPanel() {
const { data, isPending, error } = useHalachotPending(500);
const update = useUpdateHalacha();
const batch = useBatchReviewHalachot();
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
const [focusedId, setFocusedId] = useState<string | null>(null);
const groups = useMemo<Group[]>(
() => buildGroups(data?.items ?? []),
[data],
);
const totalCount = data?.items.length ?? 0;
const visibleItems = useMemo<Halacha[]>(() => {
const out: Halacha[] = [];
for (const g of groups) {
if (expandedIds.has(g.caseLawId)) out.push(...g.items);
}
return out;
}, [groups, expandedIds]);
useEffect(() => {
if (focusedId === null) return;
if (visibleItems.some((h) => h.id === focusedId)) return;
setFocusedId(visibleItems[0]?.id ?? null);
}, [focusedId, visibleItems]);
useEffect(() => {
if (!focusedId) return;
const el = document.querySelector(`[data-halacha-id="${focusedId}"]`);
el?.scrollIntoView({ block: "nearest", behavior: "smooth" });
}, [focusedId]);
const focused = focusedId
? visibleItems.find((h) => h.id === focusedId) ?? null
: null;
const moveFocus = (delta: 1 | -1) => {
if (visibleItems.length === 0) return;
const idx = focusedId
? visibleItems.findIndex((h) => h.id === focusedId)
: -1;
const next = idx < 0
? (delta > 0 ? 0 : visibleItems.length - 1)
: Math.max(0, Math.min(visibleItems.length - 1, idx + delta));
setFocusedId(visibleItems[next].id);
};
const REVIEW_TOAST: Record<string, string> = {
approved: "אושר", rejected: "נדחה", deferred: "נדחה למועד",
};
const review = async (
h: Halacha,
status: "approved" | "rejected" | "deferred",
extra?: Partial<EditState>,
) => {
try {
await update.mutateAsync({
id: h.id,
patch: { review_status: status, ...extra },
});
toast.success(REVIEW_TOAST[status] ?? "עודכן");
} catch (e) {
toast.error(e instanceof Error ? e.message : "שגיאה");
}
};
const reviewGroup = async (g: Group, status: "approved" | "rejected") => {
try {
const res = await batch.mutateAsync({
ids: g.items.map((h) => h.id), status,
});
toast.success(
`${status === "approved" ? "אושרו" : "נדחו"} ${res.updated} הלכות`,
);
} catch (e) {
toast.error(e instanceof Error ? e.message : "שגיאה");
}
};
const toggleGroup = (caseLawId: string) => {
setExpandedIds((prev) => {
const next = new Set(prev);
if (next.has(caseLawId)) {
next.delete(caseLawId);
} else {
next.add(caseLawId);
const g = groups.find((x) => x.caseLawId === caseLawId);
if (g && g.items.length) {
setTimeout(() => setFocusedId(g.items[0].id), 0);
}
}
return next;
});
};
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
const tag = (e.target as HTMLElement)?.tagName?.toLowerCase();
if (tag === "input" || tag === "textarea") return;
if (e.key === "j") {
e.preventDefault();
moveFocus(1);
} else if (e.key === "k") {
e.preventDefault();
moveFocus(-1);
} else if ((e.key === "a" || e.key === "A") && focused) {
e.preventDefault();
review(focused, "approved");
} else if ((e.key === "r" || e.key === "R") && focused) {
e.preventDefault();
if (window.confirm("לדחות הלכה זו?")) review(focused, "rejected");
} else if ((e.key === "d" || e.key === "D") && focused) {
e.preventDefault();
review(focused, "deferred");
}
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [focused, visibleItems]);
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) {
return (
<div className="space-y-3">
{[...Array(3)].map((_, i) => <Skeleton key={i} className="h-24 w-full" />)}
</div>
);
}
if (!groups.length) {
return (
<div className="text-center text-ink-muted py-16">
<p className="text-lg">אין הלכות הממתינות לאישור.</p>
<p className="text-sm mt-2">העלה פסיקה חדשה ההלכות שיחולצו ממנה יופיעו כאן.</p>
</div>
);
}
return (
<div className="space-y-4">
<div className="flex items-center gap-3 text-sm text-ink-muted flex-wrap">
<span>
<span className="text-navy font-semibold">{totalCount}</span> ממתינות
ב-<span className="text-navy font-semibold">{groups.length}</span> פסיקות
</span>
<span className="me-auto text-[0.72rem]">
ניווט: <kbd className="bg-rule-soft px-1.5 rounded">J</kbd>/<kbd className="bg-rule-soft px-1.5 rounded">K</kbd>
{" "}· אישור: <kbd className="bg-rule-soft px-1.5 rounded">A</kbd>
{" "}· דחייה: <kbd className="bg-rule-soft px-1.5 rounded">R</kbd>
{" "}· למועד: <kbd className="bg-rule-soft px-1.5 rounded">D</kbd>
{" "}· עריכה: <kbd className="bg-rule-soft px-1.5 rounded">E</kbd>
</span>
{expandedIds.size > 0 && (
<Button size="sm" variant="ghost" onClick={() => setExpandedIds(new Set())}>
סגור הכל
</Button>
)}
</div>
<div className="space-y-3">
{groups.map((g) => {
const isOpen = expandedIds.has(g.caseLawId);
return (
<div key={g.caseLawId} className="rounded-lg border border-rule bg-surface overflow-hidden">
<button
type="button"
onClick={() => toggleGroup(g.caseLawId)}
className={`
w-full flex items-center gap-3 px-4 py-3 text-right
hover:bg-gold-wash/30 transition-colors
${isOpen ? "bg-gold-wash/40 border-b border-rule" : ""}
`}
aria-expanded={isOpen}
>
{isOpen ? (
<ChevronDown className="w-4 h-4 text-ink-muted shrink-0" />
) : (
<ChevronLeft className="w-4 h-4 text-ink-muted shrink-0" />
)}
<div className="flex-1 min-w-0 text-right">
<div className="font-semibold text-navy truncate">
{cleanCitation(g.caseNumber)}
</div>
<div className="text-[0.72rem] text-ink-muted flex items-center gap-2 flex-wrap mt-0.5">
{g.court && <span>{g.court}</span>}
{g.decisionDate && <span>· {formatDate(g.decisionDate)}</span>}
{g.precedentLevel && <span>· {g.precedentLevel}</span>}
</div>
</div>
<Badge variant="outline" className="bg-gold-wash text-gold-deep border-gold/40 tabular-nums">
{g.items.length} ממתינות
</Badge>
</button>
{isOpen && (
<div className="p-4 space-y-3 bg-rule-soft/20">
<div className="flex items-center gap-2 justify-end pb-1">
<span className="me-auto text-[0.72rem] text-ink-muted">
פעולה קבוצתית על {g.items.length} ההלכות:
</span>
<Button
size="sm" variant="ghost" disabled={batch.isPending}
onClick={() => {
if (window.confirm(`לדחות את כל ${g.items.length} ההלכות בפסק זה?`))
reviewGroup(g, "rejected");
}}
className="text-danger hover:text-danger hover:bg-danger-bg"
>
<X className="w-3.5 h-3.5 me-1" /> דחה הכל
</Button>
<Button
size="sm" disabled={batch.isPending}
onClick={() => {
if (window.confirm(`לאשר את כל ${g.items.length} ההלכות בפסק זה?`))
reviewGroup(g, "approved");
}}
className="bg-gold text-navy hover:bg-gold-deep"
>
<Check className="w-3.5 h-3.5 me-1" /> אשר הכל
</Button>
</div>
{g.items.map((h) => (
<HalachaCard
key={h.id}
h={h}
focused={h.id === focusedId}
onApprove={() => review(h, "approved")}
onReject={() => {
if (window.confirm("לדחות הלכה זו?")) review(h, "rejected");
}}
onDefer={() => review(h, "deferred")}
onSave={async (patch) => {
try {
await update.mutateAsync({ id: h.id, patch });
toast.success("נשמר");
} catch (e) {
toast.error(e instanceof Error ? e.message : "שגיאה");
}
}}
/>
))}
</div>
)}
</div>
);
})}
</div>
</div>
);
}
// ─── Count badge for tabs ─────────────────────────────────────────────────────
function useHalachaCount(status: string) {
const { data } = useHalachotByStatus(status, 1000);
return data?.count ?? data?.items.length ?? null;
}
// ─── Main export ──────────────────────────────────────────────────────────────
type Tab = "pending" | "rejected" | "approved";
const TAB_LABELS: Record<Tab, string> = {
pending: "ממתינות",
rejected: "נדחו",
approved: "אושרו",
};
export function HalachaReviewPanel() {
const [tab, setTab] = useState<Tab>("pending");
const { data: pendingData } = useHalachotPending(500);
const rejectedCount = useHalachaCount("rejected");
const approvedCount = useHalachaCount("approved");
const pendingCount = pendingData?.items.length ?? null;
const counts: Record<Tab, number | null> = {
pending: pendingCount,
rejected: rejectedCount,
approved: approvedCount,
};
return (
<div className="space-y-4">
{/* Tab bar */}
<div className="flex gap-1 border-b border-rule pb-0">
{(["pending", "rejected", "approved"] as Tab[]).map((t) => (
<button
key={t}
type="button"
onClick={() => setTab(t)}
className={`
px-4 py-2 text-sm font-medium rounded-t-md transition-colors
${tab === t
? "bg-surface border border-b-surface border-rule text-navy -mb-px"
: "text-ink-muted hover:text-navy hover:bg-rule-soft/40"
}
`}
>
{TAB_LABELS[t]}
{counts[t] !== null && counts[t]! > 0 && (
<span className={`
ms-2 text-[0.7rem] px-1.5 py-0.5 rounded-full tabular-nums
${t === "pending"
? "bg-gold-wash text-gold-deep"
: t === "rejected"
? "bg-danger-bg text-danger"
: "bg-navy-soft/20 text-navy"
}
`}>
{counts[t]}
</span>
)}
</button>
))}
</div>
{/* Tab content */}
{tab === "pending" && <PendingPanel />}
{tab === "rejected" && (
<RestorePanel
status="rejected"
primaryLabel="אשר"
getPrimaryStatus={() => "approved"}
secondaryLabel="שחזר לתור"
getSecondaryStatus={() => "pending_review"}
/>
)}
{tab === "approved" && (
<RestorePanel
status="approved"
primaryLabel="בטל אישור"
getPrimaryStatus={() => "pending_review"}
secondaryLabel="דחה"
getSecondaryStatus={() => "rejected"}
/>
)}
</div>
);
}