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) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user