# Gantt flagship · round 1 · 2026-10-18

Frank on A07: *"Not diamond quality … statuses are wrong … columns order ID · customer name · factory, no factory location … should you go find out what the ideal Gantt is?"* We went to find out. Three directions, one big idea each, every state. **Recommended: G1 · Ledger.**

- Sheet: https://gantt.pina-brand-006-rj-laravel-admin.pages.dev/ · artboards: `/canvas/`
- Source: `src/` (`gdata.py` data · `gantt.py` grid · `g23.py` G2/G3 · `pages.py` states · `boards.py` · `gantt_build.py`) · shots: `shots/` · canvas: `canvas/`

---

## 1. What the real gantt is (read, not guessed)

Read-only at `RJmtm/3.0_Backend` **origin/main 6787e7873** and prod via rj-admin MCP (get/find only), 2026-10-18.

| Fact | Source |
|---|---|
| Page `/admin/gantt` → `OrderController@gantt`; scope = orders with any status row created in the chosen **calendar month** (status rows or `order_status` in `adminStatus()`) | `OrderController.php:2794-2816` |
| One row per order. Columns **ID# · CUSTOMER · P** (P = factory codes, `getFactoriesGroupedByProduct()`), then one cell per day of the month | `gantt.blade.php:1003-1012` |
| **A cell = the status active that day**, coloured by `ProcessList::STATUS_COLORS` / `OrderStatus` colour; several colours if several statuses started that day; **days after today are blank** | `GanttDayGrid::build` (line-for-line port of the Blade loop) |
| So the bar is **history**, not a plan. The only forward fact is the **factory due** = `order_assigned_factories.end_date` (the PRODUCTION IN-PROGRESS 0.2 window); it bounds the 0.2 cells via `StatusDateRangeHandler` | `StatusDateRangeHandler`, `/orders/due` |
| `+` expands to per-piece rows (`gantt-detail`: code `4510-1`, name, label JA/TR, fabric code, day colours); a cell click opens the phase summary modal (`0457 | Customer`, per-garment step timeline, SF courier no. on FACTORY SHIPPED) | `gantt.blade.php:1023, 1092, 1317`; `GanttDetailController` |
| New API: `admin.orders.gantt-grid` returns **only** `{order_id, customer_name, factory_name, days}` + picker bounds | `GanttDetailController::ganttGrid` |
| Interactions today: month + year selects, `+` expand, cell click → modal. No zoom, no today line, no filter, no sort, no lateness | Blade |
| Steps (real titles): Ready For Fulfilment · Build (Order Bill · Fabric Purchase) · INCOMING COMPONENTS ARRIVAL · `BEGIN PRODUCTION | 0.1` · `PRODUCTION IN-PROGRESS | 0.2` · `FACTORY SHIPPED | 0.25` · `ARRIVAL TO HQ | 0.3` · checkpoints 1.1–3.3 · hand finishing 3.5.x · pressing 4.x · 5.0 · storage 6.x · 7.0–7.2 · 8.x/9.x · `DELIVERED GARMENTS | 10.0` · On Hold | `ProcessList.php` |
| Factories: short codes, **no location** in the gantt. Live: **FRR · FX · F3.5 · FPS**; retired C · W · WZ … still carry stale overdue pieces | dump `factories`; MCP `view=due` |

**Prod volume (MCP, 2026-10-18):** Oct 2026 = **62 orders** on the gantt (status mix of the per-order feed: completed 25 · manufacturing 21 · delivered 12 · outbound 8 · voided 1). 0.2 window **3–10 days, median 9**; SHIPMENT-IN-PROGRESS 3–5 days; hand finishing 2 days. Almost all live load sits at **FRR**. Today (Mon 19 Oct, Shanghai day) FRR has **7 orders 1–5 days over factory due**, 4 garments due today, 8 due in 2 days, plus a stale tail 4–11 months old; C · WZ · W · FX hold 9 stale pieces from 2024–26. The month grid is 30–80 rows; the density goal (300) is a quarter-plus view.

**What the A07 board got wrong:** invented 5-digit ids, "Factory B/C/D", bars drawn as planned phases into the future (the real grid never paints the future), red due pins with no rule behind them.

## 2. Research · what world-class production gantts do (kept only what fits RJ's data)

Looked at manufacturing schedulers (Siemens Opcenter APS, Asprova, Preactor-style Gantt boards), apparel/MTM floor tools (made-to-measure WIP boards, cut-make-trim trackers), product timelines (Linear, Height, Notion timeline), and ops boards (airline gate/turn boards, rail/stand allocation).

| Pattern | Seen in | Keep for RJ? |
|---|---|---|
| **Promised vs actual**: a baseline/plan bar under the actual, variance shown as the gap | APS schedulers, MS Project baselines | **Yes**, the promise is the factory due. Drawn as a dashed tail today → due; overrun hatched red |
| **Today line** pinned, everything left is fact, right is forecast | every serious timeline | **Yes**, and it matches the data (future cells are blank) |
| **Row grouping** by resource with group summary (load/utilisation) | APS (resource view), airline boards (by gate) | **Yes**, group by factory with a per-day garments strip (G1), or factory as the row (G2) |
| **Resource load heat / overload per day** | APS capacity histograms | **Yes, relative**: no capacity field exists, so heat = garments per day vs that factory's own peak (needs backend for true capacity) |
| Critical path / dependencies / drag to reschedule | MS Project, Asprova | **No**. No dependencies, no scheduling writes in RJ; the grid is read-only (STACK §3.2) |
| **Zoom levels** day / week / month (+ fit) with ruler that changes granularity | Linear, Height, Opcenter | **Yes**; Day shows every step change, Month shows texture, Fit = whole range |
| **Sticky identity columns + sticky time header**, virtualised rows | all grid tools | **Yes**: Order · Customer · Factory sticky; @tanstack/react-virtual |
| **Hover peek** (no click), click = side sheet | Linear, Height | **Yes**: peek = now step + since, factory due, step trail, pieces |
| **Minimap / overview** for 100s of rows | code editors, Airtable timeline, ops boards | **Yes** at 300 rows: right-edge strip where red = late rows |
| **Filters as chips + URL state**: resource, status, late, express | Linear | **Yes** for factory, phase (real legend groups), late, live; **Express** needs backend (not in gantt-grid) |
| **Keyboard**: J/K rows, ←/→ pan, +/− zoom, T today, Enter open, / find | Linear, Height | **Yes** |
| **Copy-out to chat** (status line for a supplier) | ops boards, WeChat-heavy SCM | **Yes**, RJ already has it (countdown Copy → WeChat text, `FactoryChaseMessageFormatter`) |
| **Phone**: a list, not a squeezed grid; tap for a sheet | Linear mobile, airline crew apps | **Yes**: dedicated `/m/gantt` |
| Quiet colour, colour only for meaning | Linear, Stripe dashboards | **Yes**: the real legend colours desaturated; red only = late, amber only = due ≤ 1 day |

## 3. The three directions

| | **G1 · Ledger ★** | **G2 · Floor** | **G3 · Countdown** |
|---|---|---|---|
| Big idea | Rows = orders, grouped by factory; history bar + promise + overrun | Rows = factories; daily load heat on top, orders packed in lanes | Frank's countdown columns (LATER · +3 · +2 · +1 · TODAY · OVER DUE) with a 21-day history strip per order |
| Frank's columns | **Order · Customer · Factory** exactly | order ID + surname on the bar; factory = row | exactly |
| Best answer | where is 4512, is it late, why | is FRR overloaded on Wednesday | what do I chase on WeChat today |
| 300 rows | good (virtual + minimap) | best (always 4 rows) | weak (≤ 40-row tool) |
| Home | `/admin/gantt` | a **Factories** toggle inside G1 | the dashboard countdown (A05), Copy kept |

Each direction ships: 1440 default · hover peek · late/at-risk · zoomed out · 300-row proof · empty · phone (3 screens).

**Recommendation: G1 · Ledger**, with G2 as its *Factories* view toggle and G3 as the dashboard widget. Why: it is the page Frank already uses (one row per order, his three columns) made precise. It adds the three things the old page never had: the **promise** (factory due), the **overrun** (hatched red, +Nd, the countdown's own late rule), and **load per factory per day** (the group strip). It reads truthfully: everything left of today is the real day-colour history; the only thing drawn right of today is the promise. G3 is the best WeChat tool but breaks past ~40 rows; G2 is the best capacity picture but loses "where is my order".

### Craft rules in the boards
- One pixel grid: every position = `(date − d0) × dayWidth` in whole px from one View; bars, ticks, today, due markers and weekend bands share it.
- Tabular numbers everywhere (IBM Plex Mono for ids, dates, step codes, counts).
- Colour = meaning only: phase families (desaturated real legend), red = factory overrun, amber = due today/tomorrow, ink = today and due markers. Everything else is grey.
- Tape-measure axis: day ticks, half-day ticks, long Monday tick, steel ticks on a pale tape; today is an ink pill + line at the end of today.
- Motion: zoom animates day width 180 ms ease-out anchored on cursor/today, ruler labels cross-fade per level, bars never re-pack; T glides to today in 240 ms; inertial pan with sticky identity columns; hover peek after 120 ms, no leave animation; `prefers-reduced-motion` → instant.

## 4. Needs backend (small, read-only)
1. `gantt-grid` rows: add **`factory_due_at`** (from `order_assigned_factories.end_date`) and **`current_step`** (title + since). Without them, due markers, lateness and the step tag need a phase-summary call per row.
2. `gantt-grid` window: **from/to** (rolling weeks across months) instead of one calendar month (or stitch two calls client-side).
3. **Express** on the gantt row (exists on the index, not on gantt-grid).
4. Factory **capacity per day** does not exist; heat is relative to each factory's own peak until it does.
5. `/orders/due` `horizon_days` is capped at 3; G3's +7 / +14 needs ≤ 14 (or derive from factory due).

## 5. Truth notes
- Order ids are bare 4-digit and format-true (4229–4528 in the fixture; live max 4528); digits are invented. Customer names are invented. Factory codes are real codes with no location.
- STAFF NEEDED values on G3 are example values (dotted); prod shows 0 today.
- WeChat copy previews follow `FactoryChaseMessageFormatter`: header (`逾期：` / `今天要发：` / `周X要交货：`), factory, then one grouped garment label per line.
- The fixture has 71 orders on October (prod: 62) and 37 live; the late set mirrors prod's FRR shape on 19 Oct.
