diff --git a/client/pages/TrackerPage.tsx b/client/pages/TrackerPage.tsx index fcf809a..19429f0 100644 --- a/client/pages/TrackerPage.tsx +++ b/client/pages/TrackerPage.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useMemo, useCallback, useDeferredValue, type ReactNode } from 'react'; +import { useState, useEffect, useMemo, useCallback, useDeferredValue, type ComponentType, type ReactNode } from 'react'; import { useSearchParams, useNavigate } from 'react-router-dom'; import { ChevronLeft, ChevronRight, AlertCircle, CheckCircle2, Plus, Search, RefreshCw, Landmark, ArrowUpToLine, ArrowUp, ArrowDown, BellOff, EyeOff, Settings2, Download, Printer, FileSpreadsheet, CalendarDays, Keyboard } from 'lucide-react'; import { toast } from 'sonner'; @@ -91,6 +91,34 @@ interface TrackerFilters { type BoolFilterKey = 'autopay' | 'firstBucket' | 'fifteenthBucket' | 'unpaid' | 'overdue' | 'debt'; type Patch = Record; +function HeaderStat({ + icon: Icon, + label, + value, + tone = 'neutral', +}: { + icon: ComponentType<{ className?: string }>; + label: string; + value: ReactNode; + tone?: 'neutral' | 'good' | 'warn' | 'danger' | 'info'; +}) { + const toneClass = { + neutral: 'border-border/70 bg-card/65 text-muted-foreground', + good: 'border-emerald-500/25 bg-emerald-500/[0.08] text-emerald-700 dark:text-emerald-300', + warn: 'border-amber-500/30 bg-amber-500/[0.08] text-amber-700 dark:text-amber-300', + danger: 'border-rose-500/30 bg-rose-500/[0.08] text-rose-700 dark:text-rose-300', + info: 'border-sky-500/25 bg-sky-500/[0.08] text-sky-700 dark:text-sky-300', + }[tone]; + + return ( + + + ); +} + function fmtBalanceAge(isoStr: string | null | undefined): string | null { const d = parseUtc(isoStr); if (!d) return null; @@ -771,6 +799,22 @@ export default function TrackerPage() { const first = sortedRows.filter(r => r.bucket === '1st'); const second = sortedRows.filter(r => r.bucket === '15th'); const reorderEnabled = !hasFilters && !loading && !isError && !pinUpcoming; + const activeMonthLabel = `${MONTHS[month - 1]} ${year}`; + const monthStartLabel = new Date(year, month - 1, 1).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); + const monthEndLabel = new Date(year, month, 0).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }); + const paidCount = Number(summary.count_paid ?? 0); + const overdueCount = Number(summary.count_late ?? 0); + const unpaidCount = rows.filter(row => !row.is_skipped && !rowIsPaid(row)).length; + const headerStatusTone = isError ? 'danger' : loading ? 'info' : overdueCount > 0 ? 'danger' : unpaidCount > 0 ? 'warn' : 'good'; + const headerStatusText = isError + ? 'Needs attention' + : loading + ? 'Loading month' + : overdueCount > 0 + ? `${overdueCount} overdue` + : unpaidCount > 0 + ? `${unpaidCount} unpaid` + : 'All settled'; async function persistTrackerOrder(nextRows: TrackerRow[], movedBillId: number | null) { const payload = Object.fromEntries(nextRows.map((row, index) => [row.id, index])); @@ -812,22 +856,70 @@ export default function TrackerPage() { {/* ── Header ── */} - {MONTHS[month - 1]} - {year} + {activeMonthLabel} + · + {monthStartLabel} to {monthEndLabel} + {isCurrentMonth && ( + <> + · + Current month + + )} )} glyph="tracker" - meta={`${rows.length} ${rows.length === 1 ? 'bill' : 'bills'}`} + meta={( +
+ 0 ? 'good' : 'neutral'} /> + + + +
+ )} actions={(
+
+ + + {activeMonthLabel} + + +
+ + - - {MONTHS[month - 1]} {year} - - -
- )} />