From 48805b5988ac628bd274de02297076eee41465e3 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sat, 23 May 2026 20:22:17 -0400 Subject: [PATCH] docs(shopfloor): plant-view kanban redesign spec Replaces per-step-grouped kanban with department-grouped (9 fixed columns). One card per fp.job; recipe step count no longer drives layout width. - 9 fixed columns in process sequence: Receiving / Masking / Blasting / Racking / Plating / Baking / De-Racking / Final inspection / Shipping - new fp.work.centre.area_kind Selection + step_id.area_kind related - 13 mutually-exclusive card states with explicit precedence list and matching _compute_card_state dispatcher - Variant C card: WO header, customer/PN/qty/PO, recipe/spec, tag chips, current step + tank + state chip, 9-step mini-timeline, progress + operator pill + icon row - /fp/landing/plant_kanban endpoint returns columns + denormalized cards - MVP uses existing single-station pairing UX; M2M field structure is forward-compatible for cross-trained operators (Phase 2) - Feature flag x_fc_shopfloor_layout for parallel rollout Deferred to Phase 2: drag-drop, sibling grouping, bottleneck heatmap, manager-specific KPIs, phone breakpoint, sort customization, quick-action sheet. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../2026-05-23-shopfloor-plant-view-design.md | 779 ++++++++++++++++++ 1 file changed, 779 insertions(+) create mode 100644 fusion_plating/docs/superpowers/specs/2026-05-23-shopfloor-plant-view-design.md diff --git a/fusion_plating/docs/superpowers/specs/2026-05-23-shopfloor-plant-view-design.md b/fusion_plating/docs/superpowers/specs/2026-05-23-shopfloor-plant-view-design.md new file mode 100644 index 00000000..24e12067 --- /dev/null +++ b/fusion_plating/docs/superpowers/specs/2026-05-23-shopfloor-plant-view-design.md @@ -0,0 +1,779 @@ +# 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.