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) <noreply@anthropic.com>
39 KiB
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
- Every active fp.job appears in EXACTLY ONE column at all times. No duplication.
- Fixed 9-column layout that doesn't grow with recipe step count.
- Columns always render in process sequence (Receiving → … → Shipping), regardless of card distribution. Empty columns still show.
- 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.
- Every floor state the audit + battle-test catalog exposes is visually distinguishable on the card (13 states total).
- Scales infinitely: a 5-step recipe and a 30-step recipe both produce single cards moving across the same 9 columns.
- 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_dashboardwith 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:
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 (
0in 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:
active_step.work_centre_id.id IN operator.paired_work_centre_ids(operator paired to that specific station)active_step.assigned_user_id == operator.id(job is personally assigned to operator)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 < todayANDstate != '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:
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
variantfield 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):
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:
{
"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_stateis server-computed — frontend just maps state → CSS class.mini_timelineis 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.
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:
.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:
- Fetch
/fp/landing/plant_kanbanwith current mode + filter state in the request payload. - Diff against current state.
- 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
# 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 landingv2— 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
- Ship migration + backend (
area_kind,card_state,mini_timeline_json, helpers, endpoint) under the v2 flag. - Ship OWL components under the v2 flag. Both screens coexist.
- QA on entech: flip
x_fc_shopfloor_layout = 'v2', validate end-to-end. - Run battle-test scenarios (S1-S23) against the new view to confirm no regression.
- Flip default to
v2site-wide. - 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 anfp.jobin that exact data shape, assertcard_stateresolves correctlytest_card_state_precedence— overlay multiple triggers (e.g. on-hold + bake-due), assert precedence rules produce the documented winnertest_area_kind_routing— for each step kind in the mapping table, assert it routes to the correct columntest_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 incards{}share the samefp.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_qcstate 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.