# 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); the system SHALL also populate `yearRange` with the distinct calendar year(s) spanned by the windows, joined by ` - `. **Known exception:** when the current month's length is not a multiple of 6 (e.g. a 31-day month, or February), the final window's end date SHALL spill past the end of the month into the following calendar month, and orders created within that spillover range SHALL be counted in the current month's chart rather than excluded; this is a documented, not-yet-fixed defect, not an intended behavior (see `design.md`). (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 (a multiple of 6)
- **WHEN** `GET /dashboard?range=month` is requested
- **THEN** `labelChart` contains 5 buckets and every day of the month appears in exactly one bucket, with no spillover into the next month

#### Scenario: Non-6-multiple month spills into the next month (known defect)

- **GIVEN** the current month has 31 days (not a multiple of 6)
- **WHEN** `GET /dashboard?range=month` is requested
- **THEN** the final bucket's window end date falls in the following calendar month, and any order created within that spillover window is counted in the current month's `valueChart` instead of being excluded from it

#### 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`
