7.4 KiB
GFIBER-707 — Fix "Cumulative Results" percentages + add "Pending" card
Context
Jira GFIBER-707 (Bug, High priority): the "Cumulative results" dashboard panel shows incorrect percentages for the Customer-Initiated / Agent-Initiated cards. Acceptance criteria:
- A third blue card "Pending" is added to the right of the existing two.
- The 3 cards, added together, sum 100%.
Root cause found in code (utils/dashboard/metrics.ts:157-160): customerInitiatedRate and agentInitiatedRate are computed as count / team.offers * 100, where team.offers = accepted + rejected. But the numerators (customerInitiated, agentInitiated, unverifiedAccepted) are counted only over accepted roster tickets and, by construction, exactly partition accepted (every accepted ticket is customer-initiated, agent-initiated, or unclassified/null). Since offers >= accepted whenever there's at least one rejected ticket, the two existing rates under-report and can never sum to 100% — confirmed by the unit test at __tests__/utils/dashboard/metrics.test.ts:170-183 (offers = 3 accepted + 1 declined = 4, so a fully-classified accepted ticket only registers as 25%, not 100/N%).
The fix: switch the denominator for all three initiation rates to metrics.accepted (which is already what the tile's caption text claims — "{count} initiated by {initiator} / {total} total accepted offers" already uses total=metrics.accepted). This makes customerInitiatedRate + agentInitiatedRate + unverifiedAcceptedRate === 100 exactly whenever there's at least one accepted ticket, satisfying the AC. The two existing percentages will change (increase) as a side effect — this is the actual bug fix, not a regression.
Decisions confirmed with the user:
- Denominator for all 3 rates →
metrics.accepted(notoffers). - Third card label: "Pending Validation Rate", caption:
"{count} pending validation / {total} total accepted offer(s)".
Changes
1. utils/dashboard/metrics.ts
- In
buildDashboardMetrics, change:to useconst customerInitiatedRate = team.offers > 0 ? (customerInitiated / team.offers) * 100 : 0; const agentInitiatedRate = team.offers > 0 ? (agentInitiated / team.offers) * 100 : 0;team.acceptedas the denominator, and add:const unverifiedAcceptedRate = team.accepted > 0 ? (unverifiedAccepted / team.accepted) * 100 : 0; - Add
unverifiedAcceptedRate: numberto theDashboardMetricstype (next toagentInitiatedRate), with a doc comment matching the existing style (/** (unverifiedAccepted / accepted) × 100 — percentage of accepted offers not yet classified. */). - Update the doc comments on
customerInitiatedRate/agentInitiatedRateto say "percentage of accepted offers" (not "total offers"), and update the return statement to includeunverifiedAcceptedRate.
2. packages/design-system/src/components/InitiationRateTile/index.tsx
- Widen
initiator: "customer" | "agent"toinitiator: "customer" | "agent" | "pending". - Adjust the caption line: when
initiator === "pending", render"{count} pending validation / {total} total accepted offer(s)"instead of"{count} initiated by {initiator} / ...". Simplest approach: branch oninitiator === "pending"for the middle phrase ("pending validation" vsinitiated by {initiator}), keeping the rest of the sentence identical. - No style changes needed —
style.module.scssalready renders every tile with the same blue tint (--fiber-blue-light/--fiber-text-blue), matching the "blue card" requirement in the AC. - Update the doc comment on the
initiatorprop to mention the "pending" case.
3. packages/design-system/src/components/InitiationRateTile/InitiationRateTile.stories.tsx
- Add a
PendingValidationstory (label: "Pending Validation Rate",initiator: "pending", plausiblerate/count/total), and widen the story's gridrenderfromgridTemplateColumns: "1fr 1fr"to"1fr 1fr 1fr"(or leave 2-col if stories render independently — check current behavior first).
4. app/(protected)/dashboard/page.tsx (CumulativeResults, ~lines 439-463)
- Change the grid wrapper from
grid grid-cols-2 gap-3 sm:grid-cols-2to 3 columns (grid grid-cols-1 gap-3 sm:grid-cols-3, matching the responsive pattern used elsewhere in the file). - Add a third
<InitiationRateTile>:<InitiationRateTile label="Pending Validation Rate" rate={metrics.unverifiedAcceptedRate} count={metrics.unverifiedAccepted} total={metrics.accepted} initiator="pending" /> - Leave the helper-text condition (
customerInitiatedRate === 0 && agentInitiatedRate === 0) as-is — it still correctly means "nothing classified yet" (in which case pending = 100%).
5. app/(protected)/dashboard/api/export/route.ts (Sheet 3 — Performance summary, ~lines 346-352)
- After the existing
agentRateRow, add:This is a new row inserted into Block A, which shifts every subsequent row (blank separator, leaderboard header, leaderboard data rows) down by 1 in the worksheet.const pendingRateRow = sheet3.addRow(["Pending Validation Rate", metrics.unverifiedAcceptedRate / 100]); pendingRateRow.getCell(2).numFmt = "0.0%";
Tests to update
__tests__/utils/dashboard/metrics.test.ts: update existing assertions that hardcode the oldoffers-based math (e.g."only counts initiation types from roster users","counts customer_initiated and agent_initiated correctly"— the(1/4)*100expectations become(1/accepted)*100), and add:- New assertions for
unverifiedAcceptedRate. - A dedicated test asserting
customerInitiatedRate + agentInitiatedRate + unverifiedAcceptedRate === 100(within floating-point tolerance) for a realistic mixed fixture — this directly encodes the GFIBER-707 acceptance criterion.
- New assertions for
packages/design-system/.../InitiationRateTile.test.tsxand.test.cy.tsx: add a "pending" case asserting the"N pending validation / M total accepted offer(s)"caption text.__tests__/dashboard-export-route.test.ts: this file has hardcoded row indices that must shift by +1 because of the new export row:"Block A includes the new initiation type rows after Pitch Rate"(line ~436-450): loop range2..14→2..15; addexpect(metricLabels).toContain("Pending Validation Rate")."Block B leaderboard has one row per agent user"(line ~452-464): header now at row 17 (was 16), data rows 18/19 (were 17/18) — update the comment and indices."includes zero-sales agents in the leaderboard"(line ~466-476): loop range17..18→18..19.
Verification
- Run the affected unit test suites:
npx vitest run __tests__/utils/dashboard/metrics.test.ts __tests__/dashboard-export-route.test.ts - Run the design-system component tests for the tile:
npx vitest run packages/design-system/src/components/InitiationRateTile - Full test suite + typecheck to catch any other consumer of
DashboardMetrics/InitiationRateTile'sinitiatorprop:npm run build # or the repo's typecheck script — confirms the widened `initiator` union doesn't break other call sites npm test - Manually load
/dashboard(any team view) in a browser and confirm: 3 blue cards render side by side, and the 3 rate percentages sum to 100.0% for a mix of accepted tickets withcustomer_initiated,agent_initiated, andnullinitiation types.