All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 5s
הבעיה: בדף /missing-precedents לא ניתן היה לאתר פסיקה חסרה לפי מספר ההחלטה החסרה עצמה (למשל 85074). השדה היחיד לחיפוש-תיק עשה get_case_by_number על מספר ה-ערר שבו צוטטה הפסיקה — ולכן הקלדת מספר-הפסיקה החזירה רשימה ריקה, למרות שהרשומה קיימת (ערר (ת"א 85074-04-25) ... status=open). התיקון (הרחבת השדה הקיים, ללא עמוד/שדה חדש — בהנחיית חיים): - db.list_missing_precedents: פרמטר q חדש — ILIKE על mp.citation + mp.case_name + cited-in c.case_number (אינדקס-פרמטר יחיד, additive; שאר הקוראים לא נוגעים). - GET /api/missing-precedents: פרמטר q; case_id/case_number נשארים מסננים-מדויקים לקוראים תכנותיים. - web-ui: התווית "תיק (מספר ערר)" → "מספר תיק", placeholder "85074 או 1017-03-26"; השדה שולח q (חיפוש חופשי) במקום case_number. Debounce 350ms נשמר. api:types לא חודש: ה-hook בונה את ה-querystring ידנית וה-response לא השתנה; חידוש מול prod (שעוד לא נפרס) רק היה מושך drift לא-קשור. בדיקות: tsc --noEmit נקי, eslint נקי על הקבצים שהשתנו, py_compile נקי. Invariants: G2 (הרחבת היכולת הקיימת, לא מסלול-חיפוש מקביל), INV-IA1 (שער/דף יחיד לפסיקה-חסרה — בלי עמוד חדש), §6 (ללא בליעת-שגיאות). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
184 lines
7.3 KiB
TypeScript
184 lines
7.3 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState } from "react";
|
||
import Link from "next/link";
|
||
import { AppShell } from "@/components/app-shell";
|
||
import { Input } from "@/components/ui/input";
|
||
import {
|
||
useMissingPrecedents,
|
||
type MissingPrecedentStatus,
|
||
} from "@/lib/api/missing-precedents";
|
||
import { MissingPrecedentsTable } from "@/components/missing-precedents/missing-precedents-table";
|
||
|
||
/**
|
||
* Missing-precedents page (TaskMaster #35).
|
||
*
|
||
* Surfaces citations that party briefs invoke but which aren't yet in the
|
||
* precedent_library. A status filter (chips) narrows the table; each row uses
|
||
* the same table component. Drawer (sheet) opens on row click with metadata +
|
||
* upload form that routes to internal_decision_upload (ערר/בל"מ citations) or
|
||
* precedent_library_upload (court rulings).
|
||
*/
|
||
|
||
type StatusFilter = MissingPrecedentStatus | "all";
|
||
|
||
const STATUS_CHIPS: { value: StatusFilter; label: string }[] = [
|
||
{ value: "open", label: "פתוח" },
|
||
{ value: "uploaded", label: "הועלה" },
|
||
{ value: "closed", label: "נסגר" },
|
||
{ value: "irrelevant", label: "לא-רלוונטי" },
|
||
{ value: "all", label: "הכל" },
|
||
];
|
||
|
||
export default function MissingPrecedentsPage() {
|
||
const [search, setSearch] = useState("");
|
||
const [legalTopic, setLegalTopic] = useState("");
|
||
const [filter, setFilter] = useState<StatusFilter>("open");
|
||
|
||
/* Debounce the filters so the table fires one query after the user stops
|
||
* typing — not one per keystroke. The search term matches the gap's own
|
||
* מראה-מקום, case name, and cited-in appeal number (server-side ILIKE). */
|
||
const [searchQ, setSearchQ] = useState("");
|
||
const [legalTopicQ, setLegalTopicQ] = useState("");
|
||
useEffect(() => {
|
||
const t = setTimeout(() => setSearchQ(search.trim()), 350);
|
||
return () => clearTimeout(t);
|
||
}, [search]);
|
||
useEffect(() => {
|
||
const t = setTimeout(() => setLegalTopicQ(legalTopic.trim()), 350);
|
||
return () => clearTimeout(t);
|
||
}, [legalTopic]);
|
||
|
||
const counts = useMissingPrecedents({ limit: 1 });
|
||
const byStatus = counts.data?.by_status ?? {};
|
||
|
||
return (
|
||
<AppShell>
|
||
<section className="space-y-6">
|
||
<header className="space-y-3">
|
||
<nav className="text-[0.78rem] text-ink-muted">
|
||
<Link href="/" className="hover:text-gold-deep">בית</Link>
|
||
<span aria-hidden> · </span>
|
||
<span className="text-navy">פסיקה חסרה בקורפוס</span>
|
||
</nav>
|
||
|
||
{/* title + inline open-count pill (mockup 09 `.open-count`) */}
|
||
<div className="flex items-baseline gap-3.5 flex-wrap">
|
||
<h1 className="text-navy mb-0">פסיקה חסרה בקורפוס</h1>
|
||
{byStatus.open ? (
|
||
<span className="inline-flex items-baseline gap-1.5 rounded-lg border border-rule bg-warn-bg px-3.5 py-1">
|
||
<span className="text-lg font-bold text-warn tabular-nums leading-none">
|
||
{byStatus.open}
|
||
</span>
|
||
<span className="text-[0.8rem] text-ink-soft">פתוחים</span>
|
||
</span>
|
||
) : null}
|
||
</div>
|
||
<p className="text-ink-muted text-sm max-w-3xl leading-relaxed">
|
||
פסיקה שצוטטה בכתבי-הטענות אך אינה קיימת בקורפוס. השלמתה מאפשרת
|
||
אימות-הלכה ועיגון-מקור (INV-AH). סוכן המחקר רושם פערים אוטומטית;
|
||
היו"ר סוגר אותם על־ידי העלאת המסמך — ניתוב אוטומטי בין הקורפוס
|
||
הסמכותי (פסקי דין) להחלטות ועדות ערר.
|
||
</p>
|
||
</header>
|
||
|
||
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
|
||
|
||
{/* shared filters */}
|
||
<div className="flex items-end gap-3 flex-wrap">
|
||
<div className="flex-1 min-w-[200px]">
|
||
<label className="block text-[0.78rem] text-ink-muted mb-1.5">מספר תיק</label>
|
||
<Input
|
||
value={search}
|
||
onChange={(e) => setSearch(e.target.value)}
|
||
placeholder="85074 או 1017-03-26"
|
||
dir="rtl"
|
||
/>
|
||
</div>
|
||
<div className="flex-1 min-w-[200px]">
|
||
<label className="block text-[0.78rem] text-ink-muted mb-1.5">נושא משפטי</label>
|
||
<Input
|
||
value={legalTopic}
|
||
onChange={(e) => setLegalTopic(e.target.value)}
|
||
placeholder="זכות עמידה"
|
||
dir="rtl"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* status filter chips (mockup 09 `.filters`) — active = navy filled */}
|
||
<div className="flex items-center gap-2 flex-wrap">
|
||
{STATUS_CHIPS.map((c) => {
|
||
const active = filter === c.value;
|
||
const count =
|
||
c.value === "all"
|
||
? undefined
|
||
: (byStatus[c.value as MissingPrecedentStatus] ?? 0);
|
||
return (
|
||
<button
|
||
key={c.value}
|
||
type="button"
|
||
onClick={() => setFilter(c.value)}
|
||
aria-pressed={active}
|
||
className={`rounded-full border px-4 py-1.5 text-[0.82rem] transition-colors ${
|
||
active
|
||
? "bg-navy text-white border-navy font-semibold"
|
||
: "bg-surface text-ink-soft border-rule font-medium hover:bg-rule-soft/50"
|
||
}`}
|
||
>
|
||
{c.label}
|
||
{count ? (
|
||
<span className="ms-1.5 tabular-nums opacity-80">({count})</span>
|
||
) : null}
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
<MissingPrecedentsTable
|
||
status={filter === "all" ? "" : filter}
|
||
q={searchQ || undefined}
|
||
legalTopic={legalTopicQ || undefined}
|
||
/>
|
||
|
||
{/* lifecycle note (mockup 09 `.lifecycle`) */}
|
||
<div className="rounded-lg border border-rule bg-parchment px-5 py-3.5 text-[0.82rem] text-ink-muted leading-7">
|
||
<b className="text-ink-soft">מחזור-חיים:</b>{" "}
|
||
<LifecycleChip tone="open">פתוח</LifecycleChip> →{" "}
|
||
<LifecycleChip tone="up">הועלה</LifecycleChip> →{" "}
|
||
<LifecycleChip tone="closed">נסגר</LifecycleChip>. פריט נפתח אוטומטית
|
||
בעת חילוץ ציטוט שאין לו תקדים בקורפוס; בהעלאת פסק-הדין הוא מקושר לרשומת
|
||
הפסיקה דרך{" "}
|
||
<code className="rounded border border-rule bg-surface px-1.5 py-0.5 text-[0.75rem] text-gold-deep" dir="ltr">
|
||
linked_case_law_id
|
||
</code>{" "}
|
||
ונסגר. פריט שאינו רלוונטי מסומן{" "}
|
||
<LifecycleChip tone="na">לא-רלוונטי</LifecycleChip> מבלי שתידרש העלאה.
|
||
</div>
|
||
</section>
|
||
</AppShell>
|
||
);
|
||
}
|
||
|
||
type LifecycleTone = "open" | "up" | "closed" | "na";
|
||
|
||
function LifecycleChip({
|
||
tone,
|
||
children,
|
||
}: {
|
||
tone: LifecycleTone;
|
||
children: React.ReactNode;
|
||
}) {
|
||
const cls: Record<LifecycleTone, string> = {
|
||
open: "bg-warn-bg text-warn",
|
||
up: "bg-info-bg text-info",
|
||
closed: "bg-success-bg text-success",
|
||
na: "bg-rule-soft text-ink-muted",
|
||
};
|
||
return (
|
||
<span className={`inline-block rounded-full px-2.5 py-0.5 text-[0.72rem] font-semibold ${cls[tone]}`}>
|
||
{children}
|
||
</span>
|
||
);
|
||
}
|