fusion_plating_shopfloor: card styling fix + drag & drop between work centres (v19.0.1.1.0)

Cards now have visible borders and elevation shadow in both light/dark
mode. Column count badge restored to high-contrast white-on-gray.

Added HTML5 drag & drop: users can drag work order cards between work
centre columns. Backend endpoint writes workcenter_id on mrp.workorder.
Drop target columns highlight with the action colour.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-12 13:49:47 -04:00
parent a8eacc94bc
commit ccfae66975
4 changed files with 169 additions and 7 deletions

View File

@@ -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.

View File

@@ -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) {

View File

@@ -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);

View File

@@ -72,8 +72,11 @@
</span>
</div>
<!-- Cards -->
<div class="o_fp_po_col_body">
<!-- Cards (drop zone) -->
<div class="o_fp_po_col_body"
t-on-dragover="(ev) => this.onColDragOver(col, ev)"
t-on-dragleave="(ev) => this.onColDragLeave(col, ev)"
t-on-drop="(ev) => this.onColDrop(col, ev)">
<t t-if="!col.cards.length">
<div class="o_fp_po_no_cards text-muted text-center py-3">
<i class="fa fa-check-circle"/> Clear
@@ -81,6 +84,12 @@
</t>
<t t-foreach="col.cards" t-as="card" t-key="card.id">
<div t-att-class="'o_fp_po_card ' + getStateClass(card.state) + (card.priority === '2' ? ' o_fp_po_card_hot' : card.priority === '1' ? ' o_fp_po_card_urgent' : '')"
draggable="true"
t-att-data-card-id="card.id"
t-att-data-source-model="card.source_model"
t-att-data-source-wc="col.work_center_id"
t-on-dragstart="(ev) => this.onCardDragStart(card, col, ev)"
t-on-dragend="(ev) => this.onCardDragEnd(ev)"
t-on-click="() => this.onCardClick(card)">
<!-- Top row: product image + customer + step badge -->