fix(shopfloor): Manager Desk WO row layout — proper info stack + action group

Screenshot showed the new WO row was broken:
  • Kind chip text clipped ("Mas" instead of "Mask", "Rac" instead of
    "Racking")
  • WO name truncated to first 4 chars
  • The wet WO had no info column at all — kind chip + name pushed
    off-screen by the tank picker
  • "Needs:" chip showed as just an exclamation icon with "N" cut off
  • Take Over and Open WO buttons unevenly sized

Root cause: `.o_fp_mgr_wo_info` carried `nowrap + ellipsis` from the
old single-line design, but the new template stacks kind chip + name +
meta + needs across multiple lines. Plus the rigid grid
(1fr auto auto auto auto) gave the info column whatever the dropdowns
left over — usually nothing.

**Layout rewrite** — flex with wrap instead of grid:
  • `.o_fp_mgr_wo_row` — flex row, info on left, actions on right,
    wraps to two rows on narrow viewports.
  • `.o_fp_mgr_wo_info` — `flex: 1 1 280px` so it grows but never
    narrower than 280px. Contains a vertical stack: title row
    (badge + name) → meta row (workcenter / role / equipment chips)
    → needs row (yellow chip if anything missing).
  • `.o_fp_mgr_wo_actions` — `flex: 0 0 auto` with its own gap, so
    pickers + buttons align cleanly to the right.
  • Kind chip can wrap to its full label; meta row uses `flex-wrap`
    so equipment hints don't get clipped.
  • Take Over collapses to icon-only with title tooltip — the row
    was getting too wide on the wet kind (which adds the tank picker).

**Other tweaks**
  • Added `tank_id` to the controller payload so the tank picker
    pre-selects the current tank (was missing on the previous
    "current tank" highlight).

@720px the action group stacks below the info — pickers go full-width,
buttons get `min-height: $fp-touch-min` for thumb tap.

Asset cache cleared as part of the deploy so the SCSS recompiles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-19 13:05:27 -04:00
parent 8fc864623b
commit e01a2a0e35
3 changed files with 112 additions and 63 deletions

View File

@@ -116,6 +116,7 @@ class FpManagerDashboardController(http.Controller):
'duration_expected': w.duration_expected or 0, 'duration_expected': w.duration_expected or 0,
'bath': w.x_fc_bath_id.name or '', 'bath': w.x_fc_bath_id.name or '',
'tank': w.x_fc_tank_id.name or '', 'tank': w.x_fc_tank_id.name or '',
'tank_id': w.x_fc_tank_id.id if w.x_fc_tank_id else False,
'priority': w.x_fc_priority or '0', 'priority': w.x_fc_priority or '0',
'assigned_user_id': ( 'assigned_user_id': (
w.x_fc_assigned_user_id.id w.x_fc_assigned_user_id.id

View File

@@ -437,36 +437,66 @@
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// WO row inside expanded card // WO row inside expanded card
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// WO row = info column (vertical stack) + actions column (pickers + buttons)
// Flex with wrap so narrow viewports drop actions below the info naturally
// instead of squishing everything into a single broken grid line.
.o_fp_mgr_wo_row { .o_fp_mgr_wo_row {
display: grid; display: flex;
grid-template-columns: 1fr auto auto auto auto; flex-wrap: wrap;
gap: $fp-space-2; gap: $fp-space-3;
align-items: center; align-items: flex-start;
padding: $fp-space-2 $fp-space-3; justify-content: space-between;
padding: $fp-space-3;
background-color: $fp-card; background-color: $fp-card;
border: 1px solid #{$fp-border}; border: 1px solid #{$fp-border};
border-radius: $fp-radius-sm; border-radius: $fp-radius-sm;
font-size: $fp-text-sm; font-size: $fp-text-sm;
}
@media (max-width: 1400px) {
grid-template-columns: 1fr auto auto;
.o_fp_mgr_picker:nth-of-type(1) { grid-column: 1 / -1; }
.o_fp_mgr_picker:nth-of-type(2) { grid-column: 1 / -1; }
}
@media (max-width: 600px) {
grid-template-columns: 1fr;
.o_fp_mgr_picker { max-width: 100% !important; width: 100%; }
.btn { min-height: $fp-touch-min; }
}
}
.o_fp_mgr_wo_info { .o_fp_mgr_wo_info {
min-width: 0; flex: 1 1 280px; // grows but never narrower than 280px
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; // allows children to shrink properly
font-weight: $fp-weight-semibold; display: flex;
flex-direction: column;
gap: $fp-space-1;
color: $fp-ink; color: $fp-ink;
// Title row — kind badge + WO name + step number
.o_fp_mgr_wo_title {
display: flex;
align-items: center;
gap: $fp-space-2;
flex-wrap: wrap;
font-weight: $fp-weight-semibold;
font-size: $fp-text-base;
line-height: 1.25;
}
// Meta row — workcenter / role / set equipment
.o_fp_mgr_wo_meta {
display: flex;
align-items: center;
gap: $fp-space-2;
flex-wrap: wrap;
font-size: $fp-text-xs;
color: $fp-ink-mute;
i { margin-right: 2px; }
}
// Chip row — what's still missing for the manager to set
.o_fp_mgr_wo_needs {
margin-top: 2px;
}
} }
.o_fp_mgr_wo_actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: $fp-space-2;
flex: 0 0 auto;
}
.o_fp_mgr_picker { .o_fp_mgr_picker {
min-width: 140px; max-width: 220px; min-width: 180px;
min-height: 40px; min-height: 40px;
padding: 0 $fp-space-3; padding: 0 $fp-space-3;
border: 1px solid #{$fp-border}; border: 1px solid #{$fp-border};
@@ -485,10 +515,18 @@
font-weight: $fp-weight-semibold; font-weight: $fp-weight-semibold;
background-color: $fp-card-soft; background-color: $fp-card-soft;
color: $fp-ink; color: $fp-ink;
white-space: nowrap;
transition: filter $fp-dur-fast $fp-ease; transition: filter $fp-dur-fast $fp-ease;
&:hover { filter: brightness(0.95); } &:hover { filter: brightness(0.95); }
} }
@media (max-width: 720px) {
// Stack info + actions vertically on narrow viewports
.o_fp_mgr_wo_actions { width: 100%; }
.o_fp_mgr_picker { flex: 1 1 100%; max-width: 100%; }
.o_fp_mgr_wo_row .btn { min-height: $fp-touch-min; }
}
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
// Status chips (reused) // Status chips (reused)

View File

@@ -134,57 +134,67 @@
t-if="state.expandedMoId === card.mo_id or state.mode === 'detailed'"> t-if="state.expandedMoId === card.mo_id or state.mode === 'detailed'">
<t t-foreach="card.wos" t-as="wo" t-key="wo.id"> <t t-foreach="card.wos" t-as="wo" t-key="wo.id">
<div class="o_fp_mgr_wo_row"> <div class="o_fp_mgr_wo_row">
<!-- LEFT: information stack (badge, name, meta, needs) -->
<div class="o_fp_mgr_wo_info"> <div class="o_fp_mgr_wo_info">
<div> <div class="o_fp_mgr_wo_title">
<span t-attf-class="o_fp_chip o_fp_chip_kind o_fp_chip_kind_{{ wo.wo_kind }}" <span t-attf-class="o_fp_chip o_fp_chip_kind o_fp_chip_kind_{{ wo.wo_kind }}"
t-esc="wo.wo_kind_label || wo.wo_kind"/> t-esc="wo.wo_kind_label || wo.wo_kind"/>
<strong class="ms-1" t-esc="wo.name"/> <span t-esc="wo.name"/>
</div> </div>
<div class="text-muted small mt-1"> <div class="o_fp_mgr_wo_meta">
<t t-esc="wo.workcenter"/> <span><i class="fa fa-cog"/><t t-esc="wo.workcenter"/></span>
<t t-if="wo.role_name"> · <i class="fa fa-id-badge"/> <t t-esc="wo.role_name"/></t> <span t-if="wo.role_name">· <i class="fa fa-id-badge"/><t t-esc="wo.role_name"/></span>
<t t-if="wo.bath"> · <i class="fa fa-flask"/> <t t-esc="wo.bath"/></t> <span t-if="wo.bath">· <i class="fa fa-flask"/><t t-esc="wo.bath"/></span>
<t t-if="wo.oven"> · <i class="fa fa-fire"/> <t t-esc="wo.oven"/></t> <span t-if="wo.oven">· <i class="fa fa-fire"/><t t-esc="wo.oven"/></span>
<t t-if="wo.rack"> · <i class="fa fa-th"/> <t t-esc="wo.rack"/></t> <span t-if="wo.rack">· <i class="fa fa-th"/><t t-esc="wo.rack"/></span>
<t t-if="wo.masking_material"> · <i class="fa fa-tag"/> <t t-esc="wo.masking_material"/></t> <span t-if="wo.masking_material">· <i class="fa fa-tag"/><t t-esc="wo.masking_material"/></span>
</div> </div>
<div t-if="wo.missing_for_release" <div t-if="wo.missing_for_release"
class="o_fp_chip o_fp_chip_warning mt-1"> class="o_fp_mgr_wo_needs">
<i class="fa fa-exclamation-circle"/> Needs: <t t-esc="wo.missing_for_release"/> <span class="o_fp_chip o_fp_chip_warning">
<i class="fa fa-exclamation-circle me-1"/>
Needs: <t t-esc="wo.missing_for_release"/>
</span>
</div> </div>
</div> </div>
<select class="o_fp_mgr_picker"
t-on-change="(ev) => this.onAssignWorker(wo, ev.target.value)"> <!-- RIGHT: action group (pickers + buttons) -->
<option value="">— Assign worker —</option> <div class="o_fp_mgr_wo_actions">
<t t-foreach="operatorsForWO(wo)" t-as="op" t-key="op.id"> <select class="o_fp_mgr_picker"
<option t-att-value="op.id" t-on-change="(ev) => this.onAssignWorker(wo, ev.target.value)">
t-att-selected="wo.assigned_user_id === op.id" <option value="">— Assign worker —</option>
t-att-data-bucket="op.bucket"> <t t-foreach="operatorsForWO(wo)" t-as="op" t-key="op.id">
<t t-if="op.is_clocked_in"></t> <option t-att-value="op.id"
<t t-else=""></t> t-att-selected="wo.assigned_user_id === op.id"
<t t-esc="' ' + op.name + operatorBadge(op)"/> t-att-data-bucket="op.bucket">
</option> <t t-if="op.is_clocked_in"></t>
</t> <t t-else=""></t>
</select> <t t-esc="' ' + op.name + operatorBadge(op)"/>
<select t-if="wo.wo_kind === 'wet'" </option>
class="o_fp_mgr_picker" </t>
t-on-change="(ev) => this.onAssignTank(wo, ev.target.value)"> </select>
<option value="">— Tank —</option> <select t-if="wo.wo_kind === 'wet'"
<t t-foreach="state.overview.tanks" t-as="tnk" t-key="tnk.id"> class="o_fp_mgr_picker"
<option t-att-value="tnk.id"> t-on-change="(ev) => this.onAssignTank(wo, ev.target.value)">
<t t-esc="tnk.name"/> <option value="">— Tank —</option>
<t t-if="tnk.current_bath"> · <t t-esc="tnk.current_bath"/></t> <t t-foreach="state.overview.tanks" t-as="tnk" t-key="tnk.id">
</option> <option t-att-value="tnk.id"
</t> t-att-selected="wo.tank_id === tnk.id">
</select> <t t-esc="tnk.name"/>
<button class="btn" <t t-if="tnk.current_bath"> · <t t-esc="tnk.current_bath"/></t>
t-on-click="() => this.onTakeOver(wo)"> </option>
<i class="fa fa-user"/> Take Over </t>
</button> </select>
<button class="btn" <button class="btn"
t-on-click="() => this.openRecord('mrp.workorder', wo.id)"> t-on-click="() => this.onTakeOver(wo)"
Open WO title="Assign yourself">
</button> <i class="fa fa-user"/>
</button>
<button class="btn"
t-on-click="() => this.openRecord('mrp.workorder', wo.id)">
Open WO
</button>
</div>
</div> </div>
</t> </t>
</div> </div>