redesign(shopfloor): clean slate — depth by shadow, no card borders

User feedback: the previous gradient-heavy look felt cluttered, job
cards had confusing heavy borders, the hierarchy was noisy. Wiped all
three SCSS files and both OWL templates and rebuilt from scratch with
a clean minimalist design language.

Design philosophy — the single source of truth:

  * NO borders on cards — depth comes from elevation (shadow) + a
    tiny surface-tint difference between page and card
  * ONE accent colour (var(--o-action)); semantic red/amber/green only
    for status pills and state bars
  * Shadow-only cards: $fp-elev-1, $fp-elev-2, $fp-elev-3 built on
    color-mix of foreground so they adapt to dark mode automatically
  * Generous whitespace, 8pt spacing scale ($fp-space-1 through
    $fp-space-10)
  * Type-first hierarchy: 32px page titles, 44px KPI numbers, tabular
    numerics so refreshing counts don't jitter
  * Priority/state cues via narrow 4-6px coloured bars and small dots
    — never via loud backgrounds or gradient washes
  * All interactive elements at 48px touch minimum (shop-floor gloves)

New token file (_fp_shopfloor_tokens.scss) exports:
  - $fp-space-1..10, $fp-radius-sm..xl, $fp-radius-pill
  - $fp-page / $fp-card / $fp-card-soft surface tints
  - $fp-ink / $fp-ink-soft / $fp-ink-mute / $fp-ink-faint text tiers
  - $fp-elev-1..3 layered shadows
  - $fp-text-xs..3xl type scale
  - @mixin fp-pill, fp-focus-ring, fp-card, fp-hover-only
  - fp-wash() function for state-coloured soft backgrounds

Tablet Station (fusion_plating_shopfloor.scss + shopfloor_tablet.xml):
  - Clean hero: just the title, station chip, picker + scan button
  - KPI cards: no gradient overlay, just a 10px coloured dot and big
    44px number. Hover lifts with shadow
  - Active WO: soft green wash background, no border, pulsing dot
  - Panels contain queue/baths/bakes/gates/holds — all on the same
    card surface with big rounded corners, no internal borders
  - Queue rows: flat on a soft page-tinted background, hover slides
    right 2px (no lift, cleaner)
  - Bake/Gate/Hold rows: state-coloured inset shadow as a 4px stripe,
    no border
  - Empty states: centred with a 44px muted icon and friendly copy

Manager Desk (manager_dashboard.scss + manager_dashboard.xml):
  - Matching hero with live dot that calmly pulses green during a fetch
  - 4 KPI cards in the same language as the tablet
  - Three panels (Unassigned / In Progress / Team) with coloured dots
    next to their titles instead of top accent bars
  - MO cards NO borders, subtle page-tint background, 4px left stripe
    only for priority (red HOT, amber Urgent)
  - Team cards: avatar + name + live load pill, hover slides right
  - WO expanded rows use card-soft buttons/dropdowns for low contrast

Plant Overview (plant_overview.scss):
  - Columns are now shadow-lifted cards on the tinted page background
  - Kanban cards: no border, small shadow, lift on hover
  - Priority stripe is an inset box-shadow (not a border) so hover
    transform doesn't wobble

Backend contract preserved — OWL class names, prop signatures, RPC
endpoints, and stateBadge mapping all unchanged. Only visuals.

Verified:
  * Bundle compiled to /web/assets/.../web.assets_backend.min.css
    (1.45MB, id 1926)
  * All 6 new classes present in compiled CSS
  * Zero SCSS "forbidden import" warnings
  * Zero Odoo module upgrade errors

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsinghpal
2026-04-18 18:45:16 -04:00
parent 6d1efc6c43
commit 067d1f01c8
6 changed files with 980 additions and 1361 deletions

View File

@@ -1,115 +1,131 @@
// =============================================================================
// Fusion Plating — Shop-Floor Design Tokens
// Copyright 2026 Nexa Systems Inc.
// License OPL-1 (Odoo Proprietary License v1.0)
// Fusion Plating — Shop Floor Design System (v2, 2026-04)
// Copyright 2026 Nexa Systems Inc. · License OPL-1
//
// Single source of truth for the look-and-feel of every shop-floor OWL page.
// All values use CSS custom properties (--bs-*, --o-*) so the tokens adapt
// automatically between Odoo's light and dark themes — no media queries or
// duplicate palettes.
//
// Gradients use `color-mix(in srgb, var(--bs-foo) X%, transparent)` because
// transparent mixes with the page background, which already obeys the theme.
// Design philosophy:
// * NO card borders — depth comes from elevation (shadow) only
// * Generous whitespace, calm surfaces, one accent colour
// * Semantic colours (success/warning/danger) reserved for STATUS — not
// decoration
// * Type-first hierarchy: big headings + big numbers + small helpers
// * Every value resolves from Odoo CSS custom properties, so light
// and dark themes work without duplicate palettes
// =============================================================================
// ---------- Spacing scale (8-pt baseline) ------------------------------------
$fp-space-1 : 4px;
$fp-space-2 : 8px;
$fp-space-3 : 12px;
$fp-space-4 : 16px;
$fp-space-5 : 20px;
$fp-space-6 : 24px;
$fp-space-7 : 32px;
$fp-space-8 : 40px;
$fp-space-9 : 48px;
$fp-space-10 : 64px;
// ---------- Radii ------------------------------------------------------------
$fp-radius-sm : 8px;
$fp-radius-md : 12px;
$fp-radius-lg : 16px;
$fp-radius-xl : 20px;
$fp-radius-pill : 999px;
// ---------- Radius -----------------------------------------------------------
$fp-radius-sm : 10px;
$fp-radius-md : 14px;
$fp-radius-lg : 20px;
$fp-radius-xl : 28px;
$fp-radius-pill: 999px;
// ---------- Surfaces — depth by TINT, not by border --------------------------
// The page gets a slightly tinted background; cards sit on a lighter
// surface. That tint difference replaces the need for card borders.
$fp-page : color-mix(in srgb, var(--bs-body-color) 2.5%,
var(--o-view-background-color, var(--bs-body-bg)));
$fp-card : var(--o-view-background-color, var(--bs-body-bg));
$fp-card-soft : color-mix(in srgb, var(--bs-body-color) 4%,
var(--o-view-background-color, var(--bs-body-bg)));
// ---------- Elevation (shadows that respect dark mode) -----------------------
// Shadows are built on the page's foreground colour so they darken in light
// mode and deepen (but never go pure black) in dark mode.
$fp-shadow-xs : 0 1px 2px color-mix(in srgb, var(--bs-body-color) 6%, transparent);
$fp-shadow-sm : 0 2px 6px color-mix(in srgb, var(--bs-body-color) 9%, transparent);
$fp-shadow-md : 0 4px 14px color-mix(in srgb, var(--bs-body-color) 10%, transparent),
0 1px 3px color-mix(in srgb, var(--bs-body-color) 6%, transparent);
$fp-shadow-lg : 0 10px 28px color-mix(in srgb, var(--bs-body-color) 14%, transparent),
0 2px 6px color-mix(in srgb, var(--bs-body-color) 6%, transparent);
$fp-shadow-glow-primary : 0 0 0 4px color-mix(in srgb, var(--o-action) 12%, transparent);
// ---------- Text tiers -------------------------------------------------------
$fp-ink : var(--bs-body-color);
$fp-ink-soft : color-mix(in srgb, var(--bs-body-color) 70%, transparent);
$fp-ink-mute : color-mix(in srgb, var(--bs-body-color) 48%, transparent);
$fp-ink-faint : color-mix(in srgb, var(--bs-body-color) 28%, transparent);
// ---------- Elevation — soft, layered, theme-safe ----------------------------
// Shadows built on the foreground colour so they darken appropriately in
// light mode and show a subtle halo in dark mode.
$fp-elev-1 : 0 1px 2px color-mix(in srgb, var(--bs-body-color) 5%, transparent),
0 1px 3px color-mix(in srgb, var(--bs-body-color) 7%, transparent);
$fp-elev-2 : 0 2px 4px color-mix(in srgb, var(--bs-body-color) 6%, transparent),
0 6px 14px color-mix(in srgb, var(--bs-body-color) 9%, transparent);
$fp-elev-3 : 0 4px 8px color-mix(in srgb, var(--bs-body-color) 8%, transparent),
0 12px 28px color-mix(in srgb, var(--bs-body-color) 12%, transparent);
$fp-elev-hover : 0 6px 12px color-mix(in srgb, var(--bs-body-color) 10%, transparent),
0 18px 36px color-mix(in srgb, var(--bs-body-color) 14%, transparent);
// ---------- Surface layering -------------------------------------------------
// A "raised" surface tint — sits slightly above the base background. Uses
// the foreground colour to tint so it deepens correctly in dark mode.
$fp-surface : var(--o-view-background-color, var(--bs-body-bg));
$fp-surface-raised : color-mix(in srgb,
var(--bs-body-color) 2%,
var(--o-view-background-color, var(--bs-body-bg)));
$fp-surface-sunken : color-mix(in srgb,
var(--bs-body-color) 4%,
var(--o-view-background-color, var(--bs-body-bg)));
// ---------- Semantic colour helpers (NOT gradients) --------------------------
$fp-accent : var(--o-action); // the one action colour
$fp-ok : var(--bs-success);
$fp-warn : var(--bs-warning);
$fp-bad : var(--bs-danger);
$fp-info : var(--bs-info);
// Softened backgrounds for status pills / banners
@function fp-wash($color-var, $strength: 12%) {
@return color-mix(in srgb, var(#{$color-var}) #{$strength}, transparent);
}
// ---------- Border-tints -----------------------------------------------------
$fp-border : var(--bs-border-color);
$fp-border-strong : color-mix(in srgb, var(--bs-body-color) 18%, transparent);
$fp-border-accent : color-mix(in srgb, var(--o-action) 40%, var(--bs-border-color));
// ---------- Type scale ------------------------------------------------------
// Shop-floor tablets are read from 18" — baseline bumped from Odoo default.
$fp-text-xs : 0.75rem; // 12px small labels
$fp-text-sm : 0.875rem; // 14px helper text
$fp-text-base : 1rem; // 16px body
$fp-text-md : 1.125rem; // 18px emphasis
$fp-text-lg : 1.25rem; // 20px sub-headings
$fp-text-xl : 1.5rem; // 24px section headings
$fp-text-2xl : 2rem; // 32px page title
$fp-text-3xl : 2.75rem; // 44px KPI number
$fp-text-4xl : clamp(2rem, 5vw, 3rem); // hero
$fp-weight-medium : 500;
$fp-weight-semibold : 600;
$fp-weight-bold : 700;
// ---------- Typography -------------------------------------------------------
// Shop-floor tablets and phones get read from 18" away — bumps over Odoo's
// default body size. Numbers are tabular so KPIs don't jitter on refresh.
$fp-font-base : 1rem; // 16px
$fp-font-sm : 0.875rem; // 14px
$fp-font-xs : 0.78rem; // 12.5px
$fp-font-lg : 1.125rem; // 18px
$fp-font-xl : 1.375rem; // 22px
$fp-font-2xl : 1.75rem; // 28px
$fp-font-3xl : 2.25rem; // 36px
$fp-font-kpi : 2.5rem; // 40px — headline number
$fp-font-hero : clamp(1.5rem, 3.5vw, 2.25rem);
$fp-font-stack : -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Inter", "Helvetica Neue", Arial, sans-serif;
// ---------- Animation --------------------------------------------------------
// ---------- Motion -----------------------------------------------------------
$fp-ease : cubic-bezier(0.22, 1, 0.36, 1);
$fp-ease-snap : cubic-bezier(0.34, 1.56, 0.64, 1);
$fp-dur-fast : 140ms;
$fp-dur : 220ms;
$fp-ease-out : cubic-bezier(0.33, 1, 0.68, 1);
$fp-dur-fast : 120ms;
$fp-dur : 200ms;
$fp-dur-slow : 360ms;
// ---------- Semantic gradients (tone-based, theme-safe) ----------------------
// Each tone mixes the named Bootstrap token with transparent so it lays
// naturally over whatever page background it's on — light OR dark.
@mixin fp-grad($color-var, $strong: 18%, $soft: 6%) {
background-image: linear-gradient(
135deg,
color-mix(in srgb, var(#{$color-var}) #{$strong}, transparent) 0%,
color-mix(in srgb, var(#{$color-var}) #{$soft}, transparent) 100%
);
}
@mixin fp-grad-solid($color-var, $from: 100%, $to: 75%) {
background-image: linear-gradient(
135deg,
color-mix(in srgb, var(#{$color-var}) #{$from}, transparent) 0%,
color-mix(in srgb, var(#{$color-var}) #{$to}, transparent) 100%
);
color: var(--o-we-text-on-action, #fff);
}
// ---------- Touch ------------------------------------------------------------
$fp-touch-min : 48px; // larger than Apple's 44px minimum — shop floor
// ---------- Tonal helpers ----------------------------------------------------
// Border + tint combo used on banners / pills — reads as "slightly
// saturated panel" in both themes.
@mixin fp-tone($color-var, $strength: 14%) {
background-color: color-mix(in srgb, var(#{$color-var}) #{$strength}, transparent);
color: var(#{$color-var});
border: 1px solid color-mix(in srgb, var(#{$color-var}) 38%, transparent);
}
// =============================================================================
// Mixins
// =============================================================================
// ---------- Focus ring -------------------------------------------------------
@mixin fp-focus-ring() {
// Focus ring — used on all interactive inputs/buttons
@mixin fp-focus-ring {
outline: none;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--o-action) 35%, transparent);
box-shadow: 0 0 0 3px color-mix(in srgb, #{$fp-accent} 35%, transparent);
}
// Card surface — shadow-based, no border
@mixin fp-card($elev: $fp-elev-1) {
background-color: $fp-card;
border-radius: $fp-radius-lg;
box-shadow: $elev;
}
// ---------- Touch target -----------------------------------------------------
$fp-touch-min : 44px; // Apple HIG minimum
// Status pill (soft tint + colored text)
@mixin fp-pill($color-var) {
background-color: color-mix(in srgb, var(#{$color-var}) 14%, transparent);
color: var(#{$color-var});
}
// Hide hover styles on touch devices (stuck hover = bad UX on phones)
@mixin fp-hover-only {
@media (hover: hover) {
@content;
}
}

View File

@@ -1,383 +1,343 @@
// =============================================================================
// Fusion Plating — Manager Desk
// Copyright 2026 Nexa Systems Inc.
// License OPL-1 (Odoo Proprietary License v1.0)
// Copyright 2026 Nexa Systems Inc. · License OPL-1
//
// Shares design tokens + panel / KPI / chip classes from the Tablet Station
// SCSS. Only the manager-specific components live here: hero banner with
// live dot, 3-column workload grid, richer MO cards, gradient avatars.
//
// Variables / mixins come from _fp_shopfloor_tokens.scss — loaded FIRST in
// the asset bundle (see __manifest__.py). No @import; Odoo 19 forbids it.
// Shared tokens from _fp_shopfloor_tokens.scss (loaded first in the bundle).
// Shared components re-used from tablet: .o_fp_panel, .o_fp_empty, .o_fp_chip.
// This file owns only the manager-specific layout.
// =============================================================================
// Touch-device hover suppression
// --- Hover suppression on touch -----------------------------------------------
@media (hover: none) {
.o_fp_manager .o_fp_mgr_card:hover,
.o_fp_manager .o_fp_team_card:hover {
background-color: inherit !important;
border-color: var(--bs-border-color) !important;
.o_fp_manager [class*="o_fp_"]:hover {
transform: none !important;
box-shadow: inherit !important;
}
}
.o_fp_manager {
background-color: $fp-surface-sunken;
color: var(--bs-body-color);
font-family: $fp-font-stack;
background-color: $fp-page;
color: $fp-ink;
min-height: 100%;
padding: 24px 28px;
padding: $fp-space-6 $fp-space-7;
display: flex;
flex-direction: column;
gap: 18px;
gap: $fp-space-6;
@media (max-width: 900px) { padding: 16px; gap: 14px; }
@media (max-width: 600px) { padding: 10px 10px 16px; gap: 10px; }
@media (max-width: 900px) { padding: $fp-space-4; gap: $fp-space-4; }
@media (max-width: 600px) { padding: $fp-space-3; gap: $fp-space-4; }
// =========================================================================
// Hero banner — gradient wash, live dot, action cluster
// =========================================================================
// -------------------------------------------------------------------------
// Hero row
// -------------------------------------------------------------------------
.o_fp_manager_header {
position: relative;
overflow: hidden;
border-radius: $fp-radius-lg;
padding: 20px 24px;
background-color: $fp-surface-raised;
border: 1px solid $fp-border;
box-shadow: $fp-shadow-sm;
&::before {
content: "";
position: absolute;
inset: 0;
background-image:
radial-gradient(circle at 0% 50%,
color-mix(in srgb, var(--o-action) 22%, transparent) 0%,
transparent 55%),
radial-gradient(circle at 100% 0%,
color-mix(in srgb, var(--bs-info) 18%, transparent) 0%,
transparent 55%);
pointer-events: none;
}
display: grid;
grid-template-columns: 1fr auto;
gap: 16px;
align-items: center;
> * { position: relative; z-index: 1; }
gap: $fp-space-5;
align-items: end;
@media (max-width: 600px) {
grid-template-columns: 1fr;
gap: 10px;
padding: 14px;
grid-template-columns: 1fr; gap: $fp-space-3;
}
}
.o_fp_manager_title {
font-size: $fp-font-hero;
font-weight: 700;
letter-spacing: -0.01em;
display: inline-flex; align-items: center; gap: 10px;
font-size: $fp-text-2xl;
font-weight: $fp-weight-bold;
letter-spacing: -0.02em;
line-height: 1.1;
margin: 0;
color: $fp-ink;
display: inline-flex; align-items: center; gap: $fp-space-3;
}
.o_fp_manager_subtitle {
margin-top: $fp-space-2;
font-size: $fp-text-sm;
color: $fp-ink-mute;
display: flex; flex-wrap: wrap; gap: $fp-space-3; align-items: center;
}
// Small breathing dot — green at rest, brighter pulse while polling
// Live indicator — calm dot that pulses during a fetch
.o_fp_live_dot {
display: inline-block;
width: 11px; height: 11px; border-radius: 50%;
background-color: color-mix(in srgb, var(--bs-success) 75%, transparent);
width: 10px; height: 10px;
border-radius: 50%;
background-color: color-mix(in srgb, #{$fp-ok} 70%, transparent);
transition: background-color $fp-dur $fp-ease;
&[data-active="y"] {
background-color: var(--bs-success);
background-color: $fp-ok;
animation: o_fp_live_pulse 1.1s ease-in-out infinite;
}
}
@keyframes o_fp_live_pulse {
0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--bs-success) 60%, transparent); }
70% { box-shadow: 0 0 0 10px color-mix(in srgb, var(--bs-success) 0%, transparent); }
100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--bs-success) 0%, transparent); }
}
.o_fp_manager_subtitle {
font-size: $fp-font-sm;
color: var(--bs-secondary-color);
margin-top: 4px;
0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, #{$fp-ok} 55%, transparent); }
50% { box-shadow: 0 0 0 8px color-mix(in srgb, #{$fp-ok} 0%, transparent); }
}
.o_fp_manager_head_actions {
display: flex; gap: 8px; align-items: center;
display: flex; gap: $fp-space-2;
.btn {
min-height: $fp-touch-min;
padding: 8px 16px;
padding: 0 $fp-space-4;
border-radius: $fp-radius-md;
font-weight: 600;
font-weight: $fp-weight-semibold;
border: none;
background-color: $fp-card;
color: $fp-ink;
box-shadow: $fp-elev-1;
transition: transform $fp-dur-fast $fp-ease, box-shadow $fp-dur $fp-ease;
@include fp-hover-only { &:hover { box-shadow: $fp-elev-2; } }
&:active { transform: scale(0.97); }
&.btn-primary {
background-color: $fp-accent;
color: var(--o-we-text-on-action, #fff);
}
}
@media (max-width: 600px) {
width: 100%; flex-wrap: wrap;
> .btn { flex: 1; }
width: 100%; > .btn { flex: 1; }
}
}
// =========================================================================
// KPI strip — same token system as the tablet
// =========================================================================
.o_fp_kpi_strip {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(170px, 1fr));
gap: 12px;
@media (max-width: 600px) {
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
}
.o_fp_kpi {
position: relative; overflow: hidden;
padding: 18px 18px 16px;
border-radius: $fp-radius-lg;
background-color: $fp-surface-raised;
border: 1px solid $fp-border;
box-shadow: $fp-shadow-sm;
transition: transform $fp-dur $fp-ease, box-shadow $fp-dur $fp-ease;
display: flex; flex-direction: column; gap: 4px;
min-height: 104px;
// -------------------------------------------------------------------------
// Flash message — reused styling
// -------------------------------------------------------------------------
.o_fp_tablet_message {
display: flex; align-items: center; gap: $fp-space-3;
padding: $fp-space-3 $fp-space-4;
border-radius: $fp-radius-md;
font-size: $fp-text-base;
font-weight: $fp-weight-medium;
background-color: $fp-card;
box-shadow: $fp-elev-1;
color: $fp-ink;
&::before {
content: "";
position: absolute;
top: 0; left: 0; right: 0; height: 3px;
background-image: linear-gradient(90deg, transparent, currentColor, transparent);
opacity: 0.7;
width: 6px; align-self: stretch;
border-radius: 3px;
background-color: $fp-info;
}
&.o_fp_msg_info { &::before { background-color: $fp-info; } }
&.o_fp_msg_success { &::before { background-color: $fp-ok; } }
&.o_fp_msg_warning { &::before { background-color: $fp-warn; } }
&.o_fp_msg_danger { &::before { background-color: $fp-bad; } }
}
// -------------------------------------------------------------------------
// KPI strip — same language as tablet
// -------------------------------------------------------------------------
.o_fp_kpi_strip {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: $fp-space-4;
@media (max-width: 600px) { grid-template-columns: repeat(2, 1fr); gap: $fp-space-3; }
}
.o_fp_kpi {
position: relative;
padding: $fp-space-5;
border-radius: $fp-radius-lg;
background-color: $fp-card;
box-shadow: $fp-elev-1;
display: flex; flex-direction: column; gap: $fp-space-2;
transition: transform $fp-dur $fp-ease, box-shadow $fp-dur $fp-ease;
@include fp-hover-only { &:hover { transform: translateY(-2px); box-shadow: $fp-elev-2; } }
> .fa { font-size: 1.1rem; color: $fp-ink-mute; }
.o_fp_kpi_value {
font-size: $fp-text-3xl;
font-weight: $fp-weight-bold;
line-height: 1;
letter-spacing: -0.03em;
font-variant-numeric: tabular-nums;
color: $fp-ink;
}
.o_fp_kpi_label {
font-size: $fp-text-sm; color: $fp-ink-mute;
font-weight: $fp-weight-medium;
}
&::after {
content: "";
position: absolute; inset: 0; opacity: 0.35; pointer-events: none;
}
> * { position: relative; z-index: 1; }
@media (hover: hover) {
&:hover { transform: translateY(-2px); box-shadow: $fp-shadow-md; }
}
> .fa {
position: absolute;
right: 14px; top: 14px;
font-size: 1.5rem;
opacity: 0.28;
top: $fp-space-4; right: $fp-space-4;
width: 10px; height: 10px;
border-radius: 50%;
background-color: $fp-ink-faint;
}
.o_fp_kpi_value {
font-size: $fp-font-kpi;
font-weight: 800;
line-height: 1;
letter-spacing: -0.02em;
font-variant-numeric: tabular-nums;
color: var(--bs-body-color);
}
.o_fp_kpi_label {
font-size: $fp-font-xs;
color: var(--bs-secondary-color);
text-transform: uppercase;
letter-spacing: 0.06em;
font-weight: 600;
}
&.o_fp_kpi_info { color: var(--bs-info); &::after { @include fp-grad(--bs-info); } }
&.o_fp_kpi_success { color: var(--bs-success); &::after { @include fp-grad(--bs-success); } }
&.o_fp_kpi_warning { color: var(--bs-warning); &::after { @include fp-grad(--bs-warning); } }
&.o_fp_kpi_info { &::after { background-color: $fp-info; } }
&.o_fp_kpi_success { &::after { background-color: $fp-ok; } }
&.o_fp_kpi_warning { &::after { background-color: $fp-warn; } }
&.o_fp_kpi_danger {
color: var(--bs-danger);
border-color: color-mix(in srgb, var(--bs-danger) 45%, $fp-border);
&::after { @include fp-grad(--bs-danger, 22%, 8%); }
.o_fp_kpi_value { color: var(--bs-danger); }
&::after { background-color: $fp-bad; }
.o_fp_kpi_value { color: $fp-bad; }
}
&.o_fp_kpi_muted { color: var(--bs-secondary-color); &::after { opacity: 0; } }
&.o_fp_kpi_muted { &::after { display: none; } }
@media (max-width: 600px) {
min-height: 84px; padding: 12px 12px 10px;
.o_fp_kpi_value { font-size: 1.6rem; }
.o_fp_kpi_label { font-size: 0.68rem; }
> .fa { font-size: 1rem; top: 10px; right: 10px; }
padding: $fp-space-4;
.o_fp_kpi_value { font-size: $fp-text-2xl; }
.o_fp_kpi_label { font-size: $fp-text-xs; }
}
}
// =========================================================================
// Flash message (shares tablet styling)
// =========================================================================
.o_fp_tablet_message {
padding: 12px 16px;
border-radius: $fp-radius-md;
font-size: $fp-font-base;
font-weight: 500;
display: flex; align-items: center; gap: 10px;
box-shadow: $fp-shadow-xs;
&.o_fp_msg_info { @include fp-tone(--bs-info); }
&.o_fp_msg_success { @include fp-tone(--bs-success); }
&.o_fp_msg_warning { @include fp-tone(--bs-warning); }
&.o_fp_msg_danger { @include fp-tone(--bs-danger); }
}
// =========================================================================
// 3-column workload grid
// =========================================================================
// -------------------------------------------------------------------------
// Workload grid
// -------------------------------------------------------------------------
.o_fp_manager_grid {
display: grid;
grid-template-columns: minmax(0, 1.15fr) minmax(0, 1.15fr) minmax(0, 0.85fr);
gap: 16px;
grid-template-columns: minmax(0, 1.1fr) minmax(0, 1.1fr) minmax(0, 0.85fr);
gap: $fp-space-5;
@media (max-width: 1280px) {
grid-template-columns: 1fr 1fr;
.o_fp_panel_team { grid-column: span 2; }
}
@media (max-width: 900px) {
grid-template-columns: 1fr; gap: 10px;
grid-template-columns: 1fr;
.o_fp_panel_team { grid-column: auto; }
}
}
// =========================================================================
// Panels with coloured top accent
// =========================================================================
// -------------------------------------------------------------------------
// Panels
// -------------------------------------------------------------------------
.o_fp_panel {
position: relative;
overflow: hidden;
background-color: $fp-surface-raised;
border: 1px solid $fp-border;
border-radius: $fp-radius-lg;
padding: 18px 20px;
box-shadow: $fp-shadow-sm;
@media (max-width: 600px) { padding: 12px 14px; border-radius: $fp-radius-md; }
&::before {
content: "";
position: absolute; top: 0; left: 0; right: 0; height: 4px;
background-color: $fp-border-strong;
}
@include fp-card($fp-elev-1);
padding: $fp-space-5;
@media (max-width: 600px) { padding: $fp-space-4; }
}
.o_fp_panel_unassigned::before {
background-image: linear-gradient(90deg,
var(--bs-warning), color-mix(in srgb, var(--bs-warning) 50%, transparent));
}
.o_fp_panel_active::before {
background-image: linear-gradient(90deg,
var(--bs-success), color-mix(in srgb, var(--bs-success) 50%, transparent));
}
.o_fp_panel_team::before {
background-image: linear-gradient(90deg,
var(--bs-info), color-mix(in srgb, var(--bs-info) 50%, transparent));
}
.o_fp_panel_head {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 14px; padding-bottom: 12px;
border-bottom: 1px solid $fp-border;
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: $fp-space-4;
h3 {
font-size: $fp-font-lg;
font-weight: 700;
margin: 0;
display: inline-flex; align-items: center; gap: 10px;
font-size: $fp-text-lg;
font-weight: $fp-weight-bold;
letter-spacing: -0.01em;
margin: 0;
display: inline-flex; align-items: center; gap: $fp-space-2;
color: $fp-ink;
}
}
.o_fp_panel_count {
min-width: 34px;
padding: 3px 12px;
font-size: $fp-text-sm;
font-weight: $fp-weight-semibold;
color: $fp-ink-mute;
background-color: $fp-card-soft;
padding: 2px 12px;
border-radius: $fp-radius-pill;
font-weight: 700;
font-size: $fp-font-sm;
font-variant-numeric: tabular-nums;
background-color: color-mix(in srgb, var(--bs-body-color) 8%, transparent);
color: var(--bs-body-color);
border: 1px solid $fp-border;
}
// Panel accent by tone — a single coloured dot next to the title
.o_fp_panel_unassigned .o_fp_panel_head h3::before,
.o_fp_panel_active .o_fp_panel_head h3::before,
.o_fp_panel_team .o_fp_panel_head h3::before {
content: "";
width: 10px; height: 10px;
border-radius: 50%;
margin-right: $fp-space-1;
}
.o_fp_panel_unassigned .o_fp_panel_head h3::before { background-color: $fp-warn; }
.o_fp_panel_active .o_fp_panel_head h3::before { background-color: $fp-ok; }
.o_fp_panel_team .o_fp_panel_head h3::before { background-color: $fp-info; }
// -------------------------------------------------------------------------
// Empty state
// -------------------------------------------------------------------------
.o_fp_empty {
padding: 32px 16px;
padding: $fp-space-7 $fp-space-4;
text-align: center;
color: var(--bs-secondary-color);
i.fa { display: block; font-size: 2.25rem; opacity: 0.55; margin-bottom: 8px; }
color: $fp-ink-mute;
font-size: $fp-text-base;
i.fa {
display: block;
font-size: 2.75rem;
opacity: 0.5;
margin-bottom: $fp-space-3;
}
}
// =========================================================================
// MO card list (Unassigned + In Progress columns)
// =========================================================================
// -------------------------------------------------------------------------
// MO cards — NO borders, depth by shadow + surface tint
// -------------------------------------------------------------------------
.o_fp_mgr_card_list {
display: flex; flex-direction: column; gap: 10px;
display: flex; flex-direction: column; gap: $fp-space-3;
}
.o_fp_mgr_card {
position: relative;
overflow: hidden;
border: 1px solid $fp-border;
background-color: $fp-page;
border-radius: $fp-radius-md;
background-color: $fp-surface;
transition: border-color $fp-dur $fp-ease,
box-shadow $fp-dur $fp-ease,
transform $fp-dur-fast $fp-ease;
overflow: hidden;
transition: transform $fp-dur-fast $fp-ease, box-shadow $fp-dur $fp-ease;
@media (hover: hover) {
&:hover {
border-color: $fp-border-accent;
box-shadow: $fp-shadow-sm;
transform: translateY(-1px);
}
}
@include fp-hover-only { &:hover { box-shadow: $fp-elev-1; transform: translateX(2px); } }
// Priority stripe (4px) on the left — only when priority is set
&[data-priority="2"] {
border-color: color-mix(in srgb, var(--bs-danger) 50%, $fp-border);
background-color: color-mix(in srgb, #{$fp-bad} 4%, $fp-page);
&::before {
content: "";
position: absolute; top: 0; left: 0; bottom: 0; width: 4px;
background: var(--bs-danger);
position: absolute; left: 0; top: 0; bottom: 0;
width: 4px; background-color: $fp-bad;
}
}
&[data-priority="1"] {
&::before {
content: "";
position: absolute; top: 0; left: 0; bottom: 0; width: 4px;
background: var(--bs-warning);
}
&[data-priority="1"]::before {
content: "";
position: absolute; left: 0; top: 0; bottom: 0;
width: 4px; background-color: $fp-warn;
}
}
.o_fp_mgr_card_head {
display: flex; justify-content: space-between; align-items: center;
padding: 12px 14px; cursor: pointer; gap: 10px;
min-height: 60px;
@media (max-width: 600px) { flex-wrap: wrap; padding: 12px; }
padding: $fp-space-3 $fp-space-4;
cursor: pointer;
min-height: 64px;
gap: $fp-space-3;
@media (max-width: 600px) { flex-wrap: wrap; }
}
.o_fp_mgr_card_title {
font-weight: 700; font-size: $fp-font-base;
font-weight: $fp-weight-bold;
font-size: $fp-text-base;
color: $fp-ink;
letter-spacing: -0.01em;
}
.o_fp_mgr_card_sub {
color: var(--bs-secondary-color); font-size: $fp-font-sm;
margin-top: 3px;
}
.o_fp_mgr_card_chips {
display: flex; gap: 6px; flex-wrap: wrap;
font-size: $fp-text-sm; color: $fp-ink-mute; margin-top: 2px;
}
.o_fp_mgr_card_chips { display: flex; gap: $fp-space-1; flex-wrap: wrap; }
.o_fp_mgr_card_body {
border-top: 1px dashed $fp-border;
padding: 12px 14px;
display: flex; flex-direction: column; gap: 10px;
background-color: color-mix(in srgb, var(--bs-body-color) 2%, transparent);
padding: $fp-space-3 $fp-space-4 $fp-space-4;
display: flex; flex-direction: column; gap: $fp-space-2;
background-color: color-mix(in srgb, var(--bs-body-color) 3%, transparent);
}
// Per-WO row inside the expanded card
// -------------------------------------------------------------------------
// WO row inside expanded card
// -------------------------------------------------------------------------
.o_fp_mgr_wo_row {
display: grid;
grid-template-columns: 1fr auto auto auto auto;
gap: 8px;
gap: $fp-space-2;
align-items: center;
padding: 8px 10px;
background-color: $fp-surface;
border: 1px solid $fp-border;
padding: $fp-space-2 $fp-space-3;
background-color: $fp-card;
border-radius: $fp-radius-sm;
font-size: $fp-font-sm;
font-size: $fp-text-sm;
@media (max-width: 1400px) {
grid-template-columns: 1fr auto auto;
@@ -386,7 +346,6 @@
}
@media (max-width: 600px) {
grid-template-columns: 1fr;
gap: 6px;
.o_fp_mgr_picker { max-width: 100% !important; width: 100%; }
.btn { min-height: $fp-touch-min; }
}
@@ -394,91 +353,93 @@
.o_fp_mgr_wo_info {
min-width: 0;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
font-weight: 600;
font-weight: $fp-weight-semibold;
color: $fp-ink;
}
.o_fp_mgr_picker {
min-width: 130px; max-width: 200px;
min-height: 38px;
padding: 4px 10px;
min-width: 140px; max-width: 220px;
min-height: 40px;
padding: 0 $fp-space-3;
border: none;
border-radius: $fp-radius-sm;
background-color: $fp-surface;
border: 1px solid $fp-border;
color: var(--bs-body-color);
font-size: $fp-font-sm;
&:focus { @include fp-focus-ring; border-color: var(--o-action); }
background-color: $fp-card-soft;
color: $fp-ink;
font-size: $fp-text-sm;
&:focus { @include fp-focus-ring; }
}
.o_fp_mgr_wo_row .btn {
min-height: 40px;
padding: 0 $fp-space-3;
border: none;
border-radius: $fp-radius-sm;
font-size: $fp-text-sm;
font-weight: $fp-weight-semibold;
background-color: $fp-card-soft;
color: $fp-ink;
transition: filter $fp-dur-fast $fp-ease;
&:hover { filter: brightness(0.95); }
}
// =========================================================================
// Status chips (shared styles in tablet scss, but redefined in case
// manager is loaded standalone)
// =========================================================================
// -------------------------------------------------------------------------
// Status chips (reused)
// -------------------------------------------------------------------------
.o_fp_chip {
display: inline-flex;
align-items: center;
padding: 3px 10px;
display: inline-flex; align-items: center;
padding: 2px 10px;
border-radius: $fp-radius-pill;
font-size: 0.72rem;
font-weight: 700;
font-size: 0.7rem;
font-weight: $fp-weight-bold;
text-transform: uppercase;
letter-spacing: 0.04em;
letter-spacing: 0.06em;
&.o_fp_chip_info { @include fp-tone(--bs-info); }
&.o_fp_chip_success { @include fp-tone(--bs-success); }
&.o_fp_chip_warning { @include fp-tone(--bs-warning); }
&.o_fp_chip_danger { @include fp-tone(--bs-danger); }
&.o_fp_chip_muted {
background-color: color-mix(in srgb, var(--bs-body-color) 6%, transparent);
color: var(--bs-secondary-color);
border: 1px solid $fp-border;
}
&.o_fp_chip_info { @include fp-pill(--bs-info); }
&.o_fp_chip_success { @include fp-pill(--bs-success); }
&.o_fp_chip_warning { @include fp-pill(--bs-warning); }
&.o_fp_chip_danger { @include fp-pill(--bs-danger); }
&.o_fp_chip_muted { background-color: $fp-card-soft; color: $fp-ink-mute; }
}
// =========================================================================
// Team column — avatar grid
// =========================================================================
// -------------------------------------------------------------------------
// Team column — avatar + name + load
// -------------------------------------------------------------------------
.o_fp_team_grid {
display: flex; flex-direction: column; gap: 10px;
display: flex; flex-direction: column; gap: $fp-space-2;
}
.o_fp_team_card {
position: relative;
display: grid;
grid-template-columns: 56px 1fr;
grid-template-columns: 48px 1fr;
align-items: center;
gap: 14px;
padding: 12px 14px;
border: 1px solid $fp-border;
gap: $fp-space-3;
padding: $fp-space-3 $fp-space-4;
border-radius: $fp-radius-md;
background-color: $fp-surface;
background-color: $fp-page;
cursor: pointer;
min-height: 72px;
transition: border-color $fp-dur $fp-ease,
box-shadow $fp-dur $fp-ease,
transform $fp-dur-fast $fp-ease;
transition: transform $fp-dur-fast $fp-ease, background-color $fp-dur $fp-ease;
@media (hover: hover) {
@include fp-hover-only {
&:hover {
border-color: $fp-border-accent;
box-shadow: $fp-shadow-sm;
transform: translateY(-1px);
background-color: color-mix(in srgb, #{$fp-accent} 6%, $fp-page);
transform: translateX(2px);
}
}
}
.o_fp_team_avatar {
width: 48px; height: 48px;
width: 44px; height: 44px;
border-radius: 50%;
object-fit: cover;
border: 2px solid $fp-border;
background-color: color-mix(in srgb, var(--o-action) 15%, $fp-surface);
background-color: color-mix(in srgb, #{$fp-accent} 12%, $fp-card);
}
.o_fp_team_info { flex: 1; min-width: 0; }
.o_fp_team_info { min-width: 0; }
.o_fp_team_name {
font-weight: 700;
font-size: $fp-font-base;
font-weight: $fp-weight-semibold;
font-size: $fp-text-base;
color: $fp-ink;
letter-spacing: -0.01em;
}
.o_fp_team_load {
display: flex; gap: 6px; margin-top: 6px;
display: flex; gap: $fp-space-1; margin-top: 4px;
}
}

View File

@@ -1,482 +1,260 @@
// =============================================================================
// Fusion Plating — Plant Overview Dashboard
// Copyright 2026 Nexa Systems Inc.
// License OPL-1 (Odoo Proprietary License v1.0)
// Fusion Plating — Plant Overview (Kanban)
// Copyright 2026 Nexa Systems Inc. · License OPL-1
//
// Modernised 2026-04: gradient column headers, card depth, theme-safe
// using shared design tokens. Variables / mixins come from
// _fp_shopfloor_tokens.scss — loaded FIRST in the asset bundle
// (see __manifest__.py). No @import; Odoo 19 forbids it.
// Kanban of work orders grouped by work centre. Clean, shadow-based,
// no heavy chrome. Shared tokens from _fp_shopfloor_tokens.scss.
// =============================================================================
@media (hover: none) {
.o_fp_plant_overview [class*="o_fp_"]:hover {
transform: none !important;
box-shadow: inherit !important;
}
}
.o_fp_plant_overview {
font-family: $fp-font-stack;
background-color: $fp-page;
color: $fp-ink;
min-height: 100%;
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
background: $fp-surface-sunken;
padding: 0;
}
// ---- Header -----------------------------------------------------------------
// ---------- Header ----------------------------------------------------------
.o_fp_po_header {
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
padding: 20px 24px;
background-color: $fp-surface-raised;
border-bottom: 1px solid $fp-border;
box-shadow: $fp-shadow-xs;
&::before {
content: "";
position: absolute; inset: 0;
background-image:
radial-gradient(circle at 0% 50%,
color-mix(in srgb, var(--o-action) 18%, transparent) 0%,
transparent 55%);
pointer-events: none;
}
> * { position: relative; z-index: 1; }
gap: $fp-space-3;
padding: $fp-space-5 $fp-space-6;
background-color: $fp-page;
.o_fp_po_header_left { display: flex; align-items: center; }
.o_fp_po_title {
font-size: $fp-text-xl;
font-weight: $fp-weight-bold;
letter-spacing: -0.02em;
margin: 0;
font-size: $fp-font-xl;
font-weight: 700;
letter-spacing: -0.01em;
color: var(--bs-body-color);
color: $fp-ink;
}
.o_fp_po_refresh_ts { font-size: $fp-font-xs; }
.o_fp_po_refresh_ts {
font-size: $fp-text-xs; color: $fp-ink-mute;
}
.o_fp_po_header_right {
display: flex; align-items: center; gap: 10px;
display: flex; align-items: center; gap: $fp-space-2;
}
@media (max-width: 600px) {
padding: $fp-space-4;
flex-direction: column; align-items: stretch;
> * { width: 100%; }
}
}
// ---- Search -----------------------------------------------------------------
// ---------- Search ----------------------------------------------------------
.o_fp_po_search_box {
position: relative;
display: flex;
align-items: center;
.o_fp_po_search_icon {
position: absolute;
left: 10px;
color: var(--bs-secondary-color);
position: absolute; left: 14px;
color: $fp-ink-mute;
pointer-events: none;
}
.o_fp_po_search_input {
padding: 6px 32px 6px 32px;
border: 1px solid var(--bs-border-color);
border-radius: 6px;
font-size: 0.875rem;
width: 260px;
outline: none;
transition: border-color 0.15s;
background-color: var(--bs-body-bg);
color: var(--bs-body-color);
&:focus {
border-color: var(--o-action);
box-shadow: 0 0 0 0.2rem color-mix(in srgb, var(--o-action) 15%, transparent);
}
}
.o_fp_po_search_clear {
position: absolute;
right: 6px;
background: none;
padding: 0 $fp-space-4 0 $fp-space-7;
min-height: $fp-touch-min;
border: none;
color: var(--bs-secondary-color);
cursor: pointer;
padding: 2px 6px;
border-radius: $fp-radius-md;
background-color: $fp-card;
color: $fp-ink;
box-shadow: $fp-elev-1;
width: 260px;
font-size: $fp-text-base;
transition: box-shadow $fp-dur $fp-ease;
&:hover {
color: var(--bs-body-color);
}
&:focus { @include fp-focus-ring; }
@media (max-width: 600px) { width: 100%; }
}
.o_fp_po_search_clear {
position: absolute; right: 6px;
background: none; border: none;
color: $fp-ink-mute; padding: $fp-space-1 $fp-space-2;
cursor: pointer;
&:hover { color: $fp-ink; }
}
}
.o_fp_po_refresh_btn {
width: 36px;
height: 36px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
width: $fp-touch-min; height: $fp-touch-min;
display: flex; align-items: center; justify-content: center;
border: none;
border-radius: $fp-radius-md;
background-color: $fp-card;
color: $fp-ink;
box-shadow: $fp-elev-1;
cursor: pointer;
transition: transform $fp-dur-fast $fp-ease;
&:active { transform: scale(0.95); }
}
// ---- Columns container ------------------------------------------------------
// ---------- Columns container -----------------------------------------------
.o_fp_po_columns {
display: flex;
gap: 12px;
padding: 16px 20px;
gap: $fp-space-4;
padding: 0 $fp-space-6 $fp-space-6;
overflow-x: auto;
flex: 1;
min-height: 0;
align-items: flex-start;
@media (max-width: 900px) {
padding: 0 $fp-space-4 $fp-space-4;
}
@media (max-width: 600px) {
flex-direction: column;
padding: 0 $fp-space-3 $fp-space-3;
}
}
// ---- Single column (work centre) --------------------------------------------
// ---------- Column (work centre lane) ---------------------------------------
.o_fp_po_column {
flex: 0 0 280px;
min-width: 260px;
max-width: 320px;
flex: 0 0 300px;
min-width: 280px;
max-width: 340px;
display: flex;
flex-direction: column;
background-color: $fp-surface-raised;
border: 1px solid $fp-border;
background-color: $fp-card;
border-radius: $fp-radius-lg;
box-shadow: $fp-shadow-sm;
max-height: calc(100vh - 160px);
box-shadow: $fp-elev-1;
max-height: calc(100vh - 180px);
overflow: hidden;
@media (max-width: 600px) {
flex: 1 1 auto;
min-width: 100%; max-width: 100%;
max-height: none;
}
}
.o_fp_po_col_header {
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border-bottom: 1px solid $fp-border;
background-color: $fp-surface-sunken;
// Subtle gradient stripe at the top — turns the header into a "tab"
&::before {
content: "";
position: absolute; top: 0; left: 0; right: 0; height: 3px;
background-image: linear-gradient(90deg,
var(--o-action),
color-mix(in srgb, var(--o-action) 30%, transparent));
}
> * { position: relative; z-index: 1; }
display: flex; align-items: center; justify-content: space-between;
padding: $fp-space-4 $fp-space-4 $fp-space-3;
.o_fp_po_col_name {
font-weight: 700;
font-size: $fp-font-sm;
color: var(--bs-body-color);
font-weight: $fp-weight-bold;
font-size: $fp-text-sm;
color: $fp-ink;
text-transform: uppercase;
letter-spacing: 0.05em;
letter-spacing: 0.08em;
}
.o_fp_po_col_count {
@include fp-tone(--o-action, 14%);
font-weight: 700;
font-size: 0.72rem;
min-width: 26px;
font-weight: $fp-weight-semibold;
font-size: 0.75rem;
padding: 2px 10px;
border-radius: $fp-radius-pill;
background-color: $fp-card-soft;
color: $fp-ink-mute;
}
}
.o_fp_po_col_body {
overflow-y: auto;
padding: 8px;
padding: $fp-space-2 $fp-space-3 $fp-space-3;
flex: 1;
transition: background-color 0.15s, border-color 0.15s;
border: 2px solid transparent;
border-radius: 0 0 10px 10px;
transition: background-color $fp-dur $fp-ease;
border-radius: 0 0 $fp-radius-lg $fp-radius-lg;
// 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);
background-color: color-mix(in srgb, #{$fp-accent} 8%, transparent);
}
}
// ---- Card -------------------------------------------------------------------
// ---------- Card ------------------------------------------------------------
.o_fp_po_card {
background-color: $fp-surface;
border: 1px solid $fp-border;
background-color: $fp-page;
border-radius: $fp-radius-md;
padding: 12px 14px;
margin-bottom: 10px;
padding: $fp-space-3 $fp-space-4;
margin-bottom: $fp-space-2;
cursor: grab;
box-shadow: $fp-shadow-xs;
transition: box-shadow $fp-dur $fp-ease,
transform $fp-dur-fast $fp-ease,
transition: transform $fp-dur-fast $fp-ease,
box-shadow $fp-dur $fp-ease,
opacity $fp-dur $fp-ease,
border-color $fp-dur $fp-ease;
background-color $fp-dur $fp-ease;
&:hover {
box-shadow: $fp-shadow-md;
transform: translateY(-2px);
border-color: $fp-border-accent;
border-color: darken($border-color, 10%);
@include fp-hover-only {
&:hover {
background-color: $fp-card;
box-shadow: $fp-elev-2;
transform: translateY(-2px);
}
}
&:active {
&:active, &.o_fp_po_dragging {
cursor: grabbing;
opacity: 0.6;
}
&:last-child {
margin-bottom: 0;
// Priority left bar — only visible when a priority is set
position: relative; overflow: hidden;
&[data-priority="2"] {
background-color: color-mix(in srgb, #{$fp-bad} 6%, $fp-page);
box-shadow: inset 4px 0 0 0 $fp-bad;
padding-left: calc(#{$fp-space-4} + 4px);
}
// Dragging ghost state
&.o_fp_dragging {
opacity: 0.4;
border-style: dashed;
box-shadow: none;
transform: none;
&[data-priority="1"] {
box-shadow: inset 4px 0 0 0 $fp-warn;
padding-left: calc(#{$fp-space-4} + 4px);
}
// State variants
&.o_fp_card_progress {
border-left: 4px solid var(--bs-warning);
}
&.o_fp_card_ready {
border-left: 4px solid var(--bs-primary);
}
&.o_fp_card_done {
border-left: 4px solid var(--bs-success);
opacity: 0.75;
}
&.o_fp_card_pending {
border-left: 4px solid var(--bs-warning);
}
}
// ---- Card top row (image + title + step badge) --------------------------------
.o_fp_po_card_top {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.o_fp_po_card_img {
width: 32px;
height: 32px;
border-radius: 4px;
object-fit: cover;
flex-shrink: 0;
}
.o_fp_po_card_img_placeholder {
width: 32px;
height: 32px;
border-radius: 4px;
background: var(--bs-tertiary-bg);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: var(--bs-secondary-color);
font-size: 14px;
}
.o_fp_po_card_title {
flex: 1;
min-width: 0;
font-size: 0.9rem;
color: var(--bs-body-color);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.o_fp_po_card_step_badge {
width: 24px;
height: 24px;
border-radius: 50%;
background: var(--bs-info);
color: #fff;
font-size: 0.7rem;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
// ---- Priority card borders ---------------------------------------------------
.o_fp_po_card_hot {
border-left: 4px solid var(--bs-danger) !important;
background: color-mix(in srgb, var(--bs-danger) 8%, var(--bs-body-bg));
}
.o_fp_po_card_urgent {
border-left: 4px solid var(--bs-warning) !important;
background: color-mix(in srgb, var(--bs-warning) 8%, var(--bs-body-bg));
}
// ---- Product name and step display -------------------------------------------
.o_fp_po_card_product {
margin-bottom: 4px;
}
.o_fp_po_card_step {
margin-bottom: 4px;
}
.o_fp_po_card_customer {
font-size: 0.9rem;
font-weight: $fp-weight-semibold;
font-size: $fp-text-base;
color: $fp-ink;
margin-bottom: 2px;
color: var(--bs-body-color);
letter-spacing: -0.01em;
}
.o_fp_po_card_refs {
font-size: 0.8rem;
color: var(--bs-secondary-color);
margin-bottom: 6px;
.o_fp_po_card_sub {
font-size: $fp-text-sm;
color: $fp-ink-mute;
margin-bottom: $fp-space-2;
}
// ---- Parts progress bar -----------------------------------------------------
.o_fp_po_card_parts {
margin-bottom: 6px;
}
.o_fp_po_parts_bar {
height: 6px;
background: var(--bs-tertiary-bg);
border-radius: 3px;
overflow: hidden;
margin-bottom: 2px;
}
.o_fp_po_parts_fill {
height: 100%;
background: var(--bs-warning);
border-radius: 3px;
transition: width 0.3s ease;
}
.o_fp_po_parts_label {
font-size: 0.75rem;
color: var(--bs-secondary-color);
}
.o_fp_po_card_last {
font-size: 0.75rem;
margin-bottom: 6px;
}
// ---- Tags + date footer -----------------------------------------------------
.o_fp_po_card_footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 6px;
flex-wrap: wrap;
}
.o_fp_po_card_tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
.o_fp_po_card_meta {
display: flex; gap: $fp-space-2; flex-wrap: wrap; align-items: center;
font-size: $fp-text-xs; color: $fp-ink-mute;
}
.o_fp_po_tag {
display: inline-block;
font-size: 0.65rem;
font-weight: 700;
padding: 1px 8px;
border-radius: $fp-radius-pill;
font-size: 0.68rem;
font-weight: $fp-weight-bold;
text-transform: uppercase;
letter-spacing: 0.4px;
padding: 2px 6px;
border-radius: 4px;
line-height: 1.4;
&.o_fp_tag_hot {
background: var(--bs-danger);
color: #fff;
}
&.o_fp_tag_priority {
background: var(--bs-success);
color: #fff;
}
&.o_fp_tag_attention {
background: var(--bs-warning);
color: var(--bs-body-color);
}
&.o_fp_tag_default {
background: var(--bs-tertiary-bg);
color: var(--bs-secondary-color);
}
letter-spacing: 0.06em;
&[data-tone="hot"] { @include fp-pill(--bs-danger); }
&[data-tone="priority"] { @include fp-pill(--bs-warning); }
}
.o_fp_po_card_date {
font-size: 0.75rem;
font-weight: 600;
color: var(--bs-secondary-color);
background: var(--bs-tertiary-bg);
padding: 1px 6px;
border-radius: 4px;
white-space: nowrap;
}
// ---- Empty / no-cards -------------------------------------------------------
.o_fp_po_no_cards {
font-size: 0.85rem;
}
// ---- Responsive -------------------------------------------------------------
@media (max-width: 768px) {
.o_fp_po_columns {
flex-direction: column;
align-items: stretch;
padding: 12px;
gap: 10px;
}
.o_fp_po_column {
flex: 1 1 auto;
min-width: 100%;
max-width: 100%;
max-height: none;
}
.o_fp_po_search_input {
width: 180px !important;
}
.o_fp_po_header {
padding: 12px;
flex-wrap: wrap;
gap: 8px;
}
}
// Phone — further tighten + touch-first cards
@media (max-width: 600px) {
.o_fp_po_columns { padding: 8px; gap: 8px; }
.o_fp_po_col_body { padding: 6px; }
.o_fp_po_card {
padding: 10px 12px;
min-height: 64px; // comfortable tap zone
}
.o_fp_po_search_input { width: 100% !important; }
.o_fp_po_header {
flex-direction: column;
align-items: stretch;
> * { width: 100%; }
}
}
// Touch devices: disable hover-only highlights (they stick on tap)
@media (hover: none) {
.o_fp_po_card:hover { background: inherit !important; }
// ---------- Empty state ------------------------------------------------------
.o_fp_po_empty {
padding: $fp-space-6 $fp-space-3;
text-align: center;
color: $fp-ink-mute;
font-size: $fp-text-sm;
}

View File

@@ -1,60 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
Copyright 2026 Nexa Systems Inc. · License OPL-1
Fusion Plating — Manager Desk
Rebuilt 2026-04 with the shop-floor design system.
-->
<templates xml:space="preserve">
<t t-name="fusion_plating_shopfloor.ManagerDashboard">
<div class="o_fp_manager">
<!-- ===== Header ===== -->
<div class="o_fp_manager_header">
<!-- ============ Hero ============ -->
<header class="o_fp_manager_header">
<div>
<div class="o_fp_manager_title">
<i class="fa fa-user-md me-2"/>Manager Desk
<span class="o_fp_live_dot ms-2"
<h1 class="o_fp_manager_title">
<i class="fa fa-user-md"/>Manager Desk
<span class="o_fp_live_dot"
t-att-data-active="state.isFetching ? 'y' : 'n'"
t-att-title="'Updated ' + lastUpdatedLabel"/>
</div>
</h1>
<div class="o_fp_manager_subtitle" t-if="state.overview">
<span t-esc="state.overview.user_name"/>
<span class="text-muted">· live · updated <t t-esc="lastUpdatedLabel"/></span>
<span>· Live · updated <t t-esc="lastUpdatedLabel"/></span>
</div>
</div>
<div class="o_fp_manager_head_actions">
<button class="btn btn-outline-secondary"
<button class="btn"
t-on-click="refresh"
t-att-disabled="state.isFetching">
<i t-att-class="'fa fa-refresh' + (state.isFetching ? ' fa-spin' : '')"/>
</button>
<button t-att-class="'btn ' + (state.mode === 'quick' ? 'btn-primary' : 'btn-outline-primary')"
<button t-att-class="'btn ' + (state.mode === 'quick' ? 'btn-primary' : '')"
t-on-click="toggleMode">
<i t-att-class="state.mode === 'quick' ? 'fa fa-list' : 'fa fa-th'"/>
<t t-if="state.mode === 'quick'"> Quick View</t>
<t t-else=""> Detailed View</t>
<t t-if="state.mode === 'quick'">Quick View</t>
<t t-else="">Detailed View</t>
</button>
</div>
</div>
</header>
<!-- Error banner — visible instead of a stuck spinner -->
<!-- ============ Error banner ============ -->
<div t-if="state.loadError"
class="o_fp_tablet_message o_fp_msg_danger">
<i class="fa fa-exclamation-triangle me-2"/>
<i class="fa fa-exclamation-triangle"/>
<span t-esc="state.loadError"/>
<button class="btn btn-sm btn-outline-danger ms-3" t-on-click="refresh">
Retry
</button>
<button class="btn btn-sm btn-outline-danger ms-auto"
t-on-click="refresh">Retry</button>
</div>
<!-- ===== Flash message ===== -->
<!-- ============ Flash ============ -->
<div t-if="state.message"
t-att-class="'o_fp_tablet_message o_fp_msg_' + state.messageType">
<span t-esc="state.message"/>
</div>
<!-- ===== KPI strip ===== -->
<!-- ============ KPI strip ============ -->
<div class="o_fp_kpi_strip" t-if="state.overview">
<div class="o_fp_kpi o_fp_kpi_warning">
<i class="fa fa-user-times"/>
@@ -78,13 +76,13 @@
</div>
</div>
<!-- ===== 3-column layout ===== -->
<!-- ============ Workload grid ============ -->
<div class="o_fp_manager_grid" t-if="state.overview">
<!-- Column 1: Unassigned -->
<!-- Unassigned -->
<section class="o_fp_panel o_fp_panel_unassigned">
<div class="o_fp_panel_head">
<h3><i class="fa fa-inbox me-2 text-warning"/>Needs a Worker</h3>
<h3><i class="fa fa-inbox"/>Needs a Worker</h3>
<span class="o_fp_panel_count"><t t-esc="state.overview.unassigned.length"/></span>
</div>
<div t-if="!state.overview.unassigned.length" class="o_fp_empty">
@@ -94,46 +92,41 @@
<div class="o_fp_mgr_card_list" t-if="state.overview.unassigned.length">
<t t-foreach="state.overview.unassigned" t-as="card" t-key="card.mo_id">
<div class="o_fp_mgr_card"
t-att-data-priority="card.priority_any"
t-att-data-expanded="state.expandedMoId === card.mo_id ? 'y' : 'n'">
<div class="o_fp_mgr_card_head" t-on-click="() => this.toggleCard(card.mo_id)">
t-att-data-priority="card.priority_any">
<div class="o_fp_mgr_card_head"
t-on-click="() => this.toggleCard(card.mo_id)">
<div>
<div class="o_fp_mgr_card_title">
<strong t-esc="card.mo_name"/>
<span class="text-muted ms-2">· <t t-esc="card.so_name"/></span>
<t t-esc="card.mo_name"/>
<span class="text-muted ms-2 small">· <t t-esc="card.so_name"/></span>
</div>
<div class="o_fp_mgr_card_sub">
<t t-esc="card.customer"/>
· <t t-esc="card.product"/>
· Qty <t t-esc="card.qty_total"/>
<t t-if="card.date_planned">
· <t t-esc="card.date_planned"/>
</t>
<t t-if="card.date_planned"> · <t t-esc="card.date_planned"/></t>
</div>
</div>
<div class="o_fp_mgr_card_chips">
<span t-if="card.priority_any >= 2"
class="o_fp_chip o_fp_chip_danger">HOT</span>
<span t-if="card.priority_any == 1"
class="o_fp_chip o_fp_chip_warning">Urgent</span>
<span t-if="card.priority_any >= 2" class="o_fp_chip o_fp_chip_danger">HOT</span>
<span t-if="card.priority_any == 1" class="o_fp_chip o_fp_chip_warning">Urgent</span>
<span class="o_fp_chip o_fp_chip_muted">
<t t-esc="card.wos.length"/> WO
</span>
</div>
</div>
<!-- Expanded WO list -->
<div class="o_fp_mgr_card_body"
t-if="state.expandedMoId === card.mo_id or state.mode === 'detailed'">
<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_info">
<strong t-esc="wo.name"/>
<t t-esc="wo.name"/>
<span class="text-muted ms-2">
<t t-esc="wo.workcenter"/>
<t t-if="wo.bath"> · <t t-esc="wo.bath"/></t>
</span>
</div>
<select class="form-select form-select-sm o_fp_mgr_picker"
<select class="o_fp_mgr_picker"
t-on-change="(ev) => this.onAssignWorker(wo, ev.target.value)">
<option value="">— Assign worker —</option>
<t t-foreach="state.overview.operators" t-as="op" t-key="op.id">
@@ -143,7 +136,7 @@
</option>
</t>
</select>
<select class="form-select form-select-sm o_fp_mgr_picker"
<select class="o_fp_mgr_picker"
t-on-change="(ev) => this.onAssignTank(wo, ev.target.value)">
<option value="">— Tank —</option>
<t t-foreach="state.overview.tanks" t-as="tnk" t-key="tnk.id">
@@ -153,11 +146,11 @@
</option>
</t>
</select>
<button class="btn btn-sm btn-outline-warning"
<button class="btn"
t-on-click="() => this.onTakeOver(wo)">
<i class="fa fa-user"/> Take Over
</button>
<button class="btn btn-sm btn-outline-secondary"
<button class="btn"
t-on-click="() => this.openRecord('mrp.workorder', wo.id)">
Open
</button>
@@ -169,10 +162,10 @@
</div>
</section>
<!-- Column 2: In Progress -->
<!-- In Progress -->
<section class="o_fp_panel o_fp_panel_active">
<div class="o_fp_panel_head">
<h3><i class="fa fa-cogs me-2 text-success"/>In Progress</h3>
<h3><i class="fa fa-cogs"/>In Progress</h3>
<span class="o_fp_panel_count"><t t-esc="state.overview.active.length"/></span>
</div>
<div t-if="!state.overview.active.length" class="o_fp_empty">
@@ -182,13 +175,13 @@
<div class="o_fp_mgr_card_list" t-if="state.overview.active.length">
<t t-foreach="state.overview.active" t-as="card" t-key="card.mo_id">
<div class="o_fp_mgr_card"
t-att-data-priority="card.priority_any"
t-att-data-expanded="state.expandedMoId === card.mo_id ? 'y' : 'n'">
<div class="o_fp_mgr_card_head" t-on-click="() => this.toggleCard(card.mo_id)">
t-att-data-priority="card.priority_any">
<div class="o_fp_mgr_card_head"
t-on-click="() => this.toggleCard(card.mo_id)">
<div>
<div class="o_fp_mgr_card_title">
<strong t-esc="card.mo_name"/>
<span class="text-muted ms-2">· <t t-esc="card.so_name"/></span>
<t t-esc="card.mo_name"/>
<span class="text-muted ms-2 small">· <t t-esc="card.so_name"/></span>
</div>
<div class="o_fp_mgr_card_sub">
<t t-esc="card.customer"/>
@@ -196,8 +189,7 @@
</div>
</div>
<div class="o_fp_mgr_card_chips">
<span t-if="card.priority_any >= 2"
class="o_fp_chip o_fp_chip_danger">HOT</span>
<span t-if="card.priority_any >= 2" class="o_fp_chip o_fp_chip_danger">HOT</span>
<span class="o_fp_chip o_fp_chip_success">
<t t-esc="card.wos.length"/> WO
</span>
@@ -208,7 +200,7 @@
<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_info">
<strong t-esc="wo.name"/>
<t t-esc="wo.name"/>
<span class="text-muted ms-2">
<t t-esc="wo.workcenter"/>
<t t-if="wo.assigned_user_name">
@@ -220,11 +212,11 @@
<span t-att-class="'o_fp_chip o_fp_chip_' + (wo.state === 'progress' ? 'success' : 'info')">
<t t-esc="wo.state"/>
</span>
<button class="btn btn-sm btn-outline-warning"
<button class="btn"
t-on-click="() => this.onTakeOver(wo)">
Take Over
</button>
<button class="btn btn-sm btn-outline-secondary"
<button class="btn"
t-on-click="() => this.openRecord('mrp.workorder', wo.id)">
Open
</button>
@@ -236,10 +228,10 @@
</div>
</section>
<!-- Column 3: Team -->
<!-- Team -->
<section class="o_fp_panel o_fp_panel_team">
<div class="o_fp_panel_head">
<h3><i class="fa fa-users me-2 text-info"/>Team</h3>
<h3><i class="fa fa-users"/>Team</h3>
<span class="o_fp_panel_count"><t t-esc="state.overview.team.length"/></span>
</div>
<div t-if="!state.overview.team.length" class="o_fp_empty">
@@ -268,8 +260,10 @@
</section>
</div>
<!-- ============ Loading ============ -->
<div t-if="!state.overview and !state.loadError" class="o_fp_empty">
<i class="fa fa-spinner fa-spin me-2"/>Loading manager data…
<i class="fa fa-spinner fa-spin"/>
<div>Loading manager data…</div>
</div>
</div>
</t>

View File

@@ -1,38 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2026 Nexa Systems Inc.
License OPL-1 (Odoo Proprietary License v1.0)
Part of the Fusion Plating product family.
Tablet Station dashboard — KPI strip, My Queue, Active WO,
Baths, Bake Windows, First-Piece Gates, Quality Holds.
Copyright 2026 Nexa Systems Inc. · License OPL-1
Fusion Plating — Tablet Station (Worker view)
Rebuilt 2026-04 with the shop-floor design system.
-->
<templates xml:space="preserve">
<t t-name="fusion_plating_shopfloor.ShopfloorTablet">
<div class="o_fp_tablet">
<!-- ===== Header — title, station picker, scan toggle ===== -->
<div class="o_fp_tablet_header">
<!-- ============ Hero ============ -->
<header class="o_fp_tablet_header">
<div>
<div class="o_fp_tablet_title">
<i class="fa fa-tablet me-2"/>Shop Floor Tablet
</div>
<h1 class="o_fp_tablet_title">
<i class="fa fa-tablet"/>
Tablet Station
</h1>
<div class="o_fp_tablet_subtitle" t-if="state.overview">
<span t-esc="state.overview.user_name"/>
<t t-if="state.overview.station">
<span class="o_fp_tablet_chip ms-2">
<i class="fa fa-desktop me-1"/>
<span t-esc="state.overview.station.name"/>
<span t-if="state.overview.station.work_center" class="text-muted">
<span t-esc="state.overview.station.work_center"/>
</span>
</span>
</t>
<span><t t-esc="state.overview.user_name"/></span>
<span t-if="state.overview.station" class="o_fp_tablet_chip">
<i class="fa fa-desktop"/>
<span t-esc="state.overview.station.name"/>
<t t-if="state.overview.station.work_center">
· <span t-esc="state.overview.station.work_center"/>
</t>
</span>
</div>
</div>
<div class="o_fp_tablet_header_actions">
<select class="form-select o_fp_station_picker"
<select class="o_fp_station_picker"
t-on-change="onPickStation"
t-if="state.overview">
<option value="">— Pick station —</option>
@@ -44,18 +40,17 @@
</option>
</t>
</select>
<button class="btn btn-outline-primary o_fp_scan_toggle"
t-on-click="toggleScan">
<button class="o_fp_scan_toggle" t-on-click="toggleScan">
<i class="fa fa-qrcode me-1"/>Scan
</button>
</div>
</div>
</header>
<!-- ===== Optional scan drawer ===== -->
<!-- ============ Scan drawer ============ -->
<div t-if="state.showScan" class="o_fp_scan_drawer">
<input type="text"
class="o_fp_scan_input"
placeholder="Scan QR code (FP-STATION:…, FP-TANK:…, FP-BATH:…, FP-WO:…)"
placeholder="Scan QR code (FP-STATION:… FP-TANK:… FP-BATH:… FP-WO:…)"
t-ref="scanInput"
t-model="state.scannedCode"
t-on-keydown="onScanKey"/>
@@ -66,13 +61,13 @@
</button>
</div>
<!-- ===== Flash message ===== -->
<!-- ============ Flash ============ -->
<div t-if="state.message"
t-att-class="'o_fp_tablet_message o_fp_msg_' + state.messageType">
<span t-esc="state.message"/>
</div>
<!-- ===== KPI strip ===== -->
<!-- ============ KPI strip ============ -->
<div class="o_fp_kpi_strip" t-if="state.overview">
<t t-foreach="state.overview.kpis" t-as="k" t-key="k.label">
<div t-att-class="'o_fp_kpi o_fp_kpi_' + k.tone">
@@ -83,45 +78,39 @@
</t>
</div>
<!-- ===== Active WO banner (only when one is running) ===== -->
<!-- ============ Active WO ============ -->
<div class="o_fp_active_wo"
t-if="state.overview and state.overview.active_wo">
<div class="o_fp_active_wo_left">
<span class="o_fp_active_wo_pulse"/>
<div>
<div class="o_fp_active_wo_title">
<i class="fa fa-play-circle me-1"/>
Active Work Order: <strong t-esc="state.overview.active_wo.name"/>
Active: <strong t-esc="state.overview.active_wo.name"/>
</div>
<div class="o_fp_active_wo_meta">
MO <t t-esc="state.overview.active_wo.mo_name"/>
· <t t-esc="state.overview.active_wo.product_name"/>
· Qty <t t-esc="state.overview.active_wo.qty_done"/>/<t t-esc="state.overview.active_wo.qty_total"/>
<span t-if="state.overview.active_wo.workcenter" class="ms-2">
@ <t t-esc="state.overview.active_wo.workcenter"/>
</span>
<t t-if="state.overview.active_wo.workcenter"> @ <t t-esc="state.overview.active_wo.workcenter"/></t>
</div>
</div>
</div>
<button class="btn btn-sm btn-outline-primary"
<button class="o_fp_big_button"
t-on-click="() => openRecord('mrp.workorder', state.overview.active_wo.id)">
Open WO
</button>
</div>
<!-- ===== Main grid ===== -->
<!-- ============ Dashboard ============ -->
<div class="o_fp_tablet_dashboard" t-if="state.overview">
<!-- === LEFT column: My Queue (wide) === -->
<!-- LEFT: My Queue -->
<section class="o_fp_panel o_fp_panel_queue">
<div class="o_fp_panel_head">
<h3><i class="fa fa-list-ol me-2"/>My Queue</h3>
<span class="o_fp_panel_count">
<t t-esc="state.overview.my_queue.length"/>
</span>
<h3><i class="fa fa-list-ol"/>My Queue</h3>
<span class="o_fp_panel_count"><t t-esc="state.overview.my_queue.length"/></span>
</div>
<div t-if="!state.overview.my_queue.length"
class="o_fp_empty">
<div t-if="!state.overview.my_queue.length" class="o_fp_empty">
<i class="fa fa-check-circle text-success"/>
<div>All caught up — nothing waiting on you.</div>
</div>
@@ -140,30 +129,27 @@
</div>
<div class="o_fp_queue_actions">
<button t-if="row.can_start"
class="btn btn-sm btn-success"
class="btn btn-success"
t-on-click="() => this.onStartWo(row.source_id)">
<i class="fa fa-play me-1"/> Start
<i class="fa fa-play"/> Start
</button>
<button t-if="row.can_finish"
class="btn btn-sm btn-primary"
class="btn btn-primary"
t-on-click="() => this.onFinishWo(row.source_id)">
<i class="fa fa-check me-1"/> Finish
<i class="fa fa-check"/> Finish
</button>
<i class="fa fa-chevron-right text-muted"
t-if="!row.can_start and !row.can_finish"/>
</div>
</li>
</t>
</ul>
</section>
<!-- === RIGHT column: Baths + Bakes + Gates + Holds === -->
<!-- RIGHT column -->
<div class="o_fp_right_col">
<!-- Baths -->
<section class="o_fp_panel">
<div class="o_fp_panel_head">
<h3><i class="fa fa-flask me-2"/>Baths</h3>
<h3><i class="fa fa-flask"/>Baths</h3>
<span class="o_fp_panel_count"><t t-esc="state.overview.baths.length"/></span>
</div>
<div t-if="!state.overview.baths.length" class="o_fp_empty">
@@ -173,12 +159,9 @@
<div class="o_fp_tile_grid" t-if="state.overview.baths.length">
<t t-foreach="state.overview.baths" t-as="b" t-key="b.id">
<div class="o_fp_tile"
t-att-data-tone="stateBadge(b.last_log_status || b.state)"
t-on-click="() => this.openRecord('fusion.plating.bath', b.id)">
<div class="o_fp_tile_title"><t t-esc="b.name"/></div>
<div class="o_fp_tile_meta">
Tank <t t-esc="b.tank || '—'"/>
</div>
<div class="o_fp_tile_meta">Tank <t t-esc="b.tank || '—'"/></div>
<div class="o_fp_tile_chips">
<span t-att-class="'o_fp_chip o_fp_chip_' + stateBadge(b.state)">
<t t-esc="b.state"/>
@@ -187,19 +170,15 @@
t-att-class="'o_fp_chip o_fp_chip_' + stateBadge(b.last_log_status)">
log: <t t-esc="b.last_log_status"/>
</span>
<span t-if="b.mto" class="o_fp_chip o_fp_chip_muted">
MTO <t t-esc="b.mto"/>
</span>
</div>
</div>
</t>
</div>
</section>
<!-- Bake Windows -->
<section class="o_fp_panel">
<div class="o_fp_panel_head">
<h3><i class="fa fa-fire me-2"/>Bake Windows</h3>
<h3><i class="fa fa-fire"/>Bake Windows</h3>
<span class="o_fp_panel_count"><t t-esc="state.overview.bake_windows.length"/></span>
</div>
<div t-if="!state.overview.bake_windows.length" class="o_fp_empty">
@@ -211,10 +190,10 @@
<li class="o_fp_bake_row" t-att-data-state="bw.state">
<div class="o_fp_bake_main">
<div class="o_fp_bake_name">
<strong t-esc="bw.name"/>
<span class="text-muted ms-1"><t t-esc="bw.part_ref"/></span>
<t t-esc="bw.name"/>
<span class="text-muted ms-1"> <t t-esc="bw.part_ref"/></span>
</div>
<div class="o_fp_bake_meta text-muted">
<div class="o_fp_bake_meta">
<t t-esc="bw.customer"/>
· Qty <t t-esc="bw.quantity"/>
· Lot <t t-esc="bw.lot_ref"/>
@@ -227,16 +206,16 @@
</div>
<div class="o_fp_bake_actions">
<button t-if="bw.state === 'awaiting_bake'"
class="btn btn-sm btn-warning"
class="btn btn-warning"
t-on-click="() => this.onStartBake(bw.id)">
Start
</button>
<button t-if="bw.state === 'bake_in_progress'"
class="btn btn-sm btn-success"
class="btn btn-success"
t-on-click="() => this.onEndBake(bw.id)">
End
</button>
<button class="btn btn-sm btn-outline-secondary"
<button class="btn btn-light"
t-on-click="() => this.openRecord('fusion.plating.bake.window', bw.id)">
Open
</button>
@@ -246,10 +225,9 @@
</ul>
</section>
<!-- First-Piece Gates -->
<section class="o_fp_panel">
<div class="o_fp_panel_head">
<h3><i class="fa fa-flag-checkered me-2"/>First-Piece Gates</h3>
<h3><i class="fa fa-flag-checkered"/>First-Piece Gates</h3>
<span class="o_fp_panel_count"><t t-esc="state.overview.gates.length"/></span>
</div>
<div t-if="!state.overview.gates.length" class="o_fp_empty">
@@ -261,10 +239,10 @@
<li class="o_fp_bake_row" t-att-data-state="g.result">
<div class="o_fp_bake_main">
<div class="o_fp_bake_name">
<strong t-esc="g.name"/>
<span class="text-muted ms-1"><t t-esc="g.part_ref"/></span>
<t t-esc="g.name"/>
<span class="text-muted ms-1"> <t t-esc="g.part_ref"/></span>
</div>
<div class="o_fp_bake_meta text-muted">
<div class="o_fp_bake_meta">
<t t-esc="g.customer"/>
<t t-if="g.bath"> · Bath <t t-esc="g.bath"/></t>
</div>
@@ -276,16 +254,16 @@
</div>
<div class="o_fp_bake_actions">
<button t-if="g.result === 'pending'"
class="btn btn-sm btn-success"
class="btn btn-success"
t-on-click="() => this.onGateResult(g.id, 'pass')">
Pass
</button>
<button t-if="g.result === 'pending'"
class="btn btn-sm btn-danger"
class="btn btn-danger"
t-on-click="() => this.onGateResult(g.id, 'fail')">
Fail
</button>
<button class="btn btn-sm btn-outline-secondary"
<button class="btn btn-light"
t-on-click="() => this.openRecord('fusion.plating.first.piece.gate', g.id)">
Open
</button>
@@ -295,10 +273,9 @@
</ul>
</section>
<!-- Quality Holds -->
<section class="o_fp_panel" t-if="state.overview.holds.length">
<div class="o_fp_panel_head">
<h3 class="text-danger"><i class="fa fa-pause-circle me-2"/>Quality Holds</h3>
<h3><i class="fa fa-pause-circle text-danger"/>Quality Holds</h3>
<span class="o_fp_panel_count"><t t-esc="state.overview.holds.length"/></span>
</div>
<ul class="o_fp_bake_list">
@@ -306,17 +283,18 @@
<li class="o_fp_bake_row" t-att-data-state="h.state">
<div class="o_fp_bake_main">
<div class="o_fp_bake_name">
<strong t-esc="h.name"/>
<span class="text-muted ms-1"><t t-esc="h.part_ref"/></span>
<t t-esc="h.name"/>
<span class="text-muted ms-1"> <t t-esc="h.part_ref"/></span>
</div>
<div class="o_fp_bake_meta text-muted">
<div class="o_fp_bake_meta">
Qty <t t-esc="h.qty"/>
· <t t-esc="h.reason"/>
<t t-if="h.operator"> · <t t-esc="h.operator"/></t>
</div>
</div>
<div/>
<div class="o_fp_bake_actions">
<button class="btn btn-sm btn-outline-danger"
<button class="btn btn-outline-danger"
t-on-click="() => this.openRecord('fusion.plating.quality.hold', h.id)">
Review
</button>
@@ -328,17 +306,15 @@
</div>
</div>
<!-- ===== Loading / initial state ===== -->
<!-- ============ Loading ============ -->
<div t-if="!state.overview" class="o_fp_empty">
<i class="fa fa-spinner fa-spin me-2"/>Loading shop-floor data…
<i class="fa fa-spinner fa-spin"/>
<div>Loading shop-floor data…</div>
</div>
<!-- ===== Footer — server time ===== -->
<div class="o_fp_tablet_footer text-muted" t-if="state.overview">
<small>
Auto-refresh every 30 s · Last sync
<t t-esc="state.overview.server_time"/>
</small>
<!-- ============ Footer ============ -->
<div class="o_fp_tablet_footer" t-if="state.overview">
Auto-refreshed · Last sync <t t-esc="state.overview.server_time"/>
</div>
</div>
</t>