# 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` (not `offers`). - Third card label: **"Pending Validation Rate"**, caption: `"{count} pending validation / {total} total accepted offer(s)"`. ## Changes ### 1. `utils/dashboard/metrics.ts` - In `buildDashboardMetrics`, change: ```ts const customerInitiatedRate = team.offers > 0 ? (customerInitiated / team.offers) * 100 : 0; const agentInitiatedRate = team.offers > 0 ? (agentInitiated / team.offers) * 100 : 0; ``` to use `team.accepted` as the denominator, and add: ```ts const unverifiedAcceptedRate = team.accepted > 0 ? (unverifiedAccepted / team.accepted) * 100 : 0; ``` - Add `unverifiedAcceptedRate: number` to the `DashboardMetrics` type (next to `agentInitiatedRate`), with a doc comment matching the existing style (`/** (unverifiedAccepted / accepted) × 100 — percentage of accepted offers not yet classified. */`). - Update the doc comments on `customerInitiatedRate`/`agentInitiatedRate` to say "percentage of accepted offers" (not "total offers"), and update the return statement to include `unverifiedAcceptedRate`. ### 2. `packages/design-system/src/components/InitiationRateTile/index.tsx` - Widen `initiator: "customer" | "agent"` to `initiator: "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 on `initiator === "pending"` for the middle phrase ("pending validation" vs `initiated by {initiator}`), keeping the rest of the sentence identical. - No style changes needed — `style.module.scss` already 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 `initiator` prop to mention the "pending" case. ### 3. `packages/design-system/src/components/InitiationRateTile/InitiationRateTile.stories.tsx` - Add a `PendingValidation` story (`label: "Pending Validation Rate"`, `initiator: "pending"`, plausible `rate`/`count`/`total`), and widen the story's grid `render` from `gridTemplateColumns: "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-2` to 3 columns (`grid grid-cols-1 gap-3 sm:grid-cols-3`, matching the responsive pattern used elsewhere in the file). - Add a third ``: ```tsx ``` - 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: ```ts const pendingRateRow = sheet3.addRow(["Pending Validation Rate", metrics.unverifiedAcceptedRate / 100]); pendingRateRow.getCell(2).numFmt = "0.0%"; ``` 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. ## Tests to update - **`__tests__/utils/dashboard/metrics.test.ts`**: update existing assertions that hardcode the old `offers`-based math (e.g. `"only counts initiation types from roster users"`, `"counts customer_initiated and agent_initiated correctly"` — the `(1/4)*100` expectations 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. - **`packages/design-system/.../InitiationRateTile.test.tsx`** and **`.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 range `2..14` → `2..15`; add `expect(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 range `17..18` → `18..19`. ## Verification 1. Run the affected unit test suites: ```bash npx vitest run __tests__/utils/dashboard/metrics.test.ts __tests__/dashboard-export-route.test.ts ``` 2. Run the design-system component tests for the tile: ```bash npx vitest run packages/design-system/src/components/InitiationRateTile ``` 3. Full test suite + typecheck to catch any other consumer of `DashboardMetrics`/`InitiationRateTile`'s `initiator` prop: ```bash npm run build # or the repo's typecheck script — confirms the widened `initiator` union doesn't break other call sites npm test ``` 4. 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 with `customer_initiated`, `agent_initiated`, and `null` initiation types.