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:
@@ -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.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user