All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 43s
After the proceeding_type field landed, users started flipping cases to בל"מ via the edit dialog. But the case-header badge + cases-table filter were still gated on isBlamSubtype(appeal_subtype), so the badge didn't appear when only the proceeding_type changed. Now the badge shows when either proceeding_type === 'בל"מ' OR appeal_subtype is an extension_request_* variant — the legacy path stays so existing rows that never got a proceeding_type still render correctly. Also regen types.ts from prod (proceeding_type now in OpenAPI schema) and register the one-shot process_pending_blam.py script. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
233 lines
7.6 KiB
TypeScript
233 lines
7.6 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}>
|
||
{(row.original.proceeding_type === 'בל"מ' || 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 ?? [];
|
||
const isBlam = (c: Case) =>
|
||
c.proceeding_type === 'בל"מ' || isBlamSubtype(c.appeal_subtype);
|
||
if (blamFilter === "blam") return all.filter(isBlam);
|
||
if (blamFilter === "regular") return all.filter((c) => !isBlam(c));
|
||
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>
|
||
);
|
||
}
|