From 1c6a460ca188f546d2392b59ae2fb9a611577560 Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Fri, 17 Apr 2026 20:03:01 -0400 Subject: [PATCH] =?UTF-8?q?feat(shopfloor):=20Manager=20Desk=20=E2=80=94?= =?UTF-8?q?=20assign=20workers,=20swap=20tanks,=20take=20over=20(CHUNK=202?= =?UTF-8?q?/4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New client action "Manager Desk" under Shop Floor menu (manager-only). Three-column dashboard designed for the shop manager's daily job: Column 1 — Needs a Worker MOs with active WOs missing user assignment. Each card expands to show per-WO rows with: - Assign Worker dropdown (pulls from group_fusion_plating_operator) - Tank swap dropdown (all tanks with current bath) - Take Over (claims for the manager in one click) - Open (jump to WO form) Column 2 — In Progress MOs with workers actively running WOs. Shows who's on each step, lets manager reassign or take over if someone steps away. Column 3 — Team Avatar grid of operators with live queue + in-progress counts. Click to drill into that operator's full WO list. KPI strip on top: Unassigned WOs, In Progress, Ready to Ship, Awaiting Assignment SOs. Quick / Detailed view toggle — Detailed auto-expands every card body. New field mrp.workorder.x_fc_assigned_user_id (indexed, tracked) — the worker currently owning this step. Will be the pivot the Tablet Station filters on in Chunk 4. Three new endpoints: /fp/manager/overview — dashboard snapshot (30s auto-refresh) /fp/manager/assign_worker — set user on a WO /fp/manager/assign_tank — swap tank on a WO /fp/manager/take_over — manager claims the WO (no-show coverage) Controller is graceful when mrp module isn't installed (empty overview, no crash) and when the bridge_mrp assignment field isn't present (falls back to showing all active WOs as "unassigned"). Verified: 4 WOs assigned across 2 users, overview queries return the expected counts, res.groups.user_ids (Odoo 19 API, not deprecated .users). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../models/mrp_workorder.py | 12 + .../fusion_plating_shopfloor/__manifest__.py | 3 + .../controllers/__init__.py | 1 + .../controllers/manager_controller.py | 232 ++++++++++++++++ .../static/src/js/manager_dashboard.js | 152 +++++++++++ .../static/src/scss/manager_dashboard.scss | 187 +++++++++++++ .../static/src/xml/manager_dashboard.xml | 256 ++++++++++++++++++ .../views/fp_menu.xml | 13 + 8 files changed, 856 insertions(+) create mode 100644 fusion_plating/fusion_plating_shopfloor/controllers/manager_controller.py create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/js/manager_dashboard.js create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/scss/manager_dashboard.scss create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/xml/manager_dashboard.xml diff --git a/fusion_plating/fusion_plating_bridge_mrp/models/mrp_workorder.py b/fusion_plating/fusion_plating_bridge_mrp/models/mrp_workorder.py index 60290a8d..3e8a79f4 100644 --- a/fusion_plating/fusion_plating_bridge_mrp/models/mrp_workorder.py +++ b/fusion_plating/fusion_plating_bridge_mrp/models/mrp_workorder.py @@ -53,6 +53,18 @@ class MrpWorkorder(models.Model): related='workcenter_id.costs_hour', readonly=True, ) + # ------------------------------------------------------------------ + # Worker assignment — the Manager Dashboard writes this field; + # the Tablet Station filters "My Queue" by it. + # ------------------------------------------------------------------ + x_fc_assigned_user_id = fields.Many2one( + 'res.users', string='Assigned Worker', + tracking=True, index=True, + help='The operator responsible for this work order. Set by the ' + 'manager; the Tablet Station shows only WOs assigned to the ' + 'logged-in user.', + ) + # ------------------------------------------------------------------ # Workflow step tracking # ------------------------------------------------------------------ diff --git a/fusion_plating/fusion_plating_shopfloor/__manifest__.py b/fusion_plating/fusion_plating_shopfloor/__manifest__.py index 8d2472dc..b022fc00 100644 --- a/fusion_plating/fusion_plating_shopfloor/__manifest__.py +++ b/fusion_plating/fusion_plating_shopfloor/__manifest__.py @@ -60,12 +60,15 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved. 'fusion_plating_shopfloor/static/src/scss/fusion_plating_shopfloor.scss', 'fusion_plating_shopfloor/static/src/scss/plant_overview.scss', 'fusion_plating_shopfloor/static/src/scss/process_tree.scss', + 'fusion_plating_shopfloor/static/src/scss/manager_dashboard.scss', 'fusion_plating_shopfloor/static/src/xml/shopfloor_tablet.xml', 'fusion_plating_shopfloor/static/src/xml/plant_overview.xml', 'fusion_plating_shopfloor/static/src/xml/process_tree.xml', + 'fusion_plating_shopfloor/static/src/xml/manager_dashboard.xml', 'fusion_plating_shopfloor/static/src/js/shopfloor_tablet.js', 'fusion_plating_shopfloor/static/src/js/plant_overview.js', 'fusion_plating_shopfloor/static/src/js/process_tree.js', + 'fusion_plating_shopfloor/static/src/js/manager_dashboard.js', ], }, 'installable': True, diff --git a/fusion_plating/fusion_plating_shopfloor/controllers/__init__.py b/fusion_plating/fusion_plating_shopfloor/controllers/__init__.py index 29934c50..007ab933 100644 --- a/fusion_plating/fusion_plating_shopfloor/controllers/__init__.py +++ b/fusion_plating/fusion_plating_shopfloor/controllers/__init__.py @@ -3,3 +3,4 @@ # License OPL-1 (Odoo Proprietary License v1.0) from . import shopfloor_controller +from . import manager_controller diff --git a/fusion_plating/fusion_plating_shopfloor/controllers/manager_controller.py b/fusion_plating/fusion_plating_shopfloor/controllers/manager_controller.py new file mode 100644 index 00000000..795fb85a --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/controllers/manager_controller.py @@ -0,0 +1,232 @@ +# -*- coding: utf-8 -*- +# Copyright 2026 Nexa Systems Inc. +# License OPL-1 (Odoo Proprietary License v1.0) +# Part of the Fusion Plating product family. +"""JSON-RPC endpoints for the Manager Dashboard (client action).""" + +import logging +from odoo import http +from odoo.http import request + +_logger = logging.getLogger(__name__) + + +class FpManagerDashboardController(http.Controller): + """Manager-level view: unassigned jobs, in-progress jobs, team workload. + + All endpoints require the user to be a manager or above. The UI locks + the menu behind group_fusion_plating_manager. + """ + + # ------------------------------------------------------------------ + # Overview snapshot — used on initial load + 30s auto-refresh + # ------------------------------------------------------------------ + @http.route('/fp/manager/overview', type='jsonrpc', auth='user') + def overview(self, facility_id=None): + env = request.env + MrpWO = env.get('mrp.workorder') + Production = env.get('mrp.production') + if MrpWO is None or Production is None: + return { + 'ok': True, + 'kpis': {'unassigned_wos': 0, 'active_wos': 0, + 'ready_to_ship_mos': 0, 'pending_accept_sos': 0}, + 'unassigned': [], 'active': [], 'team': [], + 'operators': [], 'tanks': [], + 'user_name': env.user.name, + 'mrp_missing': True, + } + # The assignment field lives in fusion_plating_bridge_mrp. If it's + # missing, the dashboard still renders but the worker pickers are + # effectively read-only. + has_assign = 'x_fc_assigned_user_id' in MrpWO._fields + + # ---- Column 1: Unassigned (no worker on an active WO) ---------- + domain_unassigned = [ + ('state', 'in', ('pending', 'waiting', 'ready', 'progress')), + ] + if has_assign: + domain_unassigned.append(('x_fc_assigned_user_id', '=', False)) + else: + # Without the assignment field, treat ALL active WOs as unassigned + pass + if facility_id: + domain_unassigned.append( + ('workcenter_id.x_fc_facility_id', '=', int(facility_id))) + unassigned_wos = MrpWO.search(domain_unassigned, order='sequence, id') + + # Roll up to MO level + def _group_by_mo(wos): + groups = {} + for wo in wos: + mo_id = wo.production_id.id + groups.setdefault(mo_id, []).append(wo) + return groups + + def _mo_card(mo, wos): + so_name = mo.origin or '' + partner = mo.x_fc_portal_job_id.partner_id if mo.x_fc_portal_job_id else None + return { + 'mo_id': mo.id, + 'mo_name': mo.name, + 'so_name': so_name, + 'customer': partner.name if partner else '', + 'product': mo.product_id.display_name if mo.product_id else '', + 'qty_total': int(mo.product_qty or 0), + 'date_planned': str(mo.date_start)[:10] if mo.date_start else '', + 'recipe': mo.x_fc_recipe_id.name if mo.x_fc_recipe_id else '', + 'priority_any': max( + [int(w.x_fc_priority or '0') for w in wos] + [0] + ), + 'current_location': mo.x_fc_current_location or '', + 'wos': [ + { + 'id': w.id, + 'name': w.display_name or w.name, + 'workcenter': w.workcenter_id.name or '', + 'state': w.state, + 'sequence': w.sequence or 0, + 'duration_expected': w.duration_expected or 0, + 'bath': w.x_fc_bath_id.name or '', + 'tank': w.x_fc_tank_id.name or '', + 'priority': w.x_fc_priority or '0', + 'assigned_user_id': ( + w.x_fc_assigned_user_id.id + if w.x_fc_assigned_user_id else False + ), + 'assigned_user_name': ( + w.x_fc_assigned_user_id.name or '' + if w.x_fc_assigned_user_id else '' + ), + } + for w in wos + ], + } + + unassigned_cards = [] + for mo_id, wos in _group_by_mo(unassigned_wos).items(): + mo = Production.browse(mo_id) + unassigned_cards.append(_mo_card(mo, wos)) + + # ---- Column 2: In Progress (MOs with at least one active WO) ---- + domain_active = [ + ('state', 'in', ('ready', 'progress')), + ] + if has_assign: + domain_active.append(('x_fc_assigned_user_id', '!=', False)) + if facility_id: + domain_active.append( + ('workcenter_id.x_fc_facility_id', '=', int(facility_id))) + active_wos = MrpWO.search(domain_active, order='sequence, id') + active_cards = [] + for mo_id, wos in _group_by_mo(active_wos).items(): + mo = Production.browse(mo_id) + active_cards.append(_mo_card(mo, wos)) + + # ---- Column 3: Team (operators + their current load) ----------- + operator_group = env.ref( + 'fusion_plating.group_fusion_plating_operator', raise_if_not_found=False, + ) + team = [] + if operator_group and has_assign: + for user in operator_group.user_ids.sorted('name'): + open_wos = MrpWO.search([ + ('x_fc_assigned_user_id', '=', user.id), + ('state', 'in', ('ready', 'progress', 'waiting')), + ]) + team.append({ + 'user_id': user.id, + 'name': user.name, + 'open_count': len(open_wos), + 'in_progress_count': len( + open_wos.filtered(lambda w: w.state == 'progress') + ), + 'avatar_url': f'/web/image/res.users/{user.id}/avatar_128', + }) + + # ---- Pickers: operators, tanks, work centres ------------------ + operators = [ + {'id': u.id, 'name': u.name} + for u in (operator_group.user_ids if operator_group else env['res.users']) + ] + Tank = env.get('fusion.plating.tank') + tanks = [ + { + 'id': t.id, + 'name': t.name, + 'state': t.state, + 'current_bath': t.current_bath_id.name or '', + } + for t in (Tank.search([]) if Tank is not None else []) + ] + + # KPI summary + kpis = { + 'unassigned_wos': MrpWO.search_count(domain_unassigned), + 'active_wos': MrpWO.search_count(domain_active), + 'ready_to_ship_mos': Production.search_count([ + ('state', '=', 'done'), + ]) if 'x_fc_portal_job_id' not in Production._fields + else Production.search_count([ + ('state', '=', 'done'), + ('x_fc_portal_job_id.state', '=', 'ready_to_ship'), + ]), + 'pending_accept_sos': env['sale.order'].search_count( + [('x_fc_workflow_stage', '=', 'assign_work')] + ) if 'x_fc_workflow_stage' in env['sale.order']._fields else 0, + } + + return { + 'ok': True, + 'kpis': kpis, + 'unassigned': unassigned_cards, + 'active': active_cards, + 'team': team, + 'operators': operators, + 'tanks': tanks, + 'user_name': env.user.name, + } + + # ------------------------------------------------------------------ + # Assign a worker to a WO + # ------------------------------------------------------------------ + @http.route('/fp/manager/assign_worker', type='jsonrpc', auth='user') + def assign_worker(self, workorder_id, user_id): + wo = request.env['mrp.workorder'].browse(int(workorder_id)) + if not wo.exists(): + return {'ok': False, 'error': 'Work order not found.'} + wo.x_fc_assigned_user_id = int(user_id) if user_id else False + wo.message_post( + body=f'Worker assigned: {wo.x_fc_assigned_user_id.name or "Unassigned"}', + ) + return {'ok': True, 'user_name': wo.x_fc_assigned_user_id.name or ''} + + # ------------------------------------------------------------------ + # Reassign or swap tank on a WO + # ------------------------------------------------------------------ + @http.route('/fp/manager/assign_tank', type='jsonrpc', auth='user') + def assign_tank(self, workorder_id, tank_id): + wo = request.env['mrp.workorder'].browse(int(workorder_id)) + if not wo.exists(): + return {'ok': False, 'error': 'Work order not found.'} + wo.x_fc_tank_id = int(tank_id) if tank_id else False + wo.message_post( + body=f'Tank assigned: {wo.x_fc_tank_id.name or "Unassigned"}', + ) + return {'ok': True, 'tank_name': wo.x_fc_tank_id.name or ''} + + # ------------------------------------------------------------------ + # Manager takes over a WO (no-show coverage) + # ------------------------------------------------------------------ + @http.route('/fp/manager/take_over', type='jsonrpc', auth='user') + def take_over(self, workorder_id): + wo = request.env['mrp.workorder'].browse(int(workorder_id)) + if not wo.exists(): + return {'ok': False, 'error': 'Work order not found.'} + user = request.env.user + previous = wo.x_fc_assigned_user_id.name or '—' + wo.x_fc_assigned_user_id = user.id + wo.message_post( + body=f'Manager takeover: {user.name} replaces {previous}.', + ) + return {'ok': True, 'user_name': user.name} diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/js/manager_dashboard.js b/fusion_plating/fusion_plating_shopfloor/static/src/js/manager_dashboard.js new file mode 100644 index 00000000..d338d00e --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/js/manager_dashboard.js @@ -0,0 +1,152 @@ +/** @odoo-module **/ +// ============================================================================= +// Fusion Plating — Manager Dashboard (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. +// ============================================================================= + +import { Component, useState, onMounted, onWillUnmount } from "@odoo/owl"; +import { registry } from "@web/core/registry"; +import { rpc } from "@web/core/network/rpc"; +import { useService } from "@web/core/utils/hooks"; + +export class ManagerDashboard extends Component { + static template = "fusion_plating_shopfloor.ManagerDashboard"; + static props = ["*"]; + + setup() { + this.notification = useService("notification"); + this.action = useService("action"); + + this.state = useState({ + overview: null, + loading: false, + mode: "quick", // quick | detailed + expandedMoId: null, + message: "", + messageType: "info", + }); + + onMounted(async () => { + await this.refresh(); + this._interval = setInterval(() => this.refresh(), 30000); + }); + + onWillUnmount(() => { + if (this._interval) clearInterval(this._interval); + }); + } + + async refresh() { + try { + const payload = await rpc("/fp/manager/overview", {}); + if (payload && payload.ok) { + this.state.overview = payload; + } + } catch (err) { + // silent — next tick will retry + } + } + + setMessage(text, type = "info") { + this.state.message = text; + this.state.messageType = type; + setTimeout(() => { this.state.message = ""; }, 4000); + } + + toggleMode() { + this.state.mode = this.state.mode === "quick" ? "detailed" : "quick"; + } + + toggleCard(moId) { + this.state.expandedMoId = this.state.expandedMoId === moId ? null : moId; + } + + // ---------------------------------------------------------- Actions + async onAssignWorker(wo, userIdRaw) { + const userId = parseInt(userIdRaw) || null; + try { + const res = await rpc("/fp/manager/assign_worker", { + workorder_id: wo.id, user_id: userId, + }); + if (res && res.ok) { + this.setMessage( + `Assigned ${res.user_name || 'unassigned'} to ${wo.name}`, + "success", + ); + } + } catch (err) { + this.setMessage(`Assign failed: ${err.message || err}`, "danger"); + } + await this.refresh(); + } + + async onAssignTank(wo, tankIdRaw) { + const tankId = parseInt(tankIdRaw) || null; + try { + const res = await rpc("/fp/manager/assign_tank", { + workorder_id: wo.id, tank_id: tankId, + }); + if (res && res.ok) { + this.setMessage( + `Tank ${res.tank_name || 'cleared'} for ${wo.name}`, + "success", + ); + } + } catch (err) { + this.setMessage(`Tank swap failed: ${err.message || err}`, "danger"); + } + await this.refresh(); + } + + async onTakeOver(wo) { + try { + const res = await rpc("/fp/manager/take_over", { + workorder_id: wo.id, + }); + if (res && res.ok) { + this.setMessage(`You now own ${wo.name}.`, "success"); + } + } catch (err) { + this.setMessage(`Takeover failed: ${err.message || err}`, "danger"); + } + await this.refresh(); + } + + openRecord(model, id) { + this.action.doAction({ + type: "ir.actions.act_window", + res_model: model, + res_id: id, + views: [[false, "form"]], + target: "current", + }); + } + + openOperatorQueue(userId) { + this.action.doAction({ + type: "ir.actions.act_window", + name: "Operator Queue", + res_model: "mrp.workorder", + views: [[false, "list"], [false, "form"]], + domain: [ + ["x_fc_assigned_user_id", "=", userId], + ["state", "in", ["ready", "progress", "waiting"]], + ], + target: "current", + }); + } + + priorityLabel(p) { + return ({'0': 'Normal', '1': 'Urgent', '2': 'Hot'})[p] || 'Normal'; + } + + priorityTone(p) { + return ({'0': 'muted', '1': 'warning', '2': 'danger'})[p] || 'muted'; + } +} + +registry.category("actions").add("fp_manager_dashboard", ManagerDashboard); diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/scss/manager_dashboard.scss b/fusion_plating/fusion_plating_shopfloor/static/src/scss/manager_dashboard.scss new file mode 100644 index 00000000..63715753 --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/scss/manager_dashboard.scss @@ -0,0 +1,187 @@ +// ============================================================================= +// Fusion Plating — Manager Dashboard styles +// Copyright 2026 Nexa Systems Inc. +// License OPL-1 (Odoo Proprietary License v1.0) +// +// Inherits the tablet's theme variables (--bs-*, --o-action) so it flips +// light/dark without media queries. Shares .o_fp_kpi, .o_fp_chip, +// .o_fp_panel, .o_fp_empty, .o_fp_tablet_message from the tablet SCSS. +// ============================================================================= + +.o_fp_manager { + background-color: var(--o-view-background-color, var(--bs-body-bg)); + color: var(--bs-body-color); + min-height: 100%; + padding: 20px 24px; + display: flex; + flex-direction: column; + gap: 14px; + + .o_fp_manager_header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; + flex-wrap: wrap; + } + .o_fp_manager_title { + font-size: 1.5rem; + font-weight: 600; + } + .o_fp_manager_subtitle { + font-size: 0.95rem; + color: var(--bs-secondary-color); + } + + // 3-column grid: Unassigned | In Progress | Team + .o_fp_manager_grid { + display: grid; + grid-template-columns: minmax(0, 1.2fr) minmax(0, 1.2fr) minmax(0, 0.8fr); + gap: 14px; + } + @media (max-width: 1280px) { + .o_fp_manager_grid { + grid-template-columns: 1fr 1fr; + } + .o_fp_panel_team { grid-column: span 2; } + } + @media (max-width: 800px) { + .o_fp_manager_grid { grid-template-columns: 1fr; } + .o_fp_panel_team { grid-column: auto; } + } + + .o_fp_panel_unassigned { + border-left: 4px solid var(--bs-warning); + } + .o_fp_panel_active { + border-left: 4px solid var(--bs-success); + } + .o_fp_panel_team { + border-left: 4px solid var(--bs-info); + } + + .o_fp_mgr_card_list { + display: flex; + flex-direction: column; + gap: 10px; + } + .o_fp_mgr_card { + border: 1px solid var(--bs-border-color); + border-radius: 10px; + transition: border-color 120ms ease, box-shadow 120ms ease; + + &[data-priority="2"] { + border-left: 4px solid var(--bs-danger); + background-color: color-mix(in srgb, var(--bs-danger) 4%, transparent); + } + &[data-priority="1"] { + border-left: 4px solid var(--bs-warning); + } + + &:hover { + border-color: color-mix(in srgb, var(--o-action) 40%, var(--bs-border-color)); + } + } + .o_fp_mgr_card_head { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 14px; + cursor: pointer; + gap: 10px; + } + .o_fp_mgr_card_title { + font-weight: 600; + font-size: 1rem; + } + .o_fp_mgr_card_sub { + color: var(--bs-secondary-color); + font-size: 0.88rem; + margin-top: 2px; + } + .o_fp_mgr_card_chips { + display: flex; + gap: 6px; + flex-wrap: wrap; + } + .o_fp_mgr_card_body { + border-top: 1px dashed var(--bs-border-color); + padding: 10px 14px; + display: flex; + flex-direction: column; + gap: 8px; + background-color: color-mix(in srgb, var(--bs-body-color) 2%, transparent); + border-bottom-left-radius: 10px; + border-bottom-right-radius: 10px; + } + .o_fp_mgr_wo_row { + display: grid; + grid-template-columns: 1fr auto auto auto auto; + gap: 8px; + align-items: center; + padding: 6px 0; + font-size: 0.92rem; + border-bottom: 1px dashed color-mix(in srgb, var(--bs-body-color) 6%, transparent); + + &:last-child { border-bottom: 0; } + } + @media (max-width: 1400px) { + .o_fp_mgr_wo_row { + grid-template-columns: 1fr auto auto; + + .o_fp_mgr_picker:nth-of-type(2) { grid-column: span 3; } + } + } + .o_fp_mgr_wo_info { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .o_fp_mgr_picker { + min-width: 120px; + max-width: 180px; + } + + // Team grid + .o_fp_team_grid { + display: grid; + grid-template-columns: 1fr; + gap: 8px; + } + .o_fp_team_card { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 12px; + border: 1px solid var(--bs-border-color); + border-radius: 10px; + cursor: pointer; + transition: border-color 120ms ease, background-color 120ms ease; + + &:hover { + border-color: color-mix(in srgb, var(--o-action) 40%, var(--bs-border-color)); + background-color: color-mix(in srgb, var(--o-action) 6%, transparent); + } + } + .o_fp_team_avatar { + width: 44px; + height: 44px; + border-radius: 50%; + object-fit: cover; + border: 2px solid var(--bs-border-color); + } + .o_fp_team_info { + flex: 1; + min-width: 0; + } + .o_fp_team_name { + font-weight: 600; + font-size: 0.95rem; + } + .o_fp_team_load { + display: flex; + gap: 6px; + margin-top: 4px; + } +} diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/xml/manager_dashboard.xml b/fusion_plating/fusion_plating_shopfloor/static/src/xml/manager_dashboard.xml new file mode 100644 index 00000000..6dcab6a0 --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/xml/manager_dashboard.xml @@ -0,0 +1,256 @@ + + + + + +
+ + +
+
+
+ Manager Desk +
+
+ · Updated just now +
+
+
+ +
+
+ + +
+ +
+ + +
+
+ +
+
Unassigned WOs
+
+
+ +
+
In Progress
+
+
+ +
+
Ready to Ship
+
+
+ +
+
Awaiting Assignment
+
+
+ + +
+ + +
+
+

Needs a Worker

+ +
+
+ + Every active WO has a worker assigned. +
+
+ +
+
+
+
+ + · +
+
+ + · + · Qty + + · + +
+
+
+ HOT + Urgent + + WO + +
+
+ +
+ +
+
+ + + + · + +
+ + + + +
+
+
+
+
+
+
+ + +
+
+

In Progress

+ +
+
+ Nothing running right now. +
+
+ +
+
+
+
+ + · +
+
+ + · +
+
+
+ HOT + + WO + +
+
+
+ +
+
+ + + + + · + + + +
+ + + + + +
+
+
+
+
+
+
+ + +
+
+

Team

+ +
+
+ No operators configured. +
+
+ +
+ +
+
+
+ + running + + + queue + +
+
+
+
+
+
+
+ +
+ Loading manager data… +
+
+
+ +
diff --git a/fusion_plating/fusion_plating_shopfloor/views/fp_menu.xml b/fusion_plating/fusion_plating_shopfloor/views/fp_menu.xml index 88e2d6bd..56e06e87 100644 --- a/fusion_plating/fusion_plating_shopfloor/views/fp_menu.xml +++ b/fusion_plating/fusion_plating_shopfloor/views/fp_menu.xml @@ -13,6 +13,19 @@ sequence="12" groups="fusion_plating.group_fusion_plating_operator"/> + + + Manager Desk + fp_manager_dashboard + + + +