fix(shopfloor): Manager Desk speaks fp.job/fp.job.step end-to-end

The previous shopfloor consolidation kept the data layer correct
(controller queries fp.job.step) but left the UI labels, JS
variables, and RPC kwargs in legacy WO/MO vocabulary. Result:
every label said 'Unassigned WOs' / 'X WO' even though the
underlying records are fp.job.step rows.

Renames throughout:
  wo → step (variable / loop / payload key)
  WO → Step (label)
  unassigned_wos → unassigned_steps  (KPI key)
  active_wos → active_steps
  ready_to_ship_mos → ready_to_ship_jobs
  mo_id / mo_name / expandedMoId → job_id / job_name / expandedJobId
  wo_kind → kind, wo_kind_label → kind_label
  o_fp_mgr_wo_* CSS classes → o_fp_mgr_step_*

RPC routes /fp/manager/assign_worker, /fp/manager/assign_tank,
/fp/manager/take_over: primary kwarg is step_id; workorder_id
accepted as a deprecated alias for one release with a logged
warning, so any uncaught caller doesn't break.

No layout / visual changes — same UI shape, native vocabulary.
SCSS class renames are mechanical (only `_wo_` → `_step_` in
selectors); XML updated in lockstep.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-25 10:38:50 -04:00
parent 596efa0ed3
commit 18b5918d3d
4 changed files with 149 additions and 114 deletions

View File

@@ -1,15 +1,15 @@
/** @odoo-module **/
// =============================================================================
// Fusion Plating — Manager Dashboard (OWL client action)
// Fusion Plating — Manager Desk (OWL client action)
// Copyright 2026 Nexa Systems Inc.
// License OPL-1 (Odoo Proprietary License v1.0)
//
// Manager-level view: assign workers, swap tanks, cover no-shows, drill
// into detail when needed. Three columns: Unassigned / In Progress / Team.
// into detail when needed. Three columns: Needs a Worker / In Progress / Team.
//
// Native fp.job / fp.job.step edition (consolidated 2026-04-24). The
// "wo" naming inside payloads is preserved so the existing XML template
// keeps rendering — those keys now carry fp.job.step rows under the hood.
// Native fp.job / fp.job.step edition. Speaks job/step end-to-end —
// payload keys, variables, and RPC kwargs all use the job/step
// vocabulary.
// =============================================================================
import { Component, useState, onMounted, onWillUnmount } from "@odoo/owl";
@@ -29,7 +29,7 @@ export class ManagerDashboard extends Component {
overview: null,
loadError: "", // visible error instead of stuck spinner
mode: "quick", // quick | detailed
expandedMoId: null,
expandedJobId: null,
message: "",
messageType: "info",
isFetching: false, // pulses the "updating" dot in the header
@@ -134,8 +134,8 @@ export class ManagerDashboard extends Component {
this.state.mode = this.state.mode === "quick" ? "detailed" : "quick";
}
toggleCard(moId) {
this.state.expandedMoId = this.state.expandedMoId === moId ? null : moId;
toggleCard(jobId) {
this.state.expandedJobId = this.state.expandedJobId === jobId ? null : jobId;
}
toggleOffShift() {
@@ -143,7 +143,7 @@ export class ManagerDashboard extends Component {
}
/**
* Sort + filter the operator list for a specific WO's dropdown.
* Sort + filter the operator list for a specific step's dropdown.
*
* Buckets, top-down, each kept in original (alphabetical) order:
* 1. Qualified for this role AND clocked in — primary picks
@@ -155,9 +155,9 @@ export class ManagerDashboard extends Component {
* Each option carries a `bucket` so the template can render a tiny
* green/grey dot and (for buckets 3-4) a soft helper label.
*/
operatorsForWO(wo) {
operatorsForStep(step) {
const all = (this.state.overview && this.state.overview.operators) || [];
const roleId = wo && wo.role_id;
const roleId = step && step.role_id;
const out = [];
for (const op of all) {
const qualified = roleId && op.role_ids && op.role_ids.includes(roleId);
@@ -184,15 +184,15 @@ export class ManagerDashboard extends Component {
}
// ---------------------------------------------------------- Actions
async onAssignWorker(wo, userIdRaw) {
async onAssignWorker(step, userIdRaw) {
const userId = parseInt(userIdRaw) || null;
try {
const res = await rpc("/fp/manager/assign_worker", {
workorder_id: wo.id, user_id: userId,
step_id: step.id, user_id: userId,
});
if (res && res.ok) {
this.setMessage(
`Assigned ${res.user_name || 'unassigned'} to ${wo.name}`,
`Assigned ${res.user_name || 'unassigned'} to ${step.name}`,
"success",
);
}
@@ -202,15 +202,15 @@ export class ManagerDashboard extends Component {
await this.refresh();
}
async onAssignTank(wo, tankIdRaw) {
async onAssignTank(step, tankIdRaw) {
const tankId = parseInt(tankIdRaw) || null;
try {
const res = await rpc("/fp/manager/assign_tank", {
workorder_id: wo.id, tank_id: tankId,
step_id: step.id, tank_id: tankId,
});
if (res && res.ok) {
this.setMessage(
`Tank ${res.tank_name || 'cleared'} for ${wo.name}`,
`Tank ${res.tank_name || 'cleared'} for ${step.name}`,
"success",
);
}
@@ -220,13 +220,13 @@ export class ManagerDashboard extends Component {
await this.refresh();
}
async onTakeOver(wo) {
async onTakeOver(step) {
try {
const res = await rpc("/fp/manager/take_over", {
workorder_id: wo.id,
step_id: step.id,
});
if (res && res.ok) {
this.setMessage(`You now own ${wo.name}.`, "success");
this.setMessage(`You now own ${step.name}.`, "success");
}
} catch (err) {
this.setMessage(`Takeover failed: ${err.message || err}`, "danger");

View File

@@ -440,12 +440,12 @@
// -------------------------------------------------------------------------
// WO row inside expanded card
// Step row inside expanded card
// -------------------------------------------------------------------------
// WO row = info column (vertical stack) + actions column (pickers + buttons)
// Step row = info column (vertical stack) + actions column (pickers + buttons)
// Flex with wrap so narrow viewports drop actions below the info naturally
// instead of squishing everything into a single broken grid line.
.o_fp_mgr_wo_row {
.o_fp_mgr_step_row {
display: flex;
flex-wrap: wrap;
gap: $fp-space-3;
@@ -458,7 +458,7 @@
font-size: $fp-text-sm;
}
.o_fp_mgr_wo_info {
.o_fp_mgr_step_info {
flex: 1 1 280px; // grows but never narrower than 280px
min-width: 0; // allows children to shrink properly
display: flex;
@@ -466,8 +466,8 @@
gap: $fp-space-1;
color: $fp-ink;
// Title row — kind badge + WO name + step number
.o_fp_mgr_wo_title {
// Title row — kind badge + step name + sequence
.o_fp_mgr_step_title {
display: flex;
align-items: center;
gap: $fp-space-2;
@@ -477,7 +477,7 @@
line-height: 1.25;
}
// Meta row — workcenter / role / set equipment
.o_fp_mgr_wo_meta {
.o_fp_mgr_step_meta {
display: flex;
align-items: center;
gap: $fp-space-2;
@@ -487,7 +487,7 @@
i { margin-right: 2px; }
}
// Chip row — what's still missing for the manager to set
.o_fp_mgr_wo_needs {
.o_fp_mgr_step_needs {
margin-top: 2px;
}
}
@@ -496,7 +496,7 @@
// takes the remaining horizontal space (the dropdown then grows to
// fill); flex-wrap so on narrow widths the dropdown sits on its own
// line and the buttons go below at 50/50.
.o_fp_mgr_wo_actions {
.o_fp_mgr_step_actions {
display: flex;
flex-wrap: wrap;
align-items: center;
@@ -531,7 +531,7 @@
&:focus { @include fp-focus-ring; border-color: $fp-accent; }
}
.o_fp_mgr_btn,
.o_fp_mgr_wo_row .btn {
.o_fp_mgr_step_row .btn {
min-height: 40px;
padding: 0 $fp-space-3;
border: none;
@@ -549,13 +549,13 @@
@media (max-width: 900px) {
// Mobile / narrow tablet: dropdown takes full width on its own
// line; the two buttons split 50/50 underneath.
.o_fp_mgr_wo_actions {
.o_fp_mgr_step_actions {
flex: 1 1 100%;
justify-content: stretch;
}
.o_fp_mgr_picker { flex: 1 1 100%; }
.o_fp_mgr_btn,
.o_fp_mgr_wo_row .btn {
.o_fp_mgr_step_row .btn {
flex: 1 1 calc(50% - #{$fp-space-2});
min-height: $fp-touch-min;
}
@@ -580,7 +580,7 @@
&.o_fp_chip_danger { @include fp-pill(--bs-danger); }
&.o_fp_chip_muted { background-color: $fp-card-soft; color: $fp-ink-mute; }
// WO-kind colour bands so the manager can spot
// Step-kind colour bands so the manager can spot
// mask vs wet vs bake at a glance.
&.o_fp_chip_kind {
text-transform: none;

View File

@@ -2,7 +2,7 @@
<!--
Copyright 2026 Nexa Systems Inc. · License OPL-1
Fusion Plating — Manager Desk
Rebuilt 2026-04 with the shop-floor design system.
Native fp.job / fp.job.step edition. Speaks job/step end-to-end.
-->
<templates xml:space="preserve">
@@ -71,17 +71,17 @@
<div class="o_fp_kpi_strip" t-if="state.overview">
<div class="o_fp_kpi o_fp_kpi_warning">
<i class="fa fa-user-times"/>
<div class="o_fp_kpi_value"><t t-esc="state.overview.kpis.unassigned_wos"/></div>
<div class="o_fp_kpi_label">Unassigned WOs</div>
<div class="o_fp_kpi_value"><t t-esc="state.overview.kpis.unassigned_steps"/></div>
<div class="o_fp_kpi_label">Unassigned Steps</div>
</div>
<div class="o_fp_kpi o_fp_kpi_success">
<i class="fa fa-cogs"/>
<div class="o_fp_kpi_value"><t t-esc="state.overview.kpis.active_wos"/></div>
<div class="o_fp_kpi_value"><t t-esc="state.overview.kpis.active_steps"/></div>
<div class="o_fp_kpi_label">In Progress</div>
</div>
<div class="o_fp_kpi o_fp_kpi_info">
<i class="fa fa-truck"/>
<div class="o_fp_kpi_value"><t t-esc="state.overview.kpis.ready_to_ship_mos"/></div>
<div class="o_fp_kpi_value"><t t-esc="state.overview.kpis.ready_to_ship_jobs"/></div>
<div class="o_fp_kpi_label">Ready to Ship</div>
</div>
<div class="o_fp_kpi o_fp_kpi_warning">
@@ -94,7 +94,7 @@
<!-- ============ Workload grid ============ -->
<div class="o_fp_manager_grid" t-if="state.overview">
<!-- Unassigned -->
<!-- Needs a Worker -->
<section class="o_fp_panel o_fp_panel_unassigned">
<div class="o_fp_panel_head">
<h3><i class="fa fa-inbox"/>Needs a Worker</h3>
@@ -102,17 +102,17 @@
</div>
<div t-if="!state.overview.unassigned.length" class="o_fp_empty">
<i class="fa fa-check-circle text-success"/>
<div>Every active WO has a worker assigned.</div>
<div>Every active step has a worker assigned.</div>
</div>
<div class="o_fp_mgr_card_list" t-if="state.overview.unassigned.length">
<t t-foreach="state.overview.unassigned" t-as="card" t-key="card.mo_id">
<t t-foreach="state.overview.unassigned" t-as="card" t-key="card.job_id">
<div class="o_fp_mgr_card"
t-att-data-priority="card.priority_any">
<div class="o_fp_mgr_card_head"
t-on-click="() => this.toggleCard(card.mo_id)">
t-on-click="() => this.toggleCard(card.job_id)">
<div>
<div class="o_fp_mgr_card_title">
<t t-esc="card.mo_name"/>
<t t-esc="card.job_name"/>
<span class="text-muted ms-2 small">· <t t-esc="card.so_name"/></span>
</div>
<div class="o_fp_mgr_card_sub">
@@ -126,46 +126,48 @@
<span t-if="card.priority_any >= 2" class="o_fp_chip o_fp_chip_danger">HOT</span>
<span t-if="card.priority_any == 1" class="o_fp_chip o_fp_chip_warning">Urgent</span>
<span class="o_fp_chip o_fp_chip_muted">
<t t-esc="card.wos.length"/> WO
<t t-esc="card.steps.length"/>
<t t-if="card.steps.length === 1"> Step</t>
<t t-else=""> Steps</t>
</span>
</div>
</div>
<div class="o_fp_mgr_card_body"
t-if="state.expandedMoId === card.mo_id or state.mode === 'detailed'">
<t t-foreach="card.wos" t-as="wo" t-key="wo.id">
<div class="o_fp_mgr_wo_row">
t-if="state.expandedJobId === card.job_id or state.mode === 'detailed'">
<t t-foreach="card.steps" t-as="step" t-key="step.id">
<div class="o_fp_mgr_step_row">
<!-- LEFT: information stack (badge, name, meta, needs) -->
<div class="o_fp_mgr_wo_info">
<div class="o_fp_mgr_wo_title">
<span t-attf-class="o_fp_chip o_fp_chip_kind o_fp_chip_kind_{{ wo.wo_kind }}"
t-esc="wo.wo_kind_label || wo.wo_kind"/>
<span t-esc="wo.name"/>
<div class="o_fp_mgr_step_info">
<div class="o_fp_mgr_step_title">
<span t-attf-class="o_fp_chip o_fp_chip_kind o_fp_chip_kind_{{ step.kind }}"
t-esc="step.kind_label || step.kind"/>
<span t-esc="step.name"/>
</div>
<div class="o_fp_mgr_wo_meta">
<span><i class="fa fa-cog"/><t t-esc="wo.workcenter"/></span>
<span t-if="wo.role_name">· <i class="fa fa-id-badge"/><t t-esc="wo.role_name"/></span>
<span t-if="wo.bath">· <i class="fa fa-flask"/><t t-esc="wo.bath"/></span>
<span t-if="wo.oven">· <i class="fa fa-fire"/><t t-esc="wo.oven"/></span>
<span t-if="wo.rack">· <i class="fa fa-th"/><t t-esc="wo.rack"/></span>
<span t-if="wo.masking_material">· <i class="fa fa-tag"/><t t-esc="wo.masking_material"/></span>
<div class="o_fp_mgr_step_meta">
<span><i class="fa fa-cog"/><t t-esc="step.workcenter"/></span>
<span t-if="step.role_name">· <i class="fa fa-id-badge"/><t t-esc="step.role_name"/></span>
<span t-if="step.bath">· <i class="fa fa-flask"/><t t-esc="step.bath"/></span>
<span t-if="step.oven">· <i class="fa fa-fire"/><t t-esc="step.oven"/></span>
<span t-if="step.rack">· <i class="fa fa-th"/><t t-esc="step.rack"/></span>
<span t-if="step.masking_material">· <i class="fa fa-tag"/><t t-esc="step.masking_material"/></span>
</div>
<div t-if="wo.missing_for_release"
class="o_fp_mgr_wo_needs">
<div t-if="step.missing_for_release"
class="o_fp_mgr_step_needs">
<span class="o_fp_chip o_fp_chip_warning">
<i class="fa fa-exclamation-circle me-1"/>
Needs: <t t-esc="wo.missing_for_release"/>
Needs: <t t-esc="step.missing_for_release"/>
</span>
</div>
</div>
<!-- RIGHT: action group (pickers + buttons) -->
<div class="o_fp_mgr_wo_actions">
<div class="o_fp_mgr_step_actions">
<select class="o_fp_mgr_picker"
t-on-change="(ev) => this.onAssignWorker(wo, ev.target.value)">
t-on-change="(ev) => this.onAssignWorker(step, ev.target.value)">
<option value="">— Assign worker —</option>
<t t-foreach="operatorsForWO(wo)" t-as="op" t-key="op.id">
<t t-foreach="operatorsForStep(step)" t-as="op" t-key="op.id">
<option t-att-value="op.id"
t-att-selected="wo.assigned_user_id === op.id"
t-att-selected="step.assigned_user_id === op.id"
t-att-data-bucket="op.bucket">
<t t-if="op.is_clocked_in"></t>
<t t-else=""></t>
@@ -173,26 +175,26 @@
</option>
</t>
</select>
<select t-if="wo.wo_kind === 'wet'"
<select t-if="step.kind === 'wet'"
class="o_fp_mgr_picker"
t-on-change="(ev) => this.onAssignTank(wo, ev.target.value)">
t-on-change="(ev) => this.onAssignTank(step, ev.target.value)">
<option value="">— Tank —</option>
<t t-foreach="state.overview.tanks" t-as="tnk" t-key="tnk.id">
<option t-att-value="tnk.id"
t-att-selected="wo.tank_id === tnk.id">
t-att-selected="step.tank_id === tnk.id">
<t t-esc="tnk.name"/>
<t t-if="tnk.current_bath"> · <t t-esc="tnk.current_bath"/></t>
</option>
</t>
</select>
<button class="btn o_fp_mgr_btn"
t-on-click="() => this.onTakeOver(wo)"
title="Assign this WO to yourself">
t-on-click="() => this.onTakeOver(step)"
title="Assign this step to yourself">
<i class="fa fa-user me-1"/>Take Over
</button>
<button class="btn o_fp_mgr_btn"
t-on-click="() => this.openRecord('fp.job.step', wo.id)">
<i class="fa fa-external-link me-1"/>Open WO
t-on-click="() => this.openRecord('fp.job.step', step.id)">
<i class="fa fa-external-link me-1"/>Open Step
</button>
</div>
</div>
@@ -214,14 +216,14 @@
<div>Nothing running right now.</div>
</div>
<div class="o_fp_mgr_card_list" t-if="state.overview.active.length">
<t t-foreach="state.overview.active" t-as="card" t-key="card.mo_id">
<t t-foreach="state.overview.active" t-as="card" t-key="card.job_id">
<div class="o_fp_mgr_card"
t-att-data-priority="card.priority_any">
<div class="o_fp_mgr_card_head"
t-on-click="() => this.toggleCard(card.mo_id)">
t-on-click="() => this.toggleCard(card.job_id)">
<div>
<div class="o_fp_mgr_card_title">
<t t-esc="card.mo_name"/>
<t t-esc="card.job_name"/>
<span class="text-muted ms-2 small">· <t t-esc="card.so_name"/></span>
</div>
<div class="o_fp_mgr_card_sub">
@@ -232,33 +234,35 @@
<div class="o_fp_mgr_card_chips">
<span t-if="card.priority_any >= 2" class="o_fp_chip o_fp_chip_danger">HOT</span>
<span class="o_fp_chip o_fp_chip_success">
<t t-esc="card.wos.length"/> WO
<t t-esc="card.steps.length"/>
<t t-if="card.steps.length === 1"> Step</t>
<t t-else=""> Steps</t>
</span>
</div>
</div>
<div class="o_fp_mgr_card_body"
t-if="state.expandedMoId === card.mo_id or state.mode === 'detailed'">
<t t-foreach="card.wos" t-as="wo" t-key="wo.id">
<div class="o_fp_mgr_wo_row">
<div class="o_fp_mgr_wo_info">
<t t-esc="wo.name"/>
t-if="state.expandedJobId === card.job_id or state.mode === 'detailed'">
<t t-foreach="card.steps" t-as="step" t-key="step.id">
<div class="o_fp_mgr_step_row">
<div class="o_fp_mgr_step_info">
<t t-esc="step.name"/>
<span class="text-muted ms-2">
<t t-esc="wo.workcenter"/>
<t t-if="wo.assigned_user_name">
<t t-esc="step.workcenter"/>
<t t-if="step.assigned_user_name">
· <i class="fa fa-user"/>
<t t-esc="wo.assigned_user_name"/>
<t t-esc="step.assigned_user_name"/>
</t>
</span>
</div>
<span t-att-class="'o_fp_chip o_fp_chip_' + (wo.state === 'in_progress' || wo.state === 'progress' ? 'success' : 'info')">
<t t-esc="wo.state"/>
<span t-att-class="'o_fp_chip o_fp_chip_' + (step.state === 'in_progress' || step.state === 'progress' ? 'success' : 'info')">
<t t-esc="step.state"/>
</span>
<button class="btn"
t-on-click="() => this.onTakeOver(wo)">
t-on-click="() => this.onTakeOver(step)">
Take Over
</button>
<button class="btn"
t-on-click="() => this.openRecord('fp.job.step', wo.id)">
t-on-click="() => this.openRecord('fp.job.step', step.id)">
Open
</button>
</div>