# 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 `<InitiationRateTile>`:
  ```tsx
  <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:
  ```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.
