"use client"; import { useMemo, useState } from "react"; import { Trash2, Upload, Pencil, ExternalLink, ChevronDown } from "lucide-react"; import { toast } from "sonner"; import Link from "next/link"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Skeleton } from "@/components/ui/skeleton"; import { useMissingPrecedents, useDeleteMissingPrecedent, CITED_BY_PARTY_LABELS, STATUS_LABELS, type CitedByParty, type MissingPrecedent, type MissingPrecedentStatus, } from "@/lib/api/missing-precedents"; import { MissingPrecedentDetailDrawer } from "./missing-precedent-detail-drawer"; import { formatDateShort as formatDate } from "@/lib/format-date"; /** Status chip — mockup 09 tones (open=warn, uploaded=info, closed=success, * irrelevant=muted). Pill-shaped, whitespace-nowrap. */ function StatusBadge({ status }: { status: MissingPrecedentStatus }) { const variants: Record = { open: "bg-warn-bg text-warn border-transparent", uploaded: "bg-info-bg text-info border-transparent", closed: "bg-success-bg text-success border-transparent", irrelevant: "bg-rule-soft text-ink-muted border-transparent", }; return ( {STATUS_LABELS[status]} ); } /** Citing-party chip — colored by side (mockup 09 source chips). */ function SourceChip({ party }: { party: CitedByParty | null }) { if (!party) return ; const variants: Record = { appellant: "bg-info-bg text-info border-transparent", respondent: "bg-gold-wash text-gold-deep border-rule", committee: "bg-success-bg text-success border-transparent", permit_applicant: "bg-info-bg text-info border-transparent", unknown: "bg-rule-soft text-ink-muted border-transparent", }; return ( {CITED_BY_PARTY_LABELS[party]} ); } const COLS = 7; function TableHeaderRow() { return ( פסיקה נושא תיק צוטט ע״י סטטוס נוצר ); } function TableSkeleton({ cols }: { cols: number }) { return ( <> {Array.from({ length: 4 }).map((_, i) => ( {Array.from({ length: cols }).map((__, j) => ( ))} ))} ); } /** Accordion section meta — groups rows by discovery source (chair's request). * "צוטט ע״י דפנה" (corpus-decision reliance) is the high-value group and opens * by default; the noisy יומון group and the misc "אחר" group start collapsed. */ type SectionKey = "chair" | "digest" | "other"; const SECTION_META: Record< SectionKey, { label: string; title: string; desc: string; chip: string; defaultOpen: boolean } > = { chair: { label: "צוטט ע״י דפנה", title: "פסיקה שהוועדה נסמכת עליה", desc: "מצוטטת בהחלטות-הקורפוס (גרף-הציטוטים)", chip: "bg-success-bg text-success", defaultOpen: true, }, digest: { label: "יומון", title: "זוהתה ביומון יומי", desc: "מצביע, טרם צוטט בהחלטה", chip: "bg-teal-bg text-teal", defaultOpen: false, }, other: { label: "אחר", title: "כתב-טענות / ידני", desc: "צוטט בערר חי או נרשם ידנית", chip: "bg-info-bg text-info", defaultOpen: false, }, }; const SECTION_ORDER: SectionKey[] = ["chair", "digest", "other"]; type Props = { status?: MissingPrecedentStatus | ""; q?: string; legalTopic?: string; }; export function MissingPrecedentsTable({ status, q, legalTopic }: Props) { const [openId, setOpenId] = useState(null); const { data, isPending, error } = useMissingPrecedents({ status: status === "" ? undefined : status, q, legalTopic, // Load the full set so the accordion section counts (chair/digest/other) // reflect the real totals — at limit:200 the page showed only the first // page (e.g. 16 of 106 committee rows) while the header showed the true // count, so the sections didn't sum to it. Backend caps at 2000. limit: 1000, }); const del = useDeleteMissingPrecedent(); const handleDelete = async (mp: MissingPrecedent) => { if (!confirm(`למחוק את הרשומה? ${mp.case_name || mp.citation.slice(0, 60)}...`)) { return; } try { await del.mutateAsync(mp.id); toast.success("הרשומה נמחקה"); } catch (e) { toast.error("מחיקה נכשלה"); console.error(e); } }; /* Partition the current result set by discovery source so each accordion * section renders only its own rows. A row "cited by the committee" (has a * resolved chair from the citation-graph bridge) wins over its raw * discovery_source — that's the group the chair cares about. */ const groups = useMemo(() => { const g: Record = { chair: [], digest: [], other: [] }; for (const mp of data?.items ?? []) { if (mp.cited_by_chairs?.length) g.chair.push(mp); else if (mp.discovery_source === "digest") g.digest.push(mp); else g.other.push(mp); } return g; }, [data]); const renderRow = (mp: MissingPrecedent) => ( setOpenId(mp.id)} > {/* Single line when there's no distinct case_name — the old two-line layout repeated the citation (name fell back to a truncation of the same citation). Show the name row only when it adds information. */} {mp.case_name && mp.case_name.trim() ? ( <>
{mp.case_name}
{mp.citation}
) : (
{mp.citation}
)}
{mp.legal_topic || "—"} {mp.cited_by_decisions?.length ? ( /* committee decision(s) that cite this missing ruling (corpus citation-graph bridge) */ {mp.cited_by_decisions[0]} {mp.cited_by_decisions.length > 1 ? ( +{mp.cited_by_decisions.length - 1} ) : null} ) : mp.cited_in_case_number ? ( e.stopPropagation()} className="text-sm text-navy hover:text-gold-deep inline-flex items-center gap-1" > {mp.cited_in_case_number} ) : ( )} {mp.cited_by_chairs?.length ? ( /* cited by a committee DECISION in the corpus → show the chair who relied on it (e.g. דפנה תמיר). Bridge from precedent_internal_citations; takes priority over the generic discovery-source chips. */ <> {mp.cited_by_chairs[0]} {mp.cited_by_chairs.length > 1 ? (
+{mp.cited_by_chairs.length - 1} יו״ר
) : null} ) : mp.discovery_source === "cited_only" ? ( <> פסיקה בקורפוס {mp.cited_by_precedents?.length ? (
מצוטט ע״י: {mp.cited_by_precedents.join(", ")}
) : null} ) : mp.discovery_source === "digest" ? ( <> יומון {mp.yomon_number ? (
מס׳ {mp.yomon_number}
) : null} ) : ( <> {mp.cited_by_party_name ? (
{mp.cited_by_party_name}
) : null} )}
{mp.linked_case_law_number ? (
↳ {mp.linked_case_law_name || mp.linked_case_law_number}
) : null}
{formatDate(mp.created_at)}
{mp.status === "open" ? ( /* gold "העלה והשלם" CTA (mockup 09 `.btn`) */ ) : ( /* passive "done" label for non-open rows */ {mp.status === "closed" ? "קושר" : STATUS_LABELS[mp.status]} ✓ )} {mp.status !== "open" ? ( ) : null}
); if (error) { return (
{error.message}
); } if (isPending) { return (
); } if (!data?.items.length) { return (
אין פסיקות חסרות בקריטריונים הנוכחיים.
); } return ( <>
{SECTION_ORDER.map((key) => { const items = groups[key]; if (!items.length) return null; const meta = SECTION_META[key]; return (
{meta.label} {meta.title} — {meta.desc} {items.length} {items.map(renderRow)}
); })}
{ if (!open) setOpenId(null); }} /> ); }