docs(002-reporting-dashboards-ui): plan, data model, quickstart, tasks
Four new dataviz-informed UI primitives (stat tile, ranked bar list, status distribution, meter) shared across all four dashboards, no new charting dependency. 24 tasks across a shared Foundational phase and 4 independently-testable dashboard user stories. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
e7e6e853d5
commit
a37539794f
@@ -0,0 +1,104 @@
|
||||
# Data Model: Reporting and Analytics Dashboards UI
|
||||
|
||||
Response shapes mirror `supporthub-api`'s `specs/015-reporting-dashboards/data-model.md`
|
||||
exactly — reproduced here as the frontend's own typed contract (Constitution Principle IV),
|
||||
plus the one derived view-model type each dashboard actually renders.
|
||||
|
||||
## API Response Types (`lib/api/types/reports.ts`)
|
||||
|
||||
```ts
|
||||
export interface DateRangeDTO { from: string; to: string }
|
||||
|
||||
export interface ManagementDashboardDTO {
|
||||
range: DateRangeDTO;
|
||||
totalCases: number;
|
||||
aiResolved: number;
|
||||
humanEscalated: number;
|
||||
resolved: number;
|
||||
open: number;
|
||||
slaCompliance: { met: number; breached: number; rate: number | null };
|
||||
escalationCount: number;
|
||||
averageResponseSeconds: number | null;
|
||||
averageResolutionSeconds: number | null;
|
||||
}
|
||||
|
||||
export interface ProductDashboardDTO {
|
||||
productId: string;
|
||||
range: DateRangeDTO;
|
||||
supportVolume: number;
|
||||
problemsByCategory: Array<{ categoryId: string | null; count: number }>;
|
||||
recurringProblems: Array<{ categoryId: string | null; count: number }>;
|
||||
aiResolutionRate: number | null;
|
||||
humanEscalationRate: number | null;
|
||||
topErrors: Array<{ code: string; count: number }>;
|
||||
}
|
||||
|
||||
export interface SupportDashboardDTO {
|
||||
generatedAt: string;
|
||||
range: DateRangeDTO;
|
||||
workloadByAgent: Array<{ agentId: string; openAssignments: number }>;
|
||||
slaAtRisk: number;
|
||||
slaBreached: number;
|
||||
escalationCount: number;
|
||||
averageResponseSeconds: number | null;
|
||||
averageResolutionSeconds: number | null;
|
||||
}
|
||||
|
||||
export interface AiDashboardDTO {
|
||||
range: DateRangeDTO;
|
||||
totalSessions: number;
|
||||
aiResolutionRate: number | null;
|
||||
humanHandoffRate: number | null;
|
||||
failedTroubleshootingEscalationRate: number | null;
|
||||
knowledgeMatchRate: number | null;
|
||||
confidenceDistribution: { proceed: number; ask: number; escalate: number };
|
||||
toolInvocations: { success: number; failed: number };
|
||||
}
|
||||
```
|
||||
|
||||
## Display Helpers (`lib/format/duration.ts`, inline in each dashboard)
|
||||
|
||||
- **`formatDurationSeconds(value: number | null): string`** — `null` -> `"No data"`; otherwise a
|
||||
compact human duration (`"2h 15m"`, `"45s"`) — never raw seconds.
|
||||
- **`formatRate(value: number | null): string`** — `null` -> `"No data"`; otherwise a percentage,
|
||||
one decimal place (`"62.5%"`).
|
||||
- **`formatCount(value: number): string`** — thousands-comma'd (dataviz `marks-and-anatomy.md`'s
|
||||
own labeling convention), no special-casing needed since a count is never `null`.
|
||||
|
||||
## Ranked List View-Model
|
||||
|
||||
Every ranked list (`problemsByCategory`, `recurringProblems`, `topErrors`, `workloadByAgent`)
|
||||
renders through one shared `RankedBarList` primitive taking:
|
||||
|
||||
```ts
|
||||
interface RankedBarListItem {
|
||||
label: string; // categoryId, error code, or agentId — resolved to a display label upstream
|
||||
// where a friendlier name exists, falling back to the raw id otherwise
|
||||
value: number;
|
||||
}
|
||||
```
|
||||
|
||||
Bar width is `value / max(values)` — proportional within the list shown, not against some
|
||||
absolute scale, per `dataviz` choosing-a-form.md's "compare magnitude" guidance for a bounded
|
||||
top-N list.
|
||||
|
||||
## Status Distribution View-Model
|
||||
|
||||
Confidence distribution and tool invocations both render through one shared
|
||||
`StatusDistribution` primitive:
|
||||
|
||||
```ts
|
||||
interface StatusDistributionSegment {
|
||||
label: string; // "Proceed" | "Ask" | "Escalate" | "Success" | "Failed"
|
||||
value: number;
|
||||
tone: 'success' | 'warning' | 'destructive';
|
||||
}
|
||||
```
|
||||
|
||||
Mapping (fixed, never inferred from label text):
|
||||
|
||||
| Source | proceed/success | ask | escalate/failed |
|
||||
|---|---|---|---|
|
||||
| `confidenceDistribution` | `proceed` -> success | `ask` -> warning | `escalate` -> destructive |
|
||||
| `toolInvocations` | `success` -> success | — (two-segment) | `failed` -> destructive |
|
||||
| `slaCompliance` | `met` -> success | — (two-segment) | `breached` -> destructive |
|
||||
@@ -0,0 +1,124 @@
|
||||
# Implementation Plan: Reporting and Analytics Dashboards UI
|
||||
|
||||
**Branch**: `002-reporting-dashboards-ui` | **Date**: 2026-09-09 | **Spec**: [spec.md](./spec.md)
|
||||
|
||||
**Input**: Feature specification from `specs/002-reporting-dashboards-ui/spec.md`
|
||||
|
||||
## Summary
|
||||
|
||||
Presents `supporthub-api`'s already-complete 015-reporting-dashboards contract (four `GET
|
||||
/admin/reports/*` endpoints) as a tabbed admin surface, replacing nothing existing —
|
||||
`admin/reports/page.tsx` already hosts the SLA Monitor and Escalation Matrix (012-admin-list-
|
||||
views work); this feature adds Management/Product/Support/AI as four more tabs on that same
|
||||
page. New reusable dashboard primitives (stat tile, ranked bar list, status distribution bar,
|
||||
meter) are built once in `components/ui` and reused across all four dashboards, following the
|
||||
`dataviz` skill's methodology: pick the form before color, status colors (already defined in
|
||||
this project's own design tokens) for status-flavored splits, a single sequential hue for
|
||||
magnitude rankings, validated in both light and dark mode.
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: TypeScript, Next.js 14 App Router (unchanged).
|
||||
|
||||
**Primary Dependencies**: None new — TanStack Query (already the standard), the existing
|
||||
`components/ui` kit, this project's own Tailwind design tokens. No charting library added
|
||||
(spec.md Assumptions) — every visual is plain HTML/CSS per the `dataviz` skill's Tier 0/1
|
||||
component guidance (a proportional div-based bar needs no SVG/canvas dependency).
|
||||
|
||||
**Storage**: N/A — no client-side persistence; date-range selection is component state
|
||||
(spec.md Assumptions).
|
||||
|
||||
**Testing**: Vitest for the new pure helpers (duration/rate formatting, the "no data" guard);
|
||||
component tests for each dashboard's loading/empty/error/ready rendering (mocked query client,
|
||||
matching 001-agent-admin-ui's own established testing pattern); Playwright E2E against the real
|
||||
running `supporthub-api` + `supporthub-web` pair for the full tab-switching, date-range, and
|
||||
product-selection flow — this session's standing rule of never claiming a frontend scenario done
|
||||
without exercising it against real, live infrastructure.
|
||||
|
||||
**Target Platform**: Web, `(admin)` portal only (Constitution Principle III).
|
||||
|
||||
**Project Type**: Frontend — single Next.js app, no new module boundary crossed.
|
||||
|
||||
**Performance Goals**: Each dashboard fetches independently (its own `useQuery`) so switching
|
||||
tabs doesn't block on data the current tab doesn't need; date-range changes debounce-free (a
|
||||
single explicit "Apply" action, not a fetch per keystroke) to avoid hammering the backend while
|
||||
typing a date.
|
||||
|
||||
**Constraints**: FR-008 — no new/changed backend endpoint; this is presentation-only. FR-004 —
|
||||
every `null` rate/average renders as an explicit "No data" treatment, computed once in the
|
||||
mapping layer (`lib/api/reports.ts`) so every consuming component gets an already-safe shape,
|
||||
never a raw `null` a component might accidentally interpolate into text.
|
||||
|
||||
**Scale/Scope**: One new `lib/api/reports.ts` (+types), four new `features/reports/*-dashboard.tsx`
|
||||
components, four new `components/ui` primitives (stat tile, ranked bar list, status distribution
|
||||
bar, meter), one modified `admin/reports/page.tsx` (adds tabs), one shared date-range control.
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||
|
||||
| Principle / Section | Check | Result |
|
||||
|---|---|---|
|
||||
| I. Each Identity Has Exactly One Authority, and the Frontend Is Never It | Not applicable — no identity/access surface touched. | PASS — N/A |
|
||||
| II. The Backend Is the Sole Source of Business Logic | Every figure (rates, distributions, rankings) is rendered exactly as the backend computed it — this feature does no business-rule computation, only display formatting (e.g. seconds -> "2h 15m"). | PASS |
|
||||
| III. Strict Portal Boundaries | All new code lives under `(admin)`/`features/reports` — no customer- or support-portal import, no shared primitive leaks admin-only data (the new `components/ui` primitives are generic value/label/color renderers, not admin-shaped). | PASS |
|
||||
| IV. Typed API Boundary, No Ad Hoc Fetching | All four dashboards fetched through new `lib/api/reports.ts` functions wrapped in TanStack Query hooks — no direct `fetch`/`axios` in any component. | PASS |
|
||||
| V. Configuration Over Hardcoding | Not applicable — no admin-configurable enum rendered by this feature (the confidence bands/status labels are fixed, backend-defined vocabulary, not business policy). | PASS — N/A |
|
||||
| VI. Accessible, Responsive, Enterprise-Grade UI | New primitives keep density over decoration per this principle's own wording — status colors always paired with a text label (never color-alone, satisfying both this principle's WCAG requirement and the `dataviz` skill's own non-negotiable), keyboard-reachable tab/date controls, responsive down to a single column. | PASS |
|
||||
| VII. Testing Gates | Typecheck/lint clean; new Vitest coverage for the mapping/formatting helpers and each dashboard's query-state rendering; Playwright coverage added for the reports tab-switching flow (not one of the constitution's two named cross-cutting journeys, but this project's own established practice of E2E-verifying every admin feature against a real backend, per 001's own precedent). | PASS |
|
||||
|
||||
No violations requiring Complexity Tracking justification.
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
specs/002-reporting-dashboards-ui/
|
||||
├── plan.md
|
||||
├── data-model.md
|
||||
├── quickstart.md
|
||||
└── tasks.md
|
||||
```
|
||||
|
||||
### Source Code (repository root)
|
||||
|
||||
```text
|
||||
supporthub-web/
|
||||
├── src/
|
||||
│ ├── components/ui/
|
||||
│ │ ├── stat-tile.tsx # NEW
|
||||
│ │ ├── ranked-bar-list.tsx # NEW
|
||||
│ │ ├── status-distribution.tsx # NEW
|
||||
│ │ ├── meter.tsx # NEW
|
||||
│ │ └── index.ts # MODIFIED — export the four above
|
||||
│ ├── lib/
|
||||
│ │ ├── api/
|
||||
│ │ │ ├── reports.ts # NEW — 4 fetch functions
|
||||
│ │ │ └── types/
|
||||
│ │ │ └── reports.ts # NEW — response + view-model types
|
||||
│ │ └── format/
|
||||
│ │ └── duration.ts # NEW — seconds -> "2h 15m" / "No data"
|
||||
│ ├── features/reports/
|
||||
│ │ ├── management-dashboard.tsx # NEW
|
||||
│ │ ├── product-dashboard.tsx # NEW
|
||||
│ │ ├── support-dashboard.tsx # NEW
|
||||
│ │ ├── ai-dashboard.tsx # NEW
|
||||
│ │ ├── date-range-control.tsx # NEW — shared by Management/Product/AI
|
||||
│ │ ├── sla-monitor.tsx # UNCHANGED (012's own)
|
||||
│ │ └── escalation-matrix.tsx # UNCHANGED (012's own)
|
||||
│ └── app/(admin)/admin/reports/
|
||||
│ └── page.tsx # MODIFIED — adds 4 tabs alongside the 2 existing sections
|
||||
└── tests/
|
||||
├── unit/lib/format/ # duration formatting, no-data guard
|
||||
├── unit/features/reports/ # per-dashboard query-state rendering
|
||||
└── e2e/ # reports tab-switching + date-range Playwright spec
|
||||
```
|
||||
|
||||
**Structure Decision**: No new route, no new portal — extends the existing `(admin)/admin/reports`
|
||||
page and `features/reports` module already established by 012-admin-list-views' SLA Monitor/
|
||||
Escalation Matrix work.
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
*No constitution violations — table intentionally omitted.*
|
||||
@@ -0,0 +1,53 @@
|
||||
# Quickstart: Reporting and Analytics Dashboards UI
|
||||
|
||||
Manual verification against a real, running `supporthub-api` (with real ticket/SLA/AI data —
|
||||
reuse the throwaway Postgres/Redis and admin login already established this session) and a
|
||||
real, running `supporthub-web` dev server, logged in as an admin.
|
||||
|
||||
## Scenario 1 — Management dashboard (User Story 1)
|
||||
|
||||
1. Open Admin -> Reports -> Management tab.
|
||||
2. **Expected**: stat tiles for total cases, AI resolved, human escalated, resolved, open;
|
||||
an SLA compliance meter; escalation count; average response/resolution time — all real
|
||||
numbers, not placeholders.
|
||||
3. Change the date range to one with no activity.
|
||||
4. **Expected**: counts show `0`, rates/averages show "No data," never blank or `NaN`.
|
||||
5. Change the date range back.
|
||||
6. **Expected**: the dashboard re-fetches and every figure updates.
|
||||
|
||||
## Scenario 2 — Product dashboard (User Story 2)
|
||||
|
||||
1. Select Product A in the product picker.
|
||||
2. **Expected**: support volume, problem-category ranked bars, AI/human resolution rates, and
|
||||
top-error ranked bars all reflect Product A only.
|
||||
3. Switch to Product B.
|
||||
4. **Expected**: every figure fully replaces — no leftover Product A data visible mid-transition
|
||||
beyond the loading state.
|
||||
|
||||
## Scenario 3 — Support dashboard (User Story 3)
|
||||
|
||||
1. Open the Support tab.
|
||||
2. **Expected**: a ranked bar list of per-agent workload; SLA at-risk and SLA breached shown as
|
||||
two visually distinct figures; response/resolution performance stat tiles.
|
||||
|
||||
## Scenario 4 — AI dashboard (User Story 4)
|
||||
|
||||
1. Open the AI tab.
|
||||
2. **Expected**: AI resolution rate, human-handoff rate, knowledge-match rate stat tiles; a
|
||||
confidence distribution bar (proceed/ask/escalate, success/warning/destructive-toned, each
|
||||
labeled); a tool success/failure distribution bar, same treatment.
|
||||
|
||||
## Scenario 5 — Responsive and dark mode
|
||||
|
||||
1. Resize the viewport down to a narrow mobile width on each tab.
|
||||
2. **Expected**: stat tiles stack to one column, ranked bars remain fully readable, nothing
|
||||
clips or requires horizontal scroll.
|
||||
3. Toggle dark mode.
|
||||
4. **Expected**: every figure, bar, and status color remains legible and uses this project's
|
||||
existing dark-mode tokens.
|
||||
|
||||
## What "done" looks like
|
||||
|
||||
All five scenarios pass against a real running backend and frontend pair, verified visually
|
||||
(not just by reading component code) and via the Playwright spec covering tab-switching,
|
||||
date-range changes, and product selection.
|
||||
@@ -0,0 +1,128 @@
|
||||
---
|
||||
description: "Task list for 002-reporting-dashboards-ui"
|
||||
---
|
||||
|
||||
# Tasks: Reporting and Analytics Dashboards UI
|
||||
|
||||
**Input**: Design documents from `specs/002-reporting-dashboards-ui/`
|
||||
|
||||
**Organization**: Tasks are grouped by user story (US1 = P1 Management, US2 = P1 Product,
|
||||
US3 = P2 Support, US4 = P2 AI), sharing one Foundational phase (types, format helpers, the four
|
||||
new `components/ui` primitives, the shared date-range control).
|
||||
|
||||
## Format: `[ID] [P?] [Story] Description`
|
||||
|
||||
All file paths are relative to `supporthub-web/` (repo root).
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Foundational (Blocking Prerequisites)
|
||||
|
||||
- [ ] T001 [P] Add `lib/api/types/reports.ts` per data-model.md's four DTO interfaces
|
||||
- [ ] T002 [P] Add `lib/format/duration.ts` — `formatDurationSeconds`, `formatRate`,
|
||||
`formatCount` (depends on nothing — pure functions)
|
||||
- [ ] T003 [P] Add `components/ui/stat-tile.tsx` — label/value/optional-delta contract per
|
||||
`dataviz` marks-and-anatomy.md's "figures" section; sans semibold value, sentence-case
|
||||
label
|
||||
- [ ] T004 [P] Add `components/ui/ranked-bar-list.tsx` — takes `RankedBarListItem[]`, renders
|
||||
each as a label + proportional bar (single sequential hue = `primary`) + value, sorted as
|
||||
given (backend already sorts), truncating long labels with a `title` tooltip
|
||||
- [ ] T005 [P] Add `components/ui/status-distribution.tsx` — takes
|
||||
`StatusDistributionSegment[]`, renders a segmented bar (2px surface gaps between segments
|
||||
per `dataviz` marks-and-anatomy.md) plus a visible legend row (label + swatch + value for
|
||||
each segment — never color alone)
|
||||
- [ ] T006 [P] Add `components/ui/meter.tsx` — a single-ratio track/fill pair (SLA compliance
|
||||
rate), fill tone success/warning/destructive by value, unfilled track a lighter step of
|
||||
the same tone
|
||||
- [ ] T007 Export T003-T006 from `components/ui/index.ts`
|
||||
- [ ] T008 Add `features/reports/date-range-control.tsx` — two date inputs + an explicit
|
||||
"Apply" action (plan.md: no fetch-per-keystroke), shared by Management/Product/AI
|
||||
- [ ] T009 Add `lib/api/reports.ts` — `getManagementDashboard(range)`,
|
||||
`getProductDashboard(externalProductId, range)`, `getSupportDashboard()`,
|
||||
`getAiDashboard(range)`, each a thin typed wrapper over `apiClient` per Constitution
|
||||
Principle IV (depends on T001)
|
||||
|
||||
**Checkpoint**: Primitives and typed client in place. Each dashboard can now be built
|
||||
independently.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: User Story 1 - Management dashboard (Priority: P1)
|
||||
|
||||
- [ ] T010 [US1] Add `features/reports/management-dashboard.tsx` — `useQuery` +
|
||||
`getQueryState`, stat tiles (T003) for totals, `Meter` (T006) for SLA compliance,
|
||||
`formatDurationSeconds` for the two averages, `DateRangeControl` (T008) wired to
|
||||
`queryKey` (depends on T003, T006, T008, T009)
|
||||
- [ ] T011 [US1] Unit tests: `formatDurationSeconds`/`formatRate` null -> "No data" guard, a
|
||||
real-value case each, in `tests/unit/lib/format/duration.test.ts` (depends on T002)
|
||||
- [ ] T012 [US1] Component test: Management dashboard renders loading/empty/error/ready
|
||||
correctly against a mocked query client, in
|
||||
`tests/unit/features/reports/management-dashboard.test.tsx` (depends on T010)
|
||||
|
||||
**Checkpoint**: Quickstart Scenario 1 passes against a real backend.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: User Story 2 - Product dashboard (Priority: P1)
|
||||
|
||||
- [ ] T013 [US2] Add a product picker reusing the existing `GET /admin/products` list (spec.md
|
||||
Assumptions) — check for an existing products-list hook/component from
|
||||
012-admin-list-views' own admin/products page before adding a new one
|
||||
- [ ] T014 [US2] Add `features/reports/product-dashboard.tsx` — `RankedBarList` (T004) for
|
||||
problem categories and top errors, stat tiles for support volume/rates, wired to T013's
|
||||
picker + `DateRangeControl` (depends on T004, T008, T009, T013)
|
||||
- [ ] T015 [US2] Component test: switching products fully replaces the rendered figures, an
|
||||
unknown-product error state renders correctly, in
|
||||
`tests/unit/features/reports/product-dashboard.test.tsx` (depends on T014)
|
||||
|
||||
**Checkpoint**: Quickstart Scenario 2 passes against a real backend.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: User Story 3 - Support dashboard (Priority: P2)
|
||||
|
||||
- [ ] T016 [US3] Add `features/reports/support-dashboard.tsx` — `RankedBarList` (T004) for
|
||||
per-agent workload, stat tiles for at-risk/breached/escalation count and performance
|
||||
averages — no date-range control (current-state, per data-model.md) (depends on T003,
|
||||
T004, T009)
|
||||
- [ ] T017 [US3] Component test: at-risk and breached render as visually distinct figures, in
|
||||
`tests/unit/features/reports/support-dashboard.test.tsx` (depends on T016)
|
||||
|
||||
**Checkpoint**: Quickstart Scenario 3 passes against a real backend.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: User Story 4 - AI dashboard (Priority: P2)
|
||||
|
||||
- [ ] T018 [US4] Add `features/reports/ai-dashboard.tsx` — stat tiles for the four rates,
|
||||
`StatusDistribution` (T005) for confidence bands and tool invocations, `DateRangeControl`
|
||||
(depends on T003, T005, T008, T009)
|
||||
- [ ] T019 [US4] Component test: confidence/tool distributions map to the correct tone per
|
||||
data-model.md's fixed mapping table, in
|
||||
`tests/unit/features/reports/ai-dashboard.test.tsx` (depends on T018)
|
||||
|
||||
**Checkpoint**: Quickstart Scenario 4 passes against a real backend.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Polish & Cross-Cutting Concerns
|
||||
|
||||
- [ ] T020 Wire all four dashboards into `app/(admin)/admin/reports/page.tsx` as tabs
|
||||
(`components/ui/tabs`) alongside the existing SLA Monitor/Escalation Matrix sections
|
||||
(depends on T010, T014, T016, T018)
|
||||
- [ ] T021 Playwright E2E: tab-switching, date-range change, and product-selection flow against
|
||||
a real running backend + frontend pair, in `tests/e2e/reports-dashboards.spec.ts`
|
||||
(depends on T020)
|
||||
- [ ] T022 Manually verify Quickstart Scenario 5 (responsive + dark mode) directly in a browser
|
||||
- [ ] T023 Update `specs/002-reporting-dashboards-ui/checklists/requirements.md` Notes with any
|
||||
implementation-time findings
|
||||
- [ ] T024 `npm run typecheck`/`npm run lint` clean; full existing Vitest + Playwright suite
|
||||
re-run to confirm no regression in 001-agent-admin-ui's own coverage
|
||||
|
||||
---
|
||||
|
||||
## Dependencies & Execution Order
|
||||
|
||||
- **Foundational (Phase 1)**: No dependencies — BLOCKS all four user stories
|
||||
- **User Stories 2-5**: Each depends only on Foundational — independent of each other
|
||||
- **Polish (Phase 6)**: Depends on all four user stories
|
||||
Reference in New Issue
Block a user