# Dashboard and Reports: new features

Definitions used everywhere (shared with the work queue): **Waiting / not taken** = stage `ready`, nobody assigned · **Taken, not started** = `ready`, assigned · **In progress** = `in_progress` (working) or `sent_out` (at vendor) · **On hold** = `on_hold`. **Open** = Waiting + Taken + Working + At vendor + On hold. **Over the waiting limit** = waiting **more than** `ageing_warn_days` days (same rule as `FloorBuckets::isOverWaitingLimit`).

| # | Feature | Where | Why (audit ID) | How it was tested |
|---|---------|-------|----------------|-------------------|
| 1 | Department table columns Waiting, Taken, Working, At vendor, On hold; every row adds up to Open; ageing columns grouped under "Waiting, by days" add up to Waiting | Dashboard › Work in each department (`KpiQueries::computeWipCounts`, one grouped SQL query) | DASH-2, DASH-5 | `makes every department row add up…`, `uses one definition per word…` |
| 2 | Bold "All departments" totals row (2 px top border; first row on phones) | Dashboard, WIP report | DASH-6, REP-14 | `renders the department table with a totals row…`, `builds the WIP report…` |
| 3 | Whole department row clickable with chevron; each number opens the matching queue tab (pool / taken / progress / hold) for that department | Dashboard, WIP report | DASH-7, REP-14 | department table test (links contain `tab=`, `department=`) |
| 4 | One waiting limit for red; orange band removed; legend "Red = waiting more than N days"; "N jobs over the limit" count | Dashboard, WIP report, Longest waiting | DASH-8, DASH-13 | `turns waiting red only when it is more than the waiting limit…` |
| 5 | Zero shown as grey "–"; 88 px right-aligned number columns; 13 px headers "0–1 days, 2–3, 4–7, 8+" | Dashboard, all reports | DASH-9, DASH-10, REP-13 | visual check |
| 6 | 44 px section headers (title + legend), no instruction sentence, no collapse control | Dashboard | DASH-11, DASH-15 | visual check |
| 7 | Neutral tiles; unit in label (Late orders, Orders due in 7 days, Jobs not taken, Jobs on hold, Jobs at vendors, Orders ready to send); only the bad part coloured; definitions in tooltips | Dashboard tiles | DASH-3, DASH-4, DASH-2 | `renders neutral tiles…` |
| 8 | New tiles "Done today" (stages finished since midnight) and "Present x / y" (red hint when an absent person still holds work) | Dashboard tiles (`KpiQueries::staffToday`) | DASH-16 | `renders neutral tiles…` |
| 9 | Longest waiting as a 36 px table (Days · Order · Customer · Department · Stage · Item · With), jobs of the same order grouped and expandable, 10 rows; phone: 2-line rows | Dashboard (`KpiQueries::longestWaiting`) | DASH-12, DASH-14 | `groups the longest-waiting list by order` |
| 10 | Phone dashboard: compact tiles, department rows "155 waiting · 36 over limit" | Dashboard | DASH-17 | visual check 390×844 |
| 11 | Dashboard widgets refresh through `RefreshesOnFloorChange` (poll costs one cache read; redraw only when the floor changed); cache holds plain arrays only | Dashboard widgets | performance | `skips the redraw…`, `caches only plain arrays…` |
| 12 | Report tabs (On-time · Stage time · Departments · People · Vendors · Customers · WIP); last tab remembered per person (cache key `reports.last.{user}`) | Reports | REP-15 | `remembers the last report each person opened` |
| 13 | App date pickers (not the browser's) showing "03 Oct 2026", 36 px bordered fields; app selects for filters | Reports | REP-1, REP-2 (P0), REP-18 | `shows every date as "03 Oct 2026"…` (no `dd/mm/yyyy`, no `type="date"`), visual check of open picker |
| 14 | Quick ranges as one segmented control: Today, 7 days, This month, Last month, 90 days, This year; matching one highlighted, "Custom" otherwise; default 90 days | Reports | REP-3 | `highlights the matching quick range…` |
| 15 | Single "Export ▾" menu (Excel, PDF) on the filter row; card header removed | Reports | REP-4, REP-5 | visual check; export tests |
| 16 | Four headline numbers per report with change against the previous period of the same length (red only when worse) | Reports (`Reports::kpi`, `Reports::previous`) | REP-6 | `shows four headline numbers…` |
| 17 | Typed columns: text/dates left, numbers right, header on the side of its data | All reports | REP-7 | visual check |
| 18 | On-time "Days late" column: "+2" red, "–" on time, "No due date"; "All / Late only" toggle; order number links to the order page | On-time report | REP-8, REP-10 | `filters by customer, shows late only…` |
| 19 | Sortable headers, sticky header and first column, 50 rows per page with Previous/Next, default sort worst-late first; exports carry every row in the screen's order | All reports | REP-9 | `pages the screen at 50 rows…` |
| 20 | Filters: customer (On-time, Customers), department (Stage time, People), vendor (Vendors); "Group by customer" with % on time | Reports | REP-11 | filter test |
| 21 | Stage time in production order, variants indented under their stage, headers Done / Work (h) / Wait (h) / Rejected % with tooltips, thin bar behind Wait, one decimal always | Stage time report | REP-12, REP-13 | `lists stages in production order…` |
| 22 | Stage time aggregated in SQL (histograms in 0.1 h steps, medians read from them): 2 years 3.5–5 s / 68 MB instead of 47 s / 1 GB | `KpiQueries::cycleHistograms` | performance | `aggregates time per stage in SQL with medians that match…` |
| 23 | Vendor and customer reports aggregated in SQL (no models); report data cached 5 min (past periods) or until the floor changes (periods including today) | `KpiQueries`, `Reports::build` | performance | export tests |
| 24 | WIP report: dates hidden, "As of …" shown, 4–7 and 8+ cells shaded on one amber scale, red text only over the limit, totals row, numbers link to the queue | WIP report | REP-14 | `builds the WIP report…` |
| 25 | Phone reports: summary numbers first (2 × 2), filters behind a 44 px "Filters" button, rows as 2-line cards | Reports | REP-16 | visual check 390×844 |
| 26 | Empty state with "Try last 90 days" button | Reports | REP-17 | `offers a "Try last 90 days" button…` |
| 27 | Excel export: bold title and headings, real numbers, dates as "03 Oct 2026", totals row, filters and sort carried over | `ReportExportController` | PRINT-6 | `exports every report to Excel…` (×7), `keeps the screen filters and sort…` |
| 28 | PDF export: headline numbers, headings on every page, totals, "Printed … by …" and "Page x / y" footer; capped at 2,000 rows (Excel has all) so a 2-year range finishes in ~12 s instead of crashing | `ReportExportController`, `resources/views/pdf/report.blade.php` | PRINT-6 | `exports every report to PDF` (×7); live check of all 7 reports × 2 formats × 30 days and 2 years on port 8204 |
| 29 | New indexes: lot_stages(status, finished_at), (performed_by, finished_at), (status, ready_at); orders(closed_at), (customer_id, received_at), (received_at); vendor_send_outs(sent_at), (closed_at, expected_at) | `database/migrations/2026_11_01_000500_add_dashboard_and_report_indexes.php` | performance | migration run on the test database; suite |

## QA fixes (01 Nov), group X

| # | Feature | Where | Why (audit ID) | How it was tested |
|---|---------|-------|----------------|-------------------|
| 30 | "Longest waiting" uses the shared definition: waiting = ready and not taken; over the limit = waited more than `ageing_warn_days`. Title count now equals the department table's over-limit total ("580 waiting jobs are over the 3-day limit"); the list shows waiting jobs only. Cache keys bumped (`kpi.tiles.v3`, `kpi.longest.v3.*`) | Dashboard, `KpiQueries` | QA-X9 | Pest `counts "over the limit" the same…`; live: 580 = WIP total = Departments column sum |
| 31 | Grouped rows say "n departments", "n stages", "n items", "n people" consistently (a single value when every job shares it); phone line says the stage once when it is named like its department | Dashboard › Longest waiting | QA-X9 | Screenshot 1366 dark |
| 32 | `KpiQueries::wipCounts()` (public, cached) shared by the dashboard and the Departments page | `KpiQueries` | QA-X10 | Pest |
| 33 | Departments "Over N days" counts only waiting (ready, untaken) jobs over the limit, from the same counts as the dashboard; Open work counts exclude cancelled orders like the dashboard; tooltip reworded | Configuration › Departments | QA-X10 | Pest (per-department sum = dashboard, over ≤ waiting); screenshot |
| 34 | `?report=` always kept in the address (`#[Url(keep: true)]`), including the tab that opened by default | Reports | QA-X11 | Pest; screenshot of `?report=cycle` |
| 35 | Wait-time bars drawn as a subtle full-height bar behind the number instead of an underline | Reports › Stage time | QA-X11 | Screenshot 1366 dark |
| 36 | Dark WIP heat cells: weaker amber (60 %) and red-300 text so over-limit numbers read ≥ 4.5:1 | Reports › WIP | QA-X11 | Screenshot 1366 dark |
| 37 | Excel export writes dates as real Excel dates with number format `dd mmm yyyy` (sortable/filterable); `Reports::EXCEL_DATE_FORMAT` | `ReportExportController`, `Reports::excel()` | QA-X11 | Pest `writes report dates to Excel as real dates…` (reads the file back) |
