# Shop Floor Plant View — Redesign **Date:** 2026-05-23 **Status:** Design — approved through brainstorming, awaiting plan **Replaces:** the current Shop Floor kanban (per-step grouping, one card per step) **Affects:** `fusion_plating_shopfloor` (primary), `fusion_plating` (work centre taxonomy), `fusion_plating_jobs` (active-step + workflow-state computes) --- ## 1. Problem The current Shop Floor kanban groups cards by individual `fp.job.step.work_centre_id`. Every ready/pending step of a job spawns a separate card in its respective column. A 14-step recipe (e.g. `ENP-ALUM-BASIC` on WO-30019) produces **9 cards across 9 columns for ONE job**. With 17 active jobs on the floor, the board shows 100+ cards across 10+ narrow columns, most of which contain duplicates of the same WO. Confirmed by the user via screenshots taken 2026-05-23: > "the same job is appearing in multiple places, there can be 20 steps in any job and we cannot just make 20 columns for those jobs" Net effect: - Operators can't scan the board — duplicates drown the signal - Recipes with many steps (15+) make the board explode horizontally - "Where is WO-30019 right now?" is impossible to answer at a glance - The mode toggle (Station / All Plant) is cosmetic — both produce the same cluttered output The redesign re-anchors the kanban on **one card per job** at the **department level**, and scales to any recipe step count. --- ## 2. Goals & non-goals ### Goals 1. **Every active fp.job appears in EXACTLY ONE column** at all times. No duplication. 2. **Fixed 9-column layout** that doesn't grow with recipe step count. 3. **Columns always render in process sequence** (Receiving → … → Shipping), regardless of card distribution. Empty columns still show. 4. **Operator paired to a station sees their work highlighted** but can also see the whole plant — "Where is everything right now?" is the central operator question. 5. **Every floor state the audit + battle-test catalog exposes is visually distinguishable on the card** (13 states total). 6. **Scales infinitely**: a 5-step recipe and a 30-step recipe both produce single cards moving across the same 9 columns. 7. **Tablet-first** — readable on a 1080p wall-mounted tablet without horizontal scroll. ### Non-goals - **Replacing the Job Workspace** (the full-screen single-WO surface). The kanban is the entry point; the Workspace remains the place where work happens. Card tap opens the Workspace. - **Replacing the Manager Dashboard** (`fp_manager_dashboard` with workflow funnel + at-risk + heatmap). The kanban's "Manager" mode is a filter on the same board; the dedicated dashboard stays separate. - **Drag-and-drop step advancement** from the kanban. State transitions happen inside the Workspace or via Move dialogs. The kanban reflects state, doesn't drive it. - **Per-tank columns**. Tanks are surfaced as chips on the card, not as columns. --- ## 3. Decisions locked during brainstorming (2026-05-23) | # | Decision | |---|---| | D1 | **Plant-wide view with mine highlighted** is the operator default (over "filter to my station only"). Operators help each other and cover stations; visibility matters more than filtering. | | D2 | **9 fixed columns** by process area (Receiving, Masking, Blasting, Racking, Plating, Baking, De-Racking, Final inspection, Shipping). | | D3 | **All wet steps roll up into Plating** — Soak Clean, Electroclean, Acid Dip, Etch, Desmut, Zincate, Rinse, Water Break Test, E-Nickel Plating, Chrome, Anodize, Black Oxide, Drying. The tank chip on the card distinguishes them. | | D4 | **De-Masking folds into De-Racking** — same operator action in this shop's workflow; no separate column. | | D5 | **Contract Review (paperwork) cards live in Receiving** with a purple paperwork chip. Same for any pre-physical-work admin gate. | | D6 | **Variant C card design** — full-width vertical card with WO header, customer/PN/qty/PO line, recipe + spec, tag chips (Rush/FAIR/VIP), current step name, tank + state chip row, 9-column mini-timeline, progress bar + operator pill + icons. | | D7 | **13 card states** distinguishable by background tint, left-border color, state chip text/color, and timeline marker color. Full catalog in §6. | | D8 | **Columns appear in sequence and never reorder** — even empty columns show. The sequence is the visual mental model of the floor. | --- ## 4. Column layout ### 4.1 Fixed column sequence The board always renders these 9 columns in this exact order, left-to-right: ``` 1. Receiving 2. Masking 3. Blasting 4. Racking 5. Plating 6. Baking 7. De-Racking 8. Final inspection 9. Shipping ``` Columns are first-class entities, not derived from data. If no jobs are in Blasting, the column still appears with a "0" badge — it's a placeholder reminding the operator where Blasting sits in the flow. ### 4.2 Step-kind → column mapping Each `fp.job.step` routes to exactly one column based on its `recipe_node_id.default_kind`. The mapping table: | Column | Step kinds routed here | |---|---| | **Receiving** | `incoming_inspection`, `contract_review`, `gating`, `ready_for_processing`, any step where `state = 'pending'` and the job's first physical step hasn't started | | **Masking** | `masking` | | **Blasting** | `blasting`, `bead_blast`, `media_blast` | | **Racking** | `racking` | | **Plating** | `soak_clean`, `electroclean`, `acid_dip`, `etch`, `desmut`, `zincate`, `rinse`, `water_break_test`, `e_nickel_plate`, `chrome`, `anodize`, `black_oxide`, `drying`, `activation`, any step whose `work_centre.kind = 'wet_line'` | | **Baking** | `bake`, `oven_bake`, `post_bake_relief` | | **De-Racking** | `de_rack`, `de_mask`, `unrack` | | **Final inspection** | `post_plate_inspection`, `final_inspection`, `thickness_qc`, `fair`, `dimensional_check`, any step whose `work_centre.kind = 'inspect'` | | **Shipping** | `shipping`, `pack_ship` | ### 4.3 Implementation — `area_kind` field Add a new Selection field on `fp.work.centre`: ```python area_kind = fields.Selection([ ('receiving', 'Receiving'), ('masking', 'Masking'), ('blasting', 'Blasting'), ('racking', 'Racking'), ('plating', 'Plating'), ('baking', 'Baking'), ('de_racking', 'De-Racking'), ('inspection', 'Final inspection'), ('shipping', 'Shipping'), ], string='Floor Column', help='Which Shop Floor column this work centre belongs to. Drives the plant-view kanban.') ``` `fp.job.step` already carries a `recipe_node_id` and (optionally) a `work_centre_id`. The kanban grouping resolves a step's column via: ``` step.area_kind = step.work_centre_id.area_kind or _DEFAULT_KIND_BY_RECIPE_KIND.get(step.recipe_node_id.default_kind) or 'plating' # safe catch-all for unmapped wet steps ``` A `post_init_hook` backfills `area_kind` on existing `fp.work.centre` records by matching their `kind` (`wet_line`/`bake`/`mask`/`rack`/`inspect`) against the new taxonomy. Unmapped centres get flagged for manual review. ### 4.4 Column visibility rules - Always show all 9 columns in order. - Show the column-header count even when zero (`0` in grey, less prominent). - The operator's paired-station column gets a yellow tint + "📍 You're here" badge — see §7. --- ## 5. Card design — Variant C ### 5.1 Anatomy ``` ┌─────────────────────────────────────────────┐ │ WO-30049 ⭐ Due May 16 · 3d │ ← WO + due │ ABC Manufacturing │ ← customer │ PN 9876699373 Rev A · Qty 5 · PO 4501882 │ ← part/qty/PO │ Recipe: ENP-ALUM-BASIC · AMS-2404 Type II │ ← recipe + spec │ [RUSH] [FAIR] │ ← tag chips │ Racking │ ← current step name │ [Rack Station 1] [● Ready] │ ← tank + state chips │ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │ ← mini-timeline │ Rec Mask Blast [Rack] Plat Bake D-R Insp Ship│ ← timeline labels ├─────────────────────────────────────────────┤ │ Step 4/14 ▓▓░░░░░░░░░ [GS] 🔏 │ ← progress + operator + icons └─────────────────────────────────────────────┘ ``` ### 5.2 Field-by-field | Element | Source | Notes | |---|---|---| | WO # | `fp.job.display_wo_name` | Big, bold. `⭐` suffix appears when card is at operator's paired station. Tappable — opens Job Workspace. | | Due date | `fp.job.commitment_date` | Format "Due May 16 · 3d" (relative). Turns red + `⚠` when overdue. | | Customer | `fp.job.partner_id.name` | Single line; truncate with ellipsis if too long. | | PN / Qty / PO | `fp.job.part_catalog_id.part_number` + `.revision` · `fp.job.qty` · `fp.job.sale_order_id.x_fc_po_number` | One line, comma-separated. | | Recipe + spec | `fp.job.recipe_id.name` · `fp.job.customer_spec_id.code` | Muted small text. | | Tag chips | derived | Multi: Rush (partner flag) / FAIR (customer_spec.x_fc_requires_first_article) / VIP (partner flag) / AS9100 (job aerospace flag). Only renders when applicable. | | Current step name | `fp.job.active_step_id.name` or first ready step | Operator-facing label of the step the job is at. | | Tank chip | `fp.job.active_step_id.work_centre_id.code` or `.tank_id.code` | Blue chip. Specific tank/station. | | State chip | computed (§6) | One of 13 states. Color matches state. | | Mini-timeline | derived (§8) | 9-step bar showing the journey across columns. | | Step X / Y | `fp.job.active_step_id.sequence` / `count(fp.job.step_ids)` | Recipe progress, not the same as the 9-col timeline. | | Progress bar | computed | Filled to `active_step.sequence / total_steps`. Color matches state. | | Operator pill | `fp.job.active_step_id.assigned_user_id` | Initials avatar. Hidden when ready (no operator engaged yet). | | Icon row | derived | Compact status flags (see §5.3). | ### 5.3 Icon row catalog | Icon | Meaning | Trigger | |---|---|---| | 🔏 | Sign-off required | `step.requires_signoff` AND not yet signed | | ⏰ | Bake window approaching | upstream wet step done, `bake_required_by - now < 1h` | | 🔥 | Bake compliance gate | active step kind = `bake` | | 💬 | Recent chatter activity | `job.message_post` in last 24h | | 🔒 | Predecessor locked | `step.requires_predecessor_done` AND upstream not done | | 📋 | Required inputs unrecorded | `step._fp_missing_required_step_inputs()` returns non-empty | | 📷 | Photo required but missing | step has a `photo` input prompt unrecorded | | 🚚 | Inbound shipment tracking | `state = no_parts` AND `x_fc_receiving.x_fc_carrier_tracking` present | | 📜 | Cert ready / issued | `state = done` AND `fp.certificate.state in ('issued','sent')` | | ↳ | Jump to blocker | tappable; navigates to the predecessor step in the Workspace | Icons only render when their condition is true. Max 3-4 visible per card; overflow into a `⋯` tooltip. --- ## 6. Card states — exhaustive catalog 13 mutually-exclusive states, computed server-side per job. Each card carries exactly one state; precedence rules below resolve conflicts. ### 6.1 State definitions | # | State | Background | Left border | State chip | Timeline marker | Triggered when | |---|---|---|---|---|---|---| | 1 | `ready_mine` | `#fffaeb` (yellow) | `#f0a500` (yellow, 4px) | "● Ready to start" (teal) | `current` (yellow) | `active_step.state = 'ready'` AND `active_step.work_centre_id IN operator_paired_stations` | | 2 | `running_mine` | `#fffaeb` (yellow) | `#f0a500` (yellow, 4px) | "▶ Running 8m" (yellow) | `current` (yellow) | `active_step.state = 'in_progress'` AND `active_step.work_centre_id IN operator_paired_stations` | | 3 | `ready` | `#ffffff` (white) | none | "● Ready" (teal) | `current` (yellow) | `active_step.state = 'ready'` AND NOT mine | | 4 | `running` | `#ffffff` (white) | none | "▶ Running 3m" (yellow) | `current` (yellow) | `active_step.state = 'in_progress'` AND NOT mine | | 5 | `on_hold` | `#fff5f5` (red) | `#dc3545` (red, 4px) | "🔴 Quality Hold" (red) | `current.hold` (red) | `fusion.plating.quality.hold` exists on the job with `state = 'open'` | | 6 | `predecessor_locked` | `#f8f9fa` (grey) | none | "🔒 Waiting on Blasting" (grey) | `current.locked` (grey) | `step._fp_should_block_predecessors()` returns True AND any earlier-sequence step not done/skipped/cancelled | | 7 | `bake_due` | `#fff8e1` (orange) | `#ff9800` (orange, 4px) | "⏰ Bake window in 23m" (orange) | `current.bake` (orange) | `fusion.plating.bake.window` for this job has `bake_required_by - now < 1h` AND `state = 'awaiting_bake'` | | 8 | `awaiting_signoff` | `#f5f0ff` (purple) | `#6f42c1` (purple, 4px) | "🔏 Awaiting QA sign-off" (purple) | `current.signoff` (purple) | `step.requires_signoff` AND `step.state = 'done'` AND `step.signoff_user_id IS NULL` (S22 gate) | | 9 | `idle_warning` | `#fef9e7` (amber) | `#e6a800` (amber, 4px) | "⏸ Idle 14h · Carlos" (amber) | `current.idle` (amber) | `step.state = 'in_progress'` AND `now - step.last_activity_at > 8h` (S16 cron) | | 10 | `awaiting_qc` | `#e7f5fc` (cyan) | `#17a2b8` (cyan, 4px) | "🔬 QC pending · 2/6 items" (cyan) | `current.qc` (cyan) | `fusion.plating.quality.check` exists with `state IN ('draft','in_progress')` AND no other higher-precedence state | | 11 | `no_parts` | `#f5f5f5` (grey, dashed) | `#6c757d` (grey, 4px, dashed) | "📦 Parts in transit · 2d" (grey) | `current.no_parts` (grey) | `fp.job.state = 'confirmed'` AND inbound `fp.receiving.state = 'draft'` AND no step has started yet | | 12 | `contract_review` | `#ffffff` (white) | none | "📋 QA-005 Awaiting QA Manager" (purple) | `current.paperwork` (purple) | `active_step.recipe_node_id.default_kind = 'contract_review'` AND not complete | | 13 | `done` | `#f0f9f4` (green) | `#28a745` (green, 4px) | "✓ Ready for pickup" (green) | `current.done` (green) | active step is in `Shipping` column AND `fp.job.state = 'done'` | ### 6.2 Precedence rules When multiple state triggers fire simultaneously, the resolver iterates through this **explicit precedence list** and takes the first match: ``` 1. no_parts (can't co-occur with anything else; checked first) 2. on_hold (compliance bomb — always wins over operational states) 3. awaiting_signoff (S22 gate — blocks advancement even when step.state='done') 4. awaiting_qc (quality gate — sticky until QC closes) 5. bake_due (time-sensitive compliance window) 6. predecessor_locked (soft block on a step that's data-ready but workflow-locked) 7. idle_warning (long-running supersedes plain running) 8. done (terminal state — only reached if none of the above apply) 9. contract_review (paperwork — used at job entry before physical work) 10. running_mine (more specific than running) 11. ready_mine (more specific than ready) 12. running (operational default for active work) 13. ready (operational default for next-up work) ``` The numeric ordering here is the dispatch order in `_fp_resolve_card_state`, not a severity ranking. Examples: - Job both on-hold AND awaiting-signoff → `on_hold` (rule 2 fires before rule 3) - Job both bake-due AND running_mine → `bake_due` (rule 5 fires before rule 10) - Job in_progress for 14h at the operator's station → `idle_warning` (rule 7 fires before rule 10) Implementation in §9.3 mirrors this list exactly — keep them synchronized. ### 6.3 Mine resolution A card is "mine" when **any of the following** is true: 1. `active_step.work_centre_id.id IN operator.paired_work_centre_ids` (operator paired to that specific station) 2. `active_step.assigned_user_id == operator.id` (job is personally assigned to operator) 3. `active_step.area_kind == operator.preferred_area_kind` (operator's profile lists this department, for cross-trained operators) For **MVP, use rule 1 only**. Rules 2-3 are post-MVP enhancements. The `res.users.paired_work_centre_ids` is a Many2many on the data model (so it's forward-compatible with cross-trained operators), but the **MVP pairing UX keeps the existing single-station dropdown** (`fp_shopfloor_tech_store.currentStationId`). On unlock, the M2M holds exactly one record — the selected station. A Phase 2 enhancement adds a multi-select picker so cross-trained operators can pair to 2-4 stations at once; the resolver above already supports that without further code change. --- ## 7. Sticky header The header pins to the top of the kanban and remains visible during scroll. ### 7.1 Layout ``` ┌────────────────────────────────────────────────────────────────────────────┐ │ 🏭 Shop Floor [📍 Racking — Garry Singh ▾] [Station|All Plant|Manager]│ │ [📷 Scan QR] [🔓 Hand Off] [⚙] │ ├────────────────────────────────────────────────────────────────────────────┤ │ [17 Active] [3 At My Station] [2 Bakes Due ≤2h] [1 On Hold] [2 Overdue]│ ├────────────────────────────────────────────────────────────────────────────┤ │ [🔎 Search WO #, customer, part #, PO…] │ │ [All] [My Station] [Running] [Blocked] [Overdue] [FAIR] │ └────────────────────────────────────────────────────────────────────────────┘ ``` ### 7.2 KPI strip — 5 tiles, clickable filters | Tile | Source | Click behavior | |---|---|---| | **Active Jobs** | `count(fp.job WHERE state IN ('confirmed','in_progress'))` | Filter chip "All" → shows everything | | **At My Station** | count of cards with `state IN ('ready_mine','running_mine')` | Filter chip "My Station" → only mine | | **Bakes Due ≤2h** | count of cards with `state = 'bake_due'` AND `bake_required_by - now < 2h` | Highlights orange cards | | **On Hold** | count of cards with `state = 'on_hold'` | Filter to red cards; clicking opens Quality Holds list | | **Overdue** | count of cards where `commitment_date < today` AND `state != 'done'` | Filter to overdue | Each tile is a button. Active tile shows a darker border + filled chip indicator. ### 7.3 Filter chips Below KPIs, a row of toggleable filter chips. Multiple can be active (intersected with AND): - **All** (default; clears others) - **My Station** (cards where `state IN ('ready_mine','running_mine')`) - **Running** (`active_step.state = 'in_progress'`) - **Blocked** (`state IN ('on_hold','predecessor_locked','awaiting_signoff','awaiting_qc','no_parts')`) - **Overdue** (`commitment_date < today` AND `state != 'done'`) - **FAIR** (partner or spec requires FAIR; flagged via tag) Chip state persists per operator per browser session (localStorage), so an operator who always filters to "My Station" doesn't have to re-set it each shift. ### 7.4 Station picker The `[📍 Racking — Garry Singh ▾]` button: - Shows the operator's current paired station + their name - Dropdown lets them switch to a different station they're certified on (from their `paired_work_centre_ids`) - "All stations" option clears pairing - Disabled when the operator hasn't signed in (lock screen takes precedence) ### 7.5 Mode toggle Three modes: | Mode | Behavior | |---|---| | **Station** | Cards at the paired station's column get the yellow `mine` treatment. Column header shows "📍 You're here". Other columns visible but neutral. | | **All Plant** | No "mine" highlight anywhere. Pure plant overview. Use case: supervisor walking the floor without paired station. | | **Manager** | Same as All Plant + adds bottleneck heatmap row at top (`fp.work.centre.bottleneck_score` driven). KPI strip swaps to manager-specific tiles (Late Risk, Avg Wait, etc.). | Manager mode is gated by `fusion_plating.group_fusion_plating_manager`. --- ## 8. Mini-timeline derivation The 9-step bar on each card is **not** the recipe step count — it's a fixed 9-element array keyed by the 9 columns. Logic: ```python def _compute_mini_timeline(self): """Returns list of 9 dicts, one per column, with state in {'done','current','upcoming','hold','locked','bake','signoff','idle','qc','no_parts','done','paperwork'}.""" timeline = [] job_steps = self.step_ids.sorted('sequence') active = self.active_step_id active_area = active.area_kind if active else None for area in COLUMN_SEQUENCE: # ['receiving', 'masking', 'blasting', ...] steps_in_area = job_steps.filtered(lambda s: s.area_kind == area) if not steps_in_area: # area not used by this recipe — still show as 'upcoming' to keep alignment timeline.append({'area': area, 'state': 'upcoming'}) continue if all(s.state in ('done', 'skipped') for s in steps_in_area): timeline.append({'area': area, 'state': 'done'}) elif area == active_area: # The card's state determines the current marker color timeline.append({'area': area, 'state': 'current', 'variant': self.card_state}) else: timeline.append({'area': area, 'state': 'upcoming'}) return timeline ``` Notes: - Recipes that skip a column (e.g. a job that doesn't need Masking) still render that column slot as "upcoming" grey — visual alignment matters more than perfect accuracy. - The `variant` field on the current marker tells the renderer which color to use (matches the card-state color: yellow / red / orange / purple / etc.). --- ## 9. Backend changes ### 9.1 New / modified fields | Model | Field | Type | Purpose | |---|---|---|---| | `fp.work.centre` | `area_kind` | Selection (9 values) | Routes each work centre to one of the 9 columns | | `fp.job.step` | `area_kind` | Char, computed, stored, indexed | Related from `work_centre_id.area_kind` with fallback to `recipe_node_id.default_kind` lookup | | `fp.job` | `card_state` | Char, computed, stored, indexed | The 13-state classifier; computed via `_compute_card_state` with the precedence rules in §6.2 | | `fp.job` | `mini_timeline_json` | Text, computed | JSON-serialized output of `_compute_mini_timeline` | | `fp.job.step` | `last_activity_at` | Datetime, indexed | Updated on any state transition / move / chatter post; drives idle-warning detection (S16) | | `res.users` | `paired_work_centre_ids` | M2M `fp.work.centre` | Operator's certified stations; resolved on PIN unlock | `area_kind` Selection values (used by both `fp.work.centre` and `fp.job.step`): ```python COLUMN_SEQUENCE = [ ('receiving', 'Receiving'), ('masking', 'Masking'), ('blasting', 'Blasting'), ('racking', 'Racking'), ('plating', 'Plating'), ('baking', 'Baking'), ('de_racking', 'De-Racking'), ('inspection', 'Final inspection'), ('shipping', 'Shipping'), ] ``` ### 9.2 New endpoint — `/fp/landing/plant_kanban` Replaces the existing `/fp/landing/kanban`. Returns: ```json { "ok": true, "mode": "station", "paired_station": {"id": 12, "name": "Rack Station 1", "area_kind": "racking"}, "kpis": { "active_jobs": 17, "at_my_station": 3, "bakes_due_soon": 2, "on_hold": 1, "overdue": 2 }, "columns": [ { "area_kind": "receiving", "label": "Receiving", "is_mine": false, "card_ids": [2885, 2886, 2887] }, { "area_kind": "masking", "label": "Masking", "is_mine": false, "card_ids": [2884] }, ... ], "cards": { "2885": { "wo_name": "WO-30049", "is_mine": true, "card_state": "ready_mine", "due_date": "2026-05-16", "due_label": "Due May 16 · 3d", "is_overdue": false, "customer": "ABC Manufacturing", "part_number": "9876699373", "part_revision": "A", "qty": 5, "po_number": "4501882", "recipe_name": "ENP-ALUM-BASIC", "spec_code": "AMS-2404 Type II", "tags": ["rush", "fair"], "step_name": "Racking", "step_seq": 4, "step_total": 14, "tank_label": "Rack Station 1", "state_chip": {"label": "● Ready to start", "kind": "ready"}, "operator": {"id": null, "name": null, "initials": null}, "duration_label": null, "icons": ["signoff_required"], "mini_timeline": [ {"area": "receiving", "state": "done"}, {"area": "masking", "state": "done"}, {"area": "blasting", "state": "done"}, {"area": "racking", "state": "current", "variant": "ready_mine"}, ... ] }, ... } } ``` Design choices: - **Two-tier structure** (`columns` + `cards`) keeps payload small when 2 cards happen to be at the same step — no per-column-per-card duplication. - **`card_state` is server-computed** — frontend just maps state → CSS class. - **`mini_timeline` is server-computed** — frontend renders the 9 dots without knowing the recipe shape. - **Operator info is denormalized** — initials, name, color hash all in the payload so the frontend doesn't fan out RPCs. ### 9.3 State computation — `_compute_card_state` Matches the precedence list in §6.2 exactly. Both must stay in sync. ```python def _compute_card_state(self): for job in self: # Edge: job has no active step (all pending or all done) if not job.active_step_id: # rule 1 if job.state == 'confirmed' and job._fp_inbound_not_received(): job.card_state = 'no_parts' else: # Fallback to first pending step's kind; otherwise contract_review job.card_state = 'contract_review' continue step = job.active_step_id # rule 1 — no_parts (even with an active step, if inbound is still draft) if job._fp_inbound_not_received(): job.card_state = 'no_parts' continue # rule 2 — on_hold if job._fp_has_open_hold(): job.card_state = 'on_hold' continue # rule 3 — awaiting_signoff (S22) if (step.requires_signoff and step.state == 'done' and not step.signoff_user_id): job.card_state = 'awaiting_signoff' continue # rule 4 — awaiting_qc if job._fp_has_pending_qc(): job.card_state = 'awaiting_qc' continue # rule 5 — bake_due if job._fp_bake_window_due_soon(): job.card_state = 'bake_due' continue # rule 6 — predecessor_locked if (step._fp_should_block_predecessors() and step._fp_has_unfinished_predecessors()): job.card_state = 'predecessor_locked' continue # rule 7 — idle_warning (S16) if step.state == 'in_progress' and step._fp_is_idle(threshold_hours=8): job.card_state = 'idle_warning' continue # rule 8 — done (terminal, only reached when nothing above fires) if step.area_kind == 'shipping' and job.state == 'done': job.card_state = 'done' continue # rule 9 — contract_review if step.recipe_node_id.default_kind == 'contract_review': job.card_state = 'contract_review' continue # rules 10/12 — running (mine vs not) if step.state == 'in_progress': job.card_state = ('running_mine' if job._fp_is_mine() else 'running') continue # rules 11/13 — ready (mine vs not) if step.state == 'ready': job.card_state = ('ready_mine' if job._fp_is_mine() else 'ready') continue # Safe default job.card_state = 'ready' ``` Each `_fp_*` helper is a small method on `fp.job` (or `fp.job.step`) that encapsulates one precedence check. Centralizing them this way means future audits can extend the catalog without touching the dispatch. ### 9.4 Helpers | Helper | Returns | Source data | |---|---|---| | `_fp_inbound_not_received()` | bool | `fp.receiving` linked via SO; `state = 'draft'` | | `_fp_has_open_hold()` | bool | `fusion.plating.quality.hold` with `state = 'open'` linked via `job_id` | | `_fp_has_pending_qc()` | bool | `fusion.plating.quality.check` with `state IN ('draft','in_progress')` linked via `job_id` | | `_fp_bake_window_due_soon()` | bool | `fusion.plating.bake.window` linked, `bake_required_by - now < 1h`, `state = 'awaiting_bake'` | | `step._fp_is_idle(threshold_hours=8)` | bool | `now - last_activity_at > threshold` | | `_fp_is_mine()` | bool | `active_step.work_centre_id IN env.user.paired_work_centre_ids` | --- ## 10. Frontend changes ### 10.1 OWL component structure New / modified files in `fusion_plating_shopfloor/static/src/`: ``` js/ plant_kanban.js (new — replaces shopfloor_landing.js) components/ plant_card.js (new — Variant C card component) mini_timeline.js (new — 9-step horizontal bar) column_header.js (new — column header with "📍 You're here" badge) kpi_tile.js (new — clickable KPI button) filter_chip.js (new — toggleable filter chip) xml/ plant_kanban.xml (new) components/ plant_card.xml (new) mini_timeline.xml (new) column_header.xml (new) kpi_tile.xml (new) filter_chip.xml (new) scss/ plant_kanban.scss (new — board layout + sticky header) components/ _plant_card.scss (new — 13 card-state styles) _mini_timeline.scss (new — timeline dots) _column_header.scss (new) _kpi_tile.scss (new) _filter_chip.scss (new) ``` ### 10.2 Component tree ``` FpPlantKanban (top-level client action) ├── FpTabletLock (existing wrapper for PIN gate) └── (when unlocked) ├── PlantHeader │ ├── StationPicker │ ├── ModeToggle │ ├── ToolbarButtons (Scan / Hand Off / Settings) │ ├── KpiStrip (5 × KpiTile) │ └── FilterRow (search input + 6 × FilterChip) └── Board └── 9 × Column ├── ColumnHeader └── PlantCard[] ├── CardHeader (WO, due) ├── CardBody (customer, PN, recipe, tags) ├── CardStep (step name + chips) ├── MiniTimeline └── CardFooter (progress + operator + icons) ``` ### 10.3 Card state CSS All 13 states share the base `.plant-card` class with state-specific modifier classes: ```scss .plant-card { background: $card-bg; border: 1px solid $border-color; border-radius: 8px; // ... base layout &.state-ready_mine, &.state-running_mine { background: #fffaeb; border-left: 4px solid #f0a500; padding-left: 9px; } &.state-on_hold { background: #fff5f5; border-left: 4px solid #dc3545; padding-left: 9px; } &.state-bake_due { background: #fff8e1; border-left: 4px solid #ff9800; padding-left: 9px; } &.state-awaiting_signoff { background: #f5f0ff; border-left: 4px solid #6f42c1; padding-left: 9px; } &.state-idle_warning { background: #fef9e7; border-left: 4px solid #e6a800; padding-left: 9px; } &.state-awaiting_qc { background: #e7f5fc; border-left: 4px solid #17a2b8; padding-left: 9px; } &.state-predecessor_locked { background: #f8f9fa; } &.state-no_parts { background: #f5f5f5; border: 1px dashed #999; border-left: 4px solid #6c757d; padding-left: 9px; } &.state-done { background: #f0f9f4; border-left: 4px solid #28a745; padding-left: 9px; } // state-ready, state-running, state-contract_review: default neutral white } ``` Dark-mode SCSS branch follows the project pattern (`$o-webclient-color-scheme == dark` block) with adjusted hex values. ### 10.4 Auto-refresh Polling every 10s via `setInterval`. On each tick: 1. Fetch `/fp/landing/plant_kanban` with current mode + filter state in the request payload. 2. Diff against current state. 3. Apply changes to OWL reactive state — cards that moved columns animate the transition (fade-out from old column, fade-in at new column over 200ms). Hand-Off, mode toggle, station-picker, and filter chip changes trigger an immediate refresh. ### 10.5 Card tap behavior Single tap on a card → opens Job Workspace (`fp_job_workspace` client action) with the WO pre-loaded. No quick-action sheet on tablet (would compete with the Workspace's own action rail). Card has a small "ℹ" icon in the top-right that opens a quick-info popover (for supervisor walk-bys who want details without leaving the kanban). Post-MVP. --- ## 11. Migration & rollout ### 11.1 Database migration ```python # fusion_plating/migrations/19.0.21.0.0/post-migrate.py def migrate(cr, version): """Backfill fp.work.centre.area_kind from existing kind values.""" cr.execute(""" UPDATE fp_work_centre SET area_kind = CASE kind WHEN 'wet_line' THEN 'plating' WHEN 'bake' THEN 'baking' WHEN 'mask' THEN 'masking' WHEN 'rack' THEN 'racking' WHEN 'inspect' THEN 'inspection' ELSE 'plating' END WHERE area_kind IS NULL """) # Log unmapped centres for manual review cr.execute(""" SELECT id, name FROM fp_work_centre WHERE area_kind IS NULL """) for row in cr.fetchall(): _logger.warning("Work centre %s (%s) has no area_kind — defaulted to 'plating'", row[0], row[1]) ``` ### 11.2 Feature flag New config setting `x_fc_shopfloor_layout` on `res.config.settings`: - `legacy` (default during rollout) — existing landing - `v2` — new plant view Once validated on entech, default flips to `v2` and legacy code can be removed in a follow-up cleanup. The client action `fp_shopfloor_landing` resolver chooses which OWL component to mount based on this setting. ### 11.3 Rollout sequence 1. Ship migration + backend (`area_kind`, `card_state`, `mini_timeline_json`, helpers, endpoint) under the v2 flag. 2. Ship OWL components under the v2 flag. Both screens coexist. 3. QA on entech: flip `x_fc_shopfloor_layout = 'v2'`, validate end-to-end. 4. Run battle-test scenarios (S1-S23) against the new view to confirm no regression. 5. Flip default to `v2` site-wide. 6. After 2 weeks of stable v2, remove legacy code. --- ## 12. Testing strategy ### 12.1 Unit tests - `test_card_state_computation` — for each of the 13 states, construct an `fp.job` in that exact data shape, assert `card_state` resolves correctly - `test_card_state_precedence` — overlay multiple triggers (e.g. on-hold + bake-due), assert precedence rules produce the documented winner - `test_area_kind_routing` — for each step kind in the mapping table, assert it routes to the correct column - `test_mini_timeline` — for a 14-step recipe at various points, assert the 9-element output matches expectations (including skipped columns rendered as upcoming) - `test_one_card_per_job_invariant` — across a realistic 17-job board, assert no two entries in `cards{}` share the same `fp.job.id` ### 12.2 Persona walks Re-run the battle-test scenarios that drove this redesign: - **S20 walk** — operator persona traversal of the tablet. Confirm: card density readable, "mine" highlight obvious, can find a specific WO in <5s via search. - **S22 / S23 simulations** — finish a step that needs sign-off / transition form, confirm the card transitions to `awaiting_signoff` / `awaiting_qc` state correctly. - **20-step-recipe regression** — load a synthetic job with 25+ recipe steps, confirm it occupies one and only one card on the board. ### 12.3 Visual snapshot tests Per state, a Playwright/headless-chromium snapshot of a single card at fixed viewport. Diff against checked-in golden images on every PR. Catches accidental CSS regressions. --- ## 13. Open questions (deferred) These don't block MVP but should be tracked for the follow-up plan. | # | Question | Suggested resolution | |---|---|---| | Q1 | Drag-and-drop card between columns? | **No for MVP.** State transitions happen via the Workspace action rail or Move dialogs. The kanban reflects state, doesn't drive it. | | Q2 | Empty-column auto-collapse? | **No.** Column position = mental model. Collapsing breaks the sequence. | | Q3 | Sort within column? | **MVP: most urgent first** — overdue → bake-due → ready → running → idle → locked → done. Post-MVP: operator-toggleable. | | Q4 | Card tap → quick-action sheet vs. open Workspace? | **MVP: open Workspace.** Quick-action sheet is a post-MVP enhancement. | | Q5 | Manager mode KPI tile swap? | **Phase 2.** MVP ships with the same 5 KPI tiles in all modes. Phase 2 adds manager-specific tiles (late-risk %, avg wait per station, bottleneck score). | | Q6 | Sibling jobs (WO-30029-01 / -02) visual grouping? | **No special treatment for MVP.** Each is its own card. If siblings clutter, post-MVP adds a "group siblings" toggle. | | Q7 | Bottleneck heatmap row in manager mode? | **Phase 2.** Reuses existing `fp.work.centre.bottleneck_score`. | | Q8 | Mobile (phone) breakpoint? | **Phase 2.** MVP optimized for 1080p tablet. Phone view = collapse to single-column scroll. | --- ## 14. Summary | Question | Answer | |---|---| | Layout | 9 fixed columns in sequence (Receiving → … → Shipping) | | Card model | One card per `fp.job`, always in the column matching the active step's `area_kind` | | Card density | Variant C — full info with mini-timeline | | State catalog | 13 mutually-exclusive states with precedence rules | | Operator focus | Plant-wide view, paired-station column + "mine" cards highlighted | | Backend touch | New `area_kind` Selection, new `card_state` compute, new `/fp/landing/plant_kanban` endpoint | | Frontend touch | New OWL component tree under `fp_plant_kanban` client action | | Rollout | Feature flag `x_fc_shopfloor_layout`, parallel deployment, flip default after entech validation | | Recipe-step scaling | Doesn't matter — 5-step or 50-step recipes both produce one card moving across 9 fixed columns | The redesign solves the "one job in N columns" problem by re-anchoring grouping at the department level and decoupling the kanban from recipe step count. Every floor scenario in the audit + battle-test catalog (S1-S23) maps to one of the 13 documented states. Implementation plan to follow.