feat(sub12b): plant overview Racks pane (Task 16)

Controller: extend /fp/shopfloor/plant_overview return payload to
include 'racks' array (filtered to loaded/in_use/awaiting_unrack
states). Each entry has tag chips, part count, current node
breadcrumb, current step + tank code, and a precomputed
next_step_id (next sequence in the job's recipe — operator
overrides at runtime in the Move Rack dialog).

JS: state.racks populated from payload. New openMoveRackDialog()
method spawns FpMoveRackDialog. Notification when rack has no
successor (last step of job).

XML: top section above the existing work-centre columns. Renders
rack rows with tags, part count, breadcrumb, and primary MOVE RACK
button per row. Visible only when state.racks.length > 0.

SCSS: minimal styling for the racks pane (extends move_dialogs.scss
to keep all Sub 12b styles in one file).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-27 21:19:05 -04:00
parent 902f3e8398
commit 11dbbf578e
4 changed files with 147 additions and 0 deletions

View File

@@ -1274,11 +1274,56 @@ class FpShopfloorController(http.Controller):
'cards': cards_by_wc[0],
})
# Sub 12b — Racks pane payload alongside the existing parts cards.
# Filters to racks currently in active racking-state. Each entry
# has the data the OWL plant overview's Racks pane renders:
# tag chips, current node breadcrumb, part count, and the
# rack-level MOVE RACK button target.
rack_domain = [
('racking_state', 'in', ('loaded', 'in_use', 'awaiting_unrack')),
('active', '=', True),
]
if facility_id:
rack_domain.append(('facility_id', '=', int(facility_id)))
racks_payload = []
for r in env['fusion.plating.rack'].search(rack_domain):
cur_step = r.current_job_step_id
racks_payload.append({
'id': r.id,
'name': r.name,
'racking_state': r.racking_state,
'tag_ids': [
{'id': t.id, 'name': t.name, 'color': t.color}
for t in r.tag_ids
],
'current_part_count': r.current_part_count,
'current_node_name': cur_step.name if cur_step else '',
'current_tank_code': (
r.current_tank_id.code if r.current_tank_id else ''
),
'current_step_id': cur_step.id if cur_step else False,
# Default destination: next sibling step in the recipe
# sequence. Falls back to current_step_id if no successor
# (operator can pick a different one in the dialog).
'next_step_id': self._fp_next_step_id(cur_step) if cur_step else False,
})
return {
'facility_name': facility_name,
'columns': columns,
'racks': racks_payload,
}
def _fp_next_step_id(self, step):
"""Return the id of the next step in this job's recipe sequence,
or False if `step` is the last."""
if not step or not step.job_id:
return False
successors = step.job_id.step_ids.filtered(
lambda s: s.sequence > step.sequence
).sorted('sequence')
return successors[0].id if successors else False
# ------------------------------------------------------------------
# Urgency scoring (v19.0.24.8.0)
# ------------------------------------------------------------------

View File

@@ -23,6 +23,7 @@ import { registry } from "@web/core/registry";
import { rpc } from "@web/core/network/rpc";
import { useService } from "@web/core/utils/hooks";
import { QrScanner } from "./qr_scanner";
import { FpMoveRackDialog } from "./move_rack_dialog";
// =============================================================================
// TimerChip — per-card live elapsed-in-stage chip (v19.0.24.10.0)
@@ -132,10 +133,12 @@ export class PlantOverview extends Component {
setup() {
this.notification = useService("notification");
this.action = useService("action");
this.dialog = useService("dialog");
this.state = useState({
facilityName: "",
columns: [],
racks: [], // Sub 12b — Racks pane payload
searchTerm: "",
loading: false,
lastRefresh: null,
@@ -192,6 +195,7 @@ export class PlantOverview extends Component {
if (result) {
this.state.facilityName = result.facility_name || "Plant 1";
this.state.columns = result.columns || [];
this.state.racks = result.racks || [];
this.state.lastRefresh = new Date().toLocaleTimeString();
}
} catch (err) {
@@ -215,6 +219,23 @@ export class PlantOverview extends Component {
this._debouncedSearch();
}
// ===================================================== Sub 12b — racks
openMoveRackDialog(rackId, toStepId) {
if (!toStepId) {
this.notification.add(
"No destination step available — rack is at the last "
+ "step of its job. Use the rack form to manually pick "
+ "a new step.",
{ type: "warning" });
return;
}
this.dialog.add(FpMoveRackDialog, {
rackId, toStepId,
onCommit: () => this.loadData(),
});
}
_debouncedSearch() {
if (this._searchTimer) clearTimeout(this._searchTimer);
this._searchTimer = setTimeout(() => this.loadData(), 200);

View File

@@ -129,3 +129,47 @@ $fp-md-page: var(--fp-page-bg, #{$_fp_md_page_hex});
font-size: .875rem;
border-left: 4px solid $fp-md-accent;
}
// ===================================================== Plant overview racks pane
.o_fp_racks_pane {
background: $fp-md-card;
border: 1px solid $fp-md-border;
border-radius: 4px;
padding: .75rem;
margin-bottom: 1rem;
.o_fp_racks_header {
display: flex;
align-items: center;
gap: .5rem;
margin-bottom: .5rem;
h3 {
margin: 0;
font-size: 1rem;
color: $fp-md-accent;
}
}
.o_fp_rack_row {
display: flex;
align-items: center;
gap: .5rem;
padding: .5rem;
border-bottom: 1px solid $fp-md-border;
font-size: .875rem;
&:last-child { border-bottom: none; }
.o_fp_rack_name { font-weight: 600; min-width: 6rem; }
.o_fp_rack_count {
color: $fp-md-muted;
min-width: 5rem;
}
.o_fp_rack_breadcrumb {
flex: 1;
color: $fp-md-muted;
}
}
}

View File

@@ -72,6 +72,43 @@
<p class="mt-3 text-muted">No work centres with active orders found.</p>
</div>
<!-- ========== Sub 12b — RACKS PANE ========== -->
<!-- Top section above the work-centre columns. Shows racks
currently in (loaded / in_use / awaiting_unrack) state
with tag chips, part count, current node breadcrumb,
and a MOVE RACK button per row. -->
<div class="o_fp_racks_pane" t-if="state.racks.length">
<div class="o_fp_racks_header">
<h3>Racks</h3>
<span class="o_fp_po_col_count badge rounded-pill">
<t t-esc="state.racks.length"/>
</span>
</div>
<div class="o_fp_rack_row"
t-foreach="state.racks" t-as="rack" t-key="rack.id">
<span class="o_fp_rack_name"><t t-esc="rack.name"/></span>
<span class="o_fp_rack_count">
<t t-esc="rack.current_part_count"/> parts
</span>
<span class="o_fp_rack_breadcrumb">
<t t-esc="rack.current_node_name"/>
<t t-if="rack.current_tank_code">
/ <t t-esc="rack.current_tank_code"/>
</t>
</span>
<span t-foreach="rack.tag_ids" t-as="tag" t-key="tag.id"
class="o_fp_rack_tag_chip"
t-att-data-color="tag.color">
<t t-esc="tag.name"/>
</span>
<button class="btn btn-sm btn-primary"
t-att-disabled="!rack.next_step_id"
t-on-click="() => this.openMoveRackDialog(rack.id, rack.next_step_id)">
MOVE RACK
</button>
</div>
</div>
<!-- ========== COLUMNS (work centres) ========== -->
<div class="o_fp_po_columns" t-if="state.columns.length">
<t t-foreach="state.columns" t-as="col" t-key="col.work_center_id">