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
|
# 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')
|
@http.route('/fp/shopfloor/plant_overview', type='jsonrpc', auth='user')
|
||||||
def plant_overview(self, facility_id=None, search=None):
|
def plant_overview(self, facility_id=None, search=None):
|
||||||
"""Return work orders grouped by work centre for the plant overview.
|
"""Return work orders grouped by work centre for the plant overview.
|
||||||
|
|||||||
@@ -94,6 +94,97 @@ export class PlantOverview extends Component {
|
|||||||
this.loadData();
|
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 ------------------------------------------------------
|
// ----- Card actions ------------------------------------------------------
|
||||||
|
|
||||||
onCardClick(card) {
|
onCardClick(card) {
|
||||||
|
|||||||
@@ -161,8 +161,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.o_fp_po_col_count {
|
.o_fp_po_col_count {
|
||||||
background: var(--bs-secondary-bg);
|
background: var(--bs-secondary-color);
|
||||||
color: var(--bs-body-color);
|
color: #fff;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
min-width: 24px;
|
min-width: 24px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -173,6 +173,15 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
flex: 1;
|
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 -------------------------------------------------------------------
|
// ---- Card -------------------------------------------------------------------
|
||||||
@@ -183,18 +192,31 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 10px 12px;
|
padding: 10px 12px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
cursor: pointer;
|
cursor: grab;
|
||||||
transition: box-shadow 0.15s, transform 0.1s;
|
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 {
|
&: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);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dragging ghost state
|
||||||
|
&.o_fp_dragging {
|
||||||
|
opacity: 0.4;
|
||||||
|
border-style: dashed;
|
||||||
|
box-shadow: none;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
// State variants
|
// State variants
|
||||||
&.o_fp_card_progress {
|
&.o_fp_card_progress {
|
||||||
border-left: 4px solid var(--bs-warning);
|
border-left: 4px solid var(--bs-warning);
|
||||||
|
|||||||
@@ -72,8 +72,11 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Cards -->
|
<!-- Cards (drop zone) -->
|
||||||
<div class="o_fp_po_col_body">
|
<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">
|
<t t-if="!col.cards.length">
|
||||||
<div class="o_fp_po_no_cards text-muted text-center py-3">
|
<div class="o_fp_po_no_cards text-muted text-center py-3">
|
||||||
<i class="fa fa-check-circle"/> Clear
|
<i class="fa fa-check-circle"/> Clear
|
||||||
@@ -81,6 +84,12 @@
|
|||||||
</t>
|
</t>
|
||||||
<t t-foreach="col.cards" t-as="card" t-key="card.id">
|
<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' : '')"
|
<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)">
|
t-on-click="() => this.onCardClick(card)">
|
||||||
|
|
||||||
<!-- Top row: product image + customer + step badge -->
|
<!-- Top row: product image + customer + step badge -->
|
||||||
|
|||||||
Reference in New Issue
Block a user