# User Stories — Reporting (Dashboard)

> Produced by the Reversa **Writer** (phase: generation) · doc_level: `complete`
> Generated on 2026-09-21

**Confidence scale:** 🟢 CONFIRMED · 🟡 INFERRED · 🔴 GAP

**Actor:** Store Administrator.
**Owning unit:** `dashboard` (`GET /dashboard`).

The dashboard is a back-office KPI + sales-chart screen. It is **not** the landing page — root `/` 301-redirects to `/pos`, so the dashboard is reached only by navigating to it. Per-customer statistics live under [customer-management.md](customer-management.md); this journey is the store-wide dashboard. 🟢

---

### US-RPT-1 — See store KPIs at a glance

**As a** Store Administrator, **I want** headline numbers on one screen, **so that** I can gauge the shop's activity.

- **Given** the dashboard
- **When** I open `GET /dashboard`
- **Then** three KPIs render: total products (`Product::count`), total customers (`Customer::count`), and **today's `done` orders** (`DATE(created_at) = CURDATE()`) 🟢

Notes / gaps:
- Products/customers are total row counts; only the orders KPI is day-scoped. 🟢
- Dashboard latency at scale is unconfirmed (multiple aggregate queries). 🔴

Traces to: `dashboard/` (`DashboardController::index`)

---

### US-RPT-2 — See a sales-volume chart over time

**As a** Store Administrator, **I want** a chart of order volume by period, **so that** I can spot trends.

- **Given** the dashboard
- **When** I choose a `range` of `day` | `week` | `month` (default `month`)
- **Then** a chart renders order **counts** (`COUNT(*)` of orders, not revenue/units) bucketed by the range, with empty buckets shown as 0 and labels/values aligned 🟢
- **Given** `range=day`
- **Then** the day view uses three store-open 6h windows — Sáng / Trưa / Chiều (06–12 / 12–18 / 18–24) — **excluding** 00:00–06:00 (store closed overnight; confirmed intentional) 🟢

Notes / gaps:
- An out-of-whitelist `range` value falls back to `month`. ✅ Fixed 2026-09-19 (was previously undefined — a `switch` with no matching `case`, broken query). 🟢
- The chart metric is order **count**, not revenue — confirm this is the intended KPI. 🔴
- Carbon 1.25 in-place mutation in the day `CASE` is correct but fragile. 🟢

Traces to: `dashboard/` (`DashboardController::index`, raw SQL `CASE` bucketing)

---

### US-RPT-3 — See the top-selling products

**As a** Store Administrator, **I want** the best-selling products, **so that** I know what to restock.

- **Given** the dashboard
- **When** it loads
- **Then** the top-10 products by `SUM(order_product.qty)` desc render, honouring `products.deleted_at` (soft-deleted products excluded) 🟢

Traces to: `dashboard/` (`DashboardController::index`)

---

> **Related — nightly rebuild (Scheduler):** the per-customer statistics screen ([customer-management.md](customer-management.md), US-CUST-7) reads a **nightly precomputed** `customer_order_summary` rebuilt by `Order::summaryLogging` on the daily scheduler (ADR-0005). That is a system journey with no Administrator action; it is documented here for completeness because it is the store's other reporting surface. A failed/absent nightly run is invisible on the statistics screen. 🔴
