feat(tracker): summary-card redesign — de-dup Paid, surface Remaining + progress (T3)

The summary row showed 'Paid' twice (this month + last month), indistinguishable,
while the actionable remaining only appeared in the CashFlow card below. Now:
- previous-month becomes a muted 'Last month: $X' hint under Total Paid (not a
  second green box);
- a Remaining card (existing unused blue type) surfaces summary.remaining when
  there's no bank hero (bank mode already shows projected remaining on the hero);
- a compact '$X of $Y paid · N bills left' progress line under the cards.
Respects tracker_show_summary_cards. Visual baseline for / may need a refresh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
null 2026-07-03 18:49:35 -05:00
parent 4a76eb9b92
commit 2b710c459b
2 changed files with 32 additions and 2 deletions

View File

@ -9,6 +9,10 @@
- **[Tracker] Killed the getTracker N+1 (was ~23 DB round-trips × N bills every home-page load)** — inside `bills.map`, `getTracker` ran a payments query per bill (`fetchPaymentsForBillCycle`) plus `computeAmountSuggestion` per bill, and the suggestion alone fired up to 12 queries per bill (6 months × 2) — roughly 70450 queries for a 35-bill account on every Tracker load. Now one query fetches all bills' cycle payments (grouped in JS by each bill's own range) and two queries compute all amount suggestions (`computeAmountSuggestionsBatch`), replacing the per-bill loops. Behavior-preserving — `tests/amountSuggestionService.test.js` pins the batched suggestion to be byte-identical to the per-bill function, and the `trackerService` tests still pass unchanged. (Tracker P1)
### 🎨 Tracker summary cards
- **[Tracker] Fixed the two identical "Paid" boxes + surfaced Remaining + month-progress** — the summary row rendered `SummaryCard type="paid"` **twice** (this month and last month), visually indistinguishable, while the actionable *remaining* number only lived in the CashFlow card below. Now the previous-month figure is a muted **"Last month: $X" hint** under Total Paid (not a second green box), and a **Remaining** card (the existing-but-unused blue card type) surfaces `summary.remaining` when there's no bank hero (in bank mode the hero already shows the projected remaining, so it isn't duplicated). Added a compact **"$X of $Y paid · N bills left"** progress line under the cards. Respects the `tracker_show_summary_cards` setting; light/dark + a11y unchanged. *(The Playwright visual baseline for `/` may need a refresh via `npm run test:e2e:update` since the card row changed intentionally.)* (Tracker T3)
### 🚀 Tracker modern UX
- **[Tracker] Optimistic pay/skip + `toast.promise` on long syncs** — marking a bill paid/unpaid now flips the row **instantly** (optimistic local state) and rolls back on error, instead of waiting for the server round-trip before updating — on both the desktop and mobile rows. And the bank sync (Tracker) and the bill-modal "Sync" now use sonner's `toast.promise` — one toast that transitions loading → done → error, replacing the manual spinner-flag + separate success/error toasts. (Tracker M1/M2)

View File

@ -846,13 +846,39 @@ export default function TrackerPage() {
onEdit={() => setEditStartingOpen(true)}
/>
)}
<SummaryCard type="paid" value={summary.total_paid} />
<SummaryCard
type="paid"
value={summary.total_paid}
hint={summary.previous_month_total > 0 ? `Last month: ${fmt(summary.previous_month_total)}` : undefined}
/>
{/* In bank mode the hero card already surfaces the projected remaining,
so only show the Remaining card when there's no bank hero. */}
{!bankTracking?.enabled && (
<SummaryCard
type="remaining"
value={summary.remaining}
hint={summary.remaining_label || undefined}
/>
)}
<SummaryCard type="overdue" value={summary.overdue} />
<SummaryCard type="paid" value={summary.previous_month_total} hint="Previous month"/>
{summary.trend && <TrendCard trend={summary.trend} />}
</div>
) : null}
{/* Compact month-progress line — quick "how far through the month am I?" */}
{showSummaryCards && !loading && !isError && summary?.total_expected > 0 && (() => {
const billsLeft = (summary.count_upcoming ?? 0) + (summary.count_late ?? 0);
return (
<p className="px-1 -mt-1 text-[11px] text-muted-foreground">
<span className="font-mono font-semibold text-foreground">{fmt(summary.paid_toward_due)}</span>
{' of '}
<span className="font-mono">{fmt(summary.total_expected)}</span>
{' paid'}
{billsLeft > 0 && <> · {billsLeft} bill{billsLeft === 1 ? '' : 's'} left</>}
</p>
);
})()}
{/* ── Safe to Spend ── */}
{!isError && !loading && showSafeToSpend && isCurrentMonth && cashflow && (
<CashFlowCard