feat(cases): עמודת "מועד דיון" + מיון ברירת-מחדל לפי קרבת-הדיון #272

Merged
chaim merged 1 commits from worktree-hearing-date-sort into main 2026-06-16 07:41:38 +00:00

View File

@@ -9,6 +9,7 @@ import {
getSortedRowModel,
useReactTable,
type ColumnDef,
type Row,
type SortingState,
} from "@tanstack/react-table";
import {
@@ -37,6 +38,60 @@ function formatDate(iso?: string) {
}
}
/** 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<HearingClass, string> = {
soon: "text-gold-deep font-semibold",
upcoming: "text-navy font-semibold",
past: "text-ink-light",
none: "text-ink-light",
};
/**
* 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<Case>, b: Row<Case>): 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<Case>[] = [
{
accessorKey: "case_number",
@@ -82,6 +137,19 @@ const columns: ColumnDef<Case>[] = [
</span>
),
},
{
accessorKey: "hearing_date",
header: "מועד דיון",
sortingFn: hearingDateSort,
cell: ({ row }) => {
const klass = classifyHearing(row.original.hearing_date);
return (
<span className={`tabular-nums text-sm ${HEARING_CLASS_STYLE[klass]}`}>
{formatDate(row.original.hearing_date ?? undefined)}
</span>
);
},
},
{
accessorKey: "updated_at",
header: "עודכן",
@@ -104,8 +172,11 @@ export function CasesTable({
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<SortingState>([
{ id: "updated_at", desc: true },
{ id: "hearing_date", desc: false },
]);
const [globalFilter, setGlobalFilter] = useState("");
/* "all" = all cases; "blam" = only בל"מ; "regular" = exclude בל"מ */