90 lines
7.4 KiB
Markdown
90 lines
7.4 KiB
Markdown
# 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.
|