feat: Stage A finalizers + #35/#36/#37 — critical-gap closure
Some checks failed
Build & Deploy / build-and-deploy (push) Has been cancelled
Some checks failed
Build & Deploy / build-and-deploy (push) Has been cancelled
Four parallel sub-agents closed the remaining critical gaps from the 26/05 Stage A/B sprint. Each block independently tested; aggregated here. ## #30/#31 finalizers (sub-agent A) * Auto-derive practice_area in case_create from case_number prefix (1xxx→rishuy_uvniya, 8xxx→betterment_levy, 9xxx→compensation_197); default for CaseCreateRequest is now "" (the DB constraint catches any stray "appeals_committee"). * practice_area.py: derive_subtype now handles axis-B domain values (rishuy_uvniya/betterment_levy/compensation_197) without parsing the case number; new helper derive_domain_practice_area(). * Halacha re-extraction verified unnecessary — all 6 reclassified records already had is_binding=false and approved halachot. * Regression tests: 6 cases in tests/test_corpus_constraints.py covering practice_area enum, internal-committee chair/district, external-upload arar prefix, MCP guard. * UI: district input → Select dropdown (7 districts) in precedent-edit-sheet.tsx, preserving legacy free-text values. ## #37 בל"מ subtypes (sub-agent B) * 3 new appeal_subtypes: extension_request_{building_permit, betterment_levy,compensation}. APPEALS_COMMITTEE_SUBTYPES extended, SUBTYPES_BY_AREA mappings added. * New helpers: is_blam_subject(), is_blam_subtype(), derive_subtype_with_blam(case_number, subject, practice_area). case_create now uses it to auto-detect "בקשה להארכת מועד" subjects. * 3 methodology templates under docs/methodology/extension-request-*.md. * paperclip_client.py mapping updated for the 3 new subtypes (extension_request_building_permit→CMP, the other two→CMPA). * Frontend: bilingual "בל"מ" badge + filter dropdown on cases list + detail header; appeal-type-bars collapseBlam() merges בל"מ into its parent domain for aggregate bars. * Wizard auto-detects בל"מ from subject during case creation. * 3 Berlinger cases (1017/1018/1019-03-26) migrated to appeal_subtype=extension_request_building_permit via psql. ## #35 missing_precedents feature (sub-agent C) * Schema V13: missing_precedents table (citation, case_id, party, legal_topic, status, linked_case_law_id, claim_quote, ...) + FK constraints + 3 indexes. Applied via psql + idempotent migration. * 6 db.py service functions, 3 MCP tools, 6 FastAPI endpoints (POST/GET/PATCH/DELETE/upload — upload routes by citation prefix to ingest_internal_decision or ingest_precedent). * Next.js page /missing-precedents with 5 status tabs + filters + sidebar badge counter + detail drawer with metadata edit + smart upload form that switches fields per committee/court. * Bootstrap: 7 rows imported from the JSON file (3 citations × cases, all status=closed with linked_case_law_id). * legal-researcher.md: new §2ב.5 with missing_precedent_create usage + dedup semantics + tool grant. ## #36 legal_arguments aggregation (sub-agent D) * Schema V14: legal_arguments + legal_argument_propositions M:M. Applied via psql. * New service argument_aggregator.py with two functions — aggregate_claims_to_arguments() (Claude CLI / claude_session) and get_legal_arguments(). Graceful llm_unavailable handling when CLI is missing (containers). * 2 MCP tools + 2 API endpoints (POST .../aggregate-arguments as BackgroundTask, GET .../legal-arguments). * Frontend: shadcn Accordion + new legal-arguments-panel.tsx with hierarchical (party → priority badge → arguments) display, "טיעונים" tab on the case page, "חשב/חשב מחדש" buttons. * scripts/backfill_legal_arguments.py + SCRIPTS.md entry — dry-run found 8 candidate cases including 1017/1018/1019. ## Open follow-ups (intentionally deferred) * npm run api:types in web-ui (CLAUDE.md flow) — recommended before the next UI commit; not required for backend deployment. * Run backfill_legal_arguments.py --apply once the container picks up the new aggregator service. * webhook on missing-precedents upload-close to Paperclip (optional). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,512 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { Upload, Save, Loader2, CheckCircle2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription,
|
||||
} from "@/components/ui/sheet";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
useMissingPrecedent,
|
||||
useUpdateMissingPrecedent,
|
||||
useUploadMissingPrecedent,
|
||||
CITED_BY_PARTY_LABELS,
|
||||
STATUS_LABELS,
|
||||
type CitedByParty,
|
||||
type MissingPrecedentStatus,
|
||||
type MissingPrecedentPatch,
|
||||
} from "@/lib/api/missing-precedents";
|
||||
import {
|
||||
PRACTICE_AREAS, PRECEDENT_LEVELS, DISTRICTS,
|
||||
} from "@/components/precedents/practice-area";
|
||||
|
||||
type Props = {
|
||||
id: string | null;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
};
|
||||
|
||||
const ACCEPT = ".pdf,.docx,.doc,.rtf,.txt,.md";
|
||||
|
||||
function isCommitteeCitation(citation: string): boolean {
|
||||
const norm = citation.trim();
|
||||
return /^(ערר[\s(]|בל"מ[\s(]|ARAR )/.test(norm);
|
||||
}
|
||||
|
||||
export function MissingPrecedentDetailDrawer({ id, onOpenChange }: Props) {
|
||||
const open = id !== null;
|
||||
const { data: mp, isPending } = useMissingPrecedent(id);
|
||||
const update = useUpdateMissingPrecedent();
|
||||
const upload = useUploadMissingPrecedent();
|
||||
|
||||
// Edit form for metadata.
|
||||
const [legalTopic, setLegalTopic] = useState("");
|
||||
const [legalIssue, setLegalIssue] = useState("");
|
||||
const [cityParty, setCitedByParty] = useState<CitedByParty>("unknown");
|
||||
const [citedByPartyName, setCitedByPartyName] = useState("");
|
||||
const [caseName, setCaseName] = useState("");
|
||||
const [notes, setNotes] = useState("");
|
||||
const [status, setStatus] = useState<MissingPrecedentStatus>("open");
|
||||
|
||||
// Upload form fields.
|
||||
const [file, setFile] = useState<File | null>(null);
|
||||
const [decisionDate, setDecisionDate] = useState("");
|
||||
const [court, setCourt] = useState("");
|
||||
const [practiceArea, setPracticeArea] = useState<string>("");
|
||||
const [appealSubtype, setAppealSubtype] = useState("");
|
||||
const [precedentLevel, setPrecedentLevel] = useState("");
|
||||
const [chairName, setChairName] = useState("");
|
||||
const [district, setDistrict] = useState("");
|
||||
const [committeeCaseNumber, setCommitteeCaseNumber] = useState("");
|
||||
const [summary, setSummary] = useState("");
|
||||
|
||||
// Sync form from record when it loads or id changes.
|
||||
const [syncedId, setSyncedId] = useState<string | null>(null);
|
||||
if (mp && mp.id !== syncedId) {
|
||||
setSyncedId(mp.id);
|
||||
setLegalTopic(mp.legal_topic ?? "");
|
||||
setLegalIssue(mp.legal_issue ?? "");
|
||||
setCitedByParty(mp.cited_by_party ?? "unknown");
|
||||
setCitedByPartyName(mp.cited_by_party_name ?? "");
|
||||
setCaseName(mp.case_name ?? "");
|
||||
setNotes(mp.notes ?? "");
|
||||
setStatus(mp.status);
|
||||
}
|
||||
|
||||
// Reset on close. The cascading-render warning is the intended side
|
||||
// effect here — wiping the form when the drawer closes.
|
||||
useEffect(() => {
|
||||
if (open) return;
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setFile(null);
|
||||
setSyncedId(null);
|
||||
setDecisionDate(""); setCourt(""); setPracticeArea("");
|
||||
setAppealSubtype(""); setPrecedentLevel(""); setChairName("");
|
||||
setDistrict(""); setCommitteeCaseNumber(""); setSummary("");
|
||||
}, [open]);
|
||||
|
||||
const handleSaveMetadata = async () => {
|
||||
if (!mp) return;
|
||||
const patch: MissingPrecedentPatch = {
|
||||
legal_topic: legalTopic,
|
||||
legal_issue: legalIssue,
|
||||
cited_by_party: cityParty,
|
||||
cited_by_party_name: citedByPartyName,
|
||||
case_name: caseName,
|
||||
notes,
|
||||
status,
|
||||
};
|
||||
try {
|
||||
await update.mutateAsync({ id: mp.id, patch });
|
||||
toast.success("הרשומה עודכנה");
|
||||
} catch (e) {
|
||||
toast.error("העדכון נכשל");
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const isCommittee = mp ? isCommitteeCitation(mp.citation) : false;
|
||||
|
||||
const handleUpload = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!mp || !file) {
|
||||
toast.error("בחר קובץ");
|
||||
return;
|
||||
}
|
||||
if (isCommittee && (!chairName.trim() || !district.trim())) {
|
||||
toast.error("החלטת ועדת ערר דורשת שם יו״ר ומחוז");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await upload.mutateAsync({
|
||||
id: mp.id,
|
||||
file,
|
||||
case_number: isCommittee ? committeeCaseNumber || undefined : undefined,
|
||||
chair_name: isCommittee ? chairName : undefined,
|
||||
district: isCommittee ? district : undefined,
|
||||
case_name: caseName || undefined,
|
||||
court: court || undefined,
|
||||
decision_date: decisionDate || undefined,
|
||||
practice_area: practiceArea || undefined,
|
||||
appeal_subtype: appealSubtype || undefined,
|
||||
precedent_level: precedentLevel || undefined,
|
||||
source_type: isCommittee ? "appeals_committee" : "court_ruling",
|
||||
summary: summary || undefined,
|
||||
});
|
||||
toast.success(
|
||||
`הפסיקה נכנסה לקורפוס (${result.route === "internal_committee" ? "ועדת ערר" : "פסק דין"}) והרשומה נסגרה.`,
|
||||
);
|
||||
onOpenChange(false);
|
||||
} catch (e: unknown) {
|
||||
const msg =
|
||||
e instanceof Error
|
||||
? e.message
|
||||
: typeof e === "string"
|
||||
? e
|
||||
: "כשל העלאה";
|
||||
toast.error(msg);
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent
|
||||
side="left"
|
||||
className="w-full sm:max-w-2xl overflow-y-auto"
|
||||
>
|
||||
<SheetHeader className="space-y-1">
|
||||
<SheetTitle className="text-navy">
|
||||
פסיקה חסרה
|
||||
{mp ? (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="ms-2 align-middle"
|
||||
>
|
||||
{STATUS_LABELS[mp.status]}
|
||||
</Badge>
|
||||
) : null}
|
||||
</SheetTitle>
|
||||
<SheetDescription>
|
||||
פרטים מלאים והעלאת הפסיקה לקורפוס.
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
{isPending || !mp ? (
|
||||
<div className="space-y-3 px-6 py-4">
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
<Skeleton className="h-4 w-2/3" />
|
||||
<Skeleton className="h-4 w-1/2" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6 px-6 py-4">
|
||||
{/* ── Citation block (read-only) ── */}
|
||||
<section className="space-y-2">
|
||||
<div className="text-[0.78rem] text-ink-muted">מראה מקום</div>
|
||||
<div className="text-sm text-navy font-medium bg-rule-soft/40 rounded-md px-3 py-2 leading-relaxed">
|
||||
{mp.citation}
|
||||
</div>
|
||||
{mp.claim_quote ? (
|
||||
<>
|
||||
<div className="text-[0.78rem] text-ink-muted mt-3">ציטוט מכתב הטענות</div>
|
||||
<div className="text-xs text-ink bg-gold-wash/30 border-s-2 border-gold rounded-md px-3 py-2 leading-relaxed">
|
||||
{mp.claim_quote}
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
{/* ── Linked record (if closed) ── */}
|
||||
{mp.linked_case_law_id ? (
|
||||
<section className="space-y-1 bg-emerald-50 border border-emerald-200 rounded-lg p-3">
|
||||
<div className="flex items-center gap-2 text-emerald-800 font-medium text-sm">
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
מקושר ל
|
||||
</div>
|
||||
<div className="text-sm text-emerald-900 truncate">
|
||||
{mp.linked_case_law_name || "—"}
|
||||
</div>
|
||||
<div className="text-[0.72rem] text-emerald-700 truncate">
|
||||
{mp.linked_case_law_number}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{/* ── Editable metadata ── */}
|
||||
<section className="space-y-3">
|
||||
<h3 className="text-sm font-semibold text-navy">מטא־דאטה</h3>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label htmlFor="legal_topic">נושא משפטי</Label>
|
||||
<Input
|
||||
id="legal_topic"
|
||||
value={legalTopic}
|
||||
onChange={(e) => setLegalTopic(e.target.value)}
|
||||
placeholder="זכות עמידה"
|
||||
dir="rtl"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="case_name">שם פסיקה</Label>
|
||||
<Input
|
||||
id="case_name"
|
||||
value={caseName}
|
||||
onChange={(e) => setCaseName(e.target.value)}
|
||||
placeholder="אנטרים"
|
||||
dir="rtl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="legal_issue">שאלה משפטית</Label>
|
||||
<Textarea
|
||||
id="legal_issue"
|
||||
value={legalIssue}
|
||||
onChange={(e) => setLegalIssue(e.target.value)}
|
||||
rows={2}
|
||||
dir="rtl"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label htmlFor="cited_by_party">צד מצטט</Label>
|
||||
<Select
|
||||
value={cityParty}
|
||||
onValueChange={(v) => setCitedByParty(v as CitedByParty)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(Object.entries(CITED_BY_PARTY_LABELS) as [CitedByParty, string][]).map(
|
||||
([v, label]) => (
|
||||
<SelectItem key={v} value={v}>{label}</SelectItem>
|
||||
),
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="cited_by_party_name">שם צד</Label>
|
||||
<Input
|
||||
id="cited_by_party_name"
|
||||
value={citedByPartyName}
|
||||
onChange={(e) => setCitedByPartyName(e.target.value)}
|
||||
placeholder="לינדאב בע״מ"
|
||||
dir="rtl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="status">סטטוס</Label>
|
||||
<Select
|
||||
value={status}
|
||||
onValueChange={(v) => setStatus(v as MissingPrecedentStatus)}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(Object.entries(STATUS_LABELS) as [
|
||||
MissingPrecedentStatus,
|
||||
string,
|
||||
][]).map(([v, label]) => (
|
||||
<SelectItem key={v} value={v}>{label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="notes">הערות</Label>
|
||||
<Textarea
|
||||
id="notes"
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
rows={2}
|
||||
dir="rtl"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleSaveMetadata}
|
||||
disabled={update.isPending}
|
||||
variant="outline"
|
||||
className="border-rule"
|
||||
>
|
||||
{update.isPending ? (
|
||||
<Loader2 className="w-4 h-4 me-1 animate-spin" />
|
||||
) : (
|
||||
<Save className="w-4 h-4 me-1" />
|
||||
)}
|
||||
שמור פרטים
|
||||
</Button>
|
||||
</section>
|
||||
|
||||
{/* ── Upload section ── */}
|
||||
{!mp.linked_case_law_id ? (
|
||||
<section className="space-y-3 border-t border-rule pt-5">
|
||||
<h3 className="text-sm font-semibold text-navy">
|
||||
העלאת הפסיקה לקורפוס
|
||||
</h3>
|
||||
<div className="text-[0.78rem] text-ink-muted">
|
||||
ניתוב אוטומטי לפי הציטוט:
|
||||
<strong className="text-navy">
|
||||
{isCommittee ? "החלטת ועדת ערר (internal)" : "פסק דין (library)"}
|
||||
</strong>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleUpload} className="space-y-3">
|
||||
<div>
|
||||
<Label htmlFor="file">קובץ (PDF / DOCX / RTF / TXT / MD)</Label>
|
||||
<Input
|
||||
id="file"
|
||||
type="file"
|
||||
accept={ACCEPT}
|
||||
onChange={(e) => setFile(e.target.files?.[0] ?? null)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label htmlFor="court">ערכאה</Label>
|
||||
<Input
|
||||
id="court"
|
||||
value={court}
|
||||
onChange={(e) => setCourt(e.target.value)}
|
||||
placeholder="בית המשפט העליון"
|
||||
dir="rtl"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="decision_date">תאריך</Label>
|
||||
<Input
|
||||
id="decision_date"
|
||||
type="date"
|
||||
value={decisionDate}
|
||||
onChange={(e) => setDecisionDate(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label htmlFor="practice_area">תחום</Label>
|
||||
<Select value={practiceArea} onValueChange={setPracticeArea}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="ללא" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PRACTICE_AREAS.map((a) => (
|
||||
<SelectItem key={a.value} value={a.value}>
|
||||
{a.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="appeal_subtype">תת־סוג</Label>
|
||||
<Input
|
||||
id="appeal_subtype"
|
||||
value={appealSubtype}
|
||||
onChange={(e) => setAppealSubtype(e.target.value)}
|
||||
placeholder="זכות עמידה"
|
||||
dir="rtl"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isCommittee ? (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label htmlFor="chair_name">
|
||||
יו״ר <span className="text-danger">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="chair_name"
|
||||
value={chairName}
|
||||
onChange={(e) => setChairName(e.target.value)}
|
||||
placeholder="דפנה תמיר"
|
||||
dir="rtl"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="district">
|
||||
מחוז <span className="text-danger">*</span>
|
||||
</Label>
|
||||
<Select value={district} onValueChange={setDistrict}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="בחר" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DISTRICTS.map((d) => (
|
||||
<SelectItem key={d.value} value={d.value}>
|
||||
{d.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="committee_case_number">
|
||||
מספר ערר (לציטוט הקטן)
|
||||
</Label>
|
||||
<Input
|
||||
id="committee_case_number"
|
||||
value={committeeCaseNumber}
|
||||
onChange={(e) => setCommitteeCaseNumber(e.target.value)}
|
||||
placeholder="ערר 1112/22 ..."
|
||||
dir="rtl"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div>
|
||||
<Label htmlFor="precedent_level">רמת תקדים</Label>
|
||||
<Select
|
||||
value={precedentLevel}
|
||||
onValueChange={setPrecedentLevel}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="ללא" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PRECEDENT_LEVELS.map((l) => (
|
||||
<SelectItem key={l.value} value={l.value}>
|
||||
{l.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Label htmlFor="summary">תקציר</Label>
|
||||
<Textarea
|
||||
id="summary"
|
||||
value={summary}
|
||||
onChange={(e) => setSummary(e.target.value)}
|
||||
rows={2}
|
||||
dir="rtl"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!file || upload.isPending}
|
||||
className="bg-navy text-parchment hover:bg-navy-soft"
|
||||
>
|
||||
{upload.isPending ? (
|
||||
<Loader2 className="w-4 h-4 me-1 animate-spin" />
|
||||
) : (
|
||||
<Upload className="w-4 h-4 me-1" />
|
||||
)}
|
||||
העלאה וסגירה
|
||||
</Button>
|
||||
</form>
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Trash2, Upload, Pencil, ExternalLink } 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 MissingPrecedent,
|
||||
type MissingPrecedentStatus,
|
||||
} from "@/lib/api/missing-precedents";
|
||||
import { MissingPrecedentDetailDrawer } from "./missing-precedent-detail-drawer";
|
||||
|
||||
function formatDate(iso: string | null) {
|
||||
if (!iso) return "—";
|
||||
try {
|
||||
return new Date(iso).toLocaleDateString("he-IL");
|
||||
} catch {
|
||||
return iso;
|
||||
}
|
||||
}
|
||||
|
||||
function StatusBadge({ status }: { status: MissingPrecedentStatus }) {
|
||||
const variants: Record<MissingPrecedentStatus, string> = {
|
||||
open: "bg-gold-wash text-gold-deep border-gold/40",
|
||||
uploaded: "bg-rule-soft text-ink-muted border-rule",
|
||||
closed: "bg-emerald-50 text-emerald-800 border-emerald-300/60",
|
||||
irrelevant: "bg-rule-soft text-ink-muted border-rule line-through",
|
||||
};
|
||||
return (
|
||||
<Badge variant="outline" className={variants[status]}>
|
||||
{STATUS_LABELS[status]}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
function TableSkeleton({ cols }: { cols: number }) {
|
||||
return (
|
||||
<>
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<TableRow key={i} className="border-rule">
|
||||
{Array.from({ length: cols }).map((__, j) => (
|
||||
<TableCell key={j}>
|
||||
<Skeleton className="h-4 w-full" />
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type Props = {
|
||||
status?: MissingPrecedentStatus | "";
|
||||
caseNumber?: string;
|
||||
legalTopic?: string;
|
||||
};
|
||||
|
||||
export function MissingPrecedentsTable({ status, caseNumber, legalTopic }: Props) {
|
||||
const [openId, setOpenId] = useState<string | null>(null);
|
||||
const { data, isPending, error } = useMissingPrecedents({
|
||||
status: status === "" ? undefined : status,
|
||||
caseNumber,
|
||||
legalTopic,
|
||||
limit: 200,
|
||||
});
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="rounded bg-danger-bg border border-danger/40 px-6 py-4 text-danger text-center text-sm">
|
||||
{error.message}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<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" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isPending ? (
|
||||
<TableSkeleton cols={7} />
|
||||
) : !data?.items.length ? (
|
||||
<TableRow className="border-rule">
|
||||
<TableCell colSpan={7} className="text-center text-ink-muted py-8">
|
||||
אין פסיקות חסרות בקריטריונים הנוכחיים.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
data.items.map((mp) => (
|
||||
<TableRow
|
||||
key={mp.id}
|
||||
className="border-rule hover:bg-rule-soft/30 cursor-pointer"
|
||||
onClick={() => setOpenId(mp.id)}
|
||||
>
|
||||
<TableCell className="max-w-[440px]">
|
||||
<div className="text-sm text-navy font-medium truncate">
|
||||
{mp.case_name || mp.citation.split(" ").slice(0, 6).join(" ")}
|
||||
</div>
|
||||
<div className="text-[0.72rem] text-ink-muted truncate" dir="rtl">
|
||||
{mp.citation}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="text-sm text-ink">{mp.legal_topic || "—"}</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{mp.cited_in_case_number ? (
|
||||
<Link
|
||||
href={`/cases/${encodeURIComponent(mp.cited_in_case_number)}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="text-sm text-navy hover:text-gold-deep inline-flex items-center gap-1"
|
||||
>
|
||||
{mp.cited_in_case_number}
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-ink-muted text-sm">—</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-ink">
|
||||
{mp.cited_by_party
|
||||
? CITED_BY_PARTY_LABELS[mp.cited_by_party]
|
||||
: "—"}
|
||||
{mp.cited_by_party_name ? (
|
||||
<div className="text-[0.7rem] text-ink-muted truncate max-w-[160px]">
|
||||
{mp.cited_by_party_name}
|
||||
</div>
|
||||
) : null}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status={mp.status} />
|
||||
{mp.linked_case_law_number ? (
|
||||
<div className="text-[0.7rem] text-emerald-700 mt-1">
|
||||
↳ {mp.linked_case_law_name || mp.linked_case_law_number}
|
||||
</div>
|
||||
) : null}
|
||||
</TableCell>
|
||||
<TableCell className="text-[0.78rem] text-ink-muted">
|
||||
{formatDate(mp.created_at)}
|
||||
</TableCell>
|
||||
<TableCell className="text-end">
|
||||
<div className="flex items-center justify-end gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setOpenId(mp.id);
|
||||
}}
|
||||
title={mp.status === "open" ? "העלאה" : "פרטים"}
|
||||
>
|
||||
{mp.status === "open" ? (
|
||||
<Upload className="w-4 h-4" />
|
||||
) : (
|
||||
<Pencil className="w-4 h-4" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDelete(mp);
|
||||
}}
|
||||
disabled={del.isPending}
|
||||
className="text-danger hover:text-danger"
|
||||
title="מחיקה"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<MissingPrecedentDetailDrawer
|
||||
id={openId}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) setOpenId(null);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user