feat(cases): עמודת "מועד דיון" ברשימת התיקים + מיון ברירת-מחדל לפי קרבת-הדיון
מוסיף עמודת מועד-דיון לטבלת התיקים בדף-הבית (בין "מסמכים" ל"עודכן), ומשנה את מיון ברירת-המחדל מ-updated_at desc ל-hearing_date לפי הסדר: דיון קרוב ביותר למעלה → דיונים עתידיים בסדר עולה → דיונים שחלפו (האחרון-שחלף קודם) → תיקים ללא מועד דיון בתחתית. המיון ממומש כקומפרטור לא-מהפך (hearingDateSort) כך שמיון-העמודה ההתחלתי (asc) מניב בדיוק את הרצף הזה. רמז-צבע בתא: הקרוב (≤7 ימים) בזהב, עתידי בנייבי, שחלף/ללא-מועד באפור-בהיר. תצוגה/מיון בלבד על שדה hearing_date הקיים — אין מסלול-נתונים מקביל (G2 נשמר), לא נוגע ב-Paperclip (G12), לא בולע שגיאות. אושר דרך שער Claude Design (כרטיס 04b-home-cases-hearing, פרויקט X17). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 בל"מ */
|
||||
|
||||
Reference in New Issue
Block a user