# Shreeji Workflow – new and changed features (redesign, 1 Nov 2026)

Everything that did not exist before the page-by-page redesign, with where it lives, which audit item asked for it and how it was tested. 196 automated tests pass; every page was also checked in light and dark on a laptop and a phone.


## New or changed behaviour: foundation (every page)

| # | Feature | Where | Why (audit ID) | How it was tested |
|---|---|---|---|---|
| 1 | Fixed status colours: blue = in progress, amber = waiting / on hold, red = late / failed, green = ready / done, violet = at vendor | Panel colours (AppPanelProvider) and the order, order page and queue badge mappings | Rules · LOOK-1..3 | Full test suite; screenshots light and dark |
| 2 | Brand colour default is orange; red, green, amber, yellow, blue and violet are refused as a brand colour (they are status colours) and fall back to orange | AppPanelProvider::brandColor, config app.brand_color | Rules · LOOK-2 | Test suite; visual check |
| 3 | Breadcrumbs removed panel-wide (detail pages get their own back link) | AppPanelProvider | SPACE-1, SHELL-4 | Visual check of every page |
| 4 | Menu 232 px wide, 36 px items, collapses to 64 px icons | AppPanelProvider + app.css | SHELL-2, SHELL-3, SPACE-4 | Visual check at 1366×768: whole menu fits |
| 5 | Compact page header (22 px title, 16 px gaps) | app.css | SPACE-1 | Visual check |
| 6 | Tables: 40 px rows, 13 px data, fixed-width digits; 50 rows per page by default (25/50/100); labelled "Filters" button whose count shows only when filters are on | app.css, App\Support\UiDefaults, resources/js/app.js | Rules · SPACE-5, OL-10, USR-7 | Test suite; visual check |
| 7 | Outline badges (colour on the word, no tinted blocks) | app.css | Rules · LOOK-1, LOOK-7 | Visual check light and dark |
| 8 | Primary buttons: brand-700 + white (light), brand-500 + near-black (dark); green buttons dark green + white | app.css | SET-2, LOGIN-2, QUEUE-16, ATT-3 | Contrast computed ≥ 4.5:1 |
| 9 | Menu toggle shows a "menu" icon instead of a back arrow | AppServiceProvider (icon aliases) | SHELL-1 | Visual check |
| 10 | Search placeholder "Search orders, customers, designs" with a Ctrl K hint; wider box; grey clear button | lang/vendor/filament-panels, AppPanelProvider, app.css | SHELL-5, SHELL-7 | Visual check |
| 11 | Avatars in neutral greys | App\Support\InitialsAvatar | SHELL-12 | Visual check |
| 12 | Active menu item: brand left bar on neutral background; neutral menu badges | app.css | SHELL-8, SHELL-10 | Visual check |
| 13 | Edit order form full width (customer name no longer wraps letter by letter) | EditOrder | OF-1 (P0) | Visual check, test suite |
| 14 | Backups table: Size and Status columns separated | backups.blade.php | BAK-1 (P0) | Visual check |
| 15 | Licence dashboard avatar drawn locally (was a broken image) | LicensingPanelProvider | LICD-1 (P0) | Visual check |
| 16 | Assignment board checkboxes fixed size | assignment-board.blade.php | BOARD-1 (P0) | Visual check |
| 17 | Dialog buttons bottom right | App\Support\UiDefaults | Rules · Components | Visual check of Done, Hold, Note dialogs |
| 18 | "My account" in the user menu; language item reads "Language: English → हिन्दी" | AppPanelProvider user menu | SHELL-11 · ROLE-1 | Visual check; My account tests in AdminRedesignTest |
| 19 | Work queue can be filtered to one vendor's jobs (`?vendor=`), used by Vendors › Out now | Queue::applySearch | VEN-2 | Full suite; manual URL check |
| 20 | One waiting limit everywhere: the orders department strip, production grid and Departments "Over N days" now use Settings › "Warn when a stage waits more than" (was a fixed 8 days) | DepartmentStrip, production-grid view, DepartmentResource | DASH-8 · Rules | OrdersListRedesignTest and AdminRedesignTest updated and passing |
| 21 | Global search results show customer, due date, status and ref | OrderResource::getGlobalSearchResultDetails | SHELL-6 | Visual check |
| 22 | Reports is a plain menu item (no one-item group) | ReportsPage, AppPanelProvider | SHELL-9 | Visual check |
| 23 | The menu opens the group that holds the current page | resources/js/app.js, AppPanelProvider | SET-1 | Visual check on Users, Stages |
| 24 | Day toggle buttons no longer all look selected (primary button colour rule skips toggle groups) | app.css | USR-11 | Visual check on user edit and Settings › Calendar |
| 25 | Deploy script takes a "release" backup (kept separately from nightly ones) | deploy/deploy.sh | BAK-2 | Script read-through; `app:backup --kind=release` covered by AdminRedesignTest |
| 26 | Order sheet / job cards route `/orders/{order}/sheet`, opened as a download | routes/web.php, AppPanelProvider | PRINT-5 | OrderPageRedesignTest |

## New on the floor screens (work queue, assignment board, attendance)

Audit sections: "Work queue" (QUEUE-1…19), "Assignment board" (BOARD-1…13), "Attendance" (ATT-1…9) and the route line from "Order stages: a better way to see them".
Tests: `tests/Feature/FloorRedesignTest.php` (16 tests). The whole suite was re-run and passed.

## Shared definitions (also used by the dashboard)

Code: `app/Support/FloorBuckets.php`.

| Word on screen | Rule |
| --- | --- |
| Waiting / not taken | lot stage `ready` with nobody assigned |
| Taken, not started | `ready` with `assigned_to` set |
| In progress | `in_progress` or `sent_out` (at a vendor) |
| On hold | `on_hold` |
| Waiting days in red | the job has waited **more than** `ageing_warn_days` days (Settings: "Warn when a stage waits more than (days)") |

## What is new

| # | Feature | Where | Why (audit ID) | How it was tested |
| --- | --- | --- | --- | --- |
| 1 | Shared bucket rules and waiting limit (`FloorBuckets::apply / of / isOverWaitingLimit`) | `app/Support/FloorBuckets.php` | Queue "In progress" count must match the dashboard (it counted taken jobs too: 1,252 vs 809) | Pest: tab counts, bucket mapping, limit 3 → 3 days not red, 4 red |
| 2 | Queue tab "In progress" now counts only in progress + at vendor; new tab **Taken** for taken-not-started work | Work queue | QUEUE-5 + shared definition | Pest: counts per tab; taken job shows under Taken, not In progress |
| 3 | Route line component: a dot per stage, done ticked, current filled (blue here, violet at vendor, amber on hold, red when waiting past the limit), "Step 3 of 6 · next Plating (Gold)"; hover a tick for who finished it and when | `resources/views/components/route-line.blade.php`, `app/Support/RouteLine.php`; queue cards and list rows | QUEUE-9, route line | Pest: step, total, states and text; Blade render; visual check |
| 4 | List by default on 1024 px+ screens, cards below; "Cards \| List" in words; choice kept in the address (`?view=`) and remembered per person (session); list toggle hidden below 768 px | Work queue | QUEUE-1, QUEUE-2, QUEUE-19 | Pest: setLayout remembers; visual check 1366 and 390 |
| 5 | One-line 40 px list rows: Order · Customer · Item · Qty · Stage · Next · Due · Waiting · With · Note · actions; sticky Order column and sticky actions column | Work queue | QUEUE-2 | Visual check |
| 6 | One control row: status segmented control, "Department: All (717)" picker, 320 px search, layout toggle | Work queue | QUEUE-3, QUEUE-5, QUEUE-19 | Visual check |
| 7 | Department counts follow the current tab and search | Work queue | QUEUE-4 | Visual check (counts change with tab) |
| 8 | Sort control "Rush first / Due date / Waiting longest / Order no." (`?sort=`) and "717 jobs" at the top | Work queue | QUEUE-12 | Pest: waiting and due sorts order rows |
| 9 | One priority badge (RUSH, else High / Urgent), RUSH = 4 px red left edge; neutral card border; red only for late and past the waiting limit | Queue, board (`queue/priority-badge.blade.php`) | QUEUE-6, QUEUE-7 | Visual check |
| 10 | Card layout: customer 13 px, "500 pcs · lot 1" on one line, variant as text, vendor neutral with shop icon, no department chip, note one line that expands, cards size to content | Work queue | QUEUE-8, QUEUE-10, QUEUE-18, QUEUE-19 | Visual check |
| 11 | One filled button for the expected next step (Waiting: Take; taken / started: Done), the other outlined (icon-only in list rows) | Work queue | QUEUE-11 | Pest (existing): worker sees Take, not Assign |
| 12 | "Select 40 shown"; 44 px tap area on phones | Work queue | QUEUE-13 | Pest: "Select 1 shown" for supervisor |
| 13 | **Bulk actions only for people who assign work** (`queue.assign`, existing permission): checkboxes, bulk bar and `bulkTake` / `bulkComplete` server calls | Work queue | Task brief (approved behaviour change) | Pest: worker bulkTake → 403, nothing assigned; supervisor succeeds |
| 14 | Done dialog: 576 px, "Mark done · Packing", bold context line (order · item · lot · qty), instructions in one note box, "Came in: 500 pcs" as text, pcs suffixes, live "500 + 0 = 500 pcs ✓" check, no star on Rejected (still required), button "Mark done". "Work done by" defaults to the assignee, else the signed-in person (was already so; kept). Validation unchanged | Work queue | QUEUE-14, QUEUE-15 | Pest: heading, sum line, performed_by = assignee, completes |
| 15 | "Assign" visible in list rows for supervisors; divider before the red "Send back / rework"; 44 px ⋯ trigger on phones | Work queue | QUEUE-17 | Visual check; Pest (existing) worker never sees Assign |
| 16 | Board: two panes, Unassigned sticky on the left (420 px) with its own search row, people table on the right (name · attendance · ready / started / at vendor · jobs and pieces with a bar against the department average · Give); expand a row for that person's jobs | Assignment board | BOARD-5, BOARD-6, BOARD-9, BOARD-12 | Visual check |
| 17 | Three attendance states on the board: Present, Absent / On leave, Not marked | Assignment board | BOARD-2 | Pest |
| 18 | Absent person holding work: red line "Absent · 62 jobs stuck" + "Move ready jobs to pool" with a confirmation stating the count (releases their not-started jobs in this department through `TakeStage::release`) | Assignment board | BOARD-3 | Pest: confirmation text, job back in pool |
| 19 | Give button always shown, grey until something is ticked, then "Give 3" | Assignment board | BOARD-4 | Visual check |
| 20 | Person job rows: order · item · qty · due (red if late) · priority badge · status badge; "Back to pool" tooltip and "Unassign" on hover | Assignment board | BOARD-7, BOARD-8 | Visual check |
| 21 | **Auto-assign now asks first**, with a dry-run preview ("Assign 155 Casting jobs to 3 present people?" and "Bina 52 → 104" per person); outlined button; in a ⋮ menu on phones. The preview follows `AutoAssign::rebalance()` rules exactly | Assignment board, `app/Support/AutoAssignPreview.php` | BOARD-10 | Pest: preview totals and per-person "after" equal what the real run assigns |
| 22 | Department picker with waiting counts (same control as the queue) | Assignment board | BOARD-11 | Visual check |
| 23 | Phone: "Waiting (155) \| People (6)" switch and a sticky "Give 3 to… ▾" bar | Assignment board | BOARD-13 | Visual check 390 px |
| 24 | **Attendance third state "Leave"** and a way back to "not marked" (click the selected state again). Stored as `status = 'leave'` in the existing `attendance.status` column (string(10)); **no migration needed**. Auto-assign only counts `in` as present, so absent, leave and not marked are all "not present" exactly as absent was | Attendance | ATT-1 | Pest: leave stored, clear deletes the row; leave person not eligible and their auto-assigned work is re-balanced |
| 25 | **"All absent" asks first**: "Mark 6 people in Casting absent? Their 155 waiting jobs will be reassigned." (says so when auto-assign is off or for a past day); grey outlined "All present / All absent" | Attendance | ATT-2, ATT-9 | Pest: confirmation text, everyone marked out |
| 26 | Present = pale green with dark green text (dark: green 20% with light green text); main button dark green | Attendance | ATT-3 | Visual check |
| 27 | One table grouped by department (sticky group rows with present/total), 36 px person rows with status and "marked by · time", name search | Attendance | ATT-4 | Visual check |
| 28 | People in two departments shown once, under the first department, "also Polishing" in grey | Attendance | ATT-5 | Pest |
| 29 | One summary line: day · present · absent · on leave · not marked · the button · "Auto-assign: off" | Attendance | ATT-6 | Visual check |
| 30 | Day switcher "‹ 01 Nov 2026 ›" (`?date=`); past days read-only unless the person has `admin.users` (existing permission); no future days; work is re-balanced only for today; "Weekly off" badge from setting `weekly_off_days` (default Sunday) | Attendance | ATT-7 | Pest: supervisor blocked (403) on yesterday, owner can correct yesterday |
| 31 | All attendance, board and queue text wrapped in `__()` | All three pages | ATT-8 | Strings listed in the hand-off |

## Notes

- New setting key read (no UI yet): `weekly_off_days` (array of weekday numbers, 0 = Sunday). Default `[0]` when not set.
- No new permissions, no new migrations.
- Board load figures are per department on screen (jobs and pieces in this department), the department average is over people in the department who hold work.
- `Queue::routeInfo()` is kept for compatibility; new code uses `App\Support\RouteLine`.

## QA fixes (01 Nov), group X

| # | Feature | Where | Why (audit ID) | How it was tested |
|---|---------|-------|----------------|-------------------|
| 32 | Open ⋯ menu in the list sits above the sticky actions column of the rows below (the cell holding an open dropdown is raised to z-30) | Work queue, List | QA-X1 (P0) | Pest `…open menu above the sticky column (QA-X1, X2)`; screenshot 1366 light with the menu open |
| 33 | List fits 1,086 px at 1366 with no sideways scroll: fixed column widths (`table-fixed` + colgroup), Stage ≤ 200 px with truncation and a tooltip, Next merged into the Stage cell ("Packing → Dispatch"), Note shown as an amber icon with the text in a tooltip, priority/rush as a 16 px icon with tooltip, Due shown as date only (red when late, "Late" in the tooltip), "Send out" (tooltip "Send to vendor") as the compact vendor button; below 1,086 px it scrolls with a shadow on the sticky columns while more is hidden | Work queue, List | QA-X2 | Measured in Chromium: table 1086 px in a 1086 px box; Pest |
| 34 | First layout decided server-side: nothing remembered → List, or Cards when the user agent is a phone/tablet (`Queue::defaultLayout()`); new `$layoutAuto` flag lets the browser refine once (List from 1024 px, Cards below) without remembering it | Work queue | QA-X3 | Pest `picks the list for a new desktop user…` |
| 35 | Edge fade + chevron on the status tabs when they overflow (phones), tabs scroll sideways | Work queue (the board's Waiting/People switch and attendance have no overflowing tabs) | QA-X4 | Screenshot 390 light |
| 36 | Board Unassigned: item on line 1, customer (12 px grey) on line 2; pane 412 px (480 px on 2xl) | Assignment board | QA-X5 | Screenshots 1366 light/dark |
| 37 | Board people: zero Ready/Started/Vendor shown as "–"; Give column fixed width so ticking a job no longer shifts columns; phone shows attendance badge and load under the name and keeps Ready/Started/Vendor/Give on screen ("+ 3" button, word in tooltip) | Assignment board | QA-X5 | Screenshots 390 and 1366 |
| 38 | "Auto-assign now" disabled with tooltip "Auto-assign is off in Settings" when the mode is off (desktop button and phone menu item) | Assignment board | QA-X5 | Pest `disables Auto-assign with a reason when it is off` |
| 39 | Attendance "All present / All absent" are 24 px (36 px on phones) so they fit the group row; dark "Not marked" text gray-400 (≥ 4.5:1) | Attendance | QA-X6 | Screenshot 1366 dark |
| 40 | Dark "pcs"/"sets" unit text gray-400 (≥ 4.5:1) on the board and the queue list | Assignment board, Work queue | QA-X7 | Screenshot 1366 dark |
| 41 | Done dialog: instructions shown as read-only text in the amber note box (Html component), not an input | Work queue › Done | QA-X8 | Pest `shows the Done dialog instructions as an amber note` |

## Orders list, Production grid, new / edit order form — new features

Audit sections: "Orders list" (OL-1 … OL-16), "New and edit order form" (OF-1 … OF-13), and the
department strip + Production grid from "Order stages: a better way to see them".

Shared definitions (same as App\Support\FloorBuckets): in progress = lot stage `in_progress` or `sent_out`;
taken, not started = `ready` with `assigned_to`; waiting = `ready` without assignee; on hold = `on_hold`;
ready to dispatch = item whose live lots all finished every stage.

| # | Feature | Where | Why (audit ID) | How it was tested |
|---|---------|-------|----------------|-------------------|
| 1 | Department strip component: one 44 px column per active department under a row of short codes; each cell = items of the order in that department now, outlined in the status colour (blue in progress, amber on hold, violet at vendor, red when the oldest item has waited 8+ days, grey waiting/taken), shaded darker by days waiting (0–1, 2–3, 4–7, 8+, the dashboard's buckets); then "7/12 done" with a thin bar. Hover text gives the split (in progress / at vendor / taken / waiting / on hold) and the oldest wait. | `resources/views/components/department-strip.blade.php`, `app/Support/DepartmentStrip.php` | Order stages, OL-3 | Pest: counts and status split, tone and shading, batch loading in ≤ 2 queries; visual light/dark |
| 2 | Strip data from one SQL aggregate per page of orders (open lot stages grouped by order × department) plus one progress aggregate; the list no longer eager-loads items → job cards → lots → stages | `DepartmentStrip::load()`, `OrderResource::table()` | Rule 6, OL-3 | Pest query-count test; page loads in ~1 s on 2-year data |
| 3 | Production grid page: open orders × departments, items there now, row totals right, column totals at the bottom (all matching orders, not just the rows shown), "Done" per order, legend; filters customer, due (overdue / this week / next week / later / no date), priority (rush / urgent / high+); each cell opens the work queue on that department with the order searched, on the tab holding those lots (waiting → pool, otherwise in progress, otherwise hold); column totals open the department's queue; sticky header, first column and totals; 150 rows with "Show more" | `app/Filament/Resources/Orders/Pages/ProductionGrid.php`, `resources/views/filament/resources/orders/pages/production-grid.blade.php`, route `/orders/grid` | OL-16, Order stages | Pest: totals, customer filter, cell links; visual light/dark/phone |
| 4 | Orders header bar: title, tabs (left, grey counts, Late red only above 0, 2 px underline on the selected tab, count on Closed), List / Grid switch, New order (a "+" on phones) in one row; tabs are real links | `ListOrders::getHeader()`, `partials/header.blade.php` | OL-1, OL-9, OL-15, OL-16 | Visual; Pest tab test |
| 5 | Last used tab remembered per person (session); a `?tab=` link always wins | `ListOrders` | OL-8 | Pest |
| 6 | New list columns: Late ("23 d" red, "today"/"tomorrow" amber, blank otherwise, right-aligned); Ready ("Ready 3/12") on open tabs, status badge only on Closed; optional (column picker, remembered in the session) Ref, Received, Items, Qty, Priority; columns can be reordered | `OrderResource::table()` | OL-4, OL-6, OL-7 | Pest list test; visual |
| 7 | Priority column removed by default; Rush / Urgent / High badge after the order number; Priority filter, sort (via the optional column) and grouping kept | `OrderResource::orderNoHtml()` | OL-5 | Visual |
| 8 | Customer column min 200 px, one line, full name on hover; dates as "09 Oct" (year only when not this year) | `OrderResource` | OL-2, type rules | Visual |
| 9 | Filters: Customer, Priority, Due (from/until), Received (from/until), Product type, Items now in department, Has work on hold, Has work at a vendor, Rush; apply instantly; grey "Clear all"; bordered popover in dark | `OrderResource::table()`, `partials/list-footer.blade.php` | OL-10 | Pest (on hold, rush, department) |
| 10 | Group by Customer / Due date / Priority | `OrderResource::table()` | Excel-grade tables | Visual |
| 11 | Keyboard: "/" focuses the 320 px search, ↑/↓ move between rows, Enter opens | `partials/list-footer.blade.php` | OL-11, Excel-grade tables | Manual in browser |
| 12 | Sticky table header on the list (scrolls inside the card) | `partials/list-footer.blade.php` | OL-13 | Visual |
| 13 | Phone card: order no + flag + due/late on one line, customer, progress bar, "7/12 done", "stuck: Plating 9 d" (the longest-waiting department); no table header row; tabs scroll with a fade; 44 px controls | `OrderResource::mobileSummary()`, footer styles | OL-15 | Visual 390 px light/dark |
| 14 | Item grid on New order: one ~44 px row per item (Design · Description · Type · Qty · Unit · Stages), Tab moves across, Enter in a text/number cell adds a row and focuses it, "Add item", "Add 5 rows", blank rows ignored on save | `OrderResource::itemGrid()`, `CreateOrderPage::mutateFormDataBeforeCreate()` | OF-2, OF-3, OF-10 | Pest (blank rows dropped, order saved with job card); browser flow |
| 15 | "Paste from Excel": paste tab-separated rows (design ref or free text, qty, unit, description); known refs fill type, unit, description and stage choices like picking the design; blank rows are filled first; lines without a quantity are skipped and listed | `OrderResource::parsePastedRows()/pasteRows()` | OF-3 | Pest parser test; browser flow |
| 16 | "Stages" cell per row: "From D-02442 ✓" / "Type defaults ✓" / red "Choose: Plating"; opens a side panel with the stage choices, "Instructions for this stage only", "Instructions for every department" and the reference image; a notification points to it if a required choice is missing on save | `OrderResource::stagesAction()`, `CreateOrderPage::onValidationError()` | OF-3, OF-9, OF-11 | Browser flow; saved variants checked in DB |
| 17 | Free-text design ref inside the design search ("XYZ — free text, not saved as a design"); "+" saves it as a new design (pre-filled) | `OrderResource::designSelect(freeText: true)` | OF-8 | Pest; browser flow |
| 18 | Quantity right-aligned with the unit code inside the field; whole numbers only for units with 0 decimals | grid `qty` field | OF-12 | Browser flow |
| 19 | Sticky save bar: "Save order", "Save and start another" (keeps customer and dates), "Cancel", and "12 items · 1,374 pcs" | `CreateOrderPage::getFormActions()` | OF-4 | Pest total text; visual |
| 20 | Header layout for create and edit: Customer · Ref / Received · Due · Priority · Rush; Rush explained ("Jumps ahead of every priority"); notes behind "Add order note" as "Office note (not shown on the floor)"; selects truncate | `OrderResource::headerSchema()`, `EditOrder` | OF-1, OF-5, OF-7, OF-11 | Pest edit test; visual |
| 21 | Due date defaults to received + `default_lead_days` (Settings key, 14 when unset) and follows the received date while still automatic; required when Settings key `require_due_date` is true; a repeated order gets the default too | `OrderResource::headerSchema()`, `CreateOrderPage::fillForm()` | OF-6 | Pest |
| 22 | Placeholders: "Search customer…", "Design ref or name…", "Type", "Unit", "Free text" | form | OF-7 | Visual |
| 23 | Row delete actions carry no keyboard shortcut (Filament's default Ctrl+D on DeleteAction removed) | `SafeDelete::make()` / `action()` | QA Y-1 | Pest OrdersQaFixesTest |
| 24 | New order save bar is CSS-sticky inside the content column (card width, always visible, also after a failed save); phones: one row (Save · Cancel · total), "Save and start another" desktop only | `CreateOrderPage::getFormActionsContentComponent()`, `partials/form-footer.blade.php` | QA Y-2 | Playwright 1366/390 (bar 60 px, same left/width as cards) |
| 25 | Customer error clears as soon as a customer is picked; item fields labelled on phones; 44 px copy/delete/"+" targets on phones; Stages cell padding; "Add order note" as a 14 px link-style toggle | `OrderResource::headerSchema()`, `itemGridSchema()`, `form-footer` | QA Y-3 | Pest (error cleared); visual |
| 26 | Orders list: "Done" header in normal case; Ready column only on Closed (as Status) and "· 4 ready" in the Done cell instead; Group-by "None" and a labelled "Group by" button on phones in the Filters row; labelled 44 px "New order" on phones; High badge neutral grey; Priority filter non-native like Customer; Filters popover scrolls (max-height); phone card says "N ready to dispatch" instead of "stuck" on the Ready tab | `OrderResource::table()`, `mobileSummary()`, `ListOrders`, `department-strip`, `list-footer` | QA Y-4 | Pest (High badge); visual |
| 27 | Production grid: same tabs as the list (links), Filament-style selects, totals with units ("27 items ready (8 orders)"), legend chips that look like cells, late/hold cell text -800 (≥4.5:1) | `ProductionGrid`, `production-grid.blade.php`, `DepartmentStrip::cellClasses()`, `partials/header` (`link` tabs) | QA Y-5 | Pest (readyOrders, cell classes); visual light/dark |
| 28 | Global search: orders on two compact lines ("Moti Arts 60 · due 09 Oct · In progress · PO-…"); exact / suffix order-number matches ranked first, then open before closed; customers "phone · city · N open orders"; designs "name · type" | `OrderResource::modifyGlobalSearchQuery()` etc., `CustomerResource`, `DesignResource` | QA Y-6 | Pest |
| 29 | Edit order: "‹ Back to SC-06002" and "order page" link | `EditOrder` | QA Y-8 | Pest; visual |

## New features: order page, customers, designs, printouts

Tests: `tests/Feature/OrderPageRedesignTest.php` (17 tests). Visual check: light and dark at 1366×768 and 390×844 on the 2-year test data.

| # | Feature | Where | Why (audit ID) | How it was tested |
|---|---------|-------|----------------|-------------------|
| 1 | Item × stage grid: rows = items (lot sub-rows only when an item has 2+ lots), columns = every stage used by the order's items in merged route order, sticky item column, "N here" header counts, tick / circle / dash / boxed current cell with person, vendor or variant and days in state | `resources/views/components/stage-grid.blade.php` (+ `stage-grid-cells.blade.php`), data from `App\Support\StageGrid` | Order stages; OP-1, OP-6, OP-8, OP-11 | Tests "builds the grid…", "colours the current cell…", "shows a vendor stage…"; visual |
| 2 | Current-cell colours by the shared definitions: in progress (in_progress) blue, at vendor (sent_out) violet, taken-not-started grey solid, waiting grey dashed, on hold amber; red when waiting MORE THAN `ageing_warn_days` or a vendor send-out is past its expected date | `StageGrid::cell()` | OP-9, Order stages › symbols | Test "colours the current cell…" |
| 3 | Side panel (slide-over) for a lot: where it is now, actions, full trail with who/when, qty ok/rejected, skipped and sent-back attempts; opens from a cell or a row; Esc / backdrop closes | `view-panel.blade.php`, `ViewOrder::openLot()/closePanel()` | OP-1, OP-8, OP-10 | Test "opens the side panel…"; visual incl. modal over panel |
| 4 | New lot actions on the order page: Take, Done, Send to vendor, Receive (same workflow classes TakeStage / CompleteStage / VendorWork, same forms as the work queue, same department rule) | `ViewOrder::takeAction/completeAction/sendOutAction/receiveAction` | Order stages (panel actions) | Tests "takes, completes…", "splits a lot and sends…", "does not let a person take work of another department…" |
| 5 | Existing Hold / Release / Send back / Split now grey 32 px buttons (44 px on phones) in the panel; colour only on the confirm button | `ViewOrder::panelButton()` | OP-10 | Tests above; visual |
| 6 | "Merge lots" shown on the item row when two lots wait at the same stage (computed from loaded data) | grid item cell | (kept) | ScreensTest "offers merge…" |
| 7 | One-line header: ‹ Orders · order no. · customer (link to customer page) · status · "Late N d" · Rush | `view-header.blade.php`, `ViewOrder::getHeading()` | OP-2 | Test "renders the order page…" |
| 8 | Summary strip: customer + tap-to-call phone (WhatsApp link on phones), Received + "Entered by X" (department on hover), Due, Priority only if not Normal, Items, Qty per unit, "Dispatched x of y" items | `view-summary.blade.php` | OP-3, OP-4, OP-5 | Test "renders the order page…"; visual |
| 9 | Item status badge only for On hold, At vendor, Ready / Part dispatched, Dispatched, Cancelled | `StageGrid::itemBadge()` | OP-7 | Test "shows a vendor stage…" |
| 10 | Item heading "1 · REF · name · qty", 32 px thumbnail (placeholder icon when none), note icon with the instructions on hover | grid item cell | OP-13 | Test (instructions visible); visual |
| 11 | Legend behind a "?" popover beside the grid heading | grid header | OP-12 | Test "renders…" (sees "What the symbols mean") |
| 12 | Discussion log: "No notes yet" one line, "Add note" button in the card header; right column sticky when there is room, otherwise below the grid (container query) | `view.blade.php` | OP-14 | Test + visual |
| 13 | History grouped by day, "Show all" after 10; adds stage completions and dispatches that were never written to the timeline (older/imported data) | `App\Support\OrderHistory` | OP-15 | Tests "adds stage completions…", "logs and shows completions…" |
| 14 | Header: Dispatch is the only filled (primary) button; "Priority & due date"; More menu in groups with a divider before Cancel; Print group (Order sheet, Job cards, Completion summary) | `ViewOrder::getHeaderActions()` | OP-16, PRINT-5 | Test "offers the printouts…" |
| 15 | Add note modal: "Save note", item placeholder "Whole order", Ctrl+Enter (⌘+Enter) saves | `ViewOrder::commentAction()` | OP-17 | Browser check (Ctrl+Enter saved a note) |
| 16 | Phone layout: item list with route-line dots and current stage, tap opens the panel (full width); 44 px header and panel buttons | grid (md:hidden list) | OP-18 | Visual 390×844 |
| 17 | Order sheet PDF (any time) with order facts, items, totals and the black-and-white grid; `?cards=1` prints one job card per item with its grid and route table (done by / date / qty ok) | `App\Http\Controllers\OrderSheetController`, `pdf/order-sheet.blade.php`, `pdf/_stage-grid.blade.php` | PRINT-5, Order stages › print | Test "prints an order sheet and job cards…" (HTML + PDF) |
| 18 | Print header from Settings: company name, address, GSTIN, phone, logo (keys `company_address`, `company_gstin`, `company_phone`, `company_logo`; absent ones are left out) | `App\Support\PdfDocument::company()`, `pdf/_header.blade.php` | PRINT-1 | Test sets address + GSTIN and sees them |
| 19 | Completion summary: due, closed and "On time" / "N days late"; empty columns dropped; Rejected column; totals row; header repeated on each page; rows never split; one line per dispatch ("N items · qty") with a compact per-item line; "Handed to" hidden when empty; footer "Printed on … by …" and "Page x of y" | `OrderSummaryController`, `pdf/order-summary.blade.php`, `PdfDocument::stream()` | PRINT-1…PRINT-4 | Test "prints a fuller completion summary…" (HTML + PDF) |
| 20 | Customer page: details, contact people, orders (open first, then newest, filter open/closed), "Repeat last order", Edit in the header | `Customers/Pages/ViewCustomer.php`, `RelationManagers/OrdersRelationManager.php` | CUST-7 | Test "shows a customer page…" |
| 21 | Customers list: A–Z default sort; Open / Late / Orders / Last order right-aligned (SQL counts); Open and Late open the Orders list filtered by customer (`?tab=open|late&filters[customer_id][value]=ID`); grey type text, "Platform"; phone "94473 59963" tap-to-call, "–" when empty; Active column removed, inactive hidden by default with an "Inactive" filter and a grey badge; City filter; grey row menu | `CustomerResource::table()` | CUST-2…CUST-6, CUST-8, CUST-9 | Test "lists customers A–Z…" |
| 22 | New customer as a side panel: "New customer" / "Save customer" / "Save and add another"; no Active toggle on create; "+ Add contact person" with a help line; phone and GSTIN checks (GSTIN stored upper-case); city suggestions | `CustomerResource::form()`, `ManageCustomers` | CUST-10 | Test "formats phones… validates phone and GST…" |
| 23 | Designs list: 32 px thumbnail or placeholder icon, 3× preview on hover; Times ordered, Last ordered, Open orders (opens a list of those orders), Customer (when only one customer has ordered it); default sort last ordered then newest; first/last page links; "4 of 6 stages" with a bar and a Complete / Partial / None filter; "Ref or name" search; inactive hidden by default; grey row menu | `DesignResource::table()`, `withUsage()` | DES-2…DES-9 | Test "shows design usage…" |
| 24 | New design panel: "New design", next ref pre-filled (D-05001), unit filled from the product type, grey "From the product type" placeholder, hint before a type is picked, no Active on create, 96 px image picker beside Ref and Name, "Save design" / "Save and add another" | `DesignResource::formFields()`, `ManageDesigns` | DES-10, DES-11 | Test (nextRef); visual |
| 25 | Phone helper (format, tel:, WhatsApp, validation) | `App\Support\Phone` | OP-5, CUST-6, CUST-10 | Test "formats phones…" |
| 26 | In-progress cells: days in red when in progress longer than 2 × waiting limit, or longer than the limit on an order past due (box stays blue); side panel single spacing around "·"; phone route line uses ticks for done; history "1 item" / "3 items" | `StageGrid` (`slow`, `orderLate`), `stage-grid(-cells)`, `view-panel`, `OrderHistory::plural()` | QA Y-7 | Pest; visual |
| 27 | Customers: Open count neutral (Late red only > 0); instant filters with grey "Clear all"; customer page dates left-aligned; orders search "Order no. or ref" (ref searchable); delete asks "Delete Moti Arts 60? This cannot be undone." | `CustomerResource`, `OrdersRelationManager`, `SafeDelete::make()` | QA Y-9 | Pest (delete heading); visual |
| 28 | Designs: "Open orders" count neutral (was brand orange; type chips already grey) | `DesignResource` | QA Y-10 | Visual |

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

## New features: admin and setup

Settings, Backups, Users, My account, the five configuration lists (departments, stages,
product types, units, vendors), the licence dashboard, sign in, error pages and the shared
empty state. Everything below did not exist before this redesign. Automated tests are in
`tests/Feature/AdminRedesignTest.php` (21 tests); "visual" means checked in light and dark at
1366×768 and 390×844 on the 2-year test data.

| # | Feature | Where | Why (audit ID) | How it was tested |
|---|---------|-------|----------------|-------------------|
| 1 | Restore from a backup: `php artisan app:restore {id or file} [--force] [--by=user]`. Checks the zip and checksum, takes a "Before restore" backup, maintenance mode on, replays `database.sql` through PDO (no `mysql` binary), runs newer migrations, keeps the backup list and open sessions, empties caches and queued jobs, swaps `storage/app/private` for the zip's `files/`, maintenance off, logs who restored what (activity log, laravel.log, "Restored … by …" on the backup row). If anything fails, the safety backup is replayed automatically. | `app/Console/Commands/RestoreCommand.php`, `app/Support/RestoreService.php`, `app/Support/SqlStatementReader.php` | BAK-7 | Pest (SQLite): restore brings back data and files, list survives with the safety backup, restoring the safety backup undoes it; damaged/failed backups refused. Manually on MySQL/MariaDB in throwaway databases (`restore_test`, and a private copy of the 2-year data), never on `workflowmgmt`. |
| 2 | "Restore…" in each backup's ⋮ menu, owner only, type the company name to confirm | Backups page | BAK-7 | Pest: hidden for a manager with backup access; wrong company name refused. Visual. |
| 3 | "Delete" in the backup ⋮ menu (removes file and row) | Backups page | BAK-7 | Pest |
| 4 | Health line: last nightly backup with size, next run, red "Nightly backup did not run" after 26 h or "Last nightly backup failed" | Backups page; `App\Support\BackupHealth::check()` for the dashboard | BAK-2 | Pest (on time, overdue, failed). Visual. |
| 5 | New retention: nightly 7 daily / 4 weekly / 12 monthly; manual, before-update and before-restore backups kept apart (newest 5 of each); failed attempts kept 30 days | `App\Support\BackupService::idsToKeep()` / `prune()` | BAK-2, BAK-8 | Pest: 400 days of nightly backups + manual + release + failures; real prune with files. |
| 6 | Backup kinds and who made it: `backups.kind` (nightly, manual, release, pre_restore), `created_by`, `restored_at`, `restored_by`; `app:backup --kind=nightly|manual|release` (default nightly = the scheduler) | migration `2026_11_02_000300_admin_redesign_columns.php` | BAK-3 | Pest |
| 7 | Backup dump now writes column names in each INSERT, so a restore still works after later migrations add columns | `BackupService::dumpRows()` | BAK-7 | Pest + MySQL restore |
| 8 | Backup list: "Nightly / Manual by name / Before update / Before restore", age ("· 9 h ago"), grey tick for OK, red "Failed" with reason, "Backing up…" spinner, retention rule and restore steps beside the table | Backups page | BAK-3…BAK-6, BAK-8 | Visual |
| 9 | Next order number preview ("Next order number: SC-06255 · Affects new orders only"), live | Settings | SET-4 | Pest |
| 10 | One Save at the bottom, disabled until something changes, "Unsaved changes", Ctrl+S | Settings | SET-3 | Pest |
| 11 | Auto-assign mode as three option cards; unit suffixes (days, min, sec) in narrow fields; Advanced section (collapsed) with the refresh interval | Settings | SET-5…SET-8 | Pest (save), visual |
| 11c | Orders section: "Default lead time" (`default_lead_days`, integer days, default 14: new orders' due date = received + this) and "Due date is required" (`require_due_date`, default off); both defaults added to `Setting::DEFAULTS` | Settings → Orders | SET-9 (coordinator request) | Pest |
| 11b | Company details for printouts: address (textarea), GSTIN (15 characters, checked, saved upper-case), phone, logo (image upload on the public disk, relative path saved) as settings `company_address`, `company_gstin`, `company_phone`, `company_logo` | Settings → Company | PRINT-1 (coordinator request) | Pest (bad GSTIN refused, upper-casing, logo stored) |
| 11a | Calendar section with "Weekly off" as seven day toggle buttons; saved as setting `weekly_off_days` (array of weekday numbers, 0 = Sunday; default [0]) for the attendance / floor screens | Settings | SET-9 (coordinator request) | Pest |
| 12 | "Delete…" that counts what refers to the record and offers Deactivate instead; Deactivate / Activate in every row menu | Stages, product types, departments, units, vendors, users (`App\Support\SafeDelete::action()`, `App\Support\Usage`) | Brief: block deleting things history refers to; STG-11 | Pest: used stage, unit, department, user deactivated; unused unit and vendor deleted; own account can't be deleted |
| 13 | Users list tabs All · Office · Supervisors · Workers · Inactive with counts; inactive rows greyed with badge; working hours column ("09:30–18:30 Mon–Sat" or "Any"); "Never" for no login, exact time on hover | Users | USR-2, USR-3, USR-4, USR-6 | Pest (tabs, hours label), visual |
| 14 | User edit: title = person, role and last login underneath; Lock / Unlock / Sign out now / Deactivate as grey header buttons; one-column layout; working days as Mon–Sun toggle buttons | Users → edit | USR-9, USR-10, USR-11 | Pest (save days), visual |
| 15 | Permissions: summary "Role: Worker + 1 extra"; role permissions shown ticked and disabled, extras as checkboxes; role permissions are no longer copied onto the person when saving | Users → edit | USR-13 | Pest |
| 16 | My account page for everyone: name, language, theme (Light / Dark / Auto), change own password or PIN (current one required) | `/my-account` (`App\Filament\Pages\MyAccount`) | SHELL-11 | Pest (wrong current PIN refused, PIN, password, name, language) |
| 17 | Stages list: drag to reorder, variant names, "Used in" (product types, names on hover), grey ticks for Counts qty / Skippable, behaviour as text with icon | Stages | STG-1…STG-6 | Visual; ScreensTest |
| 18 | Stage edit: name as title with behaviour and variant count; behaviour option cards; floor toggles in one row; variants as a full-width table; only one default (radio-like, enforced on save); a used variant is made inactive instead of removed, with the count | Stages → edit | STG-7…STG-12 | Pest (Livewire: remove used variant, default switch, save), visual |
| 19 | Product types list: route ("Casting → Polishing → … (8)", full route on hover), Designs links to the filtered designs list, Version hidden by default, unit names | Product types | PT-2, PT-6, PT-8 | Pest, visual |
| 20 | Product type edit: details in one row, steps as a compact table with step numbers, version in the subtitle and "Saving creates version N+1…" note, suggested short code on create | Product types → edit | PT-3, PT-4, PT-5, PT-7 | Pest, visual |
| 21 | Departments: Supervisor, Members (links to filtered users), Stages, Open work (links to the department's queue), 8+ days (red when above 0), drag to reorder | Departments | DEPT-2, DEPT-3, DEPT-4 | Pest, visual |
| 22 | Units: "Equals" (e.g. 144 pcs) field and column (`units.equals_qty`, `units.equals_unit_id`), "Shown as" with an example (0.000 kg) | Units | UNIT-2, UNIT-3 | Pest |
| 23 | Vendors: tap-to-call phone, "Works on" (linked stage variants), Out now, Overdue (red above 0), Avg days out; no footer when one page | Vendors | VEN-1…VEN-5 | Pest, visual |
| 24 | Configuration lists share one look: codes as grey text, no Active column (inactive rows greyed), counts right-aligned, grey ⋮ menu, search only from 15 rows, sentence-case titles | `App\Support\ListUi` | DEPT-1, DEPT-5, DEPT-6, UNIT-1, UNIT-4, PT-1 | Visual |
| 25 | Licence dashboard: Status with the reason (`license_clients.status_changed_at/by`), "Paid until … · 370 d left" (amber/red), summary tabs Active · Due soon · In grace · Locked · Not checked in for 24 h, address link or "No address set", version amber when older than the server's, grey Record payment, filter badge only when active, no breadcrumb | Licence dashboard | LICD-2…LICD-8 | Pest (reasons, days left, outdated, summary) |
| 26 | Sign in: company name (or logo) with "Sign in to continue", no required stars, "Forgot your PIN? Ask your manager to reset it.", English · हिन्दी switch before signing in, 44 px fields on phones | `/login` | LOGIN-1…LOGIN-6 | Pest |
| 27 | Branded error pages 403, 404, 419, 500, 503 that follow the saved theme; Dashboard or Work queue by role, Go back; 404 offers "Search for SC-06002" for order/design-like addresses; 419 "Your session ended" with Sign in that returns to the page; 503 retries by itself | `resources/views/errors/*` | ERR-1…ERR-4 | Pest (404, 403 by role, 419, 500, 503), visual |
| 28 | Reusable empty state `<x-empty-state message hint icon compact>` with a slot for the actions | `resources/views/components/empty-state.blade.php` | EMPTY-1 | Pest; used on Backups |
| 29 | Backups on phones: one stacked row per backup (name, date · age · size, failure reason, ⋮ menu), no sideways scroll; desktop table aligned to its card | `resources/views/filament/pages/backups.blade.php` | QA Z-1 | Visual 390/1366 light+dark (scrollWidth 390) |
| 30 | Tests run maintenance mode in memory (cache driver, array store), so a restore test can never put the live app into 503 | `phpunit.xml` | QA Z-2 | Pest: driver is CacheBasedMaintenanceMode, no `storage/framework/down` after restore / full suite |
| 31 | App-wide list rows 41 px (row-actions cell slimmed); list tabs styled like Orders (left, underline, grey counts, colour only when > 0); placeholders and grey text ≥ 4.5:1; 44 px phone touch targets; page titles left on phones; neutral multi-select chips; toggle buttons brand colour ≥ 4.5:1; table-repeater headers aligned; forms left-aligned ≤ 960 px | `resources/css/app.css`, `resources/js/app.js` | QA Z-3/4/5/6/9/10/11/15 | Visual (row heights measured 41 px on Users/Customers/Units/Orders) |
| 32 | User menu shows role and departments under the name; theme switch labelled Light / Dark / Auto; divider before Sign out; phone search placeholder "Search" without Ctrl+K; phone drawer has a 44 px close button and a name/role/Sign out block; menu keeps the current page in view; a menu group with one item has no header | `AppPanelProvider`, `resources/views/filament/partials/{user-identity,drawer-close,drawer-user,ui-strings}.blade.php`, `app.js`, `app.css` | QA Z-7 | Pest (rendered markup) + visual 390/1366 |
| 33 | Licence panel loads the app stylesheet (compact header, Orders-style tabs); Status reason line no longer repeats "Locked" when no reason is known | `LicensingPanelProvider`, `LicenseClientResource::statusReason` | QA Z-8 | Pest (existing reason test) + visual |
| 34 | Stage variants: a variant in use has no bin, it gets a "Make inactive" button instead; same labels on phones as desktop headers | `StageResource` | QA Z-10 | Pest: delete hidden / Make inactive visible and deactivates |
| 35 | Vendors "Avg days out" shows "N so far" (average age of open send-outs) when nothing has come back yet; send-outs count in normal numerals | `VendorResource::workload/avgDaysLabel` | QA Z-12 | Pest |
| 36 | Data migration: Dozens = 12 pcs, Gross (144) = 144 pcs, any "Name (N)" unit = N pcs, only where Equals was empty | `database/migrations/2026_11_01_120000_derive_unit_equivalents.php` | QA Z-13 | Pest; run on the shared test DB |
| 37 | 404 for /orders/<id> offers "Search all orders, closed ones too" and "Orders list" (designs likewise) | `resources/views/errors/404.blade.php` | QA Z-14 | Pest |
| 38 | Departments: "Reorder" is a labelled button; Product types "Version" column always shown (no lone column-manager icon); Vendors no toggleable column | `DepartmentResource`, `ProductTypeResource`, `VendorResource` | QA Z-11 | Visual |
| 39 | Settings logo drop zone reads "Drop a logo here or browse (PNG/JPG, 2 MB)" | `Settings` page | QA Z-9 | Visual |
| 40 | No Ctrl+D shortcut on any Delete action (global DeleteAction default; licence payments row Delete explicitly) | `AppServiceProvider`, `PaymentsRelationManager` | QA Z (coordinator) | Pest: DeleteAction has no key bindings |

## Added 2 Nov 2026 (version, maker, licence info, cron health)

| # | What | Where | Asked for | Tested |
|---|------|-------|-----------|--------|
| 41 | Version (from `VERSION`) and "Made by Lemon Logic Interactive Studio" (links to lemonlogicinteractive.com) at the foot of the sidebar (hidden while it is collapsed) and under the sign-in form | `resources/views/filament/partials/{app-credit,login-credit}.blade.php`, `AppPanelProvider` | Owner request 2 Nov | Pest (sign-in page and dashboard show both), visual light + dark |
| 42 | Settings › Licence card: status (Active / Renewal due / Grace / View-only / No licence check), licensed to, plan, paid until, view-only from, last confirmed, licence key masked (first and last 4 shown), "Check now" (asks the licence server straight away), renewal contact | `Settings::licenceInfo()`, `checkLicence()`, `resources/views/filament/pages/partials/settings-status.blade.php` | Owner request 2 Nov | Pest (licence off; signed active licence shows plan, date, masked key) |
| 43 | Cron heartbeat: the scheduler writes `storage/app/scheduler-heartbeat.json` every minute; "running" = a beat within 5 minutes | `App\Support\SchedulerHeartbeat`, `routes/console.php` (`heartbeat`) | Owner request 2 Nov | Pest (beat, stale after 5 min, scheduled every minute); `php artisan schedule:run` wrote the file |
| 44 | Settings › System card: version, Scheduled jobs Running / Not running with last run, nightly backup last + next, and when not running the exact cron line for this install to paste into hPanel | `Settings::systemInfo()`, settings-status partial | Owner request 2 Nov | Pest, visual light + dark |
| 45 | Backups page: line saying whether scheduled jobs run, so nightly backups will happen ("How to fix" links to Settings for those who can open it) | `resources/views/filament/pages/backups.blade.php` | Owner request 2 Nov | Pest (stopped/running), visual |
| 46 | Live server only: red banner on every page for people who can open Settings when the cron job has stopped | `resources/views/filament/partials/scheduler-warning.blade.php` | Owner request 2 Nov | Pest (owner sees it in production, worker does not, gone after a beat) |

## Added 2 Nov 2026 (pictures: one standard, clean-up, backup)

| # | What | Where | Asked for | Tested |
|---|------|-------|-----------|--------|
| 47 | Image standard for every upload (order item reference picture ×2 forms, design picture, company logo; no other uploads exist): browser shrinks to 1280 px first; server checks it is a real JPG/PNG/WebP/GIF (≤ 15 MB, ≤ 25 MP), turns it upright (EXIF, with a built-in reader when the exif extension is missing), strips all metadata, and stores photos as JPEG ≤ 640 px and ≤ 100 KB (quality 82→52, then shrink 15 % steps to 320 px) plus a 192 px thumbnail (≤ 20 KB); logos as PNG ≤ 480 px ≤ 100 KB with transparency (256 colours if needed) | `App\Support\Images\ImageStandard`, `FileUpload::standardImage()` macro (`AppServiceProvider`) | Owner request 2 Nov | Pest: 3000×2000 noise → ≤ 640 px ≤ 100 KB, no EXIF; no enlarging; white behind transparency; logo keeps alpha; orientation 6 rotated; non-picture and 100 MP header refused; form upload; browser upload of a 2400×1600 logo stored 480×320 PNG 57 KB |
| 48 | Lists, cards and printouts use the 192 px thumbnail (designs list, queue cards, order view, production grid, order sheet PDF); tapping opens the full picture; older pictures without a thumbnail show the picture itself | `ImageFiles::url($path, thumb)`, `Queue::imageUrl`, `StageGrid`, `DesignResource`, `PdfDocument` | Owner request 2 Nov | Pest (signed links, thumbnail, fallback) |
| 49 | Logo moved to the private disk (was public, whose /storage link does not exist on Hostinger, so the Settings preview was broken there) | `Settings` logo field, migration 2026_11_02_000600 | Owner request 2 Nov | Pest |
| 50 | A picture is deleted from the server when it is replaced or cleared, or its row (order item, design, order by cascade) is deleted, once nothing else uses it (repeated orders share pictures); old logo removed when replaced or cleared | `App\Models\Concerns\HasPicture` (OrderItem, Design), `Order::booted`, `Setting::set`, `ImageFiles::forget/flush` (checked at the end of the request) | Owner request 2 Nov | Pest (replace, delete, shared picture, cascade, logo) |
| 51 | Nightly prune at 03:30: pictures nothing refers to, unfinished uploads (livewire-tmp) and leftover report downloads, older than 24 h | `ImageFiles::prune`, `php artisan app:images:prune`, `routes/console.php` | Owner request 2 Nov | Pest |
| 52 | Older pictures brought up to the standard on update (re-encode, add thumbnail, move logo off public disk, rows repointed, old file removed); also `php artisan app:images:standardize`, which also prints picture usage | `ImageStandardizer`, migration `2026_11_02_000600_standardize_existing_images` (never fails the update) | Owner request 2 Nov | Pest (second run converts nothing) |
| 53 | Backups: pictures are no longer copied into every zip; each zip lists them (`images.txt`) and they are kept once in `backups/images`, shared by all backups; a copy goes only when no kept backup lists it and it is not in use; restore rebuilds the picture folder from the list (older zips with `files/` still restore as before). Only pictures are backed up from storage now (nothing else lived there) | `App\Support\Images\ImageBackup`, `BackupService::run/prune`, `RestoreService::restoreFiles` | Owner request 2 Nov | Pest (zip contents, one copy for two backups, kept while listed, removed after; restore brings the old picture back and the safety backup undoes it) |
| 54 | Backups page explains picture backups and shows pictures in use and their size | `backups.blade.php` | Owner request 2 Nov | Visual |
| 55 | Tests keep pictures and the picture backup in a throw-away folder | `tests/TestCase.php` | Safety | Full suite |

## Fixed 2 Nov 2026 (urgent, reported by the client)

| # | What | Where | Asked for | Tested |
|---|------|-------|-----------|--------|
| 56 | New designs lost the stage variants picked in their Specification (only typed instructions were saved), so orders using the design showed the stages in red. Cause: the variant dropdowns appear after the product type is picked and their form state did not exist yet, so the browser could not send the choice back ("Livewire Entangle Error … specs.N.choice cannot be found"). Picking a product type now creates the state for every stage (`DesignSpecs::blank`), keeping what was already entered; the order form's "+ Save as new design" fills every stage too | `DesignSpecs::blank()`, `DesignResource` product type field, `OrderResource::configureNewDesignAction` | Client, first day of use | Real browser: Designs › New design (variant saved), Designs › edit (variant saved), order "+ Save as new design" (both variants saved, item shows "From D-06001 ✓"); Pest `DesignSpecsSaveTest` (fails without the fix) |
