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:
@@ -2,11 +2,11 @@
|
|||||||
# Copyright 2026 Nexa Systems Inc.
|
# Copyright 2026 Nexa Systems Inc.
|
||||||
# License OPL-1 (Odoo Proprietary License v1.0)
|
# License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
# Part of the Fusion Plating product family.
|
# Part of the Fusion Plating product family.
|
||||||
"""JSON-RPC endpoints for the Manager Dashboard (client action).
|
"""JSON-RPC endpoints for the Manager Desk (client action).
|
||||||
|
|
||||||
Native fp.job / fp.job.step edition (consolidated 2026-04-24). All
|
Native fp.job / fp.job.step edition. Speaks fp.job/fp.job.step
|
||||||
endpoint URLs are preserved (`/fp/manager/*`); the underlying data
|
end-to-end — payload keys, variables, and RPC kwargs all use the
|
||||||
layer is now fp.job + fp.job.step.
|
job/step vocabulary.
|
||||||
|
|
||||||
Manager Desk ergonomics:
|
Manager Desk ergonomics:
|
||||||
- Column 1 ("Needs a Worker") = jobs that have at least one step
|
- Column 1 ("Needs a Worker") = jobs that have at least one step
|
||||||
@@ -34,8 +34,7 @@ _NEG_JOB_STATES = ('done', 'cancelled')
|
|||||||
_ACTIVE_JOB_STATES = ('confirmed', 'in_progress', 'on_hold')
|
_ACTIVE_JOB_STATES = ('confirmed', 'in_progress', 'on_hold')
|
||||||
|
|
||||||
# A step needs an operator and (for wet/bake/mask) the right equipment
|
# A step needs an operator and (for wet/bake/mask) the right equipment
|
||||||
# before the operator can tap Start. Mirrors the legacy
|
# before the operator can tap Start.
|
||||||
# x_fc_is_release_ready compute on mrp.workorder.
|
|
||||||
def _step_release_readiness(step):
|
def _step_release_readiness(step):
|
||||||
"""Return (is_release_ready, missing_str) for a fp.job.step."""
|
"""Return (is_release_ready, missing_str) for a fp.job.step."""
|
||||||
missing = []
|
missing = []
|
||||||
@@ -55,7 +54,7 @@ def _step_release_readiness(step):
|
|||||||
|
|
||||||
|
|
||||||
def _priority_int(priority):
|
def _priority_int(priority):
|
||||||
"""fp.job.priority → int 0/1/2 (parallel of legacy x_fc_priority)."""
|
"""fp.job.priority → int 0/1/2."""
|
||||||
return {'rush': 2, 'high': 1, 'normal': 0, 'low': 0}.get(priority, 0)
|
return {'rush': 2, 'high': 1, 'normal': 0, 'low': 0}.get(priority, 0)
|
||||||
|
|
||||||
|
|
||||||
@@ -120,10 +119,10 @@ class FpManagerDashboardController(http.Controller):
|
|||||||
)
|
)
|
||||||
steps_iter = steps_iter.sorted('sequence')
|
steps_iter = steps_iter.sorted('sequence')
|
||||||
|
|
||||||
wo_rows = []
|
step_rows = []
|
||||||
for s in steps_iter:
|
for s in steps_iter:
|
||||||
ready, missing = readiness_by_step.get(s.id, (False, ''))
|
ready, missing = readiness_by_step.get(s.id, (False, ''))
|
||||||
wo_rows.append({
|
step_rows.append({
|
||||||
'id': s.id,
|
'id': s.id,
|
||||||
'name': s.name or '',
|
'name': s.name or '',
|
||||||
'workcenter': s.work_centre_id.name or '',
|
'workcenter': s.work_centre_id.name or '',
|
||||||
@@ -138,8 +137,8 @@ class FpManagerDashboardController(http.Controller):
|
|||||||
'assigned_user_name': s.assigned_user_id.name or '',
|
'assigned_user_name': s.assigned_user_id.name or '',
|
||||||
'role_id': False,
|
'role_id': False,
|
||||||
'role_name': '',
|
'role_name': '',
|
||||||
'wo_kind': s.kind or 'other',
|
'kind': s.kind or 'other',
|
||||||
'wo_kind_label': dict(s._fields['kind'].selection).get(
|
'kind_label': dict(s._fields['kind'].selection).get(
|
||||||
s.kind, '',
|
s.kind, '',
|
||||||
) if s.kind else '',
|
) if s.kind else '',
|
||||||
'is_release_ready': ready,
|
'is_release_ready': ready,
|
||||||
@@ -150,8 +149,8 @@ class FpManagerDashboardController(http.Controller):
|
|||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'mo_id': job.id,
|
'job_id': job.id,
|
||||||
'mo_name': job.name or '',
|
'job_name': job.name or '',
|
||||||
'so_name': job.origin or '',
|
'so_name': job.origin or '',
|
||||||
'customer': partner.name if partner else '',
|
'customer': partner.name if partner else '',
|
||||||
'product': job.product_id.display_name if job.product_id else '',
|
'product': job.product_id.display_name if job.product_id else '',
|
||||||
@@ -163,7 +162,7 @@ class FpManagerDashboardController(http.Controller):
|
|||||||
'recipe': job.recipe_id.name if job.recipe_id else '',
|
'recipe': job.recipe_id.name if job.recipe_id else '',
|
||||||
'priority_any': _priority_int(job.priority),
|
'priority_any': _priority_int(job.priority),
|
||||||
'current_location': job.current_location or '',
|
'current_location': job.current_location or '',
|
||||||
'wos': wo_rows,
|
'steps': step_rows,
|
||||||
}
|
}
|
||||||
|
|
||||||
unassigned_cards = [_job_card(j) for j in unassigned_jobs]
|
unassigned_cards = [_job_card(j) for j in unassigned_jobs]
|
||||||
@@ -256,14 +255,14 @@ class FpManagerDashboardController(http.Controller):
|
|||||||
ready_to_ship_jobs = Job.search_count([('state', '=', 'done')])
|
ready_to_ship_jobs = Job.search_count([('state', '=', 'done')])
|
||||||
|
|
||||||
kpis = {
|
kpis = {
|
||||||
'unassigned_wos': len(all_steps.filtered(
|
'unassigned_steps': len(all_steps.filtered(
|
||||||
lambda s: not readiness_by_step.get(s.id, (False, ''))[0],
|
lambda s: not readiness_by_step.get(s.id, (False, ''))[0],
|
||||||
)),
|
)),
|
||||||
'active_wos': len(all_steps.filtered(
|
'active_steps': len(all_steps.filtered(
|
||||||
lambda s: readiness_by_step.get(s.id, (False, ''))[0]
|
lambda s: readiness_by_step.get(s.id, (False, ''))[0]
|
||||||
and s.state in ('ready', 'in_progress'),
|
and s.state in ('ready', 'in_progress'),
|
||||||
)),
|
)),
|
||||||
'ready_to_ship_mos': ready_to_ship_jobs,
|
'ready_to_ship_jobs': ready_to_ship_jobs,
|
||||||
'pending_accept_sos': pending_accept_sos,
|
'pending_accept_sos': pending_accept_sos,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,10 +294,20 @@ class FpManagerDashboardController(http.Controller):
|
|||||||
# Assign a worker to a step
|
# Assign a worker to a step
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@http.route('/fp/manager/assign_worker', type='jsonrpc', auth='user')
|
@http.route('/fp/manager/assign_worker', type='jsonrpc', auth='user')
|
||||||
def assign_worker(self, workorder_id, user_id):
|
def assign_worker(self, step_id=None, user_id=None, workorder_id=None, **kwargs):
|
||||||
"""`workorder_id` is the canonical kwarg name from the legacy
|
"""Assign an operator to a step. ``step_id`` is the canonical
|
||||||
XML; it now resolves to a fp.job.step id."""
|
kwarg; ``workorder_id`` is accepted as a deprecated alias for
|
||||||
step = request.env['fp.job.step'].browse(int(workorder_id))
|
one release so any caller we missed doesn't break.
|
||||||
|
"""
|
||||||
|
if step_id is None and workorder_id is not None:
|
||||||
|
_logger.warning(
|
||||||
|
"workorder_id kwarg is deprecated; use step_id "
|
||||||
|
"(/fp/manager/assign_worker)",
|
||||||
|
)
|
||||||
|
step_id = workorder_id
|
||||||
|
if not step_id:
|
||||||
|
return {'ok': False, 'error': 'step_id required'}
|
||||||
|
step = request.env['fp.job.step'].browse(int(step_id))
|
||||||
if not step.exists():
|
if not step.exists():
|
||||||
return {'ok': False, 'error': 'Step not found.'}
|
return {'ok': False, 'error': 'Step not found.'}
|
||||||
step.assigned_user_id = int(user_id) if user_id else False
|
step.assigned_user_id = int(user_id) if user_id else False
|
||||||
@@ -313,8 +322,19 @@ class FpManagerDashboardController(http.Controller):
|
|||||||
# Reassign or swap tank on a step
|
# Reassign or swap tank on a step
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@http.route('/fp/manager/assign_tank', type='jsonrpc', auth='user')
|
@http.route('/fp/manager/assign_tank', type='jsonrpc', auth='user')
|
||||||
def assign_tank(self, workorder_id, tank_id):
|
def assign_tank(self, step_id=None, tank_id=None, workorder_id=None, **kwargs):
|
||||||
step = request.env['fp.job.step'].browse(int(workorder_id))
|
"""Swap the tank on a step. ``step_id`` is the canonical kwarg;
|
||||||
|
``workorder_id`` is accepted as a deprecated alias.
|
||||||
|
"""
|
||||||
|
if step_id is None and workorder_id is not None:
|
||||||
|
_logger.warning(
|
||||||
|
"workorder_id kwarg is deprecated; use step_id "
|
||||||
|
"(/fp/manager/assign_tank)",
|
||||||
|
)
|
||||||
|
step_id = workorder_id
|
||||||
|
if not step_id:
|
||||||
|
return {'ok': False, 'error': 'step_id required'}
|
||||||
|
step = request.env['fp.job.step'].browse(int(step_id))
|
||||||
if not step.exists():
|
if not step.exists():
|
||||||
return {'ok': False, 'error': 'Step not found.'}
|
return {'ok': False, 'error': 'Step not found.'}
|
||||||
step.tank_id = int(tank_id) if tank_id else False
|
step.tank_id = int(tank_id) if tank_id else False
|
||||||
@@ -329,8 +349,19 @@ class FpManagerDashboardController(http.Controller):
|
|||||||
# Manager takes over a step (no-show coverage)
|
# Manager takes over a step (no-show coverage)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@http.route('/fp/manager/take_over', type='jsonrpc', auth='user')
|
@http.route('/fp/manager/take_over', type='jsonrpc', auth='user')
|
||||||
def take_over(self, workorder_id):
|
def take_over(self, step_id=None, workorder_id=None, **kwargs):
|
||||||
step = request.env['fp.job.step'].browse(int(workorder_id))
|
"""Manager takes over a step. ``step_id`` is the canonical kwarg;
|
||||||
|
``workorder_id`` is accepted as a deprecated alias.
|
||||||
|
"""
|
||||||
|
if step_id is None and workorder_id is not None:
|
||||||
|
_logger.warning(
|
||||||
|
"workorder_id kwarg is deprecated; use step_id "
|
||||||
|
"(/fp/manager/take_over)",
|
||||||
|
)
|
||||||
|
step_id = workorder_id
|
||||||
|
if not step_id:
|
||||||
|
return {'ok': False, 'error': 'step_id required'}
|
||||||
|
step = request.env['fp.job.step'].browse(int(step_id))
|
||||||
if not step.exists():
|
if not step.exists():
|
||||||
return {'ok': False, 'error': 'Step not found.'}
|
return {'ok': False, 'error': 'Step not found.'}
|
||||||
user = request.env.user
|
user = request.env.user
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
/** @odoo-module **/
|
/** @odoo-module **/
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// Fusion Plating — Manager Dashboard (OWL client action)
|
// Fusion Plating — Manager Desk (OWL client action)
|
||||||
// Copyright 2026 Nexa Systems Inc.
|
// Copyright 2026 Nexa Systems Inc.
|
||||||
// License OPL-1 (Odoo Proprietary License v1.0)
|
// License OPL-1 (Odoo Proprietary License v1.0)
|
||||||
//
|
//
|
||||||
// Manager-level view: assign workers, swap tanks, cover no-shows, drill
|
// 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
|
// Native fp.job / fp.job.step edition. Speaks job/step end-to-end —
|
||||||
// "wo" naming inside payloads is preserved so the existing XML template
|
// payload keys, variables, and RPC kwargs all use the job/step
|
||||||
// keeps rendering — those keys now carry fp.job.step rows under the hood.
|
// vocabulary.
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
import { Component, useState, onMounted, onWillUnmount } from "@odoo/owl";
|
import { Component, useState, onMounted, onWillUnmount } from "@odoo/owl";
|
||||||
@@ -29,7 +29,7 @@ export class ManagerDashboard extends Component {
|
|||||||
overview: null,
|
overview: null,
|
||||||
loadError: "", // visible error instead of stuck spinner
|
loadError: "", // visible error instead of stuck spinner
|
||||||
mode: "quick", // quick | detailed
|
mode: "quick", // quick | detailed
|
||||||
expandedMoId: null,
|
expandedJobId: null,
|
||||||
message: "",
|
message: "",
|
||||||
messageType: "info",
|
messageType: "info",
|
||||||
isFetching: false, // pulses the "updating" dot in the header
|
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";
|
this.state.mode = this.state.mode === "quick" ? "detailed" : "quick";
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleCard(moId) {
|
toggleCard(jobId) {
|
||||||
this.state.expandedMoId = this.state.expandedMoId === moId ? null : moId;
|
this.state.expandedJobId = this.state.expandedJobId === jobId ? null : jobId;
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleOffShift() {
|
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:
|
* Buckets, top-down, each kept in original (alphabetical) order:
|
||||||
* 1. Qualified for this role AND clocked in — primary picks
|
* 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
|
* Each option carries a `bucket` so the template can render a tiny
|
||||||
* green/grey dot and (for buckets 3-4) a soft helper label.
|
* 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 all = (this.state.overview && this.state.overview.operators) || [];
|
||||||
const roleId = wo && wo.role_id;
|
const roleId = step && step.role_id;
|
||||||
const out = [];
|
const out = [];
|
||||||
for (const op of all) {
|
for (const op of all) {
|
||||||
const qualified = roleId && op.role_ids && op.role_ids.includes(roleId);
|
const qualified = roleId && op.role_ids && op.role_ids.includes(roleId);
|
||||||
@@ -184,15 +184,15 @@ export class ManagerDashboard extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------- Actions
|
// ---------------------------------------------------------- Actions
|
||||||
async onAssignWorker(wo, userIdRaw) {
|
async onAssignWorker(step, userIdRaw) {
|
||||||
const userId = parseInt(userIdRaw) || null;
|
const userId = parseInt(userIdRaw) || null;
|
||||||
try {
|
try {
|
||||||
const res = await rpc("/fp/manager/assign_worker", {
|
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) {
|
if (res && res.ok) {
|
||||||
this.setMessage(
|
this.setMessage(
|
||||||
`Assigned ${res.user_name || 'unassigned'} to ${wo.name}`,
|
`Assigned ${res.user_name || 'unassigned'} to ${step.name}`,
|
||||||
"success",
|
"success",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -202,15 +202,15 @@ export class ManagerDashboard extends Component {
|
|||||||
await this.refresh();
|
await this.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
async onAssignTank(wo, tankIdRaw) {
|
async onAssignTank(step, tankIdRaw) {
|
||||||
const tankId = parseInt(tankIdRaw) || null;
|
const tankId = parseInt(tankIdRaw) || null;
|
||||||
try {
|
try {
|
||||||
const res = await rpc("/fp/manager/assign_tank", {
|
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) {
|
if (res && res.ok) {
|
||||||
this.setMessage(
|
this.setMessage(
|
||||||
`Tank ${res.tank_name || 'cleared'} for ${wo.name}`,
|
`Tank ${res.tank_name || 'cleared'} for ${step.name}`,
|
||||||
"success",
|
"success",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -220,13 +220,13 @@ export class ManagerDashboard extends Component {
|
|||||||
await this.refresh();
|
await this.refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
async onTakeOver(wo) {
|
async onTakeOver(step) {
|
||||||
try {
|
try {
|
||||||
const res = await rpc("/fp/manager/take_over", {
|
const res = await rpc("/fp/manager/take_over", {
|
||||||
workorder_id: wo.id,
|
step_id: step.id,
|
||||||
});
|
});
|
||||||
if (res && res.ok) {
|
if (res && res.ok) {
|
||||||
this.setMessage(`You now own ${wo.name}.`, "success");
|
this.setMessage(`You now own ${step.name}.`, "success");
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setMessage(`Takeover failed: ${err.message || err}`, "danger");
|
this.setMessage(`Takeover failed: ${err.message || err}`, "danger");
|
||||||
|
|||||||
@@ -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
|
// Flex with wrap so narrow viewports drop actions below the info naturally
|
||||||
// instead of squishing everything into a single broken grid line.
|
// instead of squishing everything into a single broken grid line.
|
||||||
.o_fp_mgr_wo_row {
|
.o_fp_mgr_step_row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: $fp-space-3;
|
gap: $fp-space-3;
|
||||||
@@ -458,7 +458,7 @@
|
|||||||
font-size: $fp-text-sm;
|
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
|
flex: 1 1 280px; // grows but never narrower than 280px
|
||||||
min-width: 0; // allows children to shrink properly
|
min-width: 0; // allows children to shrink properly
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -466,8 +466,8 @@
|
|||||||
gap: $fp-space-1;
|
gap: $fp-space-1;
|
||||||
color: $fp-ink;
|
color: $fp-ink;
|
||||||
|
|
||||||
// Title row — kind badge + WO name + step number
|
// Title row — kind badge + step name + sequence
|
||||||
.o_fp_mgr_wo_title {
|
.o_fp_mgr_step_title {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: $fp-space-2;
|
gap: $fp-space-2;
|
||||||
@@ -477,7 +477,7 @@
|
|||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
// Meta row — workcenter / role / set equipment
|
// Meta row — workcenter / role / set equipment
|
||||||
.o_fp_mgr_wo_meta {
|
.o_fp_mgr_step_meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: $fp-space-2;
|
gap: $fp-space-2;
|
||||||
@@ -487,7 +487,7 @@
|
|||||||
i { margin-right: 2px; }
|
i { margin-right: 2px; }
|
||||||
}
|
}
|
||||||
// Chip row — what's still missing for the manager to set
|
// Chip row — what's still missing for the manager to set
|
||||||
.o_fp_mgr_wo_needs {
|
.o_fp_mgr_step_needs {
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -496,7 +496,7 @@
|
|||||||
// takes the remaining horizontal space (the dropdown then grows to
|
// takes the remaining horizontal space (the dropdown then grows to
|
||||||
// fill); flex-wrap so on narrow widths the dropdown sits on its own
|
// fill); flex-wrap so on narrow widths the dropdown sits on its own
|
||||||
// line and the buttons go below at 50/50.
|
// line and the buttons go below at 50/50.
|
||||||
.o_fp_mgr_wo_actions {
|
.o_fp_mgr_step_actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -531,7 +531,7 @@
|
|||||||
&:focus { @include fp-focus-ring; border-color: $fp-accent; }
|
&:focus { @include fp-focus-ring; border-color: $fp-accent; }
|
||||||
}
|
}
|
||||||
.o_fp_mgr_btn,
|
.o_fp_mgr_btn,
|
||||||
.o_fp_mgr_wo_row .btn {
|
.o_fp_mgr_step_row .btn {
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
padding: 0 $fp-space-3;
|
padding: 0 $fp-space-3;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -549,13 +549,13 @@
|
|||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
// Mobile / narrow tablet: dropdown takes full width on its own
|
// Mobile / narrow tablet: dropdown takes full width on its own
|
||||||
// line; the two buttons split 50/50 underneath.
|
// line; the two buttons split 50/50 underneath.
|
||||||
.o_fp_mgr_wo_actions {
|
.o_fp_mgr_step_actions {
|
||||||
flex: 1 1 100%;
|
flex: 1 1 100%;
|
||||||
justify-content: stretch;
|
justify-content: stretch;
|
||||||
}
|
}
|
||||||
.o_fp_mgr_picker { flex: 1 1 100%; }
|
.o_fp_mgr_picker { flex: 1 1 100%; }
|
||||||
.o_fp_mgr_btn,
|
.o_fp_mgr_btn,
|
||||||
.o_fp_mgr_wo_row .btn {
|
.o_fp_mgr_step_row .btn {
|
||||||
flex: 1 1 calc(50% - #{$fp-space-2});
|
flex: 1 1 calc(50% - #{$fp-space-2});
|
||||||
min-height: $fp-touch-min;
|
min-height: $fp-touch-min;
|
||||||
}
|
}
|
||||||
@@ -580,7 +580,7 @@
|
|||||||
&.o_fp_chip_danger { @include fp-pill(--bs-danger); }
|
&.o_fp_chip_danger { @include fp-pill(--bs-danger); }
|
||||||
&.o_fp_chip_muted { background-color: $fp-card-soft; color: $fp-ink-mute; }
|
&.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.
|
// mask vs wet vs bake at a glance.
|
||||||
&.o_fp_chip_kind {
|
&.o_fp_chip_kind {
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<!--
|
<!--
|
||||||
Copyright 2026 Nexa Systems Inc. · License OPL-1
|
Copyright 2026 Nexa Systems Inc. · License OPL-1
|
||||||
Fusion Plating — Manager Desk
|
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">
|
<templates xml:space="preserve">
|
||||||
|
|
||||||
@@ -71,17 +71,17 @@
|
|||||||
<div class="o_fp_kpi_strip" t-if="state.overview">
|
<div class="o_fp_kpi_strip" t-if="state.overview">
|
||||||
<div class="o_fp_kpi o_fp_kpi_warning">
|
<div class="o_fp_kpi o_fp_kpi_warning">
|
||||||
<i class="fa fa-user-times"/>
|
<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_value"><t t-esc="state.overview.kpis.unassigned_steps"/></div>
|
||||||
<div class="o_fp_kpi_label">Unassigned WOs</div>
|
<div class="o_fp_kpi_label">Unassigned Steps</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="o_fp_kpi o_fp_kpi_success">
|
<div class="o_fp_kpi o_fp_kpi_success">
|
||||||
<i class="fa fa-cogs"/>
|
<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 class="o_fp_kpi_label">In Progress</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="o_fp_kpi o_fp_kpi_info">
|
<div class="o_fp_kpi o_fp_kpi_info">
|
||||||
<i class="fa fa-truck"/>
|
<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 class="o_fp_kpi_label">Ready to Ship</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="o_fp_kpi o_fp_kpi_warning">
|
<div class="o_fp_kpi o_fp_kpi_warning">
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
<!-- ============ Workload grid ============ -->
|
<!-- ============ Workload grid ============ -->
|
||||||
<div class="o_fp_manager_grid" t-if="state.overview">
|
<div class="o_fp_manager_grid" t-if="state.overview">
|
||||||
|
|
||||||
<!-- Unassigned -->
|
<!-- Needs a Worker -->
|
||||||
<section class="o_fp_panel o_fp_panel_unassigned">
|
<section class="o_fp_panel o_fp_panel_unassigned">
|
||||||
<div class="o_fp_panel_head">
|
<div class="o_fp_panel_head">
|
||||||
<h3><i class="fa fa-inbox"/>Needs a Worker</h3>
|
<h3><i class="fa fa-inbox"/>Needs a Worker</h3>
|
||||||
@@ -102,17 +102,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<div t-if="!state.overview.unassigned.length" class="o_fp_empty">
|
<div t-if="!state.overview.unassigned.length" class="o_fp_empty">
|
||||||
<i class="fa fa-check-circle text-success"/>
|
<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>
|
||||||
<div class="o_fp_mgr_card_list" t-if="state.overview.unassigned.length">
|
<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"
|
<div class="o_fp_mgr_card"
|
||||||
t-att-data-priority="card.priority_any">
|
t-att-data-priority="card.priority_any">
|
||||||
<div class="o_fp_mgr_card_head"
|
<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>
|
||||||
<div class="o_fp_mgr_card_title">
|
<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>
|
<span class="text-muted ms-2 small">· <t t-esc="card.so_name"/></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="o_fp_mgr_card_sub">
|
<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 >= 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 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">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="o_fp_mgr_card_body"
|
<div class="o_fp_mgr_card_body"
|
||||||
t-if="state.expandedMoId === card.mo_id or state.mode === 'detailed'">
|
t-if="state.expandedJobId === card.job_id or state.mode === 'detailed'">
|
||||||
<t t-foreach="card.wos" t-as="wo" t-key="wo.id">
|
<t t-foreach="card.steps" t-as="step" t-key="step.id">
|
||||||
<div class="o_fp_mgr_wo_row">
|
<div class="o_fp_mgr_step_row">
|
||||||
<!-- LEFT: information stack (badge, name, meta, needs) -->
|
<!-- LEFT: information stack (badge, name, meta, needs) -->
|
||||||
<div class="o_fp_mgr_wo_info">
|
<div class="o_fp_mgr_step_info">
|
||||||
<div class="o_fp_mgr_wo_title">
|
<div class="o_fp_mgr_step_title">
|
||||||
<span t-attf-class="o_fp_chip o_fp_chip_kind o_fp_chip_kind_{{ wo.wo_kind }}"
|
<span t-attf-class="o_fp_chip o_fp_chip_kind o_fp_chip_kind_{{ step.kind }}"
|
||||||
t-esc="wo.wo_kind_label || wo.wo_kind"/>
|
t-esc="step.kind_label || step.kind"/>
|
||||||
<span t-esc="wo.name"/>
|
<span t-esc="step.name"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="o_fp_mgr_wo_meta">
|
<div class="o_fp_mgr_step_meta">
|
||||||
<span><i class="fa fa-cog"/><t t-esc="wo.workcenter"/></span>
|
<span><i class="fa fa-cog"/><t t-esc="step.workcenter"/></span>
|
||||||
<span t-if="wo.role_name">· <i class="fa fa-id-badge"/><t t-esc="wo.role_name"/></span>
|
<span t-if="step.role_name">· <i class="fa fa-id-badge"/><t t-esc="step.role_name"/></span>
|
||||||
<span t-if="wo.bath">· <i class="fa fa-flask"/><t t-esc="wo.bath"/></span>
|
<span t-if="step.bath">· <i class="fa fa-flask"/><t t-esc="step.bath"/></span>
|
||||||
<span t-if="wo.oven">· <i class="fa fa-fire"/><t t-esc="wo.oven"/></span>
|
<span t-if="step.oven">· <i class="fa fa-fire"/><t t-esc="step.oven"/></span>
|
||||||
<span t-if="wo.rack">· <i class="fa fa-th"/><t t-esc="wo.rack"/></span>
|
<span t-if="step.rack">· <i class="fa fa-th"/><t t-esc="step.rack"/></span>
|
||||||
<span t-if="wo.masking_material">· <i class="fa fa-tag"/><t t-esc="wo.masking_material"/></span>
|
<span t-if="step.masking_material">· <i class="fa fa-tag"/><t t-esc="step.masking_material"/></span>
|
||||||
</div>
|
</div>
|
||||||
<div t-if="wo.missing_for_release"
|
<div t-if="step.missing_for_release"
|
||||||
class="o_fp_mgr_wo_needs">
|
class="o_fp_mgr_step_needs">
|
||||||
<span class="o_fp_chip o_fp_chip_warning">
|
<span class="o_fp_chip o_fp_chip_warning">
|
||||||
<i class="fa fa-exclamation-circle me-1"/>
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- RIGHT: action group (pickers + buttons) -->
|
<!-- 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"
|
<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>
|
<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"
|
<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-att-data-bucket="op.bucket">
|
||||||
<t t-if="op.is_clocked_in">●</t>
|
<t t-if="op.is_clocked_in">●</t>
|
||||||
<t t-else="">○</t>
|
<t t-else="">○</t>
|
||||||
@@ -173,26 +175,26 @@
|
|||||||
</option>
|
</option>
|
||||||
</t>
|
</t>
|
||||||
</select>
|
</select>
|
||||||
<select t-if="wo.wo_kind === 'wet'"
|
<select t-if="step.kind === 'wet'"
|
||||||
class="o_fp_mgr_picker"
|
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>
|
<option value="">— Tank —</option>
|
||||||
<t t-foreach="state.overview.tanks" t-as="tnk" t-key="tnk.id">
|
<t t-foreach="state.overview.tanks" t-as="tnk" t-key="tnk.id">
|
||||||
<option t-att-value="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-esc="tnk.name"/>
|
||||||
<t t-if="tnk.current_bath"> · <t t-esc="tnk.current_bath"/></t>
|
<t t-if="tnk.current_bath"> · <t t-esc="tnk.current_bath"/></t>
|
||||||
</option>
|
</option>
|
||||||
</t>
|
</t>
|
||||||
</select>
|
</select>
|
||||||
<button class="btn o_fp_mgr_btn"
|
<button class="btn o_fp_mgr_btn"
|
||||||
t-on-click="() => this.onTakeOver(wo)"
|
t-on-click="() => this.onTakeOver(step)"
|
||||||
title="Assign this WO to yourself">
|
title="Assign this step to yourself">
|
||||||
<i class="fa fa-user me-1"/>Take Over
|
<i class="fa fa-user me-1"/>Take Over
|
||||||
</button>
|
</button>
|
||||||
<button class="btn o_fp_mgr_btn"
|
<button class="btn o_fp_mgr_btn"
|
||||||
t-on-click="() => this.openRecord('fp.job.step', wo.id)">
|
t-on-click="() => this.openRecord('fp.job.step', step.id)">
|
||||||
<i class="fa fa-external-link me-1"/>Open WO
|
<i class="fa fa-external-link me-1"/>Open Step
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -214,14 +216,14 @@
|
|||||||
<div>Nothing running right now.</div>
|
<div>Nothing running right now.</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="o_fp_mgr_card_list" t-if="state.overview.active.length">
|
<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"
|
<div class="o_fp_mgr_card"
|
||||||
t-att-data-priority="card.priority_any">
|
t-att-data-priority="card.priority_any">
|
||||||
<div class="o_fp_mgr_card_head"
|
<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>
|
||||||
<div class="o_fp_mgr_card_title">
|
<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>
|
<span class="text-muted ms-2 small">· <t t-esc="card.so_name"/></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="o_fp_mgr_card_sub">
|
<div class="o_fp_mgr_card_sub">
|
||||||
@@ -232,33 +234,35 @@
|
|||||||
<div class="o_fp_mgr_card_chips">
|
<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 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">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="o_fp_mgr_card_body"
|
<div class="o_fp_mgr_card_body"
|
||||||
t-if="state.expandedMoId === card.mo_id or state.mode === 'detailed'">
|
t-if="state.expandedJobId === card.job_id or state.mode === 'detailed'">
|
||||||
<t t-foreach="card.wos" t-as="wo" t-key="wo.id">
|
<t t-foreach="card.steps" t-as="step" t-key="step.id">
|
||||||
<div class="o_fp_mgr_wo_row">
|
<div class="o_fp_mgr_step_row">
|
||||||
<div class="o_fp_mgr_wo_info">
|
<div class="o_fp_mgr_step_info">
|
||||||
<t t-esc="wo.name"/>
|
<t t-esc="step.name"/>
|
||||||
<span class="text-muted ms-2">
|
<span class="text-muted ms-2">
|
||||||
<t t-esc="wo.workcenter"/>
|
<t t-esc="step.workcenter"/>
|
||||||
<t t-if="wo.assigned_user_name">
|
<t t-if="step.assigned_user_name">
|
||||||
· <i class="fa fa-user"/>
|
· <i class="fa fa-user"/>
|
||||||
<t t-esc="wo.assigned_user_name"/>
|
<t t-esc="step.assigned_user_name"/>
|
||||||
</t>
|
</t>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span t-att-class="'o_fp_chip o_fp_chip_' + (wo.state === 'in_progress' || wo.state === 'progress' ? 'success' : 'info')">
|
<span t-att-class="'o_fp_chip o_fp_chip_' + (step.state === 'in_progress' || step.state === 'progress' ? 'success' : 'info')">
|
||||||
<t t-esc="wo.state"/>
|
<t t-esc="step.state"/>
|
||||||
</span>
|
</span>
|
||||||
<button class="btn"
|
<button class="btn"
|
||||||
t-on-click="() => this.onTakeOver(wo)">
|
t-on-click="() => this.onTakeOver(step)">
|
||||||
Take Over
|
Take Over
|
||||||
</button>
|
</button>
|
||||||
<button class="btn"
|
<button class="btn"
|
||||||
t-on-click="() => this.openRecord('fp.job.step', wo.id)">
|
t-on-click="() => this.openRecord('fp.job.step', step.id)">
|
||||||
Open
|
Open
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user