feat(missing-precedents): חיפוש-טקסט לפי מראה-מקום בדף פסיקה-חסרה
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>
This commit is contained in:
2026-06-12 07:58:29 +00:00
parent 5272ded4f7
commit b49cde7c24
5 changed files with 40 additions and 16 deletions

View File

@@ -6405,10 +6405,16 @@ async def list_missing_precedents(
status: str | None = None, status: str | None = None,
case_id: UUID | None = None, case_id: UUID | None = None,
legal_topic: str | None = None, legal_topic: str | None = None,
q: str | None = None,
limit: int = 200, limit: int = 200,
offset: int = 0, offset: int = 0,
) -> list[dict]: ) -> list[dict]:
"""List missing precedents, joining the cited-in case_number for display.""" """List missing precedents, joining the cited-in case_number for display.
``q`` is a free-text term matched (ILIKE) across the gap's own מראה-מקום
(``mp.citation``), its case name, and the cited-in appeal case number — so a
chair can find a gap by the missing decision's number (e.g. ``85074``), not
only by the appeal it was cited in."""
pool = await get_pool() pool = await get_pool()
conditions: list[str] = [] conditions: list[str] = []
params: list = [] params: list = []
@@ -6425,6 +6431,13 @@ async def list_missing_precedents(
conditions.append(f"mp.legal_topic ILIKE ${idx}") conditions.append(f"mp.legal_topic ILIKE ${idx}")
params.append(f"%{legal_topic}%") params.append(f"%{legal_topic}%")
idx += 1 idx += 1
if q:
conditions.append(
f"(mp.citation ILIKE ${idx} OR mp.case_name ILIKE ${idx} "
f"OR c.case_number ILIKE ${idx})"
)
params.append(f"%{q}%")
idx += 1
where = f"WHERE {' AND '.join(conditions)}" if conditions else "" where = f"WHERE {' AND '.join(conditions)}" if conditions else ""
params.append(limit) params.append(limit)
params.append(offset) params.append(offset)

View File

@@ -31,19 +31,19 @@ const STATUS_CHIPS: { value: StatusFilter; label: string }[] = [
]; ];
export default function MissingPrecedentsPage() { export default function MissingPrecedentsPage() {
const [caseNumber, setCaseNumber] = useState(""); const [search, setSearch] = useState("");
const [legalTopic, setLegalTopic] = useState(""); const [legalTopic, setLegalTopic] = useState("");
const [filter, setFilter] = useState<StatusFilter>("open"); const [filter, setFilter] = useState<StatusFilter>("open");
/* Debounce the filters so the table fires one query after the user stops /* Debounce the filters so the table fires one query after the user stops
* typing — not one per keystroke. Each intermediate value used to * typing — not one per keystroke. The search term matches the gap's own
* round-trip to the API (and a non-existent case number errored mid-typing). */ * מראה-מקום, case name, and cited-in appeal number (server-side ILIKE). */
const [caseNumberQ, setCaseNumberQ] = useState(""); const [searchQ, setSearchQ] = useState("");
const [legalTopicQ, setLegalTopicQ] = useState(""); const [legalTopicQ, setLegalTopicQ] = useState("");
useEffect(() => { useEffect(() => {
const t = setTimeout(() => setCaseNumberQ(caseNumber.trim()), 350); const t = setTimeout(() => setSearchQ(search.trim()), 350);
return () => clearTimeout(t); return () => clearTimeout(t);
}, [caseNumber]); }, [search]);
useEffect(() => { useEffect(() => {
const t = setTimeout(() => setLegalTopicQ(legalTopic.trim()), 350); const t = setTimeout(() => setLegalTopicQ(legalTopic.trim()), 350);
return () => clearTimeout(t); return () => clearTimeout(t);
@@ -87,11 +87,11 @@ export default function MissingPrecedentsPage() {
{/* shared filters */} {/* shared filters */}
<div className="flex items-end gap-3 flex-wrap"> <div className="flex items-end gap-3 flex-wrap">
<div className="flex-1 min-w-[200px]"> <div className="flex-1 min-w-[200px]">
<label className="block text-[0.78rem] text-ink-muted mb-1.5">תיק (מספר ערר)</label> <label className="block text-[0.78rem] text-ink-muted mb-1.5">מספר תיק</label>
<Input <Input
value={caseNumber} value={search}
onChange={(e) => setCaseNumber(e.target.value)} onChange={(e) => setSearch(e.target.value)}
placeholder="1017-03-26" placeholder="85074 או 1017-03-26"
dir="rtl" dir="rtl"
/> />
</div> </div>
@@ -137,7 +137,7 @@ export default function MissingPrecedentsPage() {
<MissingPrecedentsTable <MissingPrecedentsTable
status={filter === "all" ? "" : filter} status={filter === "all" ? "" : filter}
caseNumber={caseNumberQ || undefined} q={searchQ || undefined}
legalTopic={legalTopicQ || undefined} legalTopic={legalTopicQ || undefined}
/> />

View File

@@ -81,15 +81,15 @@ function TableSkeleton({ cols }: { cols: number }) {
type Props = { type Props = {
status?: MissingPrecedentStatus | ""; status?: MissingPrecedentStatus | "";
caseNumber?: string; q?: string;
legalTopic?: string; legalTopic?: string;
}; };
export function MissingPrecedentsTable({ status, caseNumber, legalTopic }: Props) { export function MissingPrecedentsTable({ status, q, legalTopic }: Props) {
const [openId, setOpenId] = useState<string | null>(null); const [openId, setOpenId] = useState<string | null>(null);
const { data, isPending, error } = useMissingPrecedents({ const { data, isPending, error } = useMissingPrecedents({
status: status === "" ? undefined : status, status: status === "" ? undefined : status,
caseNumber, q,
legalTopic, legalTopic,
limit: 200, limit: 200,
}); });

View File

@@ -91,6 +91,9 @@ export type MissingPrecedentFilters = {
caseNumber?: string; caseNumber?: string;
caseId?: string; caseId?: string;
legalTopic?: string; legalTopic?: string;
/** Free-text term — matched across the gap's מראה-מקום, case name, and
* cited-in appeal number (find a gap by the missing decision's number). */
q?: string;
limit?: number; limit?: number;
}; };
@@ -110,6 +113,7 @@ export function useMissingPrecedents(filters: MissingPrecedentFilters = {}) {
if (filters.caseNumber) p.set("case_number", filters.caseNumber); if (filters.caseNumber) p.set("case_number", filters.caseNumber);
if (filters.caseId) p.set("case_id", filters.caseId); if (filters.caseId) p.set("case_id", filters.caseId);
if (filters.legalTopic) p.set("legal_topic", filters.legalTopic); if (filters.legalTopic) p.set("legal_topic", filters.legalTopic);
if (filters.q) p.set("q", filters.q);
if (filters.limit) p.set("limit", String(filters.limit)); if (filters.limit) p.set("limit", String(filters.limit));
const qs = p.toString(); const qs = p.toString();
return apiRequest<MissingPrecedentListResponse>( return apiRequest<MissingPrecedentListResponse>(

View File

@@ -7406,10 +7406,16 @@ async def missing_precedents_list(
case_id: str = "", case_id: str = "",
case_number: str = "", case_number: str = "",
legal_topic: str = "", legal_topic: str = "",
q: str = "",
limit: int = 200, limit: int = 200,
offset: int = 0, offset: int = 0,
): ):
"""List missing precedents, optionally filtered by status / case.""" """List missing precedents, optionally filtered by status / case / text.
``q`` is a free-text term matched across the gap's own מראה-מקום, case name,
and cited-in appeal number — so the chair can find a gap by the missing
decision's number (e.g. 85074), not only by the appeal it was cited in.
``case_id``/``case_number`` remain exact filters for programmatic callers."""
s = status.strip() or None s = status.strip() or None
if s and s not in _ALLOWED_MP_STATUS: if s and s not in _ALLOWED_MP_STATUS:
raise HTTPException(400, f"status לא תקין: {status}") raise HTTPException(400, f"status לא תקין: {status}")
@@ -7444,6 +7450,7 @@ async def missing_precedents_list(
status=s, status=s,
case_id=case_uuid, case_id=case_uuid,
legal_topic=legal_topic.strip() or None, legal_topic=legal_topic.strip() or None,
q=q.strip() or None,
limit=max(1, min(int(limit), 500)), limit=max(1, min(int(limit), 500)),
offset=max(0, int(offset)), offset=max(0, int(offset)),
) )