feat(precedents): split library into court rulings + appeals committee tables
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m34s
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m34s
- /api/precedent-library now accepts source_kind param (default external_upload) - list_external_case_law returns chair_name/district fields - LibraryListPanel renders two separate tables with appropriate columns - internal_decisions migration: added queue_halachot param to defer extraction - Fixed practice_area mapping from style_corpus (appeals_committee → proper enum) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1982,10 +1982,11 @@ async def list_external_case_law(
|
|||||||
search: str = "",
|
search: str = "",
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
|
source_kind: str = "external_upload",
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
"""List chair-uploaded precedents, with simple filters."""
|
"""List chair-uploaded precedents, with simple filters."""
|
||||||
pool = await get_pool()
|
pool = await get_pool()
|
||||||
conditions = ["source_kind = 'external_upload'"]
|
conditions = [f"source_kind = '{source_kind}'"]
|
||||||
params: list = []
|
params: list = []
|
||||||
idx = 1
|
idx = 1
|
||||||
if practice_area:
|
if practice_area:
|
||||||
@@ -2017,6 +2018,7 @@ async def list_external_case_law(
|
|||||||
SELECT id, case_number, case_name, court, date, practice_area,
|
SELECT id, case_number, case_name, court, date, practice_area,
|
||||||
appeal_subtype, source_type, precedent_level, is_binding,
|
appeal_subtype, source_type, precedent_level, is_binding,
|
||||||
summary, headnote, subject_tags, source_kind,
|
summary, headnote, subject_tags, source_kind,
|
||||||
|
chair_name, district,
|
||||||
extraction_status, halacha_extraction_status,
|
extraction_status, halacha_extraction_status,
|
||||||
metadata_extraction_requested_at,
|
metadata_extraction_requested_at,
|
||||||
halacha_extraction_requested_at,
|
halacha_extraction_requested_at,
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ async def ingest_internal_decision(
|
|||||||
file_path: str | Path | None = None,
|
file_path: str | Path | None = None,
|
||||||
text: str | None = None,
|
text: str | None = None,
|
||||||
document_id: UUID | None = None,
|
document_id: UUID | None = None,
|
||||||
|
queue_halachot: bool = True,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Ingest an appeals-committee decision into the internal corpus.
|
"""Ingest an appeals-committee decision into the internal corpus.
|
||||||
|
|
||||||
@@ -158,6 +159,7 @@ async def ingest_internal_decision(
|
|||||||
|
|
||||||
await db.set_case_law_extraction_status(case_law_id, "completed")
|
await db.set_case_law_extraction_status(case_law_id, "completed")
|
||||||
await db.set_case_law_halacha_status(case_law_id, "pending")
|
await db.set_case_law_halacha_status(case_law_id, "pending")
|
||||||
|
if queue_halachot:
|
||||||
await db.request_halacha_extraction(case_law_id)
|
await db.request_halacha_extraction(case_law_id)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -173,7 +175,7 @@ async def ingest_internal_decision(
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
async def migrate_from_style_corpus(dry_run: bool = False) -> dict:
|
async def migrate_from_style_corpus(dry_run: bool = False, queue_halachot: bool = True) -> dict:
|
||||||
"""Re-index all style_corpus entries as searchable internal committee decisions.
|
"""Re-index all style_corpus entries as searchable internal committee decisions.
|
||||||
|
|
||||||
Does NOT delete style_corpus rows — they remain for style analysis.
|
Does NOT delete style_corpus rows — they remain for style analysis.
|
||||||
@@ -211,16 +213,27 @@ async def migrate_from_style_corpus(dry_run: bool = False) -> dict:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
subject_tags = list(row["subject_categories"] or [])
|
subject_tags = list(row["subject_categories"] or [])
|
||||||
|
raw_pa = row["practice_area"] or ""
|
||||||
|
subtype = row["appeal_subtype"] or ""
|
||||||
|
# style_corpus stores 'appeals_committee' (source_type) instead of practice_area
|
||||||
|
_subtype_to_pa = {
|
||||||
|
"building_permit": "rishuy_uvniya",
|
||||||
|
"betterment_levy": "betterment_levy",
|
||||||
|
"compensation_197": "compensation_197",
|
||||||
|
}
|
||||||
|
practice_area = raw_pa if raw_pa in ("rishuy_uvniya", "betterment_levy", "compensation_197") \
|
||||||
|
else _subtype_to_pa.get(subtype, "")
|
||||||
await ingest_internal_decision(
|
await ingest_internal_decision(
|
||||||
case_number=case_number,
|
case_number=case_number,
|
||||||
court="ועדת הערר לתכנון ובנייה — מחוז ירושלים",
|
court="ועדת הערר לתכנון ובנייה — מחוז ירושלים",
|
||||||
decision_date=row["decision_date"],
|
decision_date=row["decision_date"],
|
||||||
chair_name="דפנה תמיר",
|
chair_name="דפנה תמיר",
|
||||||
district="ירושלים",
|
district="ירושלים",
|
||||||
practice_area=row["practice_area"] or "",
|
practice_area=practice_area,
|
||||||
appeal_subtype=row["appeal_subtype"] or "",
|
appeal_subtype=subtype,
|
||||||
subject_tags=subject_tags,
|
subject_tags=subject_tags,
|
||||||
text=row["full_text"],
|
text=row["full_text"],
|
||||||
|
queue_halachot=queue_halachot,
|
||||||
)
|
)
|
||||||
results["ingested"] += 1
|
results["ingested"] += 1
|
||||||
logger.info("Migrated style_corpus entry: %s", case_number)
|
logger.info("Migrated style_corpus entry: %s", case_number)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
type Precedent,
|
type Precedent,
|
||||||
type PracticeArea,
|
type PracticeArea,
|
||||||
} from "@/lib/api/precedent-library";
|
} from "@/lib/api/precedent-library";
|
||||||
import { PRACTICE_AREAS, PRECEDENT_LEVELS, practiceAreaShort } from "./practice-area";
|
import { PRACTICE_AREAS, practiceAreaShort } from "./practice-area";
|
||||||
import { PrecedentUploadSheet } from "./precedent-upload-sheet";
|
import { PrecedentUploadSheet } from "./precedent-upload-sheet";
|
||||||
import { PrecedentEditSheet } from "./precedent-edit-sheet";
|
import { PrecedentEditSheet } from "./precedent-edit-sheet";
|
||||||
|
|
||||||
@@ -33,17 +33,11 @@ function formatDate(iso: string | null) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The upload form (and Nevo PDFs) embed Unicode bidi marks (RTL/LTR/embedding/
|
|
||||||
* isolate) inside the citation. They render as zero-width but visually push
|
|
||||||
* the text away from the cell edge. Strip them for display only — DB still
|
|
||||||
* has the original. */
|
|
||||||
function cleanCitation(s: string | null | undefined): string {
|
function cleanCitation(s: string | null | undefined): string {
|
||||||
if (!s) return "—";
|
if (!s) return "—";
|
||||||
return s.replace(/[--]/g, "").trim();
|
return s.replace(/[--]/g, "").trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Shimmering pill — used while extraction is actively running.
|
|
||||||
* Visually distinct from the static "queued" / "completed" pills. */
|
|
||||||
function ActivePill({ label }: { label: string }) {
|
function ActivePill({ label }: { label: string }) {
|
||||||
return (
|
return (
|
||||||
<Badge
|
<Badge
|
||||||
@@ -55,12 +49,6 @@ function ActivePill({ label }: { label: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Five distinct states. The "queued" state is what the user actually
|
|
||||||
* sees most of the time (after upload, both extractions are auto-queued
|
|
||||||
* but the local MCP worker hasn't drained them yet); "מחלץ" / "מעבד"
|
|
||||||
* shimmers and only appears while the extractor is actively running.
|
|
||||||
*/
|
|
||||||
function StatusPill({ p }: { p: Precedent }) {
|
function StatusPill({ p }: { p: Precedent }) {
|
||||||
if (p.extraction_status === "failed") {
|
if (p.extraction_status === "failed") {
|
||||||
return (
|
return (
|
||||||
@@ -103,37 +91,26 @@ function StatusPill({ p }: { p: Precedent }) {
|
|||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// halacha_extraction_status === "completed"
|
|
||||||
if (p.halachot_count === 0) {
|
if (p.halachot_count === 0) {
|
||||||
return <Badge variant="outline">ללא הלכות</Badge>;
|
return <Badge variant="outline">ללא הלכות</Badge>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Badge
|
<Badge variant="outline" className="bg-gold-wash text-gold-deep border-gold/40">
|
||||||
variant="outline"
|
|
||||||
className="bg-gold-wash text-gold-deep border-gold/40"
|
|
||||||
>
|
|
||||||
{p.approved_count}/{p.halachot_count} מאושרות
|
{p.approved_count}/{p.halachot_count} מאושרות
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PrecedentRow({
|
function CourtRow({ p, onEdit }: { p: Precedent; onEdit: (id: string) => void }) {
|
||||||
p, onEdit,
|
|
||||||
}: {
|
|
||||||
p: Precedent;
|
|
||||||
onEdit: (id: string) => void;
|
|
||||||
}) {
|
|
||||||
const del = useDeletePrecedent();
|
const del = useDeletePrecedent();
|
||||||
const active = isPrecedentActive(p);
|
const active = isPrecedentActive(p);
|
||||||
|
|
||||||
const onDelete = async () => {
|
const onDelete = async () => {
|
||||||
if (active) {
|
if (active) {
|
||||||
toast.error(
|
toast.error("מתבצע עיבוד — לא ניתן למחוק עכשיו.");
|
||||||
"מתבצע עיבוד — לא ניתן למחוק עכשיו. המתיני לסיום או רעני את הדף.",
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!window.confirm(`למחוק את ${p.case_number}? cascade ימחק את ה-chunks וההלכות.`)) return;
|
if (!window.confirm(`למחוק את ${p.case_number}?`)) return;
|
||||||
try {
|
try {
|
||||||
await del.mutateAsync(p.id);
|
await del.mutateAsync(p.id);
|
||||||
toast.success("נמחק");
|
toast.success("נמחק");
|
||||||
@@ -145,12 +122,6 @@ function PrecedentRow({
|
|||||||
return (
|
return (
|
||||||
<TableRow className="border-rule hover:bg-gold-wash/30 align-top">
|
<TableRow className="border-rule hover:bg-gold-wash/30 align-top">
|
||||||
<TableCell
|
<TableCell
|
||||||
/* shadcn TableCell defaults to whitespace-nowrap which forces the
|
|
||||||
* row wider than the container; for this column we want the long
|
|
||||||
* citation to wrap onto a second line instead of triggering the
|
|
||||||
* horizontal scroll on the table wrapper. min-w/max-w keeps the
|
|
||||||
* column wide enough to avoid awkward 2-word lines while leaving
|
|
||||||
* room for the other columns. */
|
|
||||||
className="font-semibold text-navy text-right whitespace-normal break-words min-w-[280px] max-w-[420px] py-3"
|
className="font-semibold text-navy text-right whitespace-normal break-words min-w-[280px] max-w-[420px] py-3"
|
||||||
dir="rtl"
|
dir="rtl"
|
||||||
>
|
>
|
||||||
@@ -158,49 +129,32 @@ function PrecedentRow({
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-ink whitespace-normal break-words max-w-[260px] py-3">
|
<TableCell className="text-ink whitespace-normal break-words max-w-[260px] py-3">
|
||||||
<div className="font-medium">{cleanCitation(p.case_name)}</div>
|
<div className="font-medium">{cleanCitation(p.case_name)}</div>
|
||||||
{p.court ? (
|
{p.court ? <div className="text-[0.72rem] text-ink-muted">{p.court}</div> : null}
|
||||||
<div className="text-[0.72rem] text-ink-muted">{p.court}</div>
|
|
||||||
) : null}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-ink-muted">
|
|
||||||
{p.date ? formatDate(p.date) : <span className="text-ink-light">—</span>}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell className="text-ink-muted">{p.date ? formatDate(p.date) : <span className="text-ink-light">—</span>}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{p.practice_area ? (
|
{p.practice_area ? (
|
||||||
<Badge variant="outline" className="bg-navy-soft/40 text-navy border-navy/30">
|
<Badge variant="outline" className="bg-navy-soft/40 text-navy border-navy/30">
|
||||||
{practiceAreaShort(p.practice_area)}
|
{practiceAreaShort(p.practice_area)}
|
||||||
</Badge>
|
</Badge>
|
||||||
) : (
|
) : <span className="text-ink-light">—</span>}
|
||||||
<span className="text-ink-light">—</span>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-ink-muted text-[0.78rem]">
|
<TableCell className="text-ink-muted text-[0.78rem]">
|
||||||
{p.precedent_level ? (
|
{p.precedent_level || <span className="text-ink-light">—</span>}
|
||||||
p.precedent_level
|
|
||||||
) : (
|
|
||||||
<span className="text-ink-light">—</span>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<StatusPill p={p} />
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell><StatusPill p={p} /></TableCell>
|
||||||
<TableCell className="text-end">
|
<TableCell className="text-end">
|
||||||
<div className="flex items-center gap-1 justify-end">
|
<div className="flex items-center gap-1 justify-end">
|
||||||
<Button
|
<Button variant="ghost" size="sm" onClick={() => onEdit(p.id)}
|
||||||
variant="ghost" size="sm" onClick={() => onEdit(p.id)}
|
aria-label={`ערוך את ${p.case_number}`} title="ערוך"
|
||||||
aria-label={`ערוך את ${p.case_number}`}
|
className="text-ink-muted hover:text-navy">
|
||||||
title="ערוך פרטים"
|
|
||||||
className="text-ink-muted hover:text-navy"
|
|
||||||
>
|
|
||||||
<Pencil className="w-4 h-4" />
|
<Pencil className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button variant="ghost" size="sm" onClick={onDelete}
|
||||||
variant="ghost" size="sm" onClick={onDelete}
|
|
||||||
disabled={del.isPending || active}
|
disabled={del.isPending || active}
|
||||||
aria-label={`מחק את ${p.case_number}`}
|
aria-label={`מחק את ${p.case_number}`}
|
||||||
title={active ? "מתבצע עיבוד — לא ניתן למחוק" : "מחק"}
|
title={active ? "מתבצע עיבוד — לא ניתן למחוק" : "מחק"}
|
||||||
className="text-danger hover:text-danger hover:bg-danger-bg disabled:opacity-30"
|
className="text-danger hover:text-danger hover:bg-danger-bg disabled:opacity-30">
|
||||||
>
|
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -209,33 +163,112 @@ function PrecedentRow({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CommitteeRow({ p, onEdit }: { p: Precedent; onEdit: (id: string) => void }) {
|
||||||
|
const del = useDeletePrecedent();
|
||||||
|
const active = isPrecedentActive(p);
|
||||||
|
|
||||||
|
const onDelete = async () => {
|
||||||
|
if (active) {
|
||||||
|
toast.error("מתבצע עיבוד — לא ניתן למחוק עכשיו.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!window.confirm(`למחוק את ${p.case_number}?`)) return;
|
||||||
|
try {
|
||||||
|
await del.mutateAsync(p.id);
|
||||||
|
toast.success("נמחק");
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(e instanceof Error ? e.message : "שגיאה");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableRow className="border-rule hover:bg-gold-wash/30 align-top">
|
||||||
|
<TableCell
|
||||||
|
className="font-semibold text-navy text-right whitespace-normal break-words min-w-[200px] max-w-[320px] py-3"
|
||||||
|
dir="rtl"
|
||||||
|
>
|
||||||
|
<span dir="auto">{cleanCitation(p.case_number)}</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-ink whitespace-normal break-words max-w-[220px] py-3">
|
||||||
|
<div className="font-medium">{cleanCitation(p.case_name)}</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-ink-muted text-[0.78rem]">
|
||||||
|
{p.district || <span className="text-ink-light">—</span>}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-ink-muted text-[0.78rem]">
|
||||||
|
{p.chair_name || <span className="text-ink-light">—</span>}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-ink-muted">{p.date ? formatDate(p.date) : <span className="text-ink-light">—</span>}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{p.practice_area ? (
|
||||||
|
<Badge variant="outline" className="bg-navy-soft/40 text-navy border-navy/30">
|
||||||
|
{practiceAreaShort(p.practice_area)}
|
||||||
|
</Badge>
|
||||||
|
) : <span className="text-ink-light">—</span>}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell><StatusPill p={p} /></TableCell>
|
||||||
|
<TableCell className="text-end">
|
||||||
|
<div className="flex items-center gap-1 justify-end">
|
||||||
|
<Button variant="ghost" size="sm" onClick={() => onEdit(p.id)}
|
||||||
|
aria-label={`ערוך את ${p.case_number}`} title="ערוך"
|
||||||
|
className="text-ink-muted hover:text-navy">
|
||||||
|
<Pencil className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="sm" onClick={onDelete}
|
||||||
|
disabled={del.isPending || active}
|
||||||
|
aria-label={`מחק את ${p.case_number}`}
|
||||||
|
title={active ? "מתבצע עיבוד — לא ניתן למחוק" : "מחק"}
|
||||||
|
className="text-danger hover:text-danger hover:bg-danger-bg disabled:opacity-30">
|
||||||
|
<Trash2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableSkeleton({ cols }: { cols: number }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{[...Array(4)].map((_, i) => (
|
||||||
|
<TableRow key={i} className="border-rule">
|
||||||
|
{[...Array(cols)].map((_, j) => (
|
||||||
|
<TableCell key={j}><Skeleton className="h-5 w-full" /></TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function LibraryListPanel() {
|
export function LibraryListPanel() {
|
||||||
const [practiceArea, setPracticeArea] = useState<PracticeArea>("");
|
const [practiceArea, setPracticeArea] = useState<PracticeArea>("");
|
||||||
const [precedentLevel, setPrecedentLevel] = useState("");
|
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [uploadOpen, setUploadOpen] = useState(false);
|
const [uploadOpen, setUploadOpen] = useState(false);
|
||||||
const [editingId, setEditingId] = useState<string | null>(null);
|
const [editingId, setEditingId] = useState<string | null>(null);
|
||||||
|
|
||||||
const { data, isPending, error } = usePrecedents({
|
const sharedFilters = {
|
||||||
practiceArea: practiceArea || undefined,
|
practiceArea: practiceArea || undefined,
|
||||||
precedentLevel: precedentLevel || undefined,
|
|
||||||
search: search.trim() || undefined,
|
search: search.trim() || undefined,
|
||||||
limit: 200,
|
limit: 200,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
const courts = usePrecedents({ ...sharedFilters, sourceKind: "external_upload" });
|
||||||
|
const committee = usePrecedents({ ...sharedFilters, sourceKind: "internal_committee" });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-8">
|
||||||
|
{/* 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="text-[0.78rem] text-ink-muted">חיפוש (מספר תיק / שם / תקציר)</label>
|
<label className="text-[0.78rem] text-ink-muted">חיפוש (מספר תיק / שם)</label>
|
||||||
<Input
|
<Input
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
placeholder="עע"מ 3975/22"
|
placeholder="עע״מ 3975/22 / 1200-25"
|
||||||
dir="rtl"
|
dir="rtl"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-w-[180px]">
|
<div className="min-w-[180px]">
|
||||||
<label className="text-[0.78rem] text-ink-muted">תחום</label>
|
<label className="text-[0.78rem] text-ink-muted">תחום</label>
|
||||||
<Select value={practiceArea || "_all"} onValueChange={(v) => setPracticeArea(v === "_all" ? "" : v as PracticeArea)}>
|
<Select value={practiceArea || "_all"} onValueChange={(v) => setPracticeArea(v === "_all" ? "" : v as PracticeArea)}>
|
||||||
@@ -248,29 +281,25 @@ export function LibraryListPanel() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="min-w-[170px]">
|
|
||||||
<label className="text-[0.78rem] text-ink-muted">רמת תקדים</label>
|
|
||||||
<Select value={precedentLevel || "_all"} onValueChange={(v) => setPrecedentLevel(v === "_all" ? "" : v)}>
|
|
||||||
<SelectTrigger><SelectValue placeholder="הכל" /></SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="_all">הכל</SelectItem>
|
|
||||||
{PRECEDENT_LEVELS.map((l) => (
|
|
||||||
<SelectItem key={l.value} value={l.value}>{l.label}</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button onClick={() => setUploadOpen(true)} className="bg-navy text-parchment hover:bg-navy-soft">
|
<Button onClick={() => setUploadOpen(true)} className="bg-navy text-parchment hover:bg-navy-soft">
|
||||||
<Plus className="w-4 h-4 me-1" />
|
<Plus className="w-4 h-4 me-1" />
|
||||||
העלאת פסיקה
|
העלאת פסיקה
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error ? (
|
{/* Table 1 — Court rulings */}
|
||||||
<div className="rounded bg-danger-bg border border-danger/40 px-6 py-5 text-danger text-center">
|
<section>
|
||||||
{error.message}
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<h3 className="text-base font-semibold text-navy">פסיקת בתי משפט</h3>
|
||||||
|
{courts.data && (
|
||||||
|
<Badge variant="outline" className="text-ink-muted text-[0.72rem]">
|
||||||
|
{courts.data.count}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{courts.error ? (
|
||||||
|
<div className="rounded bg-danger-bg border border-danger/40 px-6 py-4 text-danger text-center text-sm">
|
||||||
|
{courts.error.message}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="rounded-lg border border-rule bg-surface shadow-sm overflow-hidden">
|
<div className="rounded-lg border border-rule bg-surface shadow-sm overflow-hidden">
|
||||||
@@ -287,31 +316,73 @@ export function LibraryListPanel() {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{isPending ? (
|
{courts.isPending ? (
|
||||||
[...Array(5)].map((_, i) => (
|
<TableSkeleton cols={7} />
|
||||||
<TableRow key={i} className="border-rule">
|
) : !courts.data?.items.length ? (
|
||||||
{[...Array(7)].map((_, j) => (
|
|
||||||
<TableCell key={j}>
|
|
||||||
<Skeleton className="h-5 w-full" />
|
|
||||||
</TableCell>
|
|
||||||
))}
|
|
||||||
</TableRow>
|
|
||||||
))
|
|
||||||
) : !data?.items.length ? (
|
|
||||||
<TableRow className="border-rule">
|
<TableRow className="border-rule">
|
||||||
<TableCell colSpan={7} className="text-center text-ink-muted py-10">
|
<TableCell colSpan={7} className="text-center text-ink-muted py-8">
|
||||||
אין פסיקה בקורפוס. העלה את פסק הדין הראשון.
|
אין פסיקת בתי משפט בקורפוס.
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
) : (
|
||||||
data.items.map((p) => (
|
courts.data.items.map((p) => (
|
||||||
<PrecedentRow key={p.id} p={p} onEdit={setEditingId} />
|
<CourtRow key={p.id} p={p} onEdit={setEditingId} />
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Table 2 — Appeals committee decisions */}
|
||||||
|
<section>
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<h3 className="text-base font-semibold text-navy">החלטות ועדות ערר</h3>
|
||||||
|
{committee.data && (
|
||||||
|
<Badge variant="outline" className="text-ink-muted text-[0.72rem]">
|
||||||
|
{committee.data.count}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{committee.error ? (
|
||||||
|
<div className="rounded bg-danger-bg border border-danger/40 px-6 py-4 text-danger text-center text-sm">
|
||||||
|
{committee.error.message}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="rounded-lg border border-rule bg-surface shadow-sm overflow-hidden">
|
||||||
|
<Table>
|
||||||
|
<TableHeader className="bg-rule-soft/60">
|
||||||
|
<TableRow className="border-rule">
|
||||||
|
<TableHead className="text-navy text-right">מספר ערר</TableHead>
|
||||||
|
<TableHead className="text-navy text-right">שם</TableHead>
|
||||||
|
<TableHead className="text-navy text-right">מחוז</TableHead>
|
||||||
|
<TableHead className="text-navy text-right">יו״ר</TableHead>
|
||||||
|
<TableHead className="text-navy text-right">תאריך</TableHead>
|
||||||
|
<TableHead className="text-navy text-right">תחום</TableHead>
|
||||||
|
<TableHead className="text-navy text-right">הלכות</TableHead>
|
||||||
|
<TableHead className="text-navy" />
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{committee.isPending ? (
|
||||||
|
<TableSkeleton cols={8} />
|
||||||
|
) : !committee.data?.items.length ? (
|
||||||
|
<TableRow className="border-rule">
|
||||||
|
<TableCell colSpan={8} className="text-center text-ink-muted py-8">
|
||||||
|
אין החלטות ועדת ערר בקורפוס.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
committee.data.items.map((p) => (
|
||||||
|
<CommitteeRow key={p.id} p={p} onEdit={setEditingId} />
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
<PrecedentUploadSheet open={uploadOpen} onOpenChange={setUploadOpen} />
|
<PrecedentUploadSheet open={uploadOpen} onOpenChange={setUploadOpen} />
|
||||||
<PrecedentEditSheet
|
<PrecedentEditSheet
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ export type Precedent = {
|
|||||||
headnote: string;
|
headnote: string;
|
||||||
subject_tags: string[];
|
subject_tags: string[];
|
||||||
source_kind: string;
|
source_kind: string;
|
||||||
|
chair_name: string | null;
|
||||||
|
district: string | null;
|
||||||
extraction_status: string;
|
extraction_status: string;
|
||||||
halacha_extraction_status: string;
|
halacha_extraction_status: string;
|
||||||
metadata_extraction_requested_at: string | null;
|
metadata_extraction_requested_at: string | null;
|
||||||
@@ -137,6 +139,7 @@ export type ListFilters = {
|
|||||||
court?: string;
|
court?: string;
|
||||||
precedentLevel?: string;
|
precedentLevel?: string;
|
||||||
sourceType?: SourceType;
|
sourceType?: SourceType;
|
||||||
|
sourceKind?: string;
|
||||||
search?: string;
|
search?: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
offset?: number;
|
offset?: number;
|
||||||
@@ -164,6 +167,7 @@ export function usePrecedents(filters: ListFilters = {}) {
|
|||||||
if (filters.court) p.set("court", filters.court);
|
if (filters.court) p.set("court", filters.court);
|
||||||
if (filters.precedentLevel) p.set("precedent_level", filters.precedentLevel);
|
if (filters.precedentLevel) p.set("precedent_level", filters.precedentLevel);
|
||||||
if (filters.sourceType) p.set("source_type", filters.sourceType);
|
if (filters.sourceType) p.set("source_type", filters.sourceType);
|
||||||
|
if (filters.sourceKind) p.set("source_kind", filters.sourceKind);
|
||||||
if (filters.search) p.set("search", filters.search);
|
if (filters.search) p.set("search", filters.search);
|
||||||
if (filters.limit) p.set("limit", String(filters.limit));
|
if (filters.limit) p.set("limit", String(filters.limit));
|
||||||
if (filters.offset) p.set("offset", String(filters.offset));
|
if (filters.offset) p.set("offset", String(filters.offset));
|
||||||
|
|||||||
@@ -4262,13 +4262,15 @@ async def precedent_library_list(
|
|||||||
precedent_level: str = "",
|
precedent_level: str = "",
|
||||||
source_type: str = "",
|
source_type: str = "",
|
||||||
search: str = "",
|
search: str = "",
|
||||||
|
source_kind: str = "external_upload",
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
):
|
):
|
||||||
rows = await db.list_external_case_law(
|
rows = await db.list_external_case_law(
|
||||||
practice_area=practice_area, court=court,
|
practice_area=practice_area, court=court,
|
||||||
precedent_level=precedent_level, source_type=source_type,
|
precedent_level=precedent_level, source_type=source_type,
|
||||||
search=search, limit=limit, offset=offset,
|
search=search, source_kind=source_kind,
|
||||||
|
limit=limit, offset=offset,
|
||||||
)
|
)
|
||||||
return {"items": rows, "count": len(rows)}
|
return {"items": rows, "count": len(rows)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user