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>
231 lines
7.5 KiB
TypeScript
231 lines
7.5 KiB
TypeScript
"use client";
|
||
|
||
import { useMemo, useState } from "react";
|
||
import Link from "next/link";
|
||
import {
|
||
flexRender,
|
||
getCoreRowModel,
|
||
getFilteredRowModel,
|
||
getSortedRowModel,
|
||
useReactTable,
|
||
type ColumnDef,
|
||
type SortingState,
|
||
} from "@tanstack/react-table";
|
||
import {
|
||
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
||
} from "@/components/ui/table";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Skeleton } from "@/components/ui/skeleton";
|
||
import { Badge } from "@/components/ui/badge";
|
||
import {
|
||
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
|
||
} from "@/components/ui/select";
|
||
import { StatusBadge } from "@/components/cases/status-badge";
|
||
import { isBlamSubtype } from "@/lib/practice-area";
|
||
import type { Case } from "@/lib/api/cases";
|
||
|
||
function formatDate(iso?: string) {
|
||
if (!iso) return "—";
|
||
try {
|
||
return new Date(iso).toLocaleDateString("he-IL", {
|
||
day: "2-digit",
|
||
month: "2-digit",
|
||
year: "numeric",
|
||
});
|
||
} catch {
|
||
return iso;
|
||
}
|
||
}
|
||
|
||
const columns: ColumnDef<Case>[] = [
|
||
{
|
||
accessorKey: "case_number",
|
||
header: "מס׳ ערר",
|
||
cell: ({ row }) => (
|
||
<Link
|
||
href={`/cases/${row.original.case_number}`}
|
||
className="text-navy font-semibold hover:text-gold-deep tabular-nums"
|
||
>
|
||
{row.original.case_number}
|
||
</Link>
|
||
),
|
||
},
|
||
{
|
||
accessorKey: "title",
|
||
header: "כותרת",
|
||
cell: ({ row }) => (
|
||
<div className="text-ink max-w-[420px] truncate flex items-center gap-2" title={row.original.title}>
|
||
{isBlamSubtype(row.original.appeal_subtype) && (
|
||
<Badge
|
||
variant="outline"
|
||
className="rounded-full px-1.5 py-0 text-[0.65rem] font-bold bg-warn/10 text-warn-deep border-warn/40 shrink-0"
|
||
title="בקשה להארכת מועד להגשת ערר"
|
||
>
|
||
בל"מ
|
||
</Badge>
|
||
)}
|
||
<span className="truncate">{row.original.title}</span>
|
||
</div>
|
||
),
|
||
},
|
||
{
|
||
accessorKey: "status",
|
||
header: "סטטוס",
|
||
cell: ({ row }) => <StatusBadge status={row.original.status} />,
|
||
},
|
||
{
|
||
accessorKey: "document_count",
|
||
header: "מסמכים",
|
||
cell: ({ row }) => (
|
||
<span className="tabular-nums text-ink-soft">
|
||
{row.original.document_count ?? "—"}
|
||
</span>
|
||
),
|
||
},
|
||
{
|
||
accessorKey: "updated_at",
|
||
header: "עודכן",
|
||
cell: ({ row }) => (
|
||
<span className="text-ink-muted text-sm">{formatDate(row.original.updated_at)}</span>
|
||
),
|
||
},
|
||
];
|
||
|
||
export function CasesTable({
|
||
cases,
|
||
loading,
|
||
error,
|
||
emptyText = "עדיין אין תיקי ערר",
|
||
searchPlaceholder = "חיפוש לפי מס׳ ערר או כותרת…",
|
||
}: {
|
||
cases?: Case[];
|
||
loading?: boolean;
|
||
error?: Error | null;
|
||
emptyText?: string;
|
||
searchPlaceholder?: string;
|
||
}) {
|
||
const [sorting, setSorting] = useState<SortingState>([
|
||
{ id: "updated_at", desc: true },
|
||
]);
|
||
const [globalFilter, setGlobalFilter] = useState("");
|
||
/* "all" = all cases; "blam" = only בל"מ; "regular" = exclude בל"מ */
|
||
const [blamFilter, setBlamFilter] = useState<"all" | "blam" | "regular">("all");
|
||
|
||
const data = useMemo(() => {
|
||
const all = cases ?? [];
|
||
if (blamFilter === "blam") return all.filter((c) => isBlamSubtype(c.appeal_subtype));
|
||
if (blamFilter === "regular") return all.filter((c) => !isBlamSubtype(c.appeal_subtype));
|
||
return all;
|
||
}, [cases, blamFilter]);
|
||
|
||
const table = useReactTable({
|
||
data,
|
||
columns,
|
||
state: { sorting, globalFilter },
|
||
onSortingChange: setSorting,
|
||
onGlobalFilterChange: setGlobalFilter,
|
||
getCoreRowModel: getCoreRowModel(),
|
||
getSortedRowModel: getSortedRowModel(),
|
||
getFilteredRowModel: getFilteredRowModel(),
|
||
globalFilterFn: (row, _colId, filterValue: string) => {
|
||
if (!filterValue) return true;
|
||
const needle = filterValue.toLowerCase();
|
||
return (
|
||
row.original.case_number.toLowerCase().includes(needle) ||
|
||
row.original.title.toLowerCase().includes(needle)
|
||
);
|
||
},
|
||
});
|
||
|
||
return (
|
||
<div className="space-y-3">
|
||
<div className="flex items-center gap-3">
|
||
<Input
|
||
value={globalFilter}
|
||
onChange={(e) => setGlobalFilter(e.target.value)}
|
||
placeholder={searchPlaceholder}
|
||
className="max-w-sm bg-surface"
|
||
dir="rtl"
|
||
/>
|
||
<Select
|
||
value={blamFilter}
|
||
onValueChange={(v) => setBlamFilter(v as "all" | "blam" | "regular")}
|
||
dir="rtl"
|
||
>
|
||
<SelectTrigger className="w-40 bg-surface">
|
||
<SelectValue />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="all">כל התיקים</SelectItem>
|
||
<SelectItem value="blam">בל"מ בלבד</SelectItem>
|
||
<SelectItem value="regular">ערר רגיל בלבד</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
<span className="text-sm text-ink-muted me-auto">
|
||
{table.getFilteredRowModel().rows.length} תיקים
|
||
</span>
|
||
</div>
|
||
|
||
<div className="rounded-lg border border-rule bg-surface shadow-sm overflow-hidden">
|
||
<Table>
|
||
<TableHeader className="bg-rule-soft/60">
|
||
{table.getHeaderGroups().map((hg) => (
|
||
<TableRow key={hg.id} className="border-rule">
|
||
{hg.headers.map((header) => (
|
||
<TableHead
|
||
key={header.id}
|
||
onClick={header.column.getToggleSortingHandler()}
|
||
className="text-navy font-semibold cursor-pointer select-none text-right"
|
||
>
|
||
{flexRender(header.column.columnDef.header, header.getContext())}
|
||
{{ asc: " ▲", desc: " ▼" }[header.column.getIsSorted() as string] ?? ""}
|
||
</TableHead>
|
||
))}
|
||
</TableRow>
|
||
))}
|
||
</TableHeader>
|
||
<TableBody>
|
||
{loading ? (
|
||
Array.from({ length: 4 }).map((_, i) => (
|
||
<TableRow key={i} className="border-rule">
|
||
{columns.map((_c, j) => (
|
||
<TableCell key={j}>
|
||
<Skeleton className="h-4 w-24" />
|
||
</TableCell>
|
||
))}
|
||
</TableRow>
|
||
))
|
||
) : error ? (
|
||
<TableRow>
|
||
<TableCell colSpan={columns.length} className="text-center text-danger py-8">
|
||
שגיאה בטעינת תיקים: {error.message}
|
||
</TableCell>
|
||
</TableRow>
|
||
) : table.getRowModel().rows.length === 0 ? (
|
||
<TableRow>
|
||
<TableCell colSpan={columns.length} className="text-center text-ink-muted py-12">
|
||
<div className="text-gold text-2xl mb-2" aria-hidden>❦</div>
|
||
{globalFilter ? "אין תיקים תואמים לחיפוש" : emptyText}
|
||
</TableCell>
|
||
</TableRow>
|
||
) : (
|
||
table.getRowModel().rows.map((row) => (
|
||
<TableRow
|
||
key={row.id}
|
||
className="border-rule hover:bg-gold-wash/40 transition-colors"
|
||
>
|
||
{row.getVisibleCells().map((cell) => (
|
||
<TableCell key={cell.id} className="py-3">
|
||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||
</TableCell>
|
||
))}
|
||
</TableRow>
|
||
))
|
||
)}
|
||
</TableBody>
|
||
</Table>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|