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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user