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:
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
Reference in New Issue
Block a user