From 5df7d5e6cf4b7015bbf6ec8904982ecf9260953e Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sat, 25 Apr 2026 06:45:15 -0400 Subject: [PATCH] refactor(shopfloor,jobs): consolidate operator UI into shopfloor Removes the parallel OWL/controller stack I built in fusion_plating_jobs (job_process_tree, job_plant_overview, job_manager_dashboard, job_tablet, job_*.scss, plus parallel controllers and action XML files). Refactors the existing fusion_plating_shopfloor components in place to bind to fp.job / fp.job.step instead of mrp.production / mrp.workorder. End state: - ONE operator UI module (shopfloor) instead of two parallel ones - Existing token system (_fp_shopfloor_tokens.scss) reused as designed - no duplicate jobs tokens - Existing /fp/shopfloor/* RPC URLs preserved (no integration breakage); workorder_id kwargs accepted as legacy aliases for step_id / job_id so older tablet clients keep working - Existing visual designs preserved - only the data layer underneath changed - Process Tree button on fp.job form now points at fusion_plating_shopfloor's fp_process_tree client action - Bake Windows / First-Piece Gates / Bake Oven / Operator Queue models stay where they were - legacy_menu_hide.xml trimmed: only the bridge_mrp Production Priorities entry remains; the 3 shopfloor menus (Manager Desk, Plant Overview, Tablet Station) are now visible (the canonical native consoles) Manifests: - fusion_plating_jobs 19.0.3.1.0 -> 19.0.4.0.0 (consolidation bump, no more bundled JS/SCSS, only job_scan controller retained) - fusion_plating_shopfloor 19.0.14.4.0 -> 19.0.15.0.0 (asset bundle cache-bust + significant controller refactor) Tests pass on entech: 0 failed, 0 errors of 41 tests. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../fusion_plating_jobs/__manifest__.py | 36 +- .../controllers/__init__.py | 7 +- .../controllers/manager_dashboard.py | 66 - .../controllers/plant_overview.py | 98 -- .../controllers/process_tree.py | 48 - .../fusion_plating_jobs/controllers/tablet.py | 188 --- .../fusion_plating_jobs/models/fp_job.py | 14 +- .../static/src/js/job_manager_dashboard.js | 183 --- .../static/src/js/job_plant_overview.js | 323 ----- .../static/src/js/job_process_tree.js | 207 --- .../static/src/js/job_tablet.js | 322 ----- .../static/src/scss/_fp_jobs_tokens.scss | 234 --- .../src/scss/job_manager_dashboard.scss | 291 ---- .../static/src/scss/job_plant_overview.scss | 321 ----- .../static/src/scss/job_process_tree.scss | 390 ----- .../static/src/scss/job_tablet.scss | 606 -------- .../static/src/xml/job_manager_dashboard.xml | 154 -- .../static/src/xml/job_plant_overview.xml | 163 --- .../static/src/xml/job_process_tree.xml | 122 -- .../static/src/xml/job_tablet.xml | 325 ----- .../tests/test_fp_job_extensions.py | 20 +- .../views/fp_job_form_inherit.xml | 5 +- .../views/job_overview_actions.xml | 36 - .../views/job_process_tree_action.xml | 12 - .../views/job_tablet_action.xml | 17 - .../views/legacy_menu_hide.xml | 28 +- .../fusion_plating_shopfloor/__manifest__.py | 2 +- .../controllers/manager_controller.py | 409 +++--- .../controllers/shopfloor_controller.py | 1272 ++++++++--------- .../static/src/js/manager_dashboard.js | 10 +- .../static/src/js/plant_overview.js | 27 +- .../static/src/js/process_tree.js | 64 +- .../static/src/js/shopfloor_tablet.js | 5 + .../static/src/xml/manager_dashboard.xml | 6 +- .../static/src/xml/process_tree.xml | 2 +- .../static/src/xml/shopfloor_tablet.xml | 6 +- 36 files changed, 891 insertions(+), 5128 deletions(-) delete mode 100644 fusion_plating/fusion_plating_jobs/controllers/manager_dashboard.py delete mode 100644 fusion_plating/fusion_plating_jobs/controllers/plant_overview.py delete mode 100644 fusion_plating/fusion_plating_jobs/controllers/process_tree.py delete mode 100644 fusion_plating/fusion_plating_jobs/controllers/tablet.py delete mode 100644 fusion_plating/fusion_plating_jobs/static/src/js/job_manager_dashboard.js delete mode 100644 fusion_plating/fusion_plating_jobs/static/src/js/job_plant_overview.js delete mode 100644 fusion_plating/fusion_plating_jobs/static/src/js/job_process_tree.js delete mode 100644 fusion_plating/fusion_plating_jobs/static/src/js/job_tablet.js delete mode 100644 fusion_plating/fusion_plating_jobs/static/src/scss/_fp_jobs_tokens.scss delete mode 100644 fusion_plating/fusion_plating_jobs/static/src/scss/job_manager_dashboard.scss delete mode 100644 fusion_plating/fusion_plating_jobs/static/src/scss/job_plant_overview.scss delete mode 100644 fusion_plating/fusion_plating_jobs/static/src/scss/job_process_tree.scss delete mode 100644 fusion_plating/fusion_plating_jobs/static/src/scss/job_tablet.scss delete mode 100644 fusion_plating/fusion_plating_jobs/static/src/xml/job_manager_dashboard.xml delete mode 100644 fusion_plating/fusion_plating_jobs/static/src/xml/job_plant_overview.xml delete mode 100644 fusion_plating/fusion_plating_jobs/static/src/xml/job_process_tree.xml delete mode 100644 fusion_plating/fusion_plating_jobs/static/src/xml/job_tablet.xml delete mode 100644 fusion_plating/fusion_plating_jobs/views/job_overview_actions.xml delete mode 100644 fusion_plating/fusion_plating_jobs/views/job_process_tree_action.xml delete mode 100644 fusion_plating/fusion_plating_jobs/views/job_tablet_action.xml diff --git a/fusion_plating/fusion_plating_jobs/__manifest__.py b/fusion_plating/fusion_plating_jobs/__manifest__.py index 654ae07b..39f42ea1 100644 --- a/fusion_plating/fusion_plating_jobs/__manifest__.py +++ b/fusion_plating/fusion_plating_jobs/__manifest__.py @@ -3,7 +3,7 @@ # License OPL-1 (Odoo Proprietary License v1.0) { 'name': 'Fusion Plating — Native Jobs', - 'version': '19.0.3.1.0', + 'version': '19.0.4.0.0', 'category': 'Manufacturing/Plating', 'summary': 'Native plating job model — replaces mrp.production / mrp.workorder bridge.', 'author': 'Nexa Systems Inc.', @@ -25,6 +25,13 @@ Activate native jobs via the x_fc_use_native_jobs settings flag (default: False). When False, SO confirm continues to create mrp.production records through bridge_mrp. When True, SO confirm creates fp.job records here. +19.0.4.0.0 (2026-04-24): Operator UI consolidation. The parallel +OWL/controller stack (job_process_tree, job_plant_overview, +job_manager_dashboard, job_tablet) was removed. The canonical +operator-facing UIs now live in fusion_plating_shopfloor and bind +directly to fp.job / fp.job.step. This module retains lifecycle hooks, +SO → fp.job creation, reports, and the QR-scan redirect. + See docs/superpowers/specs/2026-04-25-fp-native-job-model-design.md for full design rationale and §6.2 of the implementation plan for task list. """, @@ -40,15 +47,12 @@ full design rationale and §6.2 of the implementation plan for task list. 'fusion_plating_quality', # fusion.plating.customer.spec, fusion.plating.quality.hold 'fusion_plating_receiving', # fp.racking.inspection (Phase 3) 'fusion_plating_reports', # paperformat helpers, customer_line_header (Phase 5) - 'fusion_plating_shopfloor', # legacy menus restricted in views/legacy_menu_hide.xml + 'fusion_plating_shopfloor', # canonical operator UI consoles ], 'data': [ 'security/legacy_groups.xml', 'security/ir.model.access.csv', 'views/res_config_settings_views.xml', - 'views/job_process_tree_action.xml', - 'views/job_overview_actions.xml', - 'views/job_tablet_action.xml', 'views/fp_job_form_inherit.xml', 'views/legacy_menu_hide.xml', 'report/report_fp_job_sticker.xml', @@ -56,26 +60,8 @@ full design rationale and §6.2 of the implementation plan for task list. 'report/report_fp_job_margin.xml', ], 'assets': { - 'web.assets_backend': [ - # Tokens MUST be first — Odoo concatenates bundle files in - # order, and SCSS variables defined in earlier files are - # visible to later files in the same bundle. The token - # partial branches on $o-webclient-color-scheme so the dark - # bundle (web.assets_web_dark) gets a distinct palette. - 'fusion_plating_jobs/static/src/scss/_fp_jobs_tokens.scss', - 'fusion_plating_jobs/static/src/scss/job_process_tree.scss', - 'fusion_plating_jobs/static/src/scss/job_plant_overview.scss', - 'fusion_plating_jobs/static/src/scss/job_manager_dashboard.scss', - 'fusion_plating_jobs/static/src/scss/job_tablet.scss', - 'fusion_plating_jobs/static/src/js/job_process_tree.js', - 'fusion_plating_jobs/static/src/js/job_plant_overview.js', - 'fusion_plating_jobs/static/src/js/job_manager_dashboard.js', - 'fusion_plating_jobs/static/src/js/job_tablet.js', - 'fusion_plating_jobs/static/src/xml/job_process_tree.xml', - 'fusion_plating_jobs/static/src/xml/job_plant_overview.xml', - 'fusion_plating_jobs/static/src/xml/job_manager_dashboard.xml', - 'fusion_plating_jobs/static/src/xml/job_tablet.xml', - ], + # No bundled JS/SCSS — the canonical operator UIs live in + # fusion_plating_shopfloor (consolidated 2026-04-24). }, 'installable': True, 'application': False, diff --git a/fusion_plating/fusion_plating_jobs/controllers/__init__.py b/fusion_plating/fusion_plating_jobs/controllers/__init__.py index 44e28286..24fad823 100644 --- a/fusion_plating/fusion_plating_jobs/controllers/__init__.py +++ b/fusion_plating/fusion_plating_jobs/controllers/__init__.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- +# Consolidated 2026-04-24: the parallel OWL/controller stack was +# removed. job_scan is the only controller retained — it powers the +# QR-sticker scan redirect for fp.job records. from . import job_scan -from . import process_tree -from . import plant_overview -from . import manager_dashboard -from . import tablet diff --git a/fusion_plating/fusion_plating_jobs/controllers/manager_dashboard.py b/fusion_plating/fusion_plating_jobs/controllers/manager_dashboard.py deleted file mode 100644 index 3ec15473..00000000 --- a/fusion_plating/fusion_plating_jobs/controllers/manager_dashboard.py +++ /dev/null @@ -1,66 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2026 Nexa Systems Inc. -# License OPL-1 (Odoo Proprietary License v1.0) -# -# /fp/jobs/manager_dashboard — JSON endpoint powering the native-job -# Manager Dashboard. Returns a flat list of in-flight fp.job rows -# with progress / current-step / deadline info, plus state-count -# pills for the filter bar at the top of the dashboard. - -from odoo import http -from odoo.http import request - - -class FpJobsManagerDashboardController(http.Controller): - - @http.route('/fp/jobs/manager_dashboard', type='jsonrpc', auth='user', website=False) - def fp_jobs_manager_dashboard(self, state=None, **kwargs): - env = request.env - Job = env['fp.job'] - - # Default view: jobs that need triage. Specifying state= - # narrows to that one bucket; state='all' opens the floodgates. - if state and state != 'all': - domain = [('state', '=', state)] - elif state == 'all': - domain = [] - else: - domain = [('state', 'in', ('confirmed', 'in_progress', 'on_hold'))] - - jobs = Job.search( - domain, - order='priority desc, date_deadline asc, id desc', - limit=200, - ) - - rows = [] - for job in jobs: - rows.append({ - 'id': job.id, - 'name': job.name, - 'partner': job.partner_id.name or '', - 'qty': job.qty, - 'state': job.state, - 'priority': job.priority, - 'date_deadline': ( - job.date_deadline.isoformat() - if job.date_deadline else None - ), - 'current_step': ( - job.current_step_id.name - if job.current_step_id else None - ), - 'current_location': job.current_location, - 'progress_pct': job.step_progress_pct, - 'step_done': job.step_done_count, - 'step_total': job.step_count, - 'recipe': job.recipe_id.name if job.recipe_id else None, - }) - - # State-count pills for the filter bar — let the dashboard show - # the manager how big each bucket is at a glance. - counts = {} - for s in ('confirmed', 'in_progress', 'on_hold', 'done'): - counts[s] = Job.search_count([('state', '=', s)]) - - return {'rows': rows, 'counts': counts} diff --git a/fusion_plating/fusion_plating_jobs/controllers/plant_overview.py b/fusion_plating/fusion_plating_jobs/controllers/plant_overview.py deleted file mode 100644 index 985858ff..00000000 --- a/fusion_plating/fusion_plating_jobs/controllers/plant_overview.py +++ /dev/null @@ -1,98 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2026 Nexa Systems Inc. -# License OPL-1 (Odoo Proprietary License v1.0) -# -# /fp/jobs/plant_overview — JSON endpoints powering the native-job -# Plant Overview kanban (operator triage view, Phase 6 of the native -# job migration). Columns are fp.work.centre rows; cards are -# fp.job.step rows in ready / in_progress / paused state. Drag a -# card across columns to reassign that step's work_centre_id. - -from odoo import http -from odoo.http import request - - -class FpJobsPlantOverviewController(http.Controller): - - @http.route('/fp/jobs/plant_overview', type='jsonrpc', auth='user', website=False) - def fp_jobs_plant_overview(self, facility_id=None, **kwargs): - env = request.env - WorkCentre = env['fp.work.centre'] - Step = env['fp.job.step'] - - wc_domain = [('active', '=', True)] - if facility_id: - wc_domain.append(('facility_id', '=', int(facility_id))) - centres = WorkCentre.search(wc_domain, order='sequence, code, name') - - # Active steps grouped by work_centre. We pull paused too so a - # manager can see — and re-route — a step that's been paused - # on the wrong line. - step_domain = [('state', 'in', ('ready', 'in_progress', 'paused'))] - if facility_id: - step_domain.append(('facility_id', '=', int(facility_id))) - active_steps = Step.search(step_domain, order='job_id, sequence') - - cards_by_wc = {} - for step in active_steps: - wc_id = step.work_centre_id.id or 0 - cards_by_wc.setdefault(wc_id, []).append({ - 'id': step.id, - 'name': step.name, - 'state': step.state, - 'job_id': step.job_id.id, - 'job_name': step.job_id.name, - 'partner': step.job_id.partner_id.name or '', - 'sequence': step.sequence, - 'kind': step.kind, - 'duration_expected': step.duration_expected, - 'duration_actual': step.duration_actual, - 'assigned_user': ( - step.assigned_user_id.name - if step.assigned_user_id else None - ), - 'thickness_target': step.thickness_target, - 'thickness_uom': step.thickness_uom, - 'priority': step.job_id.priority, - }) - - columns = [] - for wc in centres: - columns.append({ - 'id': wc.id, - 'code': wc.code, - 'name': wc.name, - 'kind': wc.kind, - 'facility': wc.facility_id.name if wc.facility_id else None, - 'cards': cards_by_wc.get(wc.id, []), - }) - # An "Unassigned" pseudo-column for steps without a work centre — - # only rendered when there's something to show, so empty plants - # don't pick up a stray column. - if cards_by_wc.get(0): - columns.append({ - 'id': 0, - 'code': '—', - 'name': 'Unassigned', - 'kind': 'other', - 'facility': None, - 'cards': cards_by_wc[0], - }) - - return {'columns': columns} - - @http.route('/fp/jobs/plant_overview/move_card', type='jsonrpc', auth='user', website=False) - def fp_jobs_move_card(self, step_id, work_centre_id, **kwargs): - """Reassign a step to a different work centre. - - work_centre_id == 0 (or falsy) clears the work centre — the card - will land in the Unassigned pseudo-column on the next refresh. - """ - env = request.env - Step = env['fp.job.step'] - step = Step.browse(int(step_id)).exists() - if not step: - return {'ok': False, 'error': 'Step not found'} - wc_id = int(work_centre_id) if work_centre_id else False - step.work_centre_id = wc_id - return {'ok': True} diff --git a/fusion_plating/fusion_plating_jobs/controllers/process_tree.py b/fusion_plating/fusion_plating_jobs/controllers/process_tree.py deleted file mode 100644 index 7653cb46..00000000 --- a/fusion_plating/fusion_plating_jobs/controllers/process_tree.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2026 Nexa Systems Inc. -# License OPL-1 (Odoo Proprietary License v1.0) -# -# /fp/jobs/process_tree — JSON endpoint that returns the recipe tree -# for a given fp.job, with each node tagged by the matching -# fp.job.step (if any) and its current state. - -from odoo import http -from odoo.http import request - - -class FpJobProcessTreeController(http.Controller): - - @http.route('/fp/jobs/process_tree', type='jsonrpc', auth='user', website=False) - def fp_jobs_process_tree(self, job_id, **kwargs): - Job = request.env['fp.job'] - job = Job.browse(int(job_id)).exists() - if not job: - return {'error': 'Job not found'} - - # Map recipe_node_id -> step - step_by_node = {s.recipe_node_id.id: s for s in job.step_ids if s.recipe_node_id} - - def serialize(node): - step = step_by_node.get(node.id) - return { - 'id': node.id, - 'name': node.name, - 'node_type': node.node_type, - 'sequence': node.sequence, - 'step_id': step.id if step else None, - 'step_state': step.state if step else None, - 'step_assigned_user': step.assigned_user_id.name if step and step.assigned_user_id else None, - 'duration_expected': step.duration_expected if step else node.estimated_duration, - 'duration_actual': step.duration_actual if step else 0.0, - 'children': [serialize(c) for c in node.child_ids.sorted('sequence')], - } - - return { - 'job_name': job.name, - 'partner': job.partner_id.name, - 'state': job.state, - 'qty': job.qty, - 'recipe_name': job.recipe_id.name if job.recipe_id else '', - 'progress_pct': job.step_progress_pct, - 'tree': serialize(job.recipe_id) if job.recipe_id else None, - } diff --git a/fusion_plating/fusion_plating_jobs/controllers/tablet.py b/fusion_plating/fusion_plating_jobs/controllers/tablet.py deleted file mode 100644 index f41c2671..00000000 --- a/fusion_plating/fusion_plating_jobs/controllers/tablet.py +++ /dev/null @@ -1,188 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright 2026 Nexa Systems Inc. -# License OPL-1 (Odoo Proprietary License v1.0) -# -# /fp/jobs/tablet/* — JSON-RPC endpoints powering the native-job -# Tablet Station (Phase 6 of the native job migration). Operator- -# facing touchscreen UI for starting/finishing fp.job.step rows. -# -# Endpoints: -# POST /fp/jobs/tablet/jobs -> active jobs the operator can pick -# POST /fp/jobs/tablet/job_detail -> job header + ordered step list -# POST /fp/jobs/tablet/start_step -> calls fp.job.step.button_start -# POST /fp/jobs/tablet/finish_step -> calls fp.job.step.button_finish -# -# All write paths funnel through the model's button_start / button_finish -# methods so the audit / timelog / duration_actual roll-up logic from -# Phase 1 still applies. - -from odoo import http -from odoo.http import request - - -class FpJobsTabletController(http.Controller): - - @http.route('/fp/jobs/tablet/jobs', type='jsonrpc', auth='user', website=False) - def fp_jobs_tablet_jobs(self, facility_id=None, **kwargs): - """Active jobs the operator can pick from.""" - env = request.env - Job = env['fp.job'] - domain = [('state', 'in', ('confirmed', 'in_progress'))] - if facility_id: - domain.append(('facility_id', '=', int(facility_id))) - jobs = Job.search( - domain, - order='priority desc, date_deadline asc, id desc', - limit=50, - ) - return { - 'jobs': [{ - 'id': j.id, - 'name': j.name, - 'partner': j.partner_id.name or '', - 'qty': j.qty, - 'progress_pct': j.step_progress_pct, - 'state': j.state, - 'priority': j.priority, - 'current_step': ( - j.current_step_id.name if j.current_step_id else None - ), - 'deadline': ( - j.date_deadline.isoformat() if j.date_deadline else None - ), - } for j in jobs], - } - - @http.route('/fp/jobs/tablet/job_detail', type='jsonrpc', auth='user', website=False) - def fp_jobs_tablet_job_detail(self, job_id, **kwargs): - """Job header + ordered step list for the detail panel.""" - env = request.env - Job = env['fp.job'] - job = Job.browse(int(job_id)).exists() - if not job: - return {'error': 'Job not found'} - steps = [] - for step in job.step_ids.sorted('sequence'): - steps.append({ - 'id': step.id, - 'name': step.name, - 'sequence': step.sequence, - 'state': step.state, - 'kind': step.kind, - 'work_centre': ( - step.work_centre_id.name if step.work_centre_id else None - ), - 'duration_expected': step.duration_expected, - 'duration_actual': step.duration_actual, - 'thickness_target': step.thickness_target, - 'thickness_uom': step.thickness_uom, - 'assigned_user': ( - step.assigned_user_id.name - if step.assigned_user_id else None - ), - 'date_started': ( - step.date_started.isoformat() if step.date_started else None - ), - 'date_finished': ( - step.date_finished.isoformat() if step.date_finished else None - ), - }) - return { - 'id': job.id, - 'name': job.name, - 'partner': job.partner_id.name or '', - 'qty': job.qty, - 'state': job.state, - 'priority': job.priority, - 'recipe': job.recipe_id.name if job.recipe_id else None, - 'progress_pct': job.step_progress_pct, - 'step_done': job.step_done_count, - 'step_total': job.step_count, - 'steps': steps, - } - - @http.route('/fp/jobs/tablet/step_detail', type='jsonrpc', auth='user', website=False) - def fp_jobs_tablet_step_detail(self, step_id, **kwargs): - """Step detail panel — used to refresh after button_start / - button_finish so the timelog history pulls in the new row. - """ - env = request.env - step = env['fp.job.step'].browse(int(step_id)).exists() - if not step: - return {'error': 'Step not found'} - timelogs = [] - for log in step.time_log_ids.sorted('date_started', reverse=True): - timelogs.append({ - 'id': log.id, - 'user': log.user_id.name or '', - 'date_started': ( - log.date_started.isoformat() if log.date_started else None - ), - 'date_finished': ( - log.date_finished.isoformat() if log.date_finished else None - ), - 'duration_minutes': log.duration_minutes, - }) - return { - 'id': step.id, - 'name': step.name, - 'sequence': step.sequence, - 'state': step.state, - 'kind': step.kind, - 'work_centre': ( - step.work_centre_id.name if step.work_centre_id else None - ), - 'duration_expected': step.duration_expected, - 'duration_actual': step.duration_actual, - 'thickness_target': step.thickness_target, - 'thickness_uom': step.thickness_uom, - 'assigned_user': ( - step.assigned_user_id.name - if step.assigned_user_id else None - ), - 'date_started': ( - step.date_started.isoformat() if step.date_started else None - ), - 'date_finished': ( - step.date_finished.isoformat() if step.date_finished else None - ), - 'instructions': step.instructions or '', - 'timelogs': timelogs, - } - - @http.route('/fp/jobs/tablet/start_step', type='jsonrpc', auth='user', website=False) - def fp_jobs_tablet_start_step(self, step_id, **kwargs): - env = request.env - step = env['fp.job.step'].browse(int(step_id)).exists() - if not step: - return {'ok': False, 'error': 'Step not found'} - try: - step.button_start() - return { - 'ok': True, - 'state': step.state, - 'date_started': ( - step.date_started.isoformat() if step.date_started else None - ), - } - except Exception as e: - return {'ok': False, 'error': str(e)} - - @http.route('/fp/jobs/tablet/finish_step', type='jsonrpc', auth='user', website=False) - def fp_jobs_tablet_finish_step(self, step_id, **kwargs): - env = request.env - step = env['fp.job.step'].browse(int(step_id)).exists() - if not step: - return {'ok': False, 'error': 'Step not found'} - try: - step.button_finish() - return { - 'ok': True, - 'state': step.state, - 'duration_actual': step.duration_actual, - 'date_finished': ( - step.date_finished.isoformat() if step.date_finished else None - ), - } - except Exception as e: - return {'ok': False, 'error': str(e)} diff --git a/fusion_plating/fusion_plating_jobs/models/fp_job.py b/fusion_plating/fusion_plating_jobs/models/fp_job.py index 5e054f51..0b2fb519 100644 --- a/fusion_plating/fusion_plating_jobs/models/fp_job.py +++ b/fusion_plating/fusion_plating_jobs/models/fp_job.py @@ -264,15 +264,19 @@ class FpJob(models.Model): def action_open_process_tree(self): """Open the OWL process-tree visualization for this job. - Launches the fp_job_process_tree client action with job_id in - context. The component fetches /fp/jobs/process_tree and renders - the recipe -> sub_process -> operation hierarchy as cards with - per-step state badges. + Launches the fp_process_tree client action (defined in + fusion_plating_shopfloor) with job_id in context. The component + fetches /fp/shopfloor/process_tree and renders the recipe -> + sub_process -> operation hierarchy as cards with per-step state + badges. + + Consolidated 2026-04-24: this points at the canonical shopfloor + client action; the parallel fp_job_process_tree was removed. """ self.ensure_one() return { 'type': 'ir.actions.client', - 'tag': 'fp_job_process_tree', + 'tag': 'fp_process_tree', 'context': {'job_id': self.id}, 'name': 'Process Tree — %s' % (self.name or ''), 'target': 'current', diff --git a/fusion_plating/fusion_plating_jobs/static/src/js/job_manager_dashboard.js b/fusion_plating/fusion_plating_jobs/static/src/js/job_manager_dashboard.js deleted file mode 100644 index 6771b0ce..00000000 --- a/fusion_plating/fusion_plating_jobs/static/src/js/job_manager_dashboard.js +++ /dev/null @@ -1,183 +0,0 @@ -/** @odoo-module **/ -// ============================================================================= -// Fusion Plating — Manager Dashboard (native, fp.job edition) -// Copyright 2026 Nexa Systems Inc. -// License OPL-1 (Odoo Proprietary License v1.0) -// -// Manager triage view for the native job model. Renders all in-flight -// fp.job rows with progress bars, deadline, current-step location, and -// a priority side-bar (rush/high/normal/low). Click a row to open the -// job form. State-count pills filter the grid by state. -// -// Endpoint: POST /fp/jobs/manager_dashboard -> { rows, counts } -// ============================================================================= - -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 JobManagerDashboard extends Component { - static template = "fusion_plating_jobs.JobManagerDashboard"; - static props = ["*"]; - - setup() { - this.notification = useService("notification"); - this.action = useService("action"); - - this.state = useState({ - rows: [], - counts: {}, - stateFilter: null, // null = default in-flight; 'all' = no filter - loading: false, - lastRefresh: null, - }); - - this._refreshInterval = null; - - onMounted(async () => { - await this.loadData(); - // 30s cadence — same as plant overview, light enough to - // leave the dashboard up on a wall display. - this._refreshInterval = setInterval(() => this.loadData(), 30000); - }); - - onWillUnmount(() => { - if (this._refreshInterval) { - clearInterval(this._refreshInterval); - this._refreshInterval = null; - } - }); - } - - // ----- Data -------------------------------------------------------------- - - async loadData() { - this.state.loading = true; - try { - const payload = {}; - if (this.state.stateFilter) { - payload.state = this.state.stateFilter; - } - const result = await rpc("/fp/jobs/manager_dashboard", payload); - if (result) { - this.state.rows = result.rows || []; - this.state.counts = result.counts || {}; - this.state.lastRefresh = new Date().toLocaleTimeString(); - } - } catch (err) { - this.notification.add( - `Failed to load manager dashboard: ${err.message || err}`, - { type: "danger" }, - ); - } finally { - this.state.loading = false; - } - } - - onRefresh() { - this.loadData(); - } - - // ----- Filter pills ------------------------------------------------------ - - setFilter(state) { - // Clicking the active pill clears the filter back to default. - this.state.stateFilter = (state === this.state.stateFilter) ? null : state; - this.loadData(); - } - - isActiveFilter(state) { - return this.state.stateFilter === state; - } - - // ----- Row click --------------------------------------------------------- - - openJob(row) { - if (!row || !row.id) return; - this.action.doAction({ - type: "ir.actions.act_window", - res_model: "fp.job", - res_id: row.id, - views: [[false, "form"]], - target: "current", - }); - } - - // ----- Helpers ----------------------------------------------------------- - - priorityClass(p) { - switch (p) { - case "rush": return "o_fp_jmd_priority_rush"; - case "high": return "o_fp_jmd_priority_high"; - case "low": return "o_fp_jmd_priority_low"; - default: return "o_fp_jmd_priority_normal"; - } - } - - priorityLabel(p) { - switch (p) { - case "rush": return "RUSH"; - case "high": return "High"; - case "low": return "Low"; - default: return "Normal"; - } - } - - stateLabel(s) { - const map = { - draft: "Draft", - confirmed: "Confirmed", - in_progress: "In Progress", - on_hold: "On Hold", - done: "Done", - cancelled: "Cancelled", - }; - return map[s] || s || ""; - } - - stateBadgeClass(s) { - return `o_fp_jmd_state_badge_${s}`; - } - - progressLabel(row) { - const pct = (row.progress_pct || 0).toFixed(0); - const done = row.step_done || 0; - const total = row.step_total || 0; - return `${pct}% (${done}/${total})`; - } - - progressBarClass(row) { - const pct = row.progress_pct || 0; - if (pct >= 100) return "o_fp_jmd_bar_done"; - if (pct >= 50) return "o_fp_jmd_bar_mid"; - return "o_fp_jmd_bar_early"; - } - - deadlineLabel(row) { - if (!row.date_deadline) return ""; - // Render as a short, human-friendly date — strip seconds. - try { - const d = new Date(row.date_deadline); - if (isNaN(d.getTime())) return row.date_deadline; - return d.toLocaleDateString(undefined, { - year: "numeric", month: "short", day: "numeric", - }); - } catch (e) { - return row.date_deadline; - } - } - - isOverdue(row) { - if (!row.date_deadline) return false; - try { - const d = new Date(row.date_deadline); - return !isNaN(d.getTime()) && d.getTime() < Date.now() - && row.state !== "done"; - } catch (e) { - return false; - } - } -} - -registry.category("actions").add("fp_job_manager_dashboard", JobManagerDashboard); diff --git a/fusion_plating/fusion_plating_jobs/static/src/js/job_plant_overview.js b/fusion_plating/fusion_plating_jobs/static/src/js/job_plant_overview.js deleted file mode 100644 index 004d43c0..00000000 --- a/fusion_plating/fusion_plating_jobs/static/src/js/job_plant_overview.js +++ /dev/null @@ -1,323 +0,0 @@ -/** @odoo-module **/ -// ============================================================================= -// Fusion Plating — Plant Overview (native, fp.job.step edition) -// Copyright 2026 Nexa Systems Inc. -// License OPL-1 (Odoo Proprietary License v1.0) -// -// Operator triage kanban for the native job model. Columns are -// fp.work.centre rows, cards are active fp.job.step rows. Drag a card -// to a different column to reassign that step's work_centre_id; click -// a card to open the step form. -// -// Port of fusion_plating_shopfloor's plant_overview.js, rebound from -// mrp.workorder + mrp.production to fp.job.step + fp.job. Auto-refresh -// every 30s, debounced search, drag-drop with placeholder preview. -// -// Endpoints (fusion_plating_jobs/controllers/plant_overview.py): -// POST /fp/jobs/plant_overview -> { columns: [...] } -// POST /fp/jobs/plant_overview/move_card -> { ok, error? } -// ============================================================================= - -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 JobPlantOverview extends Component { - static template = "fusion_plating_jobs.JobPlantOverview"; - static props = ["*"]; - - setup() { - this.notification = useService("notification"); - this.action = useService("action"); - - this.state = useState({ - columns: [], - searchTerm: "", - loading: false, - lastRefresh: null, - }); - - this._refreshInterval = null; - this._draggedCard = null; - - onMounted(async () => { - await this.loadData(); - // 30s cadence — fast enough for a manager glancing at the - // wall, light enough to not hammer the server. - this._refreshInterval = setInterval(() => this.loadData(), 30000); - }); - - onWillUnmount(() => { - if (this._refreshInterval) { - clearInterval(this._refreshInterval); - this._refreshInterval = null; - } - }); - } - - // ----- Data -------------------------------------------------------------- - - async loadData() { - this.state.loading = true; - try { - const result = await rpc("/fp/jobs/plant_overview", {}); - if (result) { - let columns = result.columns || []; - // Client-side search — keeps the round-trip simple. - const term = (this.state.searchTerm || "").trim().toLowerCase(); - if (term) { - columns = columns - .map((col) => ({ - ...col, - cards: (col.cards || []).filter((c) => { - const hay = [ - c.name, c.job_name, c.partner, - c.assigned_user || "", - ].join(" ").toLowerCase(); - return hay.includes(term); - }), - })) - // Hide empty columns when filtering so the wall - // doesn't flood with "Clear" placeholders. - .filter((col) => col.cards.length > 0); - } - this.state.columns = columns; - this.state.lastRefresh = new Date().toLocaleTimeString(); - } - } catch (err) { - this.notification.add( - `Failed to load plant overview: ${err.message || err}`, - { type: "danger" }, - ); - } finally { - this.state.loading = false; - } - } - - // ----- Search ------------------------------------------------------------ - - onSearchInput(ev) { - this.state.searchTerm = ev.target.value; - this._debouncedSearch(); - } - - _debouncedSearch() { - if (this._searchTimer) clearTimeout(this._searchTimer); - this._searchTimer = setTimeout(() => this.loadData(), 200); - } - - onSearchKey(ev) { - if (ev.key === "Enter") { - if (this._searchTimer) clearTimeout(this._searchTimer); - this.loadData(); - } else if (ev.key === "Escape") { - this.onSearchClear(); - } - } - - onSearchClear() { - if (this._searchTimer) clearTimeout(this._searchTimer); - this.state.searchTerm = ""; - this.loadData(); - } - - onRefresh() { - this.loadData(); - } - - // ----- Drag & drop ------------------------------------------------------- - // - // A real insertion placeholder slides between cards as the operator - // drags. Plain DOM nodes (not reactive state) so mouseover updates - // don't trigger OWL re-renders mid-drag. - - _getOrCreatePlaceholder() { - let node = document.querySelector(".o_fp_jpo_drop_placeholder"); - if (!node) { - node = document.createElement("div"); - node.className = "o_fp_jpo_drop_placeholder"; - } - return node; - } - - _removePlaceholder() { - document.querySelectorAll(".o_fp_jpo_drop_placeholder") - .forEach((el) => el.remove()); - } - - onCardDragStart(card, col, ev) { - this._draggedCard = { - id: card.id, - source_wc_id: col.id, - el: ev.target, - }; - ev.dataTransfer.effectAllowed = "move"; - ev.dataTransfer.setData("text/plain", String(card.id)); - // Apply the ghost class on the next frame so the drag image - // captures the card opaque. - requestAnimationFrame(() => { - if (ev.target && ev.target.classList) { - ev.target.classList.add("o_fp_dragging"); - } - }); - } - - onCardDragEnd(ev) { - if (ev.target && ev.target.classList) { - ev.target.classList.remove("o_fp_dragging"); - } - document.querySelectorAll(".o_fp_drop_target").forEach((el) => { - el.classList.remove("o_fp_drop_target"); - }); - this._removePlaceholder(); - this._draggedCard = null; - } - - onColDragOver(col, ev) { - ev.preventDefault(); - ev.dataTransfer.dropEffect = "move"; - const body = ev.currentTarget; - if (!body) return; - if (!body.classList.contains("o_fp_drop_target")) { - body.classList.add("o_fp_drop_target"); - } - - // Find which card the cursor is closest to and slide the - // placeholder above or below it. This gives the manager a - // clear "card will land HERE" preview between siblings. - const placeholder = this._getOrCreatePlaceholder(); - const cards = [...body.querySelectorAll( - ".o_fp_jpo_card:not(.o_fp_dragging):not(.o_fp_jpo_drop_placeholder)", - )]; - const y = ev.clientY; - let insertBefore = null; - for (const cardEl of cards) { - const rect = cardEl.getBoundingClientRect(); - if (y < rect.top + rect.height / 2) { - insertBefore = cardEl; - break; - } - } - if (insertBefore) { - body.insertBefore(placeholder, insertBefore); - } else { - body.appendChild(placeholder); - } - } - - onColDragLeave(col, ev) { - const body = ev.currentTarget; - if (body && !body.contains(ev.relatedTarget)) { - body.classList.remove("o_fp_drop_target"); - this._removePlaceholder(); - } - } - - async onColDrop(col, ev) { - ev.preventDefault(); - const body = ev.currentTarget; - if (body) { - body.classList.remove("o_fp_drop_target"); - } - this._removePlaceholder(); - - const dragged = this._draggedCard; - if (!dragged) { - return; - } - // No-op if dropped on the same column - if (dragged.source_wc_id === col.id) { - this._draggedCard = null; - return; - } - - try { - const result = await rpc("/fp/jobs/plant_overview/move_card", { - step_id: dragged.id, - work_centre_id: col.id || 0, - }); - if (result && result.ok) { - this.notification.add( - `Moved to ${col.name}`, - { type: "success" }, - ); - await this.loadData(); - } else { - this.notification.add( - (result && result.error) || "Could not move card", - { type: "warning" }, - ); - } - } catch (err) { - this.notification.add( - `Move failed: ${err.message || err}`, - { type: "danger" }, - ); - } - this._draggedCard = null; - } - - // ----- Card actions ------------------------------------------------------ - - onCardClick(card) { - if (!card || !card.id) { - return; - } - this.action.doAction({ - type: "ir.actions.act_window", - res_model: "fp.job.step", - res_id: card.id, - views: [[false, "form"]], - target: "current", - }); - } - - onJobLink(card, ev) { - // Stop the parent card click from also firing. - if (ev) { - ev.stopPropagation(); - } - if (!card || !card.job_id) { - return; - } - this.action.doAction({ - type: "ir.actions.act_window", - res_model: "fp.job", - res_id: card.job_id, - views: [[false, "form"]], - target: "current", - }); - } - - // ----- Helpers ----------------------------------------------------------- - - getStateClass(state) { - switch (state) { - case "in_progress": return "o_fp_jpo_card_progress"; - case "ready": return "o_fp_jpo_card_ready"; - case "paused": return "o_fp_jpo_card_paused"; - case "done": return "o_fp_jpo_card_done"; - default: return ""; - } - } - - getPriorityClass(p) { - switch (p) { - case "rush": return "o_fp_jpo_card_rush"; - case "high": return "o_fp_jpo_card_high"; - default: return ""; - } - } - - durationLabel(card) { - const exp = card.duration_expected; - const act = card.duration_actual; - if (act && exp) return `${act.toFixed(0)}/${exp.toFixed(0)} min`; - if (exp) return `${exp.toFixed(0)} min`; - if (act) return `${act.toFixed(0)} min`; - return ""; - } -} - -registry.category("actions").add("fp_job_plant_overview", JobPlantOverview); diff --git a/fusion_plating/fusion_plating_jobs/static/src/js/job_process_tree.js b/fusion_plating/fusion_plating_jobs/static/src/js/job_process_tree.js deleted file mode 100644 index bed06eca..00000000 --- a/fusion_plating/fusion_plating_jobs/static/src/js/job_process_tree.js +++ /dev/null @@ -1,207 +0,0 @@ -/** @odoo-module **/ -// ============================================================================= -// Fusion Plating — Job Process Tree (horizontal hierarchical view, fp.job) -// Copyright 2026 Nexa Systems Inc. · License OPL-1 -// -// Renders an fp.job's recipe (recipe → sub_process → operation) as a -// horizontal bracket tree, port of fusion_plating_shopfloor's process_tree.js -// rebound to fp.job + fp.job.step (instead of mrp.production + mrp.workorder). -// -// Action context: -// job_id — required; the fp.job whose recipe to render -// back_step_id — optional; if set, the back button returns to that step -// instead of the job form -// -// Endpoint: POST /fp/jobs/process_tree (fusion_plating_jobs/controllers) -// payload : { job_id: } -// response : { job_name, partner, state, qty, recipe_name, progress_pct, -// tree: { id, name, node_type, sequence, -// step_id, step_state, step_assigned_user, -// duration_expected, duration_actual, children: [...] } } -// ============================================================================= - -import { Component, useState, onMounted } 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 JobProcessTree extends Component { - static template = "fusion_plating_jobs.JobProcessTree"; - static props = ["*"]; - - setup() { - this.notification = useService("notification"); - this.action = useService("action"); - - this.state = useState({ - jobName: "", - partner: "", - jobState: "", - qty: 0, - recipe: "", - progressPct: 0, - root: null, - loading: false, - }); - - onMounted(async () => { - await this.loadTree(); - }); - } - - // ---- Action context ----------------------------------------------------- - - get _ctx() { - const a = this.props.action || {}; - return { ...(a.context || {}), ...(a.params || {}) }; - } - get jobId() { return this._ctx.job_id || null; } - get backStepId() { return this._ctx.back_step_id || null; } - get backLabel() { - return this.backStepId ? "Back to Step" : "Back to Job"; - } - - // ---- Data --------------------------------------------------------------- - - async loadTree() { - const jobId = this.jobId; - if (!jobId) { - this.notification.add( - "No job specified for the process tree.", - { type: "warning" }, - ); - return; - } - this.state.loading = true; - try { - const r = await rpc("/fp/jobs/process_tree", { - job_id: jobId, - }); - if (r && !r.error) { - this.state.jobName = r.job_name || ""; - this.state.partner = r.partner || ""; - this.state.jobState = r.state || ""; - this.state.qty = r.qty || 0; - this.state.recipe = r.recipe_name || ""; - this.state.progressPct = r.progress_pct || 0; - this.state.root = r.tree || null; - } else if (r && r.error) { - this.notification.add(r.error, { type: "warning" }); - } - } catch (err) { - this.notification.add( - `Failed to load process tree: ${err.message || err}`, - { type: "danger" }, - ); - } finally { - this.state.loading = false; - } - } - - // ---- Navigation --------------------------------------------------------- - - onNodeClick(node) { - // Only operation cards with a matching fp.job.step are clickable — - // they open the underlying step form. - if (!node || !node.step_id) { - return; - } - this.action.doAction({ - type: "ir.actions.act_window", - res_model: "fp.job.step", - res_id: node.step_id, - views: [[false, "form"]], - target: "current", - }); - } - - onBack() { - const stepId = this.backStepId; - if (stepId) { - this.action.doAction({ - type: "ir.actions.act_window", - res_model: "fp.job.step", - res_id: parseInt(stepId, 10), - views: [[false, "form"]], - target: "current", - }); - return; - } - // Default back: open the job form. - const jobId = this.jobId; - if (jobId) { - this.action.doAction({ - type: "ir.actions.act_window", - res_model: "fp.job", - res_id: parseInt(jobId, 10), - views: [[false, "form"]], - target: "current", - }); - return; - } - // Fallback — pop the stack. - this.action.doAction({ type: "ir.actions.act_window_close" }); - } - - // ---- Helpers ------------------------------------------------------------ - - /** Return the css class chain for a node card (state + node_type). */ - getCardClass(node) { - const parts = ["o_fp_jpt_card"]; - parts.push(`o_fp_jpt_type_${node.node_type || "unknown"}`); - if (node.step_state) { - parts.push(`o_fp_jpt_state_${node.step_state}`); - } - if (node.step_id) { - parts.push("o_fp_jpt_clickable"); - } - if (this.isHighlight(node)) { - parts.push("o_fp_jpt_highlight"); - } - return parts.join(" "); - } - - /** Highlight steps that are live (ready / in_progress / paused). */ - isHighlight(node) { - return node.step_state === "ready" - || node.step_state === "in_progress" - || node.step_state === "paused"; - } - - /** Friendly label for the step state badge. */ - stateLabel(node) { - if (!node.step_state) return null; - const map = { - pending: "Pending", - ready: "Ready", - in_progress: "In Progress", - paused: "Paused", - done: "Done", - skipped: "Skipped", - cancelled: "Cancelled", - }; - return map[node.step_state] || node.step_state; - } - - /** Concise duration label: "actual / expected min" when available. */ - durationLabel(node) { - const exp = node.duration_expected; - const act = node.duration_actual; - if (act && exp) return `${act.toFixed(0)}/${exp.toFixed(0)} min`; - if (exp) return `${exp.toFixed(0)} min`; - if (act) return `${act.toFixed(0)} min`; - return ""; - } - - nodeIcon(node) { - switch (node.node_type) { - case "recipe": return "fa-cubes"; - case "sub_process": return "fa-folder"; - case "operation": return "fa-cog"; - case "step": return "fa-circle-o"; - default: return "fa-square"; - } - } -} - -registry.category("actions").add("fp_job_process_tree", JobProcessTree); diff --git a/fusion_plating/fusion_plating_jobs/static/src/js/job_tablet.js b/fusion_plating/fusion_plating_jobs/static/src/js/job_tablet.js deleted file mode 100644 index 7c330204..00000000 --- a/fusion_plating/fusion_plating_jobs/static/src/js/job_tablet.js +++ /dev/null @@ -1,322 +0,0 @@ -/** @odoo-module **/ -// ============================================================================= -// Fusion Plating — Tablet Station (native, fp.job.step edition) -// Copyright 2026 Nexa Systems Inc. -// License OPL-1 (Odoo Proprietary License v1.0) -// -// Operator-facing touchscreen UI for the native job model. Three modes: -// - 'job_picker': list of active jobs as big touch cards -// - 'job_detail': job header + step list (tap a step to view it) -// - 'step_detail': big Start / Finish buttons + timelog history -// -// Calls fp.job.step.button_start / button_finish through the tablet -// controller endpoints so the audit / timelog / duration_actual logic -// from Phase 1 is preserved. -// -// Endpoints: -// POST /fp/jobs/tablet/jobs -> { jobs: [...] } -// POST /fp/jobs/tablet/job_detail -> { id, name, ..., steps: [...] } -// POST /fp/jobs/tablet/step_detail -> { id, name, ..., timelogs: [...] } -// POST /fp/jobs/tablet/start_step -> { ok, state, ... } -// POST /fp/jobs/tablet/finish_step -> { ok, state, duration_actual, ... } -// ============================================================================= - -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 JobTablet extends Component { - static template = "fusion_plating_jobs.JobTablet"; - static props = ["*"]; - - setup() { - this.notification = useService("notification"); - this.action = useService("action"); - - this.state = useState({ - // 'job_picker' | 'job_detail' | 'step_detail' - mode: "job_picker", - jobs: [], - job: null, // selected job detail payload - step: null, // selected step detail payload - loading: false, - busy: false, // disables Start/Finish during RPC - lastRefresh: null, - }); - - // Auto-refresh interval — only ticks while we're in job_picker - // mode. job_detail / step_detail are operator-driven so we don't - // surprise them with a UI swap mid-tap. - this._refreshInterval = null; - - onMounted(async () => { - await this.loadJobs(); - this._refreshInterval = setInterval(() => { - if (this.state.mode === "job_picker") { - this.loadJobs(); - } - }, 30000); - }); - - onWillUnmount(() => { - if (this._refreshInterval) { - clearInterval(this._refreshInterval); - this._refreshInterval = null; - } - }); - } - - // ----- Data -------------------------------------------------------------- - - async loadJobs() { - this.state.loading = true; - try { - const result = await rpc("/fp/jobs/tablet/jobs", {}); - if (result) { - this.state.jobs = result.jobs || []; - this.state.lastRefresh = new Date().toLocaleTimeString(); - } - } catch (err) { - this.notification.add( - `Failed to load jobs: ${err.message || err}`, - { type: "danger" }, - ); - } finally { - this.state.loading = false; - } - } - - async loadJobDetail(jobId) { - this.state.loading = true; - try { - const result = await rpc("/fp/jobs/tablet/job_detail", { - job_id: jobId, - }); - if (result && !result.error) { - this.state.job = result; - } else { - this.notification.add( - (result && result.error) || "Could not load job", - { type: "danger" }, - ); - } - } catch (err) { - this.notification.add( - `Failed to load job: ${err.message || err}`, - { type: "danger" }, - ); - } finally { - this.state.loading = false; - } - } - - async loadStepDetail(stepId) { - this.state.loading = true; - try { - const result = await rpc("/fp/jobs/tablet/step_detail", { - step_id: stepId, - }); - if (result && !result.error) { - this.state.step = result; - } else { - this.notification.add( - (result && result.error) || "Could not load step", - { type: "danger" }, - ); - } - } catch (err) { - this.notification.add( - `Failed to load step: ${err.message || err}`, - { type: "danger" }, - ); - } finally { - this.state.loading = false; - } - } - - // ----- Navigation -------------------------------------------------------- - - async onJobPick(job) { - if (!job || !job.id) return; - await this.loadJobDetail(job.id); - if (this.state.job) { - this.state.mode = "job_detail"; - } - } - - async onStepPick(step) { - if (!step || !step.id) return; - await this.loadStepDetail(step.id); - if (this.state.step) { - this.state.mode = "step_detail"; - } - } - - onBackToJobs() { - this.state.mode = "job_picker"; - this.state.job = null; - this.state.step = null; - this.loadJobs(); - } - - onBackToJob() { - this.state.mode = "job_detail"; - this.state.step = null; - // Refresh job detail so the step list shows updated states - if (this.state.job && this.state.job.id) { - this.loadJobDetail(this.state.job.id); - } - } - - onRefresh() { - if (this.state.mode === "job_picker") { - this.loadJobs(); - } else if (this.state.mode === "job_detail" && this.state.job) { - this.loadJobDetail(this.state.job.id); - } else if (this.state.mode === "step_detail" && this.state.step) { - this.loadStepDetail(this.state.step.id); - } - } - - // ----- Step actions ------------------------------------------------------ - - async onStartStep() { - if (!this.state.step || !this.state.step.id || this.state.busy) { - return; - } - this.state.busy = true; - try { - const result = await rpc("/fp/jobs/tablet/start_step", { - step_id: this.state.step.id, - }); - if (result && result.ok) { - this.notification.add("Step started — timer running.", { - type: "success", - }); - await this.loadStepDetail(this.state.step.id); - } else { - this.notification.add( - (result && result.error) || "Could not start step", - { type: "warning" }, - ); - } - } catch (err) { - this.notification.add( - `Start failed: ${err.message || err}`, - { type: "danger" }, - ); - } finally { - this.state.busy = false; - } - } - - async onFinishStep() { - if (!this.state.step || !this.state.step.id || this.state.busy) { - return; - } - this.state.busy = true; - try { - const result = await rpc("/fp/jobs/tablet/finish_step", { - step_id: this.state.step.id, - }); - if (result && result.ok) { - this.notification.add("Step finished.", { type: "success" }); - await this.loadStepDetail(this.state.step.id); - } else { - this.notification.add( - (result && result.error) || "Could not finish step", - { type: "warning" }, - ); - } - } catch (err) { - this.notification.add( - `Finish failed: ${err.message || err}`, - { type: "danger" }, - ); - } finally { - this.state.busy = false; - } - } - - // ----- Helpers ----------------------------------------------------------- - - stateBadgeClass(state) { - // Maps fp.job.step.state -> SCSS class suffix - switch (state) { - case "pending": return "o_fp_jt_badge_pending"; - case "ready": return "o_fp_jt_badge_ready"; - case "in_progress": return "o_fp_jt_badge_progress"; - case "paused": return "o_fp_jt_badge_paused"; - case "done": return "o_fp_jt_badge_done"; - case "skipped": return "o_fp_jt_badge_skipped"; - case "cancelled": return "o_fp_jt_badge_cancelled"; - default: return "o_fp_jt_badge_pending"; - } - } - - stateLabel(state) { - const map = { - pending: "Pending", - ready: "Ready", - in_progress: "In Progress", - paused: "Paused", - done: "Done", - skipped: "Skipped", - cancelled: "Cancelled", - }; - return map[state] || state || ""; - } - - priorityClass(priority) { - switch (priority) { - case "rush": return "o_fp_jt_card_rush"; - case "high": return "o_fp_jt_card_high"; - default: return ""; - } - } - - priorityLabel(priority) { - const map = { low: "Low", normal: "", high: "High", rush: "RUSH" }; - return map[priority] || ""; - } - - canStart(state) { - return state === "ready" || state === "paused"; - } - - canFinish(state) { - return state === "in_progress"; - } - - durationLabel(step) { - const exp = step && step.duration_expected; - const act = step && step.duration_actual; - if (act && exp) return `${act.toFixed(0)} / ${exp.toFixed(0)} min`; - if (exp) return `${exp.toFixed(0)} min`; - if (act) return `${act.toFixed(0)} min`; - return ""; - } - - formatDateTime(isoStr) { - if (!isoStr) return ""; - try { - const d = new Date(isoStr); - return d.toLocaleString(); - } catch (e) { - return isoStr; - } - } - - formatDeadline(isoStr) { - if (!isoStr) return ""; - try { - const d = new Date(isoStr); - return d.toLocaleDateString(); - } catch (e) { - return isoStr; - } - } -} - -registry.category("actions").add("fp_job_tablet", JobTablet); diff --git a/fusion_plating/fusion_plating_jobs/static/src/scss/_fp_jobs_tokens.scss b/fusion_plating/fusion_plating_jobs/static/src/scss/_fp_jobs_tokens.scss deleted file mode 100644 index e4e212a7..00000000 --- a/fusion_plating/fusion_plating_jobs/static/src/scss/_fp_jobs_tokens.scss +++ /dev/null @@ -1,234 +0,0 @@ -// ============================================================================= -// Fusion Plating — Job model design system (v1, 2026-04) -// File: fusion_plating_jobs/static/src/scss/_fp_jobs_tokens.scss -// Copyright 2026 Nexa Systems Inc. · License OPL-1 -// -// Parallels fusion_plating_shopfloor/static/src/scss/_fp_shopfloor_tokens.scss -// — same spacing scale, same radius scale, same compile-time dark-mode -// branching pattern. Lives in fusion_plating_jobs so the new client-action -// SCSS files (job_process_tree, job_plant_overview, job_manager_dashboard, -// job_tablet) can reference these tokens without taking a hard SCSS-level -// dependency on the shopfloor module's bundle ordering. -// -// Design philosophy (same as shopfloor): -// * Three-layer contrast: page (grayest) → container (mid) → card -// (brightest) — that's what makes cards pop in BOTH themes. -// * Every value resolves from compile-time SCSS variables that branch -// on $o-webclient-color-scheme, so light and dark themes get distinct -// palettes without runtime CSS custom-property toggling (which Odoo -// 19 does NOT do for surface colours). -// * Semantic state colours (success/warning/danger/info) reserved for -// STATUS — not decoration. -// ============================================================================= - -// ---------- Spacing scale (8-pt baseline) ------------------------------------ -$fp-space-1 : 4px; -$fp-space-2 : 8px; -$fp-space-3 : 12px; -$fp-space-4 : 16px; -$fp-space-5 : 20px; -$fp-space-6 : 24px; -$fp-space-7 : 32px; -$fp-space-8 : 40px; -$fp-space-9 : 48px; -$fp-space-10 : 64px; - -// ---------- Radius ----------------------------------------------------------- -$fp-radius-sm : 10px; -$fp-radius-md : 14px; -$fp-radius-lg : 20px; -$fp-radius-xl : 28px; -$fp-radius-pill: 999px; - -// ---------- Surfaces — COMPILE-TIME branch on Odoo's dark scheme ------------- -// -// Odoo 19 compiles TWO asset bundles: web.assets_backend (light) and -// web.assets_web_dark (dark). The two bundles differ only in the value -// of the SCSS variable $o-webclient-color-scheme — `bright` for light, -// `dark` for dark (defined in primary_variables.scss / -// primary_variables.dark.scss in web_enterprise). -// -// Odoo does NOT redefine --bs-body-bg / --bs-card-bg as CSS custom -// properties at runtime. It bakes the chosen palette into the bundle -// at compile time via Bootstrap SCSS variables. So our tokens must do -// the same: branch on $o-webclient-color-scheme at compile time and -// emit the right hex values into each bundle. - -$o-webclient-color-scheme: bright !default; - -// Default (light / bright) palette -$_fp-page-hex : #f3f4f6; -$_fp-card-hex : #ffffff; -$_fp-card-soft-hex : #f1f3f5; -$_fp-border-hex : #d8dadd; -$_fp-border-strong-hex : #b6babf; -$_fp-ink-hex : #1f2937; -$_fp-ink-soft-hex : #4b5563; -$_fp-ink-mute-hex : #6b7280; -$_fp-ink-faint-hex : #9ca3af; - -// Dark palette — engaged when the dark bundle is compiled -@if $o-webclient-color-scheme == dark { - $_fp-page-hex : #1a1d21 !global; - $_fp-card-hex : #22262d !global; - $_fp-card-soft-hex : #1c2027 !global; - $_fp-border-hex : #343942 !global; - $_fp-border-strong-hex : #4a505a !global; - $_fp-ink-hex : #e5e7eb !global; - $_fp-ink-soft-hex : #c8ccd2 !global; - $_fp-ink-mute-hex : #8a909a !global; - $_fp-ink-faint-hex : #5a606b !global; -} - -// Public tokens — CSS custom property fallback chain remains so a -// deployment can still override via --fp-* without touching SCSS. -$fp-page : var(--fp-page-bg, $_fp-page-hex); -$fp-card : var(--fp-card-bg, $_fp-card-hex); -$fp-card-soft : var(--fp-card-soft-bg, $_fp-card-soft-hex); -$fp-border : var(--fp-border-color, $_fp-border-hex); -$fp-border-strong : var(--fp-border-strong, $_fp-border-strong-hex); -$fp-ink : var(--fp-ink, $_fp-ink-hex); -$fp-ink-soft : var(--fp-ink-soft, $_fp-ink-soft-hex); -$fp-ink-mute : var(--fp-ink-mute, $_fp-ink-mute-hex); -$fp-ink-faint : var(--fp-ink-faint, $_fp-ink-faint-hex); - -// Action colour — Odoo's primary. Same in both bundles (brand purple). -$fp-accent : var(--o-action, #714B67); - -// ---------- Elevation — explicit rgba shadows -------------------------------- -// Explicit rgba values (not color-mix) so they render identically across -// browsers and themes. In dark mode the shadows still work against the -// darker surfaces because they're translucent. -$fp-elev-1 : 0 1px 2px rgba(0, 0, 0, 0.06), - 0 1px 3px rgba(0, 0, 0, 0.08); -$fp-elev-2 : 0 2px 4px rgba(0, 0, 0, 0.06), - 0 6px 14px rgba(0, 0, 0, 0.10); -$fp-elev-3 : 0 4px 8px rgba(0, 0, 0, 0.10), - 0 12px 28px rgba(0, 0, 0, 0.14); -$fp-elev-hover : 0 6px 12px rgba(0, 0, 0, 0.12), - 0 18px 36px rgba(0, 0, 0, 0.16); - -// ---------- Semantic colour helpers ------------------------------------------ -$fp-ok : var(--bs-success, #28a745); -$fp-warn : var(--bs-warning, #ffc107); -$fp-bad : var(--bs-danger, #dc3545); -$fp-info : var(--bs-info, #17a2b8); - -// State-colour hexes (used directly for badges / borders / chips so the -// rendering doesn't depend on Bootstrap variable presence). Different -// hexes per scheme keep contrast crisp on both backgrounds. -$_fp-state-ready-hex : #ffc107; -$_fp-state-ready-text-hex : #b58105; -$_fp-state-progress-hex : #0d6efd; -$_fp-state-progress-text-hex : #084298; -$_fp-state-paused-hex : #fd7e14; -$_fp-state-paused-text-hex : #97480d; -$_fp-state-done-hex : #198754; -$_fp-state-done-text-hex : #0f5132; -$_fp-state-cancel-hex : #dc3545; -$_fp-state-cancel-text-hex : #842029; -$_fp-state-rush-hex : #dc3545; -$_fp-state-high-hex : #fd7e14; -$_fp-state-low-hex : #6c757d; -$_fp-state-pending-bg-hex : #e9ecef; -$_fp-state-pending-text-hex : #6c757d; - -@if $o-webclient-color-scheme == dark { - // Slightly brighter / desaturated for legibility against the dark - // card surface ($_fp-card-hex = #22262d). - $_fp-state-ready-hex : #ffd866 !global; - $_fp-state-ready-text-hex : #ffd866 !global; - $_fp-state-progress-hex : #6ea8fe !global; - $_fp-state-progress-text-hex : #6ea8fe !global; - $_fp-state-paused-hex : #ffb86b !global; - $_fp-state-paused-text-hex : #ffb86b !global; - $_fp-state-done-hex : #75d4a4 !global; - $_fp-state-done-text-hex : #75d4a4 !global; - $_fp-state-cancel-hex : #f1aeb5 !global; - $_fp-state-cancel-text-hex : #f1aeb5 !global; - $_fp-state-rush-hex : #e85d6c !global; - $_fp-state-high-hex : #ff9a4d !global; - $_fp-state-low-hex : #8a909a !global; - $_fp-state-pending-bg-hex : #2a2f37 !global; - $_fp-state-pending-text-hex : #c8ccd2 !global; -} - -$fp-state-ready : $_fp-state-ready-hex; -$fp-state-ready-text : $_fp-state-ready-text-hex; -$fp-state-progress : $_fp-state-progress-hex; -$fp-state-progress-text : $_fp-state-progress-text-hex; -$fp-state-paused : $_fp-state-paused-hex; -$fp-state-paused-text : $_fp-state-paused-text-hex; -$fp-state-done : $_fp-state-done-hex; -$fp-state-done-text : $_fp-state-done-text-hex; -$fp-state-cancel : $_fp-state-cancel-hex; -$fp-state-cancel-text : $_fp-state-cancel-text-hex; -$fp-state-rush : $_fp-state-rush-hex; -$fp-state-high : $_fp-state-high-hex; -$fp-state-low : $_fp-state-low-hex; -$fp-state-pending-bg : $_fp-state-pending-bg-hex; -$fp-state-pending-text : $_fp-state-pending-text-hex; - -// Softened backgrounds for status pills / banners -@function fp-wash($color-var, $strength: 12%) { - @return color-mix(in srgb, var(#{$color-var}) #{$strength}, transparent); -} - -// ---------- Type scale ------------------------------------------------------ -$fp-text-xs : 0.75rem; // 12px small labels -$fp-text-sm : 0.875rem; // 14px helper text -$fp-text-base : 1rem; // 16px body -$fp-text-md : 1.125rem; // 18px emphasis -$fp-text-lg : 1.25rem; // 20px sub-headings -$fp-text-xl : 1.5rem; // 24px section headings -$fp-text-2xl : 2rem; // 32px page title -$fp-text-3xl : 2.75rem; // 44px KPI number -$fp-text-4xl : clamp(2rem, 5vw, 3rem); // hero - -$fp-weight-medium : 500; -$fp-weight-semibold : 600; -$fp-weight-bold : 700; - -$fp-font-stack : -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, - "Inter", "Helvetica Neue", Arial, sans-serif; - -// ---------- Motion ----------------------------------------------------------- -$fp-ease : cubic-bezier(0.22, 1, 0.36, 1); -$fp-ease-out : cubic-bezier(0.33, 1, 0.68, 1); -$fp-dur-fast : 120ms; -$fp-dur : 200ms; -$fp-dur-slow : 360ms; - -// ---------- Touch ------------------------------------------------------------ -$fp-touch-min : 48px; // larger than Apple's 44px minimum — shop floor - - -// ============================================================================= -// Mixins -// ============================================================================= - -// Focus ring — used on all interactive inputs/buttons -@mixin fp-focus-ring { - outline: none; - box-shadow: 0 0 0 3px color-mix(in srgb, #{$fp-accent} 35%, transparent); -} - -// Card surface — shadow-based, no border -@mixin fp-card($elev: $fp-elev-1) { - background-color: $fp-card; - border-radius: $fp-radius-lg; - box-shadow: $elev; -} - -// Status pill (soft tint + colored text) -@mixin fp-pill($color-var) { - background-color: color-mix(in srgb, var(#{$color-var}) 14%, transparent); - color: var(#{$color-var}); -} - -// Hide hover styles on touch devices (stuck hover = bad UX on phones) -@mixin fp-hover-only { - @media (hover: hover) { - @content; - } -} diff --git a/fusion_plating/fusion_plating_jobs/static/src/scss/job_manager_dashboard.scss b/fusion_plating/fusion_plating_jobs/static/src/scss/job_manager_dashboard.scss deleted file mode 100644 index b09f2f9c..00000000 --- a/fusion_plating/fusion_plating_jobs/static/src/scss/job_manager_dashboard.scss +++ /dev/null @@ -1,291 +0,0 @@ -// ============================================================================= -// Fusion Plating — Manager Dashboard (native, fp.job) -// Copyright 2026 Nexa Systems Inc. · License OPL-1 -// -// Class prefix: .o_fp_jmd_* (Job Manager Dashboard) -// -// Theme-aware: every surface, border and text colour resolves through -// the design tokens defined in _fp_jobs_tokens.scss. -// -// Three-layer contrast: -// page = $fp-page (grayest) -// header / filter bar wrapper = $fp-card-soft (mid) -// rows = $fp-card (brightest) -// ============================================================================= - -.o_fp_job_manager_dashboard { - height: 100%; - overflow: auto; - -webkit-overflow-scrolling: touch; - padding: $fp-space-4 $fp-space-6; - display: flex; - flex-direction: column; - gap: $fp-space-3; - background-color: $fp-page; - color: $fp-ink; - - @media (max-width: 600px) { padding: $fp-space-3; gap: $fp-space-3; } - - - // ------------------------------------------------------------------------- - // Header strip - // ------------------------------------------------------------------------- - .o_fp_jmd_header { - display: flex; - align-items: center; - justify-content: space-between; - gap: $fp-space-4; - flex-wrap: wrap; - padding: $fp-space-3 $fp-space-4; - background-color: $fp-card; - border: 1px solid #{$fp-border}; - border-radius: $fp-radius-md; - box-shadow: $fp-elev-1; - } - .o_fp_jmd_header_left { - display: flex; - align-items: baseline; - gap: $fp-space-3; - } - .o_fp_jmd_title { - font-size: $fp-text-md; - font-weight: $fp-weight-bold; - margin: 0; - color: $fp-ink; - } - - - // ------------------------------------------------------------------------- - // Filter pill bar — sits on the page; the bar itself is transparent - // ------------------------------------------------------------------------- - .o_fp_jmd_filter_bar { - display: flex; - flex-wrap: wrap; - gap: $fp-space-2; - padding: $fp-space-2 4px; - } - .o_fp_jmd_pill { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 4px 12px; - border: 1px solid #{$fp-border}; - background-color: $fp-card; - color: $fp-ink; - border-radius: $fp-radius-pill; - font-size: 0.8rem; - cursor: pointer; - transition: background-color $fp-dur-fast $fp-ease, - border-color $fp-dur-fast $fp-ease, - color $fp-dur-fast $fp-ease; - - @include fp-hover-only { - &:hover { - background-color: $fp-card-soft; - border-color: $fp-border-strong; - } - } - &.o_fp_jmd_pill_active { - background-color: $fp-accent; - border-color: $fp-accent; - color: #ffffff; - font-weight: $fp-weight-semibold; - } - } - .o_fp_jmd_pill_count { - background-color: color-mix(in srgb, #{$fp-ink} 8%, transparent); - color: $fp-ink-soft; - border-radius: $fp-radius-pill; - padding: 0 7px; - font-size: 0.7rem; - font-weight: $fp-weight-bold; - min-width: 1.5em; - text-align: center; - } - .o_fp_jmd_pill_active .o_fp_jmd_pill_count { - background-color: rgba(255, 255, 255, 0.25); - color: #ffffff; - } - - - // ------------------------------------------------------------------------- - // Empty / loading - // ------------------------------------------------------------------------- - .o_fp_jmd_empty, - .o_fp_jmd_loading { - background-color: $fp-card; - color: $fp-ink-mute; - border: 1px solid #{$fp-border}; - border-radius: $fp-radius-md; - box-shadow: $fp-elev-1; - } - - - // ------------------------------------------------------------------------- - // Rows - // ------------------------------------------------------------------------- - .o_fp_jmd_rows { - display: flex; - flex-direction: column; - gap: $fp-space-2; - } - .o_fp_jmd_row { - display: flex; - align-items: stretch; - gap: 0; - background-color: $fp-card; - color: $fp-ink; - border: 1px solid #{$fp-border}; - border-radius: $fp-radius-md; - cursor: pointer; - overflow: hidden; - box-shadow: $fp-elev-1; - transition: transform $fp-dur-fast $fp-ease, - box-shadow $fp-dur $fp-ease, - border-color $fp-dur $fp-ease; - - @include fp-hover-only { - &:hover { - transform: translateY(-1px); - box-shadow: $fp-elev-2; - border-color: $fp-border-strong; - } - } - } - .o_fp_jmd_priority_bar { - flex: 0 0 6px; - background-color: $fp-state-low; // normal default - } - .o_fp_jmd_priority_rush .o_fp_jmd_priority_bar { background-color: $fp-state-rush; } - .o_fp_jmd_priority_high .o_fp_jmd_priority_bar { background-color: $fp-state-high; } - .o_fp_jmd_priority_normal .o_fp_jmd_priority_bar { background-color: $fp-state-progress; } - .o_fp_jmd_priority_low .o_fp_jmd_priority_bar { background-color: $fp-ink-faint; } - - .o_fp_jmd_row_body { - flex: 1 1 auto; - padding: 10px 14px; - display: flex; - flex-direction: column; - gap: 6px; - min-width: 0; - } - .o_fp_jmd_row_open { - flex: 0 0 auto; - align-self: center; - padding: 0 14px; - color: $fp-ink-faint; - } - .o_fp_jmd_row_top { - display: flex; - align-items: center; - justify-content: space-between; - gap: $fp-space-3; - flex-wrap: wrap; - } - .o_fp_jmd_row_id { - font-size: 0.95rem; - flex: 1 1 auto; - min-width: 0; - color: $fp-ink; - } - .o_fp_jmd_row_chips { - display: inline-flex; - gap: 6px; - flex-wrap: wrap; - } - .o_fp_jmd_row_meta { - font-size: 0.75rem; - color: $fp-ink-mute; - display: flex; - flex-wrap: wrap; - gap: 2px 4px; - } - .o_fp_jmd_overdue { - color: $fp-state-cancel; - font-weight: $fp-weight-semibold; - } - - - // ------------------------------------------------------------------------- - // State badge - // ------------------------------------------------------------------------- - .o_fp_jmd_state_badge { - display: inline-flex; - align-items: center; - padding: 2px 8px; - border-radius: $fp-radius-pill; - font-size: 0.65rem; - font-weight: $fp-weight-bold; - line-height: 1.4; - text-transform: uppercase; - letter-spacing: 0.02em; - - &.o_fp_jmd_state_badge_draft { background-color: $fp-state-pending-bg; color: $fp-state-pending-text; } - &.o_fp_jmd_state_badge_confirmed { background-color: color-mix(in srgb, #{$fp-state-progress} 18%, transparent); color: $fp-state-progress-text; } - &.o_fp_jmd_state_badge_in_progress { background-color: color-mix(in srgb, #{$fp-state-progress} 28%, transparent); color: $fp-state-progress-text; } - &.o_fp_jmd_state_badge_on_hold { background-color: color-mix(in srgb, #{$fp-state-paused} 20%, transparent); color: $fp-state-paused-text; } - &.o_fp_jmd_state_badge_done { background-color: color-mix(in srgb, #{$fp-state-done} 20%, transparent); color: $fp-state-done-text; } - &.o_fp_jmd_state_badge_cancelled { background-color: color-mix(in srgb, #{$fp-state-cancel} 18%, transparent); color: $fp-state-cancel-text; } - } - - - // ------------------------------------------------------------------------- - // Priority chips (top-right of row) - // ------------------------------------------------------------------------- - .o_fp_jmd_chip { - display: inline-flex; - align-items: center; - padding: 2px 8px; - border-radius: $fp-radius-pill; - font-size: 0.65rem; - font-weight: $fp-weight-bold; - line-height: 1.4; - text-transform: uppercase; - letter-spacing: 0.02em; - color: #ffffff; - - &.o_fp_jmd_chip_rush { background-color: $fp-state-rush; } - &.o_fp_jmd_chip_high { background-color: $fp-state-high; } - } - - - // ------------------------------------------------------------------------- - // Progress bar - // ------------------------------------------------------------------------- - .o_fp_jmd_row_progress { - display: flex; - align-items: center; - gap: 10px; - } - .o_fp_jmd_bar_track { - flex: 1 1 auto; - height: 8px; - background-color: color-mix(in srgb, #{$fp-ink} 8%, transparent); - border-radius: $fp-radius-pill; - overflow: hidden; - } - .o_fp_jmd_bar_fill { - height: 100%; - border-radius: $fp-radius-pill; - transition: width $fp-dur-slow $fp-ease; - - &.o_fp_jmd_bar_early { background-color: $fp-state-ready; } - &.o_fp_jmd_bar_mid { background-color: $fp-state-progress; } - &.o_fp_jmd_bar_done { background-color: $fp-state-done; } - } - .o_fp_jmd_bar_label { - flex: 0 0 auto; - white-space: nowrap; - font-variant-numeric: tabular-nums; - color: $fp-ink-soft; - } -} - - -// Suppress hover lift on touch. -@media (hover: none) { - .o_fp_job_manager_dashboard .o_fp_jmd_row:hover { - transform: none !important; - box-shadow: inherit !important; - } -} diff --git a/fusion_plating/fusion_plating_jobs/static/src/scss/job_plant_overview.scss b/fusion_plating/fusion_plating_jobs/static/src/scss/job_plant_overview.scss deleted file mode 100644 index 564ff4d0..00000000 --- a/fusion_plating/fusion_plating_jobs/static/src/scss/job_plant_overview.scss +++ /dev/null @@ -1,321 +0,0 @@ -// ============================================================================= -// Fusion Plating — Plant Overview (native, fp.job.step) -// Copyright 2026 Nexa Systems Inc. · License OPL-1 -// -// Class prefix: .o_fp_jpo_* (Job Plant Overview) -// -// Theme-aware: every surface, border and text colour resolves through -// the design tokens defined in _fp_jobs_tokens.scss, which compile-time -// branch on $o-webclient-color-scheme so light and dark bundles get the -// right palette. NO hardcoded hex on theme-sensitive surfaces. -// -// Three-layer contrast: -// page = $fp-page (grayest) -// columns = $fp-card-soft (mid) -// cards = $fp-card (brightest) -// ============================================================================= - -.o_fp_job_plant_overview { - height: 100%; - display: flex; - flex-direction: column; - padding: $fp-space-4 $fp-space-6; - gap: $fp-space-4; - background-color: $fp-page; - color: $fp-ink; - overflow: hidden; - - @media (max-width: 600px) { padding: $fp-space-3; gap: $fp-space-3; } - - - // ------------------------------------------------------------------------- - // Header strip — sits on the page, surfaced as a card layer - // ------------------------------------------------------------------------- - .o_fp_jpo_header { - display: flex; - align-items: center; - justify-content: space-between; - gap: $fp-space-4; - flex-wrap: wrap; - padding: $fp-space-3 $fp-space-4; - background-color: $fp-card; - border: 1px solid #{$fp-border}; - border-radius: $fp-radius-md; - box-shadow: $fp-elev-1; - } - .o_fp_jpo_header_left { - display: flex; - align-items: baseline; - gap: $fp-space-3; - } - .o_fp_jpo_title { - font-size: $fp-text-md; - font-weight: $fp-weight-bold; - margin: 0; - color: $fp-ink; - } - .o_fp_jpo_header_right { - display: flex; - align-items: center; - gap: $fp-space-2; - } - .o_fp_jpo_search_box { - display: inline-flex; - align-items: center; - background-color: $fp-card-soft; - border: 1px solid #{$fp-border}; - border-radius: $fp-radius-pill; - padding: 4px 10px; - gap: $fp-space-2; - min-width: 240px; - } - .o_fp_jpo_search_icon { color: $fp-ink-mute; } - .o_fp_jpo_search_input { - border: none; - background: transparent; - outline: none; - font-size: $fp-text-sm; - flex: 1; - color: $fp-ink; - &::placeholder { color: $fp-ink-faint; } - } - .o_fp_jpo_search_clear { - border: none; - background: transparent; - color: $fp-ink-mute; - padding: 0 2px; - cursor: pointer; - &:hover { color: $fp-ink; } - } - - - // ------------------------------------------------------------------------- - // Empty / loading - // ------------------------------------------------------------------------- - .o_fp_jpo_empty, - .o_fp_jpo_loading { - background-color: $fp-card; - color: $fp-ink-mute; - border: 1px solid #{$fp-border}; - border-radius: $fp-radius-md; - box-shadow: $fp-elev-1; - } - - - // ------------------------------------------------------------------------- - // Columns - // ------------------------------------------------------------------------- - .o_fp_jpo_columns { - display: flex; - gap: $fp-space-3; - overflow-x: auto; - flex: 1 1 auto; - align-items: stretch; - padding-bottom: 4px; - } - .o_fp_jpo_column { - flex: 0 0 280px; - display: flex; - flex-direction: column; - background-color: $fp-card-soft; - border: 1px solid #{$fp-border}; - border-radius: $fp-radius-md; - max-height: 100%; - overflow: hidden; - } - .o_fp_jpo_col_header { - display: flex; - align-items: center; - justify-content: space-between; - gap: $fp-space-2; - padding: 10px 12px 4px; - font-weight: $fp-weight-bold; - font-size: 0.95rem; - color: $fp-ink; - } - .o_fp_jpo_col_subhead { - padding: 0 12px 6px; - color: $fp-ink-mute; - } - .o_fp_jpo_col_count { - background-color: color-mix(in srgb, #{$fp-ink} 8%, transparent); - color: $fp-ink-soft; - font-weight: $fp-weight-semibold; - font-size: 0.7rem; - padding: 2px 8px; - border-radius: $fp-radius-pill; - } - .o_fp_jpo_col_body { - flex: 1 1 auto; - overflow-y: auto; - padding: 6px 8px 10px; - display: flex; - flex-direction: column; - gap: $fp-space-2; - - &.o_fp_drop_target { - background-color: color-mix(in srgb, #{$fp-accent} 8%, transparent); - } - } - - - // ------------------------------------------------------------------------- - // Cards (brightest layer) - // ------------------------------------------------------------------------- - .o_fp_jpo_card { - background-color: $fp-card; - border: 1px solid #{$fp-border}; - border-radius: $fp-radius-sm; - padding: $fp-space-2 10px; - display: flex; - flex-direction: column; - gap: 4px; - cursor: grab; - color: $fp-ink; - box-shadow: $fp-elev-1; - transition: transform $fp-dur-fast $fp-ease, - box-shadow $fp-dur $fp-ease, - border-color $fp-dur $fp-ease; - - @include fp-hover-only { - &:hover { - transform: translateY(-1px); - box-shadow: $fp-elev-2; - border-color: $fp-border-strong; - } - } - &:active { cursor: grabbing; } - - // ---- State accents (left border) -------------------------------- - &.o_fp_jpo_card_progress { border-left: 3px solid $fp-state-progress; } - &.o_fp_jpo_card_ready { border-left: 3px solid $fp-state-ready; } - &.o_fp_jpo_card_paused { border-left: 3px solid $fp-state-paused; } - &.o_fp_jpo_card_done { border-left: 3px solid $fp-state-done; opacity: 0.75; } - - // ---- Priority overlay ------------------------------------------- - &.o_fp_jpo_card_rush { - box-shadow: 0 0 0 1px rgba(220, 53, 69, 0.45), - 0 2px 8px rgba(220, 53, 69, 0.18); - } - &.o_fp_jpo_card_high { - box-shadow: 0 0 0 1px rgba(253, 126, 20, 0.4), - 0 2px 8px rgba(253, 126, 20, 0.16); - } - } - - .o_fp_jpo_card_top { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: $fp-space-2; - } - .o_fp_jpo_card_title { - flex: 1 1 auto; - font-size: 0.9rem; - line-height: 1.25; - word-break: break-word; - color: $fp-ink; - } - .o_fp_jpo_card_refs { - font-size: 0.8rem; - color: $fp-ink-soft; - } - .o_fp_jpo_job_link { - color: $fp-accent; - cursor: pointer; - text-decoration: none; - &:hover { text-decoration: underline; } - } - .o_fp_jpo_card_meta { - font-size: 0.72rem; - color: $fp-ink-mute; - display: flex; - flex-wrap: wrap; - gap: 2px 4px; - } - .o_fp_jpo_card_footer { - display: flex; - gap: $fp-space-2; - margin-top: 2px; - } - - - // ------------------------------------------------------------------------- - // State badges (top-right of card) - // ------------------------------------------------------------------------- - .o_fp_jpo_state_badge { - display: inline-flex; - align-items: center; - padding: 1px 7px; - border-radius: $fp-radius-pill; - font-size: 0.65rem; - font-weight: $fp-weight-bold; - line-height: 1.4; - white-space: nowrap; - text-transform: uppercase; - letter-spacing: 0.02em; - - &.o_fp_jpo_state_badge_pending { background-color: $fp-state-pending-bg; color: $fp-state-pending-text; } - &.o_fp_jpo_state_badge_ready { background-color: color-mix(in srgb, #{$fp-state-ready} 18%, transparent); color: $fp-state-ready-text; } - &.o_fp_jpo_state_badge_in_progress { background-color: color-mix(in srgb, #{$fp-state-progress} 18%, transparent); color: $fp-state-progress-text; } - &.o_fp_jpo_state_badge_paused { background-color: color-mix(in srgb, #{$fp-state-paused} 20%, transparent); color: $fp-state-paused-text; } - &.o_fp_jpo_state_badge_done { background-color: color-mix(in srgb, #{$fp-state-done} 20%, transparent); color: $fp-state-done-text; } - &.o_fp_jpo_state_badge_skipped { background-color: $fp-state-pending-bg; color: $fp-state-pending-text; } - &.o_fp_jpo_state_badge_cancelled { background-color: color-mix(in srgb, #{$fp-state-cancel} 18%, transparent); color: $fp-state-cancel-text; } - } - - - // ------------------------------------------------------------------------- - // Priority chip (footer) - // ------------------------------------------------------------------------- - .o_fp_jpo_chip { - display: inline-flex; - align-items: center; - padding: 1px 8px; - border-radius: $fp-radius-pill; - font-size: 0.65rem; - font-weight: $fp-weight-bold; - line-height: 1.5; - text-transform: uppercase; - letter-spacing: 0.02em; - color: #ffffff; - - &.o_fp_jpo_chip_rush { background-color: $fp-state-rush; } - &.o_fp_jpo_chip_high { background-color: $fp-state-high; } - &.o_fp_jpo_chip_low { background-color: $fp-state-low; } - } - - - // ------------------------------------------------------------------------- - // Drag-drop placeholder + ghost - // ------------------------------------------------------------------------- - .o_fp_dragging { - opacity: 0.4; - } - .o_fp_jpo_drop_placeholder { - height: 56px; - border: 2px dashed $fp-accent; - border-radius: $fp-radius-sm; - background-color: color-mix(in srgb, #{$fp-accent} 8%, transparent); - margin: 0; - } - - - // ------------------------------------------------------------------------- - // No-cards filler - // ------------------------------------------------------------------------- - .o_fp_jpo_no_cards { - color: $fp-ink-mute; - font-size: 0.8rem; - } -} - - -// Suppress the lift transform on touch so taps don't leave cards in -// hover state. -@media (hover: none) { - .o_fp_job_plant_overview .o_fp_jpo_card:hover { - transform: none !important; - box-shadow: inherit !important; - } -} diff --git a/fusion_plating/fusion_plating_jobs/static/src/scss/job_process_tree.scss b/fusion_plating/fusion_plating_jobs/static/src/scss/job_process_tree.scss deleted file mode 100644 index 3dd16085..00000000 --- a/fusion_plating/fusion_plating_jobs/static/src/scss/job_process_tree.scss +++ /dev/null @@ -1,390 +0,0 @@ -// ============================================================================= -// Fusion Plating — Job Process Tree (horizontal hierarchical, v1, 2026-04) -// Copyright 2026 Nexa Systems Inc. · License OPL-1 -// -// Class prefix: .o_fp_jpt_* (Job Process Tree) -// -// Theme-aware: page, header and connector colours resolve through the -// design tokens defined in _fp_jobs_tokens.scss (compile-time branch -// on $o-webclient-color-scheme). The node CARDS keep an intentional -// Steelhead-style dark-slate fill in BOTH themes — this is a design -// choice, not a theme bug: dark cards on light or dark page give the -// same visual hierarchy as the Steelhead reference UI. -// -// Hierarchical bracket tree layout — see body comments below. -// ============================================================================= - - -// Suppress hover transforms on touch devices so taps don't leave cards -// stuck in the hover state. -@media (hover: none) { - .o_fp_job_process_tree [class*="o_fp_jpt_"]:hover { - transform: none !important; - box-shadow: inherit !important; - } -} - - -// --- Connector geometry ------------------------------------------------------ -// Tweaking these recalculates the whole bracket-tree layout. -$jpt-card-h : 44px; // nominal card height (centre stays at h/2) -$jpt-row-gap : 12px; // vertical gap between sibling children -$jpt-indent : 36px; // horizontal gap from parent → children -$jpt-stub : 28px; // horizontal connector segment length -$jpt-line-width : 2px; - - -.o_fp_job_process_tree.o_fp_jpt_v1 { - height: 100%; - overflow: auto; // both axes — wide trees scroll horizontally - -webkit-overflow-scrolling: touch; - padding: $fp-space-4 $fp-space-6; - display: flex; - flex-direction: column; - gap: $fp-space-3; - background-color: $fp-page; - color: $fp-ink; - - @media (max-width: 600px) { padding: $fp-space-3; gap: $fp-space-3; } - - - // ------------------------------------------------------------------------- - // Header (compact strip — sits on the page as a card) - // ------------------------------------------------------------------------- - .o_fp_jpt_header { - display: flex; - align-items: center; - gap: $fp-space-3; - flex-wrap: wrap; - padding: $fp-space-3 $fp-space-4; - background-color: $fp-card; - color: $fp-ink; - border: 1px solid #{$fp-border}; - border-radius: $fp-radius-md; - box-shadow: $fp-elev-1; - position: sticky; - top: 0; - z-index: 5; - } - .o_fp_jpt_back { - display: inline-flex; - align-items: center; - padding: 6px 12px; - border-radius: $fp-radius-pill; - background-color: $fp-card-soft; - color: $fp-ink; - font-weight: $fp-weight-medium; - font-size: $fp-text-sm; - border: 1px solid #{$fp-border}; - cursor: pointer; - transition: background-color $fp-dur-fast $fp-ease, - border-color $fp-dur-fast $fp-ease, - color $fp-dur-fast $fp-ease; - @include fp-hover-only { - &:hover { - background-color: color-mix(in srgb, #{$fp-ink} 7%, $fp-card-soft); - border-color: $fp-border-strong; - } - } - } - .o_fp_jpt_title_block { flex: 1 1 auto; min-width: 0; } - .o_fp_jpt_title { - font-size: $fp-text-base; - font-weight: $fp-weight-bold; - margin: 0; - color: $fp-ink; - display: inline-flex; align-items: center; gap: 4px; - .o_fp_jpt_job_name { - font-weight: $fp-weight-semibold; - color: $fp-ink-soft; - } - } - .o_fp_jpt_subtitle { - margin-top: 2px; - font-size: $fp-text-xs; - color: $fp-ink-mute; - display: flex; flex-wrap: wrap; align-items: center; gap: 2px; - .fa { margin-right: 2px; color: $fp-ink-faint; } - } - - - // ------------------------------------------------------------------------- - // Empty / loading - // ------------------------------------------------------------------------- - .o_fp_jpt_empty { - text-align: center; - padding: $fp-space-8 $fp-space-6; - background-color: $fp-card; - color: $fp-ink-mute; - border: 1px solid #{$fp-border}; - border-radius: $fp-radius-md; - box-shadow: $fp-elev-1; - font-size: $fp-text-sm; - max-width: 520px; - > .fa { font-size: 1.75rem; margin-bottom: 8px; color: $fp-ink-faint; } - } - - - // ------------------------------------------------------------------------- - // Tree canvas — horizontally scrollable - // ------------------------------------------------------------------------- - .o_fp_jpt_canvas { - padding: $fp-space-3 0; - min-width: max-content; // let cards push the canvas wider for scroll - } - - - // ------------------------------------------------------------------------- - // Recursive node — flex row of [card | children-column] - // ------------------------------------------------------------------------- - .o_fp_jpt_node { - display: flex; - align-items: flex-start; - position: relative; - } - - - // ------------------------------------------------------------------------- - // Card (Steelhead-style: dark fill, rounded — intentional in both themes) - // - // The dark slate is a deliberate visual choice (Steelhead parity). - // The contrasting page surface is themed via $fp-page above, so the - // overall composition still feels right in light + dark mode. - // ------------------------------------------------------------------------- - .o_fp_jpt_card { - display: inline-flex; - align-items: center; - gap: 10px; - min-width: 220px; - max-width: 340px; - min-height: $jpt-card-h; - padding: $fp-space-2 $fp-space-3; - background-color: #2b2f36; // dark slate (Steelhead parity) - color: #f1f3f5; - border-radius: 6px; - box-shadow: $fp-elev-1; - font-size: $fp-text-sm; - line-height: 1.25; - flex: 0 0 auto; - position: relative; - z-index: 1; // sit above connector lines - transition: transform $fp-dur-fast $fp-ease, - box-shadow $fp-dur $fp-ease, - background-color $fp-dur-fast $fp-ease; - - &.o_fp_jpt_clickable { - cursor: pointer; - @include fp-hover-only { - &:hover { - transform: translateY(-1px); - box-shadow: $fp-elev-2; - background-color: #353a42; - } - } - } - - // ---- Card type tints (subtle) ------------------------------------- - &.o_fp_jpt_type_recipe { - background-color: #1f2329; - font-weight: $fp-weight-bold; - } - &.o_fp_jpt_type_sub_process { - background-color: #262a31; - font-weight: $fp-weight-semibold; - } - &.o_fp_jpt_type_step { - background-color: #353a42; - font-size: 0.8rem; - min-height: 36px; - } - - // ---- Live state highlight ---------------------------------------- - &.o_fp_jpt_state_in_progress { - background-color: #c0392b; // warm red — active step - color: #fff; - box-shadow: 0 0 0 1px rgba(192, 57, 43, .6), - 0 4px 14px rgba(192, 57, 43, .35); - } - &.o_fp_jpt_highlight.o_fp_jpt_state_ready { - background-color: #c0392b; // ready also red - color: #fff; - box-shadow: 0 0 0 1px rgba(192, 57, 43, .6), - 0 4px 14px rgba(192, 57, 43, .35); - } - &.o_fp_jpt_state_paused { - background-color: #b5651d; // amber — paused - color: #fff; - } - &.o_fp_jpt_state_done { - background-color: #1e8449; // green for completed - color: #fff; - } - &.o_fp_jpt_state_skipped, - &.o_fp_jpt_state_cancelled { opacity: 0.55; } - } - - .o_fp_jpt_card_icon { - flex: 0 0 auto; - width: 18px; - text-align: center; - opacity: 0.85; - font-size: 0.95em; - } - - .o_fp_jpt_card_body { - flex: 1 1 auto; - min-width: 0; - display: flex; - flex-direction: column; - gap: 2px; - } - .o_fp_jpt_card_title { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - .o_fp_jpt_card_meta { - font-size: 0.72rem; - opacity: 0.75; - display: flex; - flex-wrap: wrap; - gap: 2px 6px; - .fa { opacity: 0.8; } - } - - .o_fp_jpt_card_right { - flex: 0 0 auto; - display: inline-flex; - align-items: center; - gap: 6px; - } - - .o_fp_jpt_card_open { - opacity: 0.55; - font-size: 0.85em; - } - - - // ------------------------------------------------------------------------- - // State badge (right side of operation cards) - // ------------------------------------------------------------------------- - .o_fp_jpt_state_badge { - display: inline-flex; - align-items: center; - padding: 1px 7px; - border-radius: $fp-radius-pill; - font-size: 0.65rem; - font-weight: $fp-weight-bold; - line-height: 1.4; - white-space: nowrap; - text-transform: uppercase; - letter-spacing: 0.02em; - - // Cards are dark-slate filled in BOTH themes, so badge palette - // is tuned for that dark surface — light text on translucent - // tints. NOT theme-sensitive (both bundles render the same way). - &.o_fp_jpt_state_badge_pending { background-color: rgba(255,255,255,.12); color: #c8ccd2; } - &.o_fp_jpt_state_badge_ready { background-color: rgba(255, 193, 7, .25); color: #ffd866; } - &.o_fp_jpt_state_badge_in_progress { background-color: rgba(13, 110, 253, .25); color: #6ea8fe; } - &.o_fp_jpt_state_badge_paused { background-color: rgba(255, 145, 0, .28); color: #ffb86b; } - &.o_fp_jpt_state_badge_done { background-color: rgba(25, 135, 84, .28); color: #75d4a4; } - &.o_fp_jpt_state_badge_skipped { background-color: rgba(108, 117, 125, .35); color: #d0d4d9; } - &.o_fp_jpt_state_badge_cancelled { background-color: rgba(220, 53, 69, .25); color: #f1aeb5; } - } - - - // ------------------------------------------------------------------------- - // Children column (recursed nodes laid out vertically to the right) - // - // The ::before pseudo draws the horizontal connector that bridges the - // parent card's right edge → the bus column at left: 0 of this - // container. - // ------------------------------------------------------------------------- - .o_fp_jpt_children { - display: flex; - flex-direction: column; - gap: $jpt-row-gap; - margin-left: $jpt-indent; - position: relative; - - &::before { - content: ""; - position: absolute; - left: -#{$jpt-indent}; - top: calc(#{$jpt-card-h} / 2); // parent-card vertical centre - width: $jpt-indent; - height: $jpt-line-width; - background-color: $fp-border-strong; - z-index: 0; - } - } - - - // ------------------------------------------------------------------------- - // Connector lines (bracket style, drawn from CSS only) - // - // Each child .o_fp_jpt_node owns its own connector segments: - // ::before → horizontal stub from the bus column → card centre - // ::after → vertical bus segment for this row - // - // First/last/single children trim the vertical so the bracket stops - // exactly at the card centre. - // ------------------------------------------------------------------------- - .o_fp_jpt_children > .o_fp_jpt_node { - position: relative; - padding-left: $jpt-stub; // room for the horizontal stub - - // -- horizontal stub from bus column → card -------------------------- - &::before { - content: ""; - position: absolute; - left: 0; - top: calc(#{$jpt-card-h} / 2); // align with card vertical centre - width: $jpt-stub; - height: $jpt-line-width; - background-color: $fp-border-strong; - z-index: 0; - } - - // -- vertical bus segment (default: full row, top → bottom) ---------- - &::after { - content: ""; - position: absolute; - left: 0; - top: calc(-#{$jpt-row-gap} / 2); // bridge gap to sibling above - bottom: calc(-#{$jpt-row-gap} / 2); // bridge gap to sibling below - width: $jpt-line-width; - background-color: $fp-border-strong; - z-index: 0; - } - - // First child — vertical only from card centre → bottom of row - &:first-child::after { - top: calc(#{$jpt-card-h} / 2); - } - // Last child — vertical only from top of row → card centre - &:last-child::after { - bottom: calc(100% - (#{$jpt-card-h} / 2)); - } - // Only child — vertical only at the card centre point - &:first-child:last-child::after { - top: calc(#{$jpt-card-h} / 2); - bottom: calc(100% - (#{$jpt-card-h} / 2)); - } - } - - - // ------------------------------------------------------------------------- - // Pulse on live (in_progress / ready) cards - // ------------------------------------------------------------------------- - @keyframes o_fp_jpt_pulse { - 0%, 100% { box-shadow: 0 0 0 1px rgba(192, 57, 43, .55), - 0 4px 14px rgba(192, 57, 43, .35); } - 50% { box-shadow: 0 0 0 4px rgba(192, 57, 43, .25), - 0 4px 18px rgba(192, 57, 43, .45); } - } - .o_fp_jpt_card.o_fp_jpt_state_in_progress, - .o_fp_jpt_card.o_fp_jpt_highlight.o_fp_jpt_state_ready { - animation: o_fp_jpt_pulse 2.4s ease-in-out infinite; - } -} diff --git a/fusion_plating/fusion_plating_jobs/static/src/scss/job_tablet.scss b/fusion_plating/fusion_plating_jobs/static/src/scss/job_tablet.scss deleted file mode 100644 index 66209ffd..00000000 --- a/fusion_plating/fusion_plating_jobs/static/src/scss/job_tablet.scss +++ /dev/null @@ -1,606 +0,0 @@ -// ============================================================================= -// Fusion Plating — Tablet Station (native, fp.job.step) -// Copyright 2026 Nexa Systems Inc. · License OPL-1 -// -// Class prefix: .o_fp_jt_* (Job Tablet) -// -// Theme-aware: every surface, border and text colour resolves through -// the design tokens defined in _fp_jobs_tokens.scss. -// -// Three-layer contrast: -// page = $fp-page (grayest) -// mode panels (header / body / job-header / step-header) = $fp-card-soft (mid) -// cards / step rows / table rows = $fp-card (brightest) -// -// Touch-first: min 60px tap targets, 16-20pt text, high contrast. -// ============================================================================= - -.o_fp_job_tablet { - height: 100%; - display: flex; - flex-direction: column; - padding: $fp-space-4 $fp-space-6; - gap: $fp-space-4; - background-color: $fp-page; - color: $fp-ink; - overflow: hidden; - font-size: $fp-text-base; - - @media (max-width: 800px) { padding: 10px; gap: 10px; } - - - // ------------------------------------------------------------------------ - // Header strip - // ------------------------------------------------------------------------ - .o_fp_jt_header { - display: flex; - align-items: center; - justify-content: space-between; - gap: $fp-space-3; - padding: $fp-space-3 $fp-space-4; - background-color: $fp-card; - color: $fp-ink; - border: 1px solid #{$fp-border}; - border-radius: $fp-radius-md; - box-shadow: $fp-elev-1; - } - .o_fp_jt_header_left { - display: flex; - align-items: center; - gap: $fp-space-3; - flex: 1 1 auto; - min-width: 0; - } - .o_fp_jt_title { - font-size: 1.4rem; - font-weight: $fp-weight-bold; - margin: 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - color: $fp-ink; - } - .o_fp_jt_back_btn { - min-width: 60px; - min-height: 60px; - border: 1px solid #{$fp-border}; - border-radius: $fp-radius-sm; - background-color: $fp-card-soft; - color: $fp-ink; - font-size: 1.4rem; - cursor: pointer; - flex: 0 0 auto; - - &:hover { background-color: color-mix(in srgb, #{$fp-ink} 7%, $fp-card-soft); } - &:active { background-color: color-mix(in srgb, #{$fp-ink} 12%, $fp-card-soft); } - } - .o_fp_jt_header_right { - display: flex; - align-items: center; - gap: $fp-space-2; - } - .o_fp_jt_refresh_btn { - min-width: 60px; - min-height: 60px; - border: 1px solid #{$fp-border}; - border-radius: $fp-radius-sm; - background-color: $fp-card-soft; - color: $fp-ink; - font-size: 1.3rem; - cursor: pointer; - - &:hover { background-color: color-mix(in srgb, #{$fp-ink} 7%, $fp-card-soft); } - &:disabled { opacity: 0.5; cursor: not-allowed; } - } - - - // ------------------------------------------------------------------------ - // Body container — holds whichever mode is active - // ------------------------------------------------------------------------ - .o_fp_jt_body { - flex: 1 1 auto; - overflow-y: auto; - background-color: $fp-card-soft; - color: $fp-ink; - border: 1px solid #{$fp-border}; - border-radius: $fp-radius-md; - padding: 20px; - - @media (max-width: 800px) { padding: $fp-space-3; } - } - - - // ------------------------------------------------------------------------ - // Loading / empty - // ------------------------------------------------------------------------ - .o_fp_jt_loading, - .o_fp_jt_empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - text-align: center; - padding: 60px 20px; - color: $fp-ink-mute; - font-size: 1.2rem; - } - - - // ======================================================================== - // JOB PICKER MODE - // ======================================================================== - .o_fp_jt_job_grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); - gap: $fp-space-4; - } - - .o_fp_jt_job_card { - background-color: $fp-card; - color: $fp-ink; - border: 2px solid #{$fp-border}; - border-radius: 12px; - padding: $fp-space-4; - display: flex; - flex-direction: column; - gap: 10px; - cursor: pointer; - min-height: 180px; - box-shadow: $fp-elev-1; - transition: transform $fp-dur-fast $fp-ease, - box-shadow $fp-dur $fp-ease, - border-color $fp-dur $fp-ease; - - @include fp-hover-only { - &:hover { - transform: translateY(-2px); - box-shadow: $fp-elev-2; - border-color: $fp-accent; - } - } - &:active { - transform: translateY(0); - box-shadow: $fp-elev-1; - } - - // Priority emphasis - &.o_fp_jt_card_rush { - border-color: $fp-state-rush; - box-shadow: 0 0 0 1px rgba(220, 53, 69, 0.30); - } - &.o_fp_jt_card_high { - border-color: $fp-state-high; - box-shadow: 0 0 0 1px rgba(253, 126, 20, 0.25); - } - } - - .o_fp_jt_job_card_top { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: $fp-space-2; - } - .o_fp_jt_job_card_name { - font-size: 1.25rem; - font-weight: $fp-weight-bold; - word-break: break-word; - color: $fp-ink; - } - .o_fp_jt_job_card_partner { - font-size: $fp-text-base; - color: $fp-ink-mute; - font-weight: $fp-weight-medium; - } - .o_fp_jt_job_card_meta { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 10px; - font-size: 0.9rem; - } - .o_fp_jt_meta_item { - color: $fp-ink-mute; - } - .o_fp_jt_job_card_progress { - display: flex; - align-items: center; - gap: $fp-space-2; - margin-top: auto; - } - .o_fp_jt_job_card_current { - font-size: 0.95rem; - padding-top: 6px; - border-top: 1px solid #{$fp-border}; - color: $fp-state-progress-text; - } - - - // ------------------------------------------------------------------------ - // Progress bar (shared by job cards + job header) - // ------------------------------------------------------------------------ - .o_fp_jt_progress_bar { - flex: 1 1 auto; - height: 12px; - background-color: color-mix(in srgb, #{$fp-ink} 8%, transparent); - border-radius: $fp-radius-pill; - overflow: hidden; - } - .o_fp_jt_progress_fill { - height: 100%; - background-color: $fp-state-done; - transition: width $fp-dur-slow $fp-ease; - } - .o_fp_jt_progress_label { - font-size: 0.85rem; - font-weight: $fp-weight-semibold; - color: $fp-ink-mute; - white-space: nowrap; - } - - - // ======================================================================== - // JOB DETAIL MODE - // ======================================================================== - .o_fp_jt_job_header { - // Body wraps this section; this header sits inside the body's - // $fp-card-soft surface and uses $fp-card so it pops as the - // brightest layer in the body region. - background-color: $fp-card; - color: $fp-ink; - border: 1px solid #{$fp-border}; - border-radius: $fp-radius-md; - padding: $fp-space-4; - margin-bottom: $fp-space-4; - box-shadow: $fp-elev-1; - } - .o_fp_jt_job_header_row { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); - gap: $fp-space-3; - margin-bottom: 14px; - } - .o_fp_jt_job_header_label { - font-size: 0.75rem; - text-transform: uppercase; - letter-spacing: 0.04em; - color: $fp-ink-mute; - font-weight: $fp-weight-semibold; - } - .o_fp_jt_job_header_value { - font-size: 1.1rem; - font-weight: $fp-weight-semibold; - margin-top: 2px; - color: $fp-ink; - } - .o_fp_jt_job_header_progress { - display: flex; - align-items: center; - gap: $fp-space-3; - } - - .o_fp_jt_section_title { - font-size: 1.15rem; - font-weight: $fp-weight-bold; - margin: 0 0 $fp-space-3 0; - color: $fp-ink; - } - - .o_fp_jt_step_list { - display: flex; - flex-direction: column; - gap: $fp-space-2; - } - - .o_fp_jt_step_row { - display: flex; - align-items: center; - gap: 14px; - padding: 14px 16px; - background-color: $fp-card; - color: $fp-ink; - border: 1px solid #{$fp-border}; - border-radius: $fp-radius-md; - cursor: pointer; - min-height: 72px; - box-shadow: $fp-elev-1; - transition: background-color $fp-dur-fast $fp-ease, - border-color $fp-dur $fp-ease, - box-shadow $fp-dur $fp-ease, - transform $fp-dur-fast $fp-ease; - - @include fp-hover-only { - &:hover { - border-color: $fp-accent; - background-color: color-mix(in srgb, #{$fp-accent} 4%, $fp-card); - box-shadow: $fp-elev-2; - transform: translateX(2px); - } - } - &:active { - background-color: color-mix(in srgb, #{$fp-ink} 6%, $fp-card); - } - } - .o_fp_jt_step_seq { - flex: 0 0 auto; - width: 36px; - height: 36px; - border-radius: 50%; - background-color: $fp-card-soft; - color: $fp-ink-mute; - display: flex; - align-items: center; - justify-content: center; - font-weight: $fp-weight-bold; - font-size: 0.95rem; - } - .o_fp_jt_step_main { - flex: 1 1 auto; - min-width: 0; - } - .o_fp_jt_step_name { - font-size: 1.1rem; - font-weight: $fp-weight-semibold; - word-break: break-word; - color: $fp-ink; - } - .o_fp_jt_step_meta { - margin-top: 4px; - font-size: 0.85rem; - color: $fp-ink-mute; - display: flex; - flex-wrap: wrap; - gap: 4px 6px; - } - .o_fp_jt_step_chevron { - color: $fp-ink-mute; - font-size: 1.1rem; - } - - - // ======================================================================== - // STEP DETAIL MODE - // ======================================================================== - .o_fp_jt_step_header { - background-color: $fp-card; - color: $fp-ink; - border: 1px solid #{$fp-border}; - border-radius: $fp-radius-md; - padding: 20px; - margin-bottom: 20px; - box-shadow: $fp-elev-1; - } - .o_fp_jt_step_header_top { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 14px; - margin-bottom: $fp-space-4; - } - .o_fp_jt_step_header_seq { - font-size: 0.85rem; - text-transform: uppercase; - letter-spacing: 0.05em; - color: $fp-ink-mute; - font-weight: $fp-weight-semibold; - } - .o_fp_jt_step_header_name { - font-size: 1.6rem; - font-weight: $fp-weight-bold; - margin: 4px 0 0 0; - word-break: break-word; - color: $fp-ink; - } - .o_fp_jt_step_header_grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); - gap: 14px; - } - .o_fp_jt_step_header_label { - font-size: 0.75rem; - text-transform: uppercase; - letter-spacing: 0.04em; - color: $fp-ink-mute; - font-weight: $fp-weight-semibold; - } - .o_fp_jt_step_header_value { - font-size: 1.1rem; - font-weight: $fp-weight-semibold; - margin-top: 2px; - color: $fp-ink; - } - .o_fp_jt_step_instructions { - margin-top: $fp-space-4; - padding-top: $fp-space-4; - border-top: 1px solid #{$fp-border}; - - h3 { - font-size: 1rem; - font-weight: $fp-weight-bold; - margin: 0 0 $fp-space-2 0; - text-transform: uppercase; - letter-spacing: 0.04em; - color: $fp-ink-mute; - } - } - - - // ------------------------------------------------------------------------ - // Big action buttons (Start / Finish) - // ------------------------------------------------------------------------ - .o_fp_jt_action_buttons { - display: flex; - gap: $fp-space-3; - margin-bottom: 20px; - flex-wrap: wrap; - } - - .o_fp_jt_btn_start, - .o_fp_jt_btn_finish { - flex: 1 1 240px; - min-height: 80px; - border: none; - border-radius: 12px; - font-size: 1.5rem; - font-weight: $fp-weight-bold; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - color: #ffffff; - transition: filter $fp-dur-fast $fp-ease, - transform $fp-dur-fast $fp-ease, - box-shadow $fp-dur $fp-ease; - - &:hover { filter: brightness(0.92); } - &:active { transform: translateY(1px); } - &:disabled { - opacity: 0.55; - cursor: not-allowed; - filter: none !important; - } - } - // Start / Finish buttons are CTAs — they keep semantic green / blue - // in both themes for consistent recognition on the shop floor. - .o_fp_jt_btn_start { - background-color: #198754; - box-shadow: 0 4px 12px rgba(25, 135, 84, 0.30); - } - .o_fp_jt_btn_finish { - background-color: #0d6efd; - box-shadow: 0 4px 12px rgba(13, 110, 253, 0.30); - } - - .o_fp_jt_no_actions { - flex: 1 1 100%; - padding: 18px; - background-color: color-mix(in srgb, #{$fp-state-ready} 18%, $fp-card); - border: 1px solid color-mix(in srgb, #{$fp-state-ready} 50%, #{$fp-border}); - border-radius: $fp-radius-md; - color: $fp-state-ready-text; - font-size: 1rem; - display: flex; - align-items: center; - } - - - // ------------------------------------------------------------------------ - // Timelog table - // ------------------------------------------------------------------------ - .o_fp_jt_timelogs { - margin-top: 20px; - } - .o_fp_jt_timelog_table { - width: 100%; - border-collapse: separate; - border-spacing: 0; - background-color: $fp-card; - color: $fp-ink; - border: 1px solid #{$fp-border}; - border-radius: $fp-radius-md; - overflow: hidden; - box-shadow: $fp-elev-1; - - th { - background-color: $fp-card-soft; - padding: 10px 14px; - text-align: left; - font-size: 0.8rem; - text-transform: uppercase; - letter-spacing: 0.04em; - color: $fp-ink-mute; - font-weight: $fp-weight-semibold; - border-bottom: 1px solid #{$fp-border}; - } - td { - padding: 10px 14px; - font-size: 0.95rem; - color: $fp-ink; - border-bottom: 1px solid #{$fp-border}; - } - tr:last-child td { border-bottom: none; } - } - .o_fp_jt_running { - color: $fp-state-progress-text; - font-style: italic; - font-weight: $fp-weight-semibold; - } - - - // ======================================================================== - // State badges (small + extra-large) - // ======================================================================== - .o_fp_jt_state_badge, - .o_fp_jt_state_badge_xl { - display: inline-flex; - align-items: center; - border-radius: $fp-radius-pill; - font-weight: $fp-weight-bold; - line-height: 1.4; - white-space: nowrap; - text-transform: uppercase; - letter-spacing: 0.03em; - } - .o_fp_jt_state_badge { - padding: 3px 10px; - font-size: 0.75rem; - } - .o_fp_jt_state_badge_xl { - padding: 8px 18px; - font-size: 1rem; - } - - // Color variants — match plant_overview palette - .o_fp_jt_badge_pending { background-color: $fp-state-pending-bg; color: $fp-state-pending-text; } - .o_fp_jt_badge_ready { background-color: color-mix(in srgb, #{$fp-state-progress} 18%, transparent); color: $fp-state-progress-text; } - .o_fp_jt_badge_progress { - background-color: color-mix(in srgb, #{$fp-state-paused} 20%, transparent); - color: $fp-state-paused-text; - animation: o_fp_jt_pulse 2s ease-in-out infinite; - } - .o_fp_jt_badge_paused { background-color: color-mix(in srgb, #{$fp-state-ready} 22%, transparent); color: $fp-state-ready-text; } - .o_fp_jt_badge_done { background-color: color-mix(in srgb, #{$fp-state-done} 22%, transparent); color: $fp-state-done-text; } - .o_fp_jt_badge_skipped { background-color: $fp-state-pending-bg; color: $fp-state-pending-text; } - .o_fp_jt_badge_cancelled { background-color: color-mix(in srgb, #{$fp-state-cancel} 18%, transparent); color: $fp-state-cancel-text; } - - - // ======================================================================== - // Priority chip (job picker cards) - // ======================================================================== - .o_fp_jt_chip { - display: inline-flex; - align-items: center; - padding: 3px 10px; - border-radius: $fp-radius-pill; - font-size: 0.75rem; - font-weight: $fp-weight-bold; - line-height: 1.4; - text-transform: uppercase; - letter-spacing: 0.03em; - color: #ffffff; - - &.o_fp_jt_chip_rush { background-color: $fp-state-rush; } - &.o_fp_jt_chip_high { background-color: $fp-state-high; } - &.o_fp_jt_chip_low { background-color: $fp-state-low; } - } -} - - -// ---------------------------------------------------------------------------- -// Pulse animation for in_progress state badges -// ---------------------------------------------------------------------------- -@keyframes o_fp_jt_pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.65; } -} - - -// Suppress hover lift on touch — taps shouldn't leave cards in hover state. -@media (hover: none) { - .o_fp_job_tablet { - .o_fp_jt_job_card:hover, - .o_fp_jt_step_row:hover { - transform: none !important; - box-shadow: inherit !important; - } - } -} diff --git a/fusion_plating/fusion_plating_jobs/static/src/xml/job_manager_dashboard.xml b/fusion_plating/fusion_plating_jobs/static/src/xml/job_manager_dashboard.xml deleted file mode 100644 index ca88ffd8..00000000 --- a/fusion_plating/fusion_plating_jobs/static/src/xml/job_manager_dashboard.xml +++ /dev/null @@ -1,154 +0,0 @@ - - - - - -
- - -
-
-

- - Manager Dashboard -

- - Updated - -
-
- -
-
- - -
- - - - - -
- - -
- -

Loading jobs...

-
- - -
- -

No jobs in this bucket.

-
- - -
- -
- - -
- - -
- - -
-
- - - · - -
-
- - RUSH - High -
-
- - -
- - Qty - - - · - - - · - - - · - - - · - - (overdue) - -
- - -
-
-
-
- - - -
-
- - -
- -
-
- -
- -
-
- - diff --git a/fusion_plating/fusion_plating_jobs/static/src/xml/job_plant_overview.xml b/fusion_plating/fusion_plating_jobs/static/src/xml/job_plant_overview.xml deleted file mode 100644 index fc8c0c55..00000000 --- a/fusion_plating/fusion_plating_jobs/static/src/xml/job_plant_overview.xml +++ /dev/null @@ -1,163 +0,0 @@ - - - - - -
- - -
-
-

- - Plant Overview -

- - Updated - -
-
- - -
-
- - -
- -

Loading plant data...

-
- - -
- -

- No active steps in any work centre. -

-
- - -
- -
- - -
- - - - -
-
- - · - -
- - - - -
-
-
- -
-
- -
diff --git a/fusion_plating/fusion_plating_jobs/static/src/xml/job_process_tree.xml b/fusion_plating/fusion_plating_jobs/static/src/xml/job_process_tree.xml deleted file mode 100644 index 943db807..00000000 --- a/fusion_plating/fusion_plating_jobs/static/src/xml/job_process_tree.xml +++ /dev/null @@ -1,122 +0,0 @@ - - - - - - -
- - -
- -
-
-
- - - - - · - - -
-
- - -
- - -
-
- - -
- - - - - -
-
- - - - - -
- - -
- -
-

- Process - - · - -

-
- - - - · - · Qty - · - · % -
-
-
- - -
- -

Loading process...

-
- - -
- -
No job selected.
-
-
- -
No recipe assigned to this job.
-
- - -
- - - -
- -
-
- - diff --git a/fusion_plating/fusion_plating_jobs/static/src/xml/job_tablet.xml b/fusion_plating/fusion_plating_jobs/static/src/xml/job_tablet.xml deleted file mode 100644 index 9d5fdaf1..00000000 --- a/fusion_plating/fusion_plating_jobs/static/src/xml/job_tablet.xml +++ /dev/null @@ -1,325 +0,0 @@ - - - - - -
- - -
-
- -

- - Tablet Station - - Step Detail -

-
-
- - Updated - - -
-
- - -
- -
- -

Loading jobs...

-
- -
- -

No active jobs.

-

- Confirm a job to see it here. -

-
- -
- -
- -
-
- -
- -
- -
- - - qty - - - - - - -
- -
-
-
-
- - % - -
- -
- - Now: - -
-
- -
-
- - -
- -
-
-
-
Customer
-
-
-
-
Quantity
-
-
-
-
Recipe
-
-
-
-
State
- -
-
-
-
-
-
- - / steps - (%) - -
-
- -

Steps

- -
- -

No steps on this job yet.

-
- -
- -
-
- -
-
-
-
- - - - - · - - - - - - · - - -
-
- - -
- -
-
- - -
- -
-
-
-
- Step # -
-

-

- -
- -
-
-
Work Centre
-
-
-
-
Kind
-
-
-
-
Expected
-
- min -
-
-
-
Actual
-
- min -
-
-
-
Target Thickness
-
- - -
-
-
-
Assigned
-
-
-
- -
-

Instructions

-
-
-
- - -
- - -
- - - - This step is pending. Earlier steps must complete first. - - - This step is complete. - - - This step was skipped. - - - This step was cancelled. - - - No actions available in state . - - -
-
- - -
-

Time Log

- - - - - - - - - - - - - - - - - -
OperatorStartedFinishedDuration
- - - - running - - - min - - -
-
-
- -
- - - diff --git a/fusion_plating/fusion_plating_jobs/tests/test_fp_job_extensions.py b/fusion_plating/fusion_plating_jobs/tests/test_fp_job_extensions.py index 0f4b60c4..adf1b07f 100644 --- a/fusion_plating/fusion_plating_jobs/tests/test_fp_job_extensions.py +++ b/fusion_plating/fusion_plating_jobs/tests/test_fp_job_extensions.py @@ -546,16 +546,19 @@ class TestPhase6Controllers(TransactionCase): }) def test_scan_controller_route_registered(self): - # Verify the route is registered in the controller registry. - # Odoo auto-registers @http.route decorated methods on module load. - # We don't HTTP-call from a TransactionCase; just confirm import works. - from odoo.addons.fusion_plating_jobs.controllers import job_scan, process_tree + # Verify the QR-scan controller is registered. The parallel + # process_tree / plant_overview / manager_dashboard / tablet + # controllers were consolidated into fusion_plating_shopfloor on + # 2026-04-24; the only controller left in this module is + # job_scan (the QR-sticker scan redirect). + from odoo.addons.fusion_plating_jobs.controllers import job_scan self.assertTrue(hasattr(job_scan, 'FpJobScanController')) - self.assertTrue(hasattr(process_tree, 'FpJobProcessTreeController')) def test_process_tree_endpoint_logic(self): - # Direct method invocation (not HTTP) to verify serialization logic - # works for a job with steps + recipe. + # The native process_tree endpoint now lives in + # fusion_plating_shopfloor (consolidated 2026-04-24). This test + # verifies the recipe-node → step lookup that the endpoint + # depends on still works for fp.job rows seeded from a recipe. recipe = self.env['fusion.plating.process.node'].create({ 'name': 'R', 'node_type': 'recipe', }) @@ -568,9 +571,6 @@ class TestPhase6Controllers(TransactionCase): 'job_id': self.job.id, 'name': 'Op1', 'sequence': 10, 'recipe_node_id': op.id, }) - # Direct call to the controller method body via a fake request - # context — in Odoo TransactionCase we can't easily simulate http.request, - # so this test just verifies the underlying step-serialization works. step_by_node = {s.recipe_node_id.id: s for s in self.job.step_ids if s.recipe_node_id} self.assertIn(op.id, step_by_node) self.assertEqual(step_by_node[op.id].name, 'Op1') diff --git a/fusion_plating/fusion_plating_jobs/views/fp_job_form_inherit.xml b/fusion_plating/fusion_plating_jobs/views/fp_job_form_inherit.xml index 52ef952a..26687d01 100644 --- a/fusion_plating/fusion_plating_jobs/views/fp_job_form_inherit.xml +++ b/fusion_plating/fusion_plating_jobs/views/fp_job_form_inherit.xml @@ -1,8 +1,9 @@ diff --git a/fusion_plating/fusion_plating_jobs/views/job_overview_actions.xml b/fusion_plating/fusion_plating_jobs/views/job_overview_actions.xml deleted file mode 100644 index e9dd99ac..00000000 --- a/fusion_plating/fusion_plating_jobs/views/job_overview_actions.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - Plant Overview - fp_job_plant_overview - - - - Manager Dashboard - fp_job_manager_dashboard - - - - - - diff --git a/fusion_plating/fusion_plating_jobs/views/job_process_tree_action.xml b/fusion_plating/fusion_plating_jobs/views/job_process_tree_action.xml deleted file mode 100644 index 7879b2e6..00000000 --- a/fusion_plating/fusion_plating_jobs/views/job_process_tree_action.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - Job Process Tree - fp_job_process_tree - - diff --git a/fusion_plating/fusion_plating_jobs/views/job_tablet_action.xml b/fusion_plating/fusion_plating_jobs/views/job_tablet_action.xml deleted file mode 100644 index 20a1f9d3..00000000 --- a/fusion_plating/fusion_plating_jobs/views/job_tablet_action.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - Tablet Station - fp_job_tablet - - - - diff --git a/fusion_plating/fusion_plating_jobs/views/legacy_menu_hide.xml b/fusion_plating/fusion_plating_jobs/views/legacy_menu_hide.xml index 2c83f564..f7293ff5 100644 --- a/fusion_plating/fusion_plating_jobs/views/legacy_menu_hide.xml +++ b/fusion_plating/fusion_plating_jobs/views/legacy_menu_hide.xml @@ -1,26 +1,26 @@ - + The group_fusion_plating_legacy_menus group is preserved so a + site that needs to bring legacy menus back can simply add a + user to the group. --> - + - + - + - +