"use client"; import { useMemo, useState } from "react"; import Link from "next/link"; import { flexRender, getCoreRowModel, getFilteredRowModel, getSortedRowModel, useReactTable, type ColumnDef, type Row, 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; } } /** Midnight today, in ms — the pivot between an upcoming and a past hearing. */ function startOfToday() { const d = new Date(); d.setHours(0, 0, 0, 0); return d.getTime(); } /** Day-precision ms for a hearing date, or null when absent/unparseable. */ function hearingMs(iso?: string | null): number | null { if (!iso) return null; const t = new Date(iso).setHours(0, 0, 0, 0); return Number.isNaN(t) ? null : t; } type HearingClass = "soon" | "upcoming" | "past" | "none"; /** Classify a hearing date for the cell's colour cue (within ~7 days = "soon"). */ function classifyHearing(iso?: string | null): HearingClass { const t = hearingMs(iso); if (t === null) return "none"; const today = startOfToday(); if (t < today) return "past"; return t - today <= 7 * 86_400_000 ? "soon" : "upcoming"; } const HEARING_CLASS_STYLE: Record = { soon: "text-gold-deep font-semibold", upcoming: "text-navy font-semibold", past: "text-ink-light", none: "text-ink-light", }; /** Relative-time label beside the hearing date (mockup 04b `.hd-rel`). */ function relativeHearing(iso?: string | null): string | null { const t = hearingMs(iso); if (t === null) return null; const days = Math.round((t - startOfToday()) / 86_400_000); if (days === 0) return "היום"; if (days === 1) return "מחר"; if (days === -1) return "אתמול"; return days > 0 ? `בעוד ${days} ימים` : `לפני ${Math.abs(days)} ימים`; } /** * Default ordering for the case list: the nearest upcoming hearing on top, * past hearings sinking below it (most-recently-passed first), and cases with * no hearing date last. Authored as a non-inverting comparator, so the column's * initial (ascending) sort yields exactly this sequence. */ function hearingDateSort(a: Row, b: Row): number { const today = startOfToday(); // group: 0 = upcoming (incl. today), 1 = past, 2 = no date const rank = (iso?: string | null): { g: number; t: number } => { const t = hearingMs(iso); if (t === null) return { g: 2, t: 0 }; return t >= today ? { g: 0, t } : { g: 1, t }; }; const ra = rank(a.original.hearing_date); const rb = rank(b.original.hearing_date); if (ra.g !== rb.g) return ra.g - rb.g; if (ra.g === 0) return ra.t - rb.t; // upcoming: ascending (nearest first) if (ra.g === 1) return rb.t - ra.t; // past: descending (most recent first) return 0; // both undated — keep stable order } const columns: ColumnDef[] = [ { accessorKey: "case_number", header: "מס׳ ערר", cell: ({ row }) => ( {row.original.case_number} ), }, { accessorKey: "title", header: "כותרת", cell: ({ row }) => (
{(row.original.proceeding_type === 'בל"מ' || isBlamSubtype(row.original.appeal_subtype)) && ( בל"מ )} {row.original.title}
), }, { accessorKey: "status", header: "סטטוס", cell: ({ row }) => , }, { accessorKey: "document_count", header: "מסמכים", cell: ({ row }) => ( {row.original.document_count ?? "—"} ), }, { accessorKey: "hearing_date", header: "מועד דיון", sortingFn: hearingDateSort, cell: ({ row }) => { const klass = classifyHearing(row.original.hearing_date); const rel = relativeHearing(row.original.hearing_date); return (
{formatDate(row.original.hearing_date ?? undefined)} {rel ? ( {rel} ) : null}
); }, }, { accessorKey: "updated_at", header: "עודכן", cell: ({ row }) => ( {formatDate(row.original.updated_at)} ), }, ]; export function CasesTable({ cases, loading, error, emptyText = "עדיין אין תיקי ערר", searchPlaceholder = "חיפוש לפי מס׳ ערר או כותרת…", }: { cases?: Case[]; loading?: boolean; error?: Error | null; emptyText?: string; searchPlaceholder?: string; }) { // Default: nearest upcoming hearing on top; past hearings sink below // (most-recently-passed first); undated cases last. desc:false keeps the // hearingDateSort comparator's intended (non-inverted) sequence. const [sorting, setSorting] = useState([ { id: "hearing_date", desc: false }, ]); 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 (
setGlobalFilter(e.target.value)} placeholder={searchPlaceholder} className="max-w-sm bg-surface" dir="rtl" /> {table.getFilteredRowModel().rows.length} תיקים
{table.getHeaderGroups().map((hg) => ( {hg.headers.map((header) => ( {flexRender(header.column.columnDef.header, header.getContext())} {{ asc: " ▲", desc: " ▼" }[header.column.getIsSorted() as string] ?? ""} ))} ))} {loading ? ( Array.from({ length: 4 }).map((_, i) => ( {columns.map((_c, j) => ( ))} )) ) : error ? ( שגיאה בטעינת תיקים: {error.message} ) : table.getRowModel().rows.length === 0 ? (
{globalFilter ? "אין תיקים תואמים לחיפוש" : emptyText}
) : ( table.getRowModel().rows.map((row) => ( {row.getVisibleCells().map((cell) => ( {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} )) )}
); }