feat(sp30): Dashboard Recent batches widget with billing outcome

- backend: GET /api/batches now returns acceptedCount/rejectedCount/
  pendingCount/billedTotal/topRejectionReason/hasProblem per item
  (one GROUP BY query + one ordered scan, no N+1)
- backend: 2 tests pin the 837p full-bucket case + the 835 zero case
- frontend: BatchSummary extended with 6 optional fields
  (backwards compat preserved)
- frontend: new RecentBatchesWidget renders one row per batch with
  status icon + billed total + accepted count + top rejection reason
- frontend: 837p rows show $ total + 'N/M accepted'; 835 rows show
  payment count (Remittance has no batch-level total_charge)
- frontend: full-width row between KPI tiles and Activity on the
  Dashboard; click navigates to /batches?batch=ID
- frontend: 3 component tests cover empty/clean/problem/835 branches
This commit is contained in:
Nora
2026-07-02 14:18:55 -06:00
parent bde3060e9e
commit 97512ec4a7
7 changed files with 661 additions and 0 deletions
+15
View File
@@ -193,3 +193,18 @@ describe("Dashboard · Recent activity event routing (SP21 Task 2.5)", () => {
expect(probe.getAttribute("data-search")).toBe("");
});
});
// ---------------------------------------------------------------------------
// SP30: Dashboard integration is exercised by:
// - The 3 component tests in src/components/RecentBatchesWidget.test.tsx
// (which pin the click → onRowClick behavior + the empty/clean/problem
// rendering branches + the 837p vs 835 row split).
// - The Playwright smoke test (login → / → click row → URL changes
// to /batches?batch=ID).
//
// A Dashboard-level test would need to flip api.isConfigured mid-test
// and re-import Dashboard under a fresh module graph, which fights
// the existing top-level mock that pins isConfigured=false for the
// activity-routing tests. Skipping the Dashboard-level test keeps
// coverage without the module-graph gymnastics.
// ---------------------------------------------------------------------------
+30
View File
@@ -20,9 +20,15 @@ import { eventKindToUrl } from "@/lib/event-routing";
import { useAuth } from "@/auth/useAuth";
import { useDashboardKpis } from "@/hooks/useDashboardKpis";
import { useActivity } from "@/hooks/useActivity";
import { useBatches } from "@/hooks/useBatches";
import { RecentBatchesWidget } from "@/components/RecentBatchesWidget";
import { toast } from "sonner";
const MONTHS_BACK = 6;
// SP30: cap the "Recent batches" widget at 5 rows. Hard-coded constant
// so changing it is one edit. Five is the at-a-glance sweet spot —
// too few hides context, too many scrolls past the KPI fold.
const RECENT_BATCHES_LIMIT = 5;
// Zero-shaped KPI totals used when the server response hasn't arrived
// yet or the backend isn't configured. Mirrors the empty-DB shape of
@@ -53,6 +59,10 @@ export function Dashboard() {
// in SQL once over the full population.
const kpisQuery = useDashboardKpis({ months: MONTHS_BACK });
const activityQuery = useActivity({ limit: 10 });
// SP30: "Recent batches" widget data. queryKey ["batches", 5] is
// distinct from the ["batches", null] cache used by Upload's History
// tab so the two don't collide.
const recentBatchesQuery = useBatches(RECENT_BATCHES_LIMIT);
const kpisData = kpisQuery.data;
// Fall back to a zero-shaped object so the KpiTiles still render a
@@ -244,6 +254,26 @@ export function Dashboard() {
</DrillableCell>
</section>
{/* SP30: Recent batches — how the last few batches billed out.
Sits as its own full-width row between the KPI tiles and the
Activity + Top providers grid. animationDelay +200ms lands it
visually after the KPI stagger (kpiBase + kpiStep*5 + 80)
and before the Activity row (sectionBase) so the eye flows
KPIs → Recent batches → Activity without breaking the
fade-in choreography. */}
<section
className="animate-fade-in-up"
style={{ animationDelay: `${sectionBase + 200}ms` }}
>
<RecentBatchesWidget
batches={recentBatchesQuery.data ?? []}
limit={RECENT_BATCHES_LIMIT}
onRowClick={(id) =>
navigate(`/batches?batch=${encodeURIComponent(id)}`)
}
/>
</section>
{/* Activity + Top providers */}
<section
className="grid gap-4 lg:grid-cols-3 animate-fade-in-up"