feat(missing-precedents): חיפוש לפי מראה‑מקום בדף פסיקה‑חסרה (85074 מאתר את הפער) #229
@@ -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)
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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>(
|
||||||
|
|||||||
@@ -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)),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user