diff --git a/fusion-plating/fusion_plating_shopfloor/controllers/shopfloor_controller.py b/fusion-plating/fusion_plating_shopfloor/controllers/shopfloor_controller.py index 25c05335..ca7016ca 100644 --- a/fusion-plating/fusion_plating_shopfloor/controllers/shopfloor_controller.py +++ b/fusion-plating/fusion_plating_shopfloor/controllers/shopfloor_controller.py @@ -343,6 +343,46 @@ class FpShopfloorController(http.Controller): # ================================================================== # Plant Overview Dashboard # ================================================================== + + @http.route('/fp/shopfloor/plant_overview/move_card', + type='jsonrpc', auth='user') + def plant_overview_move_card(self, card_id, source_model, + target_workcenter_id): + """Move a work order card to a different work centre (drag & drop). + + Only mrp.workorder is supported for now — other source models + will return an error so the frontend can display it gracefully. + """ + if source_model != 'mrp.workorder': + return {'ok': False, + 'error': 'Drag & drop is only supported for work orders.'} + + MrpWO = request.env.get('mrp.workorder') + if MrpWO is None: + return {'ok': False, 'error': 'MRP module not available.'} + + wo = MrpWO.browse(int(card_id)) + if not wo.exists(): + return {'ok': False, 'error': f'Work order {card_id} not found.'} + + wc = request.env['mrp.workcenter'].browse(int(target_workcenter_id)) + if not wc.exists(): + return {'ok': False, + 'error': f'Work centre {target_workcenter_id} not found.'} + + try: + wo.write({'workcenter_id': wc.id}) + _logger.info( + 'Plant Overview: moved WO %s (%s) → WC %s (%s) by uid %s', + wo.id, wo.display_name, wc.id, wc.name, + request.env.uid, + ) + except Exception as exc: + _logger.exception('Plant Overview move_card failed') + return {'ok': False, 'error': str(exc)} + + return {'ok': True} + @http.route('/fp/shopfloor/plant_overview', type='jsonrpc', auth='user') def plant_overview(self, facility_id=None, search=None): """Return work orders grouped by work centre for the plant overview. diff --git a/fusion-plating/fusion_plating_shopfloor/static/src/js/plant_overview.js b/fusion-plating/fusion_plating_shopfloor/static/src/js/plant_overview.js index e216afc4..6fcb7860 100644 --- a/fusion-plating/fusion_plating_shopfloor/static/src/js/plant_overview.js +++ b/fusion-plating/fusion_plating_shopfloor/static/src/js/plant_overview.js @@ -94,6 +94,97 @@ export class PlantOverview extends Component { this.loadData(); } + // ----- Drag & drop -------------------------------------------------------- + + onCardDragStart(card, col, ev) { + this._draggedCard = { + id: card.id, + source_model: card.source_model || "mrp.workorder", + source_wc_id: col.work_center_id, + }; + ev.dataTransfer.effectAllowed = "move"; + ev.dataTransfer.setData("text/plain", String(card.id)); + // Add ghost class to the dragged card after a tick (so the drag image isn't affected) + requestAnimationFrame(() => { + if (ev.target && ev.target.classList) { + ev.target.classList.add("o_fp_dragging"); + } + }); + } + + onCardDragEnd(ev) { + this._draggedCard = null; + if (ev.target && ev.target.classList) { + ev.target.classList.remove("o_fp_dragging"); + } + // Remove any lingering drop-target highlights + document.querySelectorAll(".o_fp_drop_target").forEach((el) => { + el.classList.remove("o_fp_drop_target"); + }); + } + + onColDragOver(col, ev) { + ev.preventDefault(); + ev.dataTransfer.dropEffect = "move"; + const body = ev.currentTarget; + if (body && !body.classList.contains("o_fp_drop_target")) { + body.classList.add("o_fp_drop_target"); + } + } + + onColDragLeave(col, ev) { + // Only remove highlight if we actually left the column body + // (not just hovering over a child element) + const body = ev.currentTarget; + if (body && !body.contains(ev.relatedTarget)) { + body.classList.remove("o_fp_drop_target"); + } + } + + async onColDrop(col, ev) { + ev.preventDefault(); + const body = ev.currentTarget; + if (body) { + body.classList.remove("o_fp_drop_target"); + } + + const dragged = this._draggedCard; + if (!dragged) { + return; + } + // No-op if dropped on the same column + if (dragged.source_wc_id === col.work_center_id) { + this._draggedCard = null; + return; + } + + try { + const result = await rpc("/fp/shopfloor/plant_overview/move_card", { + card_id: dragged.id, + source_model: dragged.source_model, + target_workcenter_id: col.work_center_id, + }); + if (result && result.ok) { + this.notification.add( + `Moved to ${col.work_center_name}`, + { type: "success" }, + ); + await this.loadData(); + } else { + this.notification.add( + 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) { diff --git a/fusion-plating/fusion_plating_shopfloor/static/src/scss/plant_overview.scss b/fusion-plating/fusion_plating_shopfloor/static/src/scss/plant_overview.scss index df5fee58..cbaa70ee 100644 --- a/fusion-plating/fusion_plating_shopfloor/static/src/scss/plant_overview.scss +++ b/fusion-plating/fusion_plating_shopfloor/static/src/scss/plant_overview.scss @@ -161,8 +161,8 @@ } .o_fp_po_col_count { - background: var(--bs-secondary-bg); - color: var(--bs-body-color); + background: var(--bs-secondary-color); + color: #fff; font-size: 0.75rem; min-width: 24px; text-align: center; @@ -173,6 +173,15 @@ overflow-y: auto; padding: 8px; flex: 1; + transition: background-color 0.15s, border-color 0.15s; + border: 2px solid transparent; + border-radius: 0 0 10px 10px; + + // Drop target highlight when dragging a card over this column + &.o_fp_drop_target { + background-color: color-mix(in srgb, var(--o-action) 8%, transparent); + border-color: color-mix(in srgb, var(--o-action) 40%, transparent); + } } // ---- Card ------------------------------------------------------------------- @@ -183,18 +192,31 @@ border-radius: 8px; padding: 10px 12px; margin-bottom: 8px; - cursor: pointer; - transition: box-shadow 0.15s, transform 0.1s; + cursor: grab; + box-shadow: 0 1px 3px color-mix(in srgb, var(--bs-body-color) 8%, transparent); + transition: box-shadow 0.15s, transform 0.1s, opacity 0.15s; &:hover { - box-shadow: 0 2px 8px color-mix(in srgb, var(--bs-body-color) 12%, transparent); + box-shadow: 0 3px 10px color-mix(in srgb, var(--bs-body-color) 14%, transparent); transform: translateY(-1px); } + &:active { + cursor: grabbing; + } + &:last-child { margin-bottom: 0; } + // Dragging ghost state + &.o_fp_dragging { + opacity: 0.4; + border-style: dashed; + box-shadow: none; + transform: none; + } + // State variants &.o_fp_card_progress { border-left: 4px solid var(--bs-warning); diff --git a/fusion-plating/fusion_plating_shopfloor/static/src/xml/plant_overview.xml b/fusion-plating/fusion_plating_shopfloor/static/src/xml/plant_overview.xml index 9ffe704f..e41e55fa 100644 --- a/fusion-plating/fusion_plating_shopfloor/static/src/xml/plant_overview.xml +++ b/fusion-plating/fusion_plating_shopfloor/static/src/xml/plant_overview.xml @@ -72,8 +72,11 @@ - -