# Dashboard Specification

## Purpose

The `dashboard` unit exposes a single authenticated analytics screen at `GET /dashboard` that renders three KPI counters, a sales-volume chart bucketed by day / week / month, and a top-10 best-selling products list. All values are computed live per request from the operational tables and presented as a server-rendered HTML page.

## Requirements

### Requirement: Authentication gate

The system SHALL redirect unauthenticated requests for `GET /dashboard` to `auth/login` with a `302` status and SHALL render the dashboard only for requests carrying a valid admin session. (Implemented in `routes/web.php:24-28,42`, `config/admin.php:29`)

#### Scenario: Authenticated access succeeds

- **GIVEN** a user holds a valid admin session cookie
- **WHEN** they send `GET /dashboard`
- **THEN** the system responds `200 OK` with the `pages.dashboard` HTML view

#### Scenario: Unauthenticated access is rejected

- **GIVEN** no valid session cookie is present
- **WHEN** an anonymous request is sent to `GET /dashboard`
- **THEN** the system responds `302 Found` with `Location: auth/login`

---

### Requirement: Range parameter validation

The system SHALL accept a `range` query parameter with the values `day`, `week`, or `month`, and SHALL normalize any absent, blank, or out-of-whitelist value to `month` without producing an error. (Implemented in `app/Http/Controllers/DashboardController.php:47-50`)

#### Scenario: Absent range defaults to month

- **GIVEN** an authenticated admin session
- **WHEN** `GET /dashboard` is requested with no `range` parameter
- **THEN** the response renders the month-bucketed chart

#### Scenario: Invalid range falls back safely

- **GIVEN** an authenticated admin session
- **WHEN** `GET /dashboard?range=year` is requested
- **THEN** the response renders the month-bucketed chart with no error

#### Scenario: Valid range is honored

- **GIVEN** an authenticated admin session
- **WHEN** `GET /dashboard?range=day` is requested
- **THEN** the response renders the day-bucketed chart

---

### Requirement: KPI — total products

The system SHALL include in the view a `total.products` integer equal to the unfiltered count of all product records. (Implemented in `app/Http/Controllers/DashboardController.php:35`)

#### Scenario: Products KPI reflects full table count

- **GIVEN** the products table contains N rows
- **WHEN** `GET /dashboard` is requested
- **THEN** `total.products` in the rendered view equals N

---

### Requirement: KPI — total customers

The system SHALL include in the view a `total.customers` integer equal to the unfiltered count of all customer records. (Implemented in `app/Http/Controllers/DashboardController.php:36`)

#### Scenario: Customers KPI reflects full table count

- **GIVEN** the customers table contains M rows
- **WHEN** `GET /dashboard` is requested
- **THEN** `total.customers` in the rendered view equals M

---

### Requirement: KPI — today's completed orders

The system SHALL include in the view a `total.orders` integer equal to the count of orders where `status = 'done'` and `DATE(created_at)` equals the current server date, excluding all other statuses and prior-day orders. (Implemented in `app/Http/Controllers/DashboardController.php:37-38`)

#### Scenario: Only today's done orders are counted

- **GIVEN** two orders with `status='done'` created today, one with `status='done'` created yesterday, and one with `status='pending'` created today
- **WHEN** `GET /dashboard` is requested
- **THEN** `total.orders` equals 2

---

### Requirement: Day chart bucketing

For `range=day` the system SHALL produce exactly three chart buckets named `Sáng`, `Trưa`, and `Chiều`, covering the time windows 06:00–12:00, 12:00–18:00, and 18:00–24:00 respectively, and SHALL exclude orders created between 00:00 and 06:00 from all buckets. (Implemented in `app/Http/Controllers/DashboardController.php:52-64`)

#### Scenario: Orders fall into the correct store-open windows

- **GIVEN** orders created today at 07:00, 14:00, and 20:00
- **WHEN** `GET /dashboard?range=day` is requested
- **THEN** `labelChart` is `["Sáng", "Trưa", "Chiều"]` and `valueChart` is `[1, 1, 1]`

#### Scenario: Overnight order is excluded

- **GIVEN** an order created today at 03:00 and no other orders today
- **WHEN** `GET /dashboard?range=day` is requested
- **THEN** all three `valueChart` entries are `0` and the 03:00 order appears in none of them

---

### Requirement: Week chart bucketing

For `range=week` the system SHALL produce one bucket per calendar day from `startOfWeek` to `endOfWeek`, with each bucket labelled by its ISO date (`YYYY-MM-DD`) and counting orders whose `DATE(created_at)` equals that day. (Implemented in `app/Http/Controllers/DashboardController.php:65-77`)

#### Scenario: One bucket per day of the current week

- **GIVEN** the current week spans Monday through Sunday
- **WHEN** `GET /dashboard?range=week` is requested
- **THEN** `labelChart` contains exactly 7 ISO-date labels covering each day of the week in order

#### Scenario: Per-day count is accurate

- **GIVEN** two orders created on Wednesday of the current week
- **WHEN** `GET /dashboard?range=week` is requested
- **THEN** the bucket whose label equals Wednesday's ISO date has `valueChart` value 2

---

### Requirement: Month chart bucketing

For `range=month` the system SHALL produce contiguous 6-day windows across the current month, each labelled `dd/mm-dd/mm` (window start to window end inclusive), with no day of the month dropped or counted twice; the system SHALL also populate `yearRange` with the distinct calendar year(s) spanned by the windows, joined by ` - `. (Implemented in `app/Http/Controllers/DashboardController.php:78-99`)

#### Scenario: Buckets tile a 30-day month without gaps

- **GIVEN** the current month has 30 days
- **WHEN** `GET /dashboard?range=month` is requested
- **THEN** `labelChart` contains 5 buckets and every day of the month appears in exactly one bucket

#### Scenario: yearRange reflects covered year

- **GIVEN** the current month falls entirely within a single calendar year Y
- **WHEN** `GET /dashboard?range=month` is requested
- **THEN** `yearRange` equals `"Y"`

#### Scenario: yearRange spans two years when windows cross a year boundary

- **GIVEN** the current month's 6-day windows span two calendar years Y1 and Y2
- **WHEN** `GET /dashboard?range=month` is requested
- **THEN** `yearRange` equals `"Y1 - Y2"`

---

### Requirement: Empty bucket zero-fill

The system SHALL include every expected bucket label in `labelChart` regardless of whether any orders exist in that window, and SHALL set the corresponding `valueChart` entry to `0` for buckets with no orders, preserving the original label order. (Implemented in `app/Http/Controllers/DashboardController.php:104-112`)

#### Scenario: Empty bucket appears as zero, not a gap

- **GIVEN** no orders were created in a particular bucket window
- **WHEN** `GET /dashboard` is requested for that range
- **THEN** the bucket label appears in `labelChart` at its expected position and its `valueChart` value is `0`

---

### Requirement: Chart values count orders not revenue

The system SHALL populate each `valueChart` entry with the `COUNT(*)` of `orders` rows whose `created_at` falls within the bucket window, regardless of order status, monetary value, or item quantity. (Implemented in `app/Http/Controllers/DashboardController.php:59,69,84,102`)

#### Scenario: Two orders in a bucket produce a count of 2

- **GIVEN** two orders with different totals created within the same bucket window
- **WHEN** `GET /dashboard` is requested for that range
- **THEN** the bucket's `valueChart` value is `2`

---

### Requirement: Top-10 best-selling products

The system SHALL include in the view a `products` list of at most 10 entries ordered by `SUM(order_product.qty)` descending, where each entry carries `id`, `name`, and `quantity` (the summed qty), and SHALL exclude any product whose `deleted_at` is not null. (Implemented in `app/Http/Controllers/DashboardController.php:114-119`)

#### Scenario: Ranking is by total quantity sold

- **GIVEN** product A with 50 total sold units and product B with 30 total sold units
- **WHEN** `GET /dashboard` is requested
- **THEN** product A appears before product B in the `products` list

#### Scenario: List is capped at ten entries

- **GIVEN** 15 products each with sales history
- **WHEN** `GET /dashboard` is requested
- **THEN** `products` contains exactly 10 entries

#### Scenario: Soft-deleted product is excluded

- **GIVEN** a product with `deleted_at` set has historical `order_product` rows
- **WHEN** `GET /dashboard` is requested
- **THEN** that product does not appear in the `products` list

---

### Requirement: View payload completeness

The system SHALL pass all seven view variables — `total`, `chartRange`, `range`, `yearRange`, `labelChart`, `valueChart`, and `products` — to the `pages.dashboard` template on every successful `200` response. (Implemented in `app/Http/Controllers/DashboardController.php:121`)

#### Scenario: All view variables are present

- **GIVEN** an authenticated admin session
- **WHEN** `GET /dashboard` is requested
- **THEN** the rendered template receives `total`, `chartRange`, `range`, `yearRange`, `labelChart`, `valueChart`, and `products` without any variable being undefined

#### Scenario: chartRange label map is fixed

- **GIVEN** an authenticated admin session
- **WHEN** `GET /dashboard` is requested with any valid range
- **THEN** `chartRange` equals `{day: "Day", week: "Week", month: "Month"}`

#### Scenario: Active range is echoed back

- **GIVEN** an authenticated admin session
- **WHEN** `GET /dashboard?range=week` is requested
- **THEN** the `range` variable in the template equals `"week"`

---

### Requirement: No write side-effects

The system SHALL treat `GET /dashboard` as fully read-only and idempotent, persisting no data and producing no observable state change as a result of serving the request. (Implemented in `app/Http/Controllers/DashboardController.php:28-122`)

#### Scenario: Repeated requests leave data unchanged

- **GIVEN** the database state at time T
- **WHEN** `GET /dashboard` is requested multiple times
- **THEN** the database state remains identical to its state at time T after every request

---

### Requirement: Non-dashboard resource verbs are not exposed

The system SHALL NOT expose `POST`, `PUT`, `PATCH`, or `DELETE` on `/dashboard`, nor the `create`, `show`, `edit`, `store`, `update`, or `destroy` actions, even if the routing framework auto-registers them. (Implemented in `routes/web.php:42`)

#### Scenario: POST to dashboard is not handled

- **GIVEN** an authenticated admin session
- **WHEN** `POST /dashboard` is sent
- **THEN** the system does not execute any dashboard write action (framework default 405 or 404 is acceptable)

---

### Requirement: Dashboard is not the application landing page

The system SHALL NOT route the application root `/` to the dashboard; the root SHALL redirect to `/pos`, leaving `/dashboard` reachable only by direct navigation. (Implemented in `routes/web.php:22`)

#### Scenario: Root request redirects to POS, not dashboard

- **GIVEN** any session state
- **WHEN** `GET /` is requested
- **THEN** the response is a redirect to `/pos`, not to `/dashboard`
