Merge pull request 'polish(ui): יישור 5 פריטי קטגוריה-A למוקאפי X17 המאושרים' (#279) from worktree-catA-polish into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 41s
G12 Leak-Guard / leak-guard (push) Successful in 3s
Lint — undefined names / undefined-names (push) Successful in 10s

This commit was merged in pull request #279.
This commit is contained in:
2026-06-16 19:16:40 +00:00
5 changed files with 73 additions and 18 deletions

View File

@@ -168,9 +168,21 @@ export default function ArchivePage() {
]); ]);
const [globalFilter, setGlobalFilter] = useState(""); const [globalFilter, setGlobalFilter] = useState("");
const [typeFilter, setTypeFilter] = useState<string>("all"); const [typeFilter, setTypeFilter] = useState<string>("all");
const [yearFilter, setYearFilter] = useState<string>("all");
const rows = useMemo(() => data ?? [], [data]); const rows = useMemo(() => data ?? [], [data]);
// years present in the archive (by archive date) — for the year filter (mockup 05)
const years = useMemo(() => {
const set = new Set<string>();
for (const c of rows) {
if (!c.archived_at) continue;
const y = new Date(c.archived_at).getFullYear();
if (!Number.isNaN(y)) set.add(String(y));
}
return [...set].sort((a, b) => Number(b) - Number(a));
}, [rows]);
const table = useReactTable({ const table = useReactTable({
data: rows, data: rows,
columns, columns,
@@ -192,10 +204,16 @@ export default function ArchivePage() {
// domain filter applied client-side (subtypeOf collapses בל"מ variants) // domain filter applied client-side (subtypeOf collapses בל"מ variants)
const filteredRows = useMemo(() => { const filteredRows = useMemo(() => {
const all = table.getFilteredRowModel().rows; let all = table.getFilteredRowModel().rows;
if (typeFilter === "all") return all; if (typeFilter !== "all") all = all.filter((r) => subtypeOf(r.original) === typeFilter);
return all.filter((r) => subtypeOf(r.original) === typeFilter); if (yearFilter !== "all") {
}, [table, typeFilter, globalFilter, sorting, rows]); all = all.filter((r) => {
const iso = r.original.archived_at;
return iso != null && String(new Date(iso).getFullYear()) === yearFilter;
});
}
return all;
}, [table, typeFilter, yearFilter, globalFilter, sorting, rows]);
const total = rows.length; const total = rows.length;
const shown = filteredRows.length; const shown = filteredRows.length;
@@ -242,6 +260,18 @@ export default function ArchivePage() {
<option value="betterment_levy">היטל השבחה</option> <option value="betterment_levy">היטל השבחה</option>
<option value="compensation_197">פיצויים (ס׳ 197)</option> <option value="compensation_197">פיצויים (ס׳ 197)</option>
</select> </select>
{years.length > 0 ? (
<select
value={yearFilter}
onChange={(e) => setYearFilter(e.target.value)}
className="cursor-pointer text-[0.84rem] text-ink-soft bg-surface border border-rule rounded-lg px-3.5 py-2"
>
<option value="all">כל השנים</option>
{years.map((y) => (
<option key={y} value={y}>{y}</option>
))}
</select>
) : null}
<span className="ms-auto text-[0.82rem] text-ink-muted tabular-nums"> <span className="ms-auto text-[0.82rem] text-ink-muted tabular-nums">
מציג {shown} מתוך {total} מציג {shown} מתוך {total}
</span> </span>
@@ -301,7 +331,7 @@ export default function ArchivePage() {
<div className="text-gold text-2xl mb-2" aria-hidden> <div className="text-gold text-2xl mb-2" aria-hidden>
</div> </div>
{globalFilter || typeFilter !== "all" {globalFilter || typeFilter !== "all" || yearFilter !== "all"
? "אין תיקים תואמים לחיפוש" ? "אין תיקים תואמים לחיפוש"
: "אין תיקים בארכיון"} : "אין תיקים בארכיון"}
</TableCell> </TableCell>

View File

@@ -264,9 +264,9 @@ function BurstControl({ s }: { s: OpsService }) {
onChange={(e) => setUntil(e.target.value)} onChange={(e) => setUntil(e.target.value)}
/> />
<div className="flex flex-wrap gap-1.5 pt-1"> <div className="flex flex-wrap gap-1.5 pt-1">
<button type="button" className="text-[0.72rem] text-gold-deep bg-gold-wash border border-rule rounded-full px-2.5 py-0.5 hover:border-gold" <button type="button" className="text-[0.72rem] text-gold-deep bg-gold-wash border border-gold rounded-full px-2.5 py-0.5 font-semibold hover:border-gold-deep"
onClick={() => setUntil(toLocalInput(nextSaturday18()))}> onClick={() => setUntil(toLocalInput(nextSaturday18()))}>
שבת 18:00 שבת 18:00 <span className="font-normal text-ink-muted">(ברירת-מחדל)</span>
</button> </button>
<button type="button" className="text-[0.72rem] text-gold-deep bg-gold-wash border border-rule rounded-full px-2.5 py-0.5 hover:border-gold" <button type="button" className="text-[0.72rem] text-gold-deep bg-gold-wash border border-rule rounded-full px-2.5 py-0.5 hover:border-gold"
onClick={() => setUntil(toLocalInput(new Date(Date.now() + 5 * 3600 * 1000)))}> onClick={() => setUntil(toLocalInput(new Date(Date.now() + 5 * 3600 * 1000)))}>

View File

@@ -70,6 +70,17 @@ const HEARING_CLASS_STYLE: Record<HearingClass, string> = {
none: "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, * Default ordering for the case list: the nearest upcoming hearing on top,
* past hearings sinking below it (most-recently-passed first), and cases with * past hearings sinking below it (most-recently-passed first), and cases with
@@ -143,10 +154,16 @@ const columns: ColumnDef<Case>[] = [
sortingFn: hearingDateSort, sortingFn: hearingDateSort,
cell: ({ row }) => { cell: ({ row }) => {
const klass = classifyHearing(row.original.hearing_date); const klass = classifyHearing(row.original.hearing_date);
const rel = relativeHearing(row.original.hearing_date);
return ( return (
<div className="leading-tight">
<span className={`tabular-nums text-sm ${HEARING_CLASS_STYLE[klass]}`}> <span className={`tabular-nums text-sm ${HEARING_CLASS_STYLE[klass]}`}>
{formatDate(row.original.hearing_date ?? undefined)} {formatDate(row.original.hearing_date ?? undefined)}
</span> </span>
{rel ? (
<span className="block text-[0.7rem] text-ink-muted">{rel}</span>
) : null}
</div>
); );
}, },
}, },

View File

@@ -90,8 +90,8 @@ export function GoldenRatiosPanel() {
if (isLoading) { if (isLoading) {
return ( return (
<div className="flex items-center justify-center py-12 text-ink-faint"> <div className="flex items-center justify-center py-12 text-ink-muted">
<Loader2 className="w-5 h-5 animate-spin ml-2" /> <Loader2 className="w-5 h-5 animate-spin me-2" />
טוען... טוען...
</div> </div>
); );
@@ -113,10 +113,10 @@ export function GoldenRatiosPanel() {
{/* Table */} {/* Table */}
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>
<tr className="text-ink-faint text-[11px]"> <tr className="text-ink-muted text-[11px]">
<th className="text-right py-1">Section</th> <th className="text-start py-1">בלוק</th>
<th className="text-center py-1">Min %</th> <th className="text-center py-1">מינ׳ %</th>
<th className="text-center py-1">Max %</th> <th className="text-center py-1">מקס׳ %</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -155,7 +155,7 @@ export function GoldenRatiosPanel() {
disabled={!card.dirty || update.isPending} disabled={!card.dirty || update.isPending}
onClick={() => handleSave(card)} onClick={() => handleSave(card)}
> >
{update.isPending ? <Loader2 className="w-3 h-3 animate-spin ml-1" /> : <Save className="w-3 h-3 ml-1" />} {update.isPending ? <Loader2 className="w-3 h-3 animate-spin me-1" /> : <Save className="w-3 h-3 me-1" />}
שמור שמור
</Button> </Button>
{card.isOverride && ( {card.isOverride && (
@@ -165,7 +165,7 @@ export function GoldenRatiosPanel() {
disabled={reset.isPending} disabled={reset.isPending}
onClick={() => handleReset(card)} onClick={() => handleReset(card)}
> >
<RotateCcw className="w-3 h-3 ml-1" /> <RotateCcw className="w-3 h-3 me-1" />
איפוס איפוס
</Button> </Button>
)} )}

View File

@@ -123,6 +123,14 @@ function PanelDeliberation({ round }: { round: NonNullable<Halacha["panel_round"
</p> </p>
</div> </div>
)} )}
{/* seedline (mockup 18): the chair's call IS the gold label the
active-learning loop trains on — surfaced on every deliberation. */}
<p
className="border-t border-rule-soft bg-gold-wash/40 px-3 py-1.5 text-[0.64rem] text-ink-muted leading-snug"
dir="rtl"
>
הכרעתך תיקלט כתווית-הזהב שממנה לומדת לולאת-הלמידה-הפעילה.
</p>
</div> </div>
); );
} }