feat(plant_kanban): polish KPI strip + chips + toolbar buttons
User feedback after first deploy: 7 KPI tiles wrapped to second line
(grid was repeat(5, 1fr) but I had added 2 new ones), and the
controls felt cramped.
Layout fix:
- .kpi-strip grid: repeat(5, 1fr) → repeat(8, 1fr) so the row stays
one line and there's room for the new Awaiting QC tile.
Missing KPI added:
- Awaiting QC — fp.job.card_state='awaiting_qc' count. Operators
couldn't see when QC was blocking job close from the KPI strip
(only visible inside the column). Server-side count + filter
clause + matching filter chip.
Visual polish (all light + dark via existing token system):
- KPI tiles: padding 6→10px, value font 20→26px, label font 9→10px,
subtle 135deg linear-gradient bg per kind (urgent/warn/good/qc),
hover lifts the tile with translateY + shadow.
- Filter chips: padding 4/12→7/16px, font 11→13px, gradient bg,
active state has gradient blue + shadow.
- Search input: padding 5/10→9/14px, font 12→14px, focus ring.
- Toolbar buttons (Station/All Plant/Manager/Scan QR/Hand Off):
padding 5/10→8/14px, font 12→14px, gradients, hover lift.
Dark mode handled automatically — all gradients reference
$plant-* tokens which already have @if $o-webclient-color-scheme ==
dark global overrides in _plant_tokens.scss.
Version bump fusion_plating_shopfloor 19.0.34.0.0 → 19.0.34.1.0.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{
|
{
|
||||||
'name': 'Fusion Plating — Shop Floor',
|
'name': 'Fusion Plating — Shop Floor',
|
||||||
'version': '19.0.34.0.0',
|
'version': '19.0.34.1.0',
|
||||||
'category': 'Manufacturing/Plating',
|
'category': 'Manufacturing/Plating',
|
||||||
'summary': 'Shop-floor tablet stations, QR scanning, bake window enforcer.',
|
'summary': 'Shop-floor tablet stations, QR scanning, bake window enforcer.',
|
||||||
'description': """
|
'description': """
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ class PlantKanbanController(http.Controller):
|
|||||||
)))
|
)))
|
||||||
if filters.get('mine'):
|
if filters.get('mine'):
|
||||||
domain.append(('card_state', 'in', ('ready_mine', 'running_mine')))
|
domain.append(('card_state', 'in', ('ready_mine', 'running_mine')))
|
||||||
|
if filters.get('awaiting_qc'):
|
||||||
|
domain.append(('card_state', '=', 'awaiting_qc'))
|
||||||
# Spec 2026-05-25 — post-shop state filter chips
|
# Spec 2026-05-25 — post-shop state filter chips
|
||||||
if filters.get('awaiting_cert'):
|
if filters.get('awaiting_cert'):
|
||||||
domain.append(('state', '=', 'awaiting_cert'))
|
domain.append(('state', '=', 'awaiting_cert'))
|
||||||
@@ -145,6 +147,9 @@ class PlantKanbanController(http.Controller):
|
|||||||
'on_hold': sum(
|
'on_hold': sum(
|
||||||
1 for j in jobs if j.card_state == 'on_hold'
|
1 for j in jobs if j.card_state == 'on_hold'
|
||||||
),
|
),
|
||||||
|
'awaiting_qc': sum(
|
||||||
|
1 for j in jobs if j.card_state == 'awaiting_qc'
|
||||||
|
),
|
||||||
# Spec 2026-05-25 — post-shop state KPIs
|
# Spec 2026-05-25 — post-shop state KPIs
|
||||||
'awaiting_cert': sum(
|
'awaiting_cert': sum(
|
||||||
1 for j in jobs if j.state == 'awaiting_cert'
|
1 for j in jobs if j.state == 'awaiting_cert'
|
||||||
|
|||||||
@@ -1,20 +1,27 @@
|
|||||||
// _filter_chip.scss — depends on _plant_tokens.scss
|
// _filter_chip.scss — depends on _plant_tokens.scss
|
||||||
|
// 2026-05-25: bigger touch target + gradient bg.
|
||||||
|
|
||||||
.o_fp_filter_chip {
|
.o_fp_filter_chip {
|
||||||
padding: 4px 12px;
|
padding: 7px 16px;
|
||||||
font-size: 11px;
|
font-size: 13px;
|
||||||
background: $plant-card-bg;
|
font-weight: 500;
|
||||||
|
background: linear-gradient(135deg, $plant-card-bg 0%, $plant-bg 100%);
|
||||||
border: 1px solid $plant-card-border;
|
border: 1px solid $plant-card-border;
|
||||||
border-radius: 14px;
|
border-radius: 18px;
|
||||||
color: $plant-muted;
|
color: $plant-muted;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
transition: transform 0.1s ease, box-shadow 0.1s ease;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
background: #1d4ed8;
|
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
|
||||||
border-color: #1d4ed8;
|
border-color: #1d4ed8;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
box-shadow: 0 2px 4px rgba(29, 78, 216, 0.25);
|
||||||
|
}
|
||||||
|
&:hover:not(.active) {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.06);
|
||||||
}
|
}
|
||||||
&:hover:not(.active) { background: $plant-bg; }
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,66 @@
|
|||||||
// _kpi_tile.scss — depends on _plant_tokens.scss
|
// _kpi_tile.scss — depends on _plant_tokens.scss
|
||||||
|
//
|
||||||
|
// 2026-05-25: redesigned for the 8-tile row. Narrower individual width
|
||||||
|
// (grid handles that), more vertical presence via padding + larger
|
||||||
|
// typography, subtle 135deg gradients per kind that work in both light
|
||||||
|
// and dark modes (gradient stops use $plant-* tokens — dark variants
|
||||||
|
// flip automatically via the @if $o-webclient-color-scheme branch).
|
||||||
|
|
||||||
.o_fp_kpi_tile {
|
.o_fp_kpi_tile {
|
||||||
padding: 6px 10px;
|
padding: 10px 12px;
|
||||||
background: $plant-card-bg;
|
background: linear-gradient(135deg, $plant-card-bg 0%, $plant-bg 100%);
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
border: 1px solid $plant-card-border;
|
border: 1px solid $plant-card-border;
|
||||||
display: flex; flex-direction: column; gap: 1px;
|
display: flex; flex-direction: column; gap: 3px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.1s;
|
transition: transform 0.1s ease, box-shadow 0.1s ease, border-color 0.1s ease;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
color: $plant-text;
|
color: $plant-text;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
box-shadow: 0 1px 2px rgba(0,0,0,0.04);
|
||||||
|
min-width: 0; // grid-track minmax handles sizing; let label truncate
|
||||||
|
|
||||||
&:hover { background: $plant-bg; }
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
|
||||||
|
}
|
||||||
&.active {
|
&.active {
|
||||||
border-color: $plant-mine-border;
|
border-color: $plant-mine-border;
|
||||||
background: $plant-mine-bg;
|
background: linear-gradient(135deg, $plant-mine-bg 0%, $plant-card-bg 100%);
|
||||||
|
box-shadow: 0 2px 6px rgba(0,0,0,0.10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kind-specific subtle gradients. Each pulls from existing tokens
|
||||||
|
// so dark mode auto-flips via the @if $o-webclient-color-scheme
|
||||||
|
// == dark global override in _plant_tokens.scss.
|
||||||
|
&.urgent {
|
||||||
|
background: linear-gradient(135deg, $plant-card-bg 0%, $plant-hold-bg 100%);
|
||||||
|
.kpi-val { color: $plant-hold-border; }
|
||||||
|
}
|
||||||
|
&.warn {
|
||||||
|
background: linear-gradient(135deg, $plant-card-bg 0%, $plant-bake-bg 100%);
|
||||||
|
.kpi-val { color: $plant-idle-border; }
|
||||||
|
}
|
||||||
|
&.good {
|
||||||
|
background: linear-gradient(135deg, $plant-card-bg 0%, $plant-done-bg 100%);
|
||||||
|
.kpi-val { color: $plant-done-border; }
|
||||||
|
}
|
||||||
|
&.qc {
|
||||||
|
background: linear-gradient(135deg, $plant-card-bg 0%, $plant-qc-bg 100%);
|
||||||
|
.kpi-val { color: $plant-qc-border; }
|
||||||
}
|
}
|
||||||
&.urgent .kpi-val { color: $plant-hold-border; }
|
|
||||||
&.warn .kpi-val { color: $plant-idle-border; }
|
|
||||||
&.good .kpi-val { color: $plant-done-border; }
|
|
||||||
|
|
||||||
.kpi-val {
|
.kpi-val {
|
||||||
font-size: 20px; font-weight: 700;
|
font-size: 26px; font-weight: 700;
|
||||||
color: $plant-text; line-height: 1;
|
color: $plant-text; line-height: 1;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
.kpi-lbl {
|
.kpi-lbl {
|
||||||
font-size: 9px; font-weight: 600;
|
font-size: 10px; font-weight: 600;
|
||||||
color: $plant-muted;
|
color: $plant-muted;
|
||||||
text-transform: uppercase; letter-spacing: 0.04em;
|
text-transform: uppercase; letter-spacing: 0.04em;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,67 +34,87 @@
|
|||||||
.floor-title { font-size: 16px; font-weight: 700; }
|
.floor-title { font-size: 16px; font-weight: 700; }
|
||||||
.floor-controls { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
|
.floor-controls { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
|
||||||
|
|
||||||
|
// 2026-05-25 — toolbar buttons bigger + gradients for visual weight
|
||||||
.station-picker {
|
.station-picker {
|
||||||
padding: 5px 10px;
|
padding: 8px 14px;
|
||||||
background: $plant-mine-bg;
|
background: linear-gradient(135deg, $plant-mine-bg 0%, $plant-card-bg 100%);
|
||||||
border: 1px solid $plant-mine-border;
|
border: 1px solid $plant-mine-border;
|
||||||
border-radius: 6px;
|
border-radius: 7px;
|
||||||
font-size: 12px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #856404;
|
color: $plant-text;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
||||||
}
|
}
|
||||||
.mode-toggle {
|
.mode-toggle {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
border: 1px solid $plant-card-border;
|
border: 1px solid $plant-card-border;
|
||||||
border-radius: 6px;
|
border-radius: 7px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
||||||
.mode-btn {
|
.mode-btn {
|
||||||
padding: 5px 12px;
|
padding: 8px 16px;
|
||||||
font-size: 12px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
background: $plant-card-bg;
|
background: linear-gradient(135deg, $plant-card-bg 0%, $plant-bg 100%);
|
||||||
color: $plant-muted;
|
color: $plant-muted;
|
||||||
border: 0;
|
border: 0;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-right: 1px solid $plant-card-border;
|
border-right: 1px solid $plant-card-border;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
&:last-child { border-right: 0; }
|
&:last-child { border-right: 0; }
|
||||||
&.active { background: #1d4ed8; color: #fff; }
|
&.active {
|
||||||
|
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
&:hover:not(.active) { background: $plant-bg; }
|
&:hover:not(.active) { background: $plant-bg; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.toolbar-btn {
|
.toolbar-btn {
|
||||||
padding: 5px 10px;
|
padding: 8px 14px;
|
||||||
font-size: 12px;
|
font-size: 14px;
|
||||||
background: $plant-card-bg;
|
font-weight: 500;
|
||||||
|
background: linear-gradient(135deg, $plant-card-bg 0%, $plant-bg 100%);
|
||||||
border: 1px solid $plant-card-border;
|
border: 1px solid $plant-card-border;
|
||||||
border-radius: 6px;
|
border-radius: 7px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: $plant-text;
|
color: $plant-text;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
&:hover { background: $plant-bg; }
|
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
|
||||||
|
transition: transform 0.1s ease, box-shadow 0.1s ease;
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
|
||||||
|
}
|
||||||
&.handoff {
|
&.handoff {
|
||||||
background: #ffc107;
|
background: linear-gradient(135deg, #ffd966 0%, #ffc107 100%);
|
||||||
border-color: #d39e00;
|
border-color: #d39e00;
|
||||||
color: #856404;
|
color: #5e4400;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.kpi-strip { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; }
|
// 8 tiles — Work Orders, At My Station, Bakes Due, On Hold,
|
||||||
|
// Awaiting QC, Awaiting CoC, Ready to Ship, Overdue.
|
||||||
|
.kpi-strip { display: grid; grid-template-columns: repeat(8, 1fr); gap: 8px; }
|
||||||
|
|
||||||
.search-row { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
|
.search-row { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
|
||||||
.search-input {
|
.search-input {
|
||||||
flex: 1; min-width: 200px;
|
flex: 1; min-width: 220px;
|
||||||
padding: 5px 10px;
|
padding: 9px 14px;
|
||||||
border: 1px solid $plant-card-border;
|
border: 1px solid $plant-card-border;
|
||||||
border-radius: 6px;
|
border-radius: 8px;
|
||||||
background: $plant-card-bg;
|
background: $plant-card-bg;
|
||||||
color: $plant-text;
|
color: $plant-text;
|
||||||
font-size: 12px;
|
font-size: 14px;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
box-shadow: 0 1px 2px rgba(0,0,0,0.04) inset;
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #1d4ed8;
|
||||||
|
box-shadow: 0 0 0 3px rgba(29, 78, 216, 0.15);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Board: fixed-width columns with horizontal scroll on smaller
|
// Board: fixed-width columns with horizontal scroll on smaller
|
||||||
|
|||||||
@@ -47,6 +47,11 @@
|
|||||||
kind="'urgent'"
|
kind="'urgent'"
|
||||||
active="!!state.filters.on_hold"
|
active="!!state.filters.on_hold"
|
||||||
onClick="() => this.toggleFilter('on_hold')"/>
|
onClick="() => this.toggleFilter('on_hold')"/>
|
||||||
|
<FpKpiTile value="state.data.kpis.awaiting_qc"
|
||||||
|
label="'Awaiting QC'"
|
||||||
|
kind="'qc'"
|
||||||
|
active="!!state.filters.awaiting_qc"
|
||||||
|
onClick="() => this.toggleFilter('awaiting_qc')"/>
|
||||||
<!-- Spec 2026-05-25 — post-shop state tiles -->
|
<!-- Spec 2026-05-25 — post-shop state tiles -->
|
||||||
<FpKpiTile value="state.data.kpis.awaiting_cert"
|
<FpKpiTile value="state.data.kpis.awaiting_cert"
|
||||||
label="'Awaiting CoC'"
|
label="'Awaiting CoC'"
|
||||||
@@ -89,6 +94,9 @@
|
|||||||
<FpFilterChip label="'FAIR'"
|
<FpFilterChip label="'FAIR'"
|
||||||
active="!!state.filters.fair"
|
active="!!state.filters.fair"
|
||||||
onToggle="() => this.toggleFilter('fair')"/>
|
onToggle="() => this.toggleFilter('fair')"/>
|
||||||
|
<FpFilterChip label="'Awaiting QC'"
|
||||||
|
active="!!state.filters.awaiting_qc"
|
||||||
|
onToggle="() => this.toggleFilter('awaiting_qc')"/>
|
||||||
<!-- Spec 2026-05-25 — post-shop state chips -->
|
<!-- Spec 2026-05-25 — post-shop state chips -->
|
||||||
<FpFilterChip label="'Awaiting CoC'"
|
<FpFilterChip label="'Awaiting CoC'"
|
||||||
active="!!state.filters.awaiting_cert"
|
active="!!state.filters.awaiting_cert"
|
||||||
|
|||||||
Reference in New Issue
Block a user