fix(tracker): pin-Due sort uses reconciled status; add filtered-empty state

- Pin-upcoming sort ranked on raw row.status, so a bill paid-by-threshold
  but still carrying a stale 'late'/'missed' status was pinned to the top.
  Rank on rowEffectiveStatus (same source the overdue filter uses) so it
  sorts to the bottom instead.
- Filtering to zero results showed a blank content area (rows.length checks
  the unfiltered list). Add a "No bills match your filters" state with a
  Clear-filters action.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
null 2026-07-05 14:35:03 -05:00
parent ef566b12f0
commit db1be4b0bd
1 changed files with 22 additions and 2 deletions

View File

@ -622,8 +622,11 @@ export default function TrackerPage() {
? sortTrackerRows(filteredRows, sortKey, sortDir) ? sortTrackerRows(filteredRows, sortKey, sortDir)
: pinUpcoming : pinUpcoming
? [...filteredRows].sort((a, b) => { ? [...filteredRows].sort((a, b) => {
const ua = URGENCY_ORDER[a.status] ?? 99; // Rank on the reconciled status (same source the overdue filter uses),
const ub = URGENCY_ORDER[b.status] ?? 99; // so a bill that's paid-by-threshold but still carries a stale raw
// 'late'/'missed' status sorts to the bottom instead of being pinned up.
const ua = URGENCY_ORDER[rowEffectiveStatus(a)] ?? 99;
const ub = URGENCY_ORDER[rowEffectiveStatus(b)] ?? 99;
if (ua !== ub) return ua - ub; if (ua !== ub) return ua - ub;
return (a.due_day ?? 99) - (b.due_day ?? 99); return (a.due_day ?? 99) - (b.due_day ?? 99);
}) })
@ -1060,6 +1063,23 @@ export default function TrackerPage() {
</div> </div>
</div> </div>
)} )}
{/* ── Filtered-to-empty state — bills exist this month but none match the active filters/search ── */}
{!isError && !loading && rows.length > 0 && first.length === 0 && second.length === 0 && (
<div className="flex flex-col items-center justify-center py-20 text-center rounded-xl border border-border bg-muted/20">
<div className="h-10 w-10 rounded-full bg-muted flex items-center justify-center mb-3">
<Search className="h-5 w-5 text-muted-foreground" />
</div>
<p className="text-sm font-medium text-foreground">No bills match your filters</p>
<p className="mt-1 text-xs text-muted-foreground">{rows.length} bill{rows.length === 1 ? '' : 's'} this month are hidden by the current search or filters.</p>
{hasFilters && (
<Button size="sm" variant="outline" onClick={resetFilters} className="mt-4 gap-1.5 text-xs">
<RefreshCw className="h-3 w-3" />
Clear filters
</Button>
)}
</div>
)}
{!isError && (first.length > 0 || second.length > 0) && ( {!isError && (first.length > 0 || second.length > 0) && (
<div className="space-y-5"> <div className="space-y-5">
{first.length > 0 && <Bucket label="1st 14th" rows={first} year={year} month={month} refresh={invalidateData} onEditBill={handleOpenEditBill} loading={loading} reorderEnabled={reorderEnabled} movingBillId={movingBillId} sortKey={sortKey} sortDir={sortDir} onSort={handleSortHeader} onReorderRows={(next) => handleReorderBucket('1st', next)} driftedIds={driftedIds} visibleColumns={visibleTableColumns} />} {first.length > 0 && <Bucket label="1st 14th" rows={first} year={year} month={month} refresh={invalidateData} onEditBill={handleOpenEditBill} loading={loading} reorderEnabled={reorderEnabled} movingBillId={movingBillId} sortKey={sortKey} sortDir={sortDir} onSort={handleSortHeader} onReorderRows={(next) => handleReorderBucket('1st', next)} driftedIds={driftedIds} visibleColumns={visibleTableColumns} />}