fix(plant-overview): real drop insertion indicator + small logo back

Three direct fixes responding to user feedback:

1. Drag-drop "simulation" — now works like Trello/Linear. As the
   cursor moves over a column, a live DOM placeholder node is
   INJECTED into the card list at the exact position the dragged
   card will drop. The placeholder is a 4px pulsing accent-coloured
   bar with a soft glow ring. Slides smoothly between cards as the
   cursor moves. Column body also gets a tinted background + inset
   accent outline for the "whole column is receptive" cue.

   Previous version only tinted the column — no indicator of WHERE
   the card would land. The new approach actually mimics the physical
   gesture: cards visually make room for the incoming card.

2. Customer logo restored at 32×32px.
   Removing it was the wrong call. It's back now as a small
   thumbnail avatar (rounded 10px corners, soft border, object-fit
   contain so wide logos don't squish). Sits to the left of the
   customer name in the card top row. Fallback icon for customers
   without a logo. Takes the same space as the step badge on the
   right — compact and organised.

3. Module version bumped 19.0.1.0.0 → 19.0.2.0.0 so the asset
   bundle content hash changes. The new compiled CSS is served at
   /web/assets/022171c/web.assets_backend.min.css (previously
   /web/assets/278b43c/...). Fresh URL forces browser to refetch —
   this is what was causing the "still no border" complaint.

Verified in compiled CSS: o_fp_po_card_avatar, o_fp_po_drop_placeholder,
o_fp_placeholder_pulse keyframes, o_fp_drop_target — all present.
Zero SCSS warnings. Module upgrade clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-18 19:06:40 -04:00
parent 83a999afad
commit 2588a2b651
4 changed files with 99 additions and 25 deletions

View File

@@ -5,7 +5,7 @@
{
'name': 'Fusion Plating — Shop Floor',
'version': '19.0.1.0.0',
'version': '19.0.2.0.0',
'category': 'Manufacturing/Plating',
'summary': 'Shop-floor tablet stations, QR scanning, bake window enforcer, '
'first-piece inspection gates.',

View File

@@ -95,12 +95,32 @@ export class PlantOverview extends Component {
}
// ----- Drag & drop --------------------------------------------------------
//
// A real insertion placeholder is injected into the column body as the
// card is dragged. The placeholder slides between cards to show exactly
// where the dragged card will land when released. The placeholder is a
// plain DOM node (not a reactive state field) so mouseover updates don't
// trigger OWL re-renders.
_getOrCreatePlaceholder() {
let node = document.querySelector(".o_fp_po_drop_placeholder");
if (!node) {
node = document.createElement("div");
node.className = "o_fp_po_drop_placeholder";
}
return node;
}
_removePlaceholder() {
document.querySelectorAll(".o_fp_po_drop_placeholder").forEach((el) => el.remove());
}
onCardDragStart(card, col, ev) {
this._draggedCard = {
id: card.id,
source_model: card.source_model || "mrp.workorder",
source_wc_id: col.work_center_id,
el: ev.target,
};
ev.dataTransfer.effectAllowed = "move";
ev.dataTransfer.setData("text/plain", String(card.id));
@@ -113,31 +133,55 @@ export class PlantOverview extends Component {
}
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");
});
this._removePlaceholder();
this._draggedCard = null;
}
onColDragOver(col, ev) {
ev.preventDefault();
ev.dataTransfer.dropEffect = "move";
const body = ev.currentTarget;
if (body && !body.classList.contains("o_fp_drop_target")) {
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 insert the
// placeholder above or below it. This gives the manager a
// clear "card will land HERE" preview between existing cards.
const placeholder = this._getOrCreatePlaceholder();
const cards = [...body.querySelectorAll(
".o_fp_po_card:not(.o_fp_dragging):not(.o_fp_po_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) {
// 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");
// Only remove placeholder if we left the body entirely —
// otherwise the child card enter fires dragleave on the body
this._removePlaceholder();
}
}
@@ -147,6 +191,7 @@ export class PlantOverview extends Component {
if (body) {
body.classList.remove("o_fp_drop_target");
}
this._removePlaceholder();
const dragged = this._draggedCard;
if (!dragged) {

View File

@@ -173,28 +173,35 @@
overflow-y: auto;
padding: $fp-space-3;
flex: 1;
transition: background-color $fp-dur $fp-ease;
transition: background-color $fp-dur $fp-ease,
box-shadow $fp-dur $fp-ease;
// Drop zone highlight solid tinted background + dashed outline
// so the manager sees EXACTLY where the card will land.
// Drop-zone highlight: soft accent tint + inset outline. Shows the
// whole column is receptive. The actual insertion point is drawn
// by the real placeholder node (inserted by JS) between cards.
&.o_fp_drop_target {
background-color: color-mix(in srgb, #{$fp-accent} 8%, transparent);
box-shadow: inset 0 0 0 2px color-mix(in srgb, #{$fp-accent} 45%, transparent);
// Placeholder bar at the bottom of the list — that's where
// the card will drop when released.
&::after {
content: "";
display: block;
height: 56px;
margin-top: $fp-space-2;
border: 2px dashed color-mix(in srgb, #{$fp-accent} 55%, transparent);
border-radius: $fp-radius-md;
background-color: color-mix(in srgb, #{$fp-accent} 6%, transparent);
}
background-color: color-mix(in srgb, #{$fp-accent} 6%, transparent);
box-shadow: inset 0 0 0 2px color-mix(in srgb, #{$fp-accent} 40%, transparent);
}
}
// Insertion placeholder — a live DOM node inserted between cards as
// the cursor moves so the manager sees exactly where the drop will
// slot in. 4px solid accent bar + small glow + smooth slide.
.o_fp_po_drop_placeholder {
height: 4px;
margin: $fp-space-2 0;
background-color: $fp-accent;
border-radius: 4px;
box-shadow: 0 0 0 3px color-mix(in srgb, #{$fp-accent} 25%, transparent),
0 2px 8px color-mix(in srgb, #{$fp-accent} 35%, transparent);
animation: o_fp_placeholder_pulse 1s ease-in-out infinite alternate;
}
@keyframes o_fp_placeholder_pulse {
from { opacity: 0.75; transform: scaleY(1); }
to { opacity: 1; transform: scaleY(1.4); }
}
.o_fp_po_no_cards {
padding: $fp-space-5 $fp-space-3;
text-align: center;
@@ -256,6 +263,21 @@
display: flex; align-items: center; gap: $fp-space-2;
margin-bottom: $fp-space-1;
// Small customer avatar — 32px thumbnail. Just identifies the
// customer at a glance; not a billboard.
.o_fp_po_card_avatar {
flex-shrink: 0;
width: 32px; height: 32px;
border-radius: $fp-radius-sm;
object-fit: contain;
background-color: $fp-card-soft;
border: 1px solid var(--bs-border-color);
padding: 2px;
}
.o_fp_po_card_avatar_blank {
display: flex; align-items: center; justify-content: center;
color: $fp-ink-mute; font-size: 0.95rem;
}
.o_fp_po_card_title {
flex: 1; min-width: 0;
font-size: $fp-text-base;

View File

@@ -92,9 +92,16 @@
t-on-dragend="(ev) => this.onCardDragEnd(ev)"
t-on-click="() => this.onCardClick(card)">
<!-- Top row: customer name + step badge
(logo removed — it was too dominant) -->
<!-- Top row: small customer avatar + name + step pill -->
<div class="o_fp_po_card_top">
<img t-if="card.customer_logo_url"
t-att-src="card.customer_logo_url"
class="o_fp_po_card_avatar"
alt=""/>
<div class="o_fp_po_card_avatar o_fp_po_card_avatar_blank"
t-else="">
<i class="fa fa-building"/>
</div>
<div class="o_fp_po_card_title">
<strong t-esc="card.customer_name || 'Walk-In'"/>
</div>