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>
224 lines
8.0 KiB
TypeScript
224 lines
8.0 KiB
TypeScript
"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);
|
||
}}
|
||
/>
|
||
</>
|
||
);
|
||
}
|