diff --git a/fusion_plating/fusion_plating_shopfloor/__manifest__.py b/fusion_plating/fusion_plating_shopfloor/__manifest__.py index 2581d813..4559c844 100644 --- a/fusion_plating/fusion_plating_shopfloor/__manifest__.py +++ b/fusion_plating/fusion_plating_shopfloor/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Plating — Shop Floor', - 'version': '19.0.31.0.0', + 'version': '19.0.32.0.0', 'category': 'Manufacturing/Plating', 'summary': 'Shop-floor tablet stations, QR scanning, bake window enforcer, ' 'first-piece inspection gates.', @@ -95,6 +95,9 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved. 'fusion_plating_shopfloor/static/src/scss/components/_idle_warning.scss', 'fusion_plating_shopfloor/static/src/xml/components/idle_warning.xml', 'fusion_plating_shopfloor/static/src/js/components/idle_warning.js', + # 2026-05-24 lock-screen redesign — tokens MUST precede tablet_lock.scss + # so the $lock-* vars are visible to the consumer (project rule 8). + 'fusion_plating_shopfloor/static/src/scss/_tablet_lock_tokens.scss', 'fusion_plating_shopfloor/static/src/scss/tablet_lock.scss', 'fusion_plating_shopfloor/static/src/xml/tablet_lock.xml', 'fusion_plating_shopfloor/static/src/js/tablet_lock.js', diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/js/tablet_lock.js b/fusion_plating/fusion_plating_shopfloor/static/src/js/tablet_lock.js index 032b508b..ccac4ffd 100644 --- a/fusion_plating/fusion_plating_shopfloor/static/src/js/tablet_lock.js +++ b/fusion_plating/fusion_plating_shopfloor/static/src/js/tablet_lock.js @@ -40,6 +40,12 @@ export class FpTabletLock extends Component { selectedTileUserId: null, idleSecondsRemaining: null, loadingTiles: false, + // 2026-05-24 redesign — clock + company branding + // Seeded synchronously so the first render shows real values + // (no flash of empty content). + clockText: this._formatTime(new Date()), + dateText: this._formatDate(new Date()), + company: null, }); onMounted(async () => { @@ -52,11 +58,19 @@ export class FpTabletLock extends Component { .catch(() => {}); } }, 60000); + // Clock tick — update visible HH:MM and date label every 60s. + // 60s is enough; the displayed precision is minute-level only. + this._clockInterval = setInterval(() => { + const now = new Date(); + this.state.clockText = this._formatTime(now); + this.state.dateText = this._formatDate(now); + }, 60000); }); onWillUnmount(() => { if (this._tick) clearInterval(this._tick); if (this._ping) clearInterval(this._ping); + if (this._clockInterval) clearInterval(this._clockInterval); }); } @@ -70,7 +84,14 @@ export class FpTabletLock extends Component { const stationId = parseInt(localStorage.getItem("fp_landing_station_id")) || null; const res = await rpc("/fp/tablet/tiles", { station_id: stationId }); if (res && res.ok) { - this.state.tiles = res.tiles; + this.state.company = res.company || null; + // Decorate each tile with an animation-delay (50ms staggered, + // capped at 300ms so the screen doesn't take 3s to settle on + // shops with 20+ operators). + this.state.tiles = (res.tiles || []).map((tile, idx) => ({ + ...tile, + animDelay: Math.min(idx * 50, 300), + })); } } catch (err) { // Quiet fail — tile grid stays empty; user gets prompted @@ -132,4 +153,36 @@ export class FpTabletLock extends Component { this.state.idleSecondsRemaining = null; this._loadTiles(); } + + // === 2026-05-24 redesign helpers ===================================== + + _formatTime(d) { + // 24-hour HH:MM with leading zeros. Per project rule 20 this MUST + // live in JS, not the template — padStart isn't in OWL scope. + const hh = String(d.getHours()).padStart(2, "0"); + const mm = String(d.getMinutes()).padStart(2, "0"); + return hh + ":" + mm; + } + + _formatDate(d) { + // 'SATURDAY · MAY 23' style. Uses Intl for locale-correct weekday + // + month abbreviations, then upcases for the industrial tracking. + const weekday = d.toLocaleDateString(undefined, { weekday: "long" }); + const month = d.toLocaleDateString(undefined, { month: "short" }); + const day = d.getDate(); + return (weekday + " · " + month + " " + day).toUpperCase(); + } + + tileStyle(tile) { + // Inline animation-delay so each tile's entrance staggers. + // Returned as a string per project rule 20 — the template can't + // call String() inside t-att-style. + return "animation-delay: " + tile.animDelay + "ms"; + } + + avatarClass(tile) { + return tile.is_clocked_in + ? "o_fp_lock_avatar is-clocked" + : "o_fp_lock_avatar"; + } } diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/scss/_tablet_lock_tokens.scss b/fusion_plating/fusion_plating_shopfloor/static/src/scss/_tablet_lock_tokens.scss new file mode 100644 index 00000000..d5b2bbfa --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/scss/_tablet_lock_tokens.scss @@ -0,0 +1,75 @@ +// ===================================================================== +// Tablet lock-screen design tokens (2026-05-24 redesign). +// MUST load before tablet_lock.scss. Per project rule 8 (SCSS @import +// forbidden in Odoo 19 custom code), the manifest registers each SCSS +// file separately; ordering IS the variable scope. +// ===================================================================== + +$o-webclient-color-scheme: bright !default; + +// === Light-mode defaults === +$_lock-bg-top-hex: #fafafa; +$_lock-bg-bottom-hex: #f0f0f3; +$_lock-accent-1-rgba: rgba(240, 165, 0, 0.12); +$_lock-accent-2-rgba: rgba(99, 102, 241, 0.06); +$_lock-text-hex: #1d1f1e; +$_lock-muted-hex: #71717a; +$_lock-prompt-hex: #b45309; +$_lock-prompt-bg-rgba: rgba(240, 165, 0, 0.10); +$_lock-prompt-border-rgba: rgba(240, 165, 0, 0.25); +$_lock-tile-bg-rgba: rgba(255, 255, 255, 0.70); +$_lock-tile-border-rgba: rgba(0, 0, 0, 0.05); +$_lock-tile-hover-bg-rgba: rgba(255, 255, 255, 0.95); +$_lock-tile-hover-border-rgba: rgba(240, 165, 0, 0.50); +$_lock-tile-hover-shadow: (0 12px 24px rgba(240, 165, 0, 0.18)); +$_lock-frame-bg-rgba: rgba(255, 255, 255, 0.85); +$_lock-frame-border-rgba: rgba(0, 0, 0, 0.05); +$_lock-frame-shadow: (0 8px 24px rgba(0, 0, 0, 0.06), inset 0 1px 0 rgba(255, 255, 255, 0.8)); +$_lock-status-clocked-hex: #16a34a; +$_lock-status-pin-hex: #d97706; +$_lock-pulse-dot-border-hex: #ffffff; + +@if $o-webclient-color-scheme == dark { + $_lock-bg-top-hex: #1a1d21 !global; + $_lock-bg-bottom-hex: #2d3138 !global; + $_lock-accent-1-rgba: rgba(240, 165, 0, 0.08) !global; + $_lock-accent-2-rgba: rgba(99, 102, 241, 0.06) !global; + $_lock-text-hex: #f5f5f7 !global; + $_lock-muted-hex: #adb5bd !global; + $_lock-prompt-hex: #f0a500 !global; + $_lock-prompt-bg-rgba: rgba(240, 165, 0, 0.08) !global; + $_lock-prompt-border-rgba: rgba(240, 165, 0, 0.20) !global; + $_lock-tile-bg-rgba: rgba(255, 255, 255, 0.06) !global; + $_lock-tile-border-rgba: rgba(255, 255, 255, 0.08) !global; + $_lock-tile-hover-bg-rgba: rgba(240, 165, 0, 0.10) !global; + $_lock-tile-hover-border-rgba: rgba(240, 165, 0, 0.40) !global; + $_lock-tile-hover-shadow: (0 12px 24px rgba(240, 165, 0, 0.15), 0 0 0 1px rgba(240, 165, 0, 0.2)) !global; + $_lock-frame-bg-rgba: rgba(255, 255, 255, 0.08) !global; + $_lock-frame-border-rgba: rgba(255, 255, 255, 0.10) !global; + $_lock-frame-shadow: (0 8px 24px rgba(0, 0, 0, 0.30), inset 0 1px 0 rgba(255, 255, 255, 0.08)) !global; + $_lock-status-clocked-hex: #34c759 !global; + $_lock-status-pin-hex: #ff9f0a !global; + $_lock-pulse-dot-border-hex: #2d3138 !global; +} + +// === CSS-custom-property wrappers so future themes can override === +$lock-bg-top: var(--fp-lock-bg-top, $_lock-bg-top-hex); +$lock-bg-bottom: var(--fp-lock-bg-bottom, $_lock-bg-bottom-hex); +$lock-accent-1: $_lock-accent-1-rgba; +$lock-accent-2: $_lock-accent-2-rgba; +$lock-text: var(--fp-lock-text, $_lock-text-hex); +$lock-muted: var(--fp-lock-muted, $_lock-muted-hex); +$lock-prompt: var(--fp-lock-prompt, $_lock-prompt-hex); +$lock-prompt-bg: $_lock-prompt-bg-rgba; +$lock-prompt-border: $_lock-prompt-border-rgba; +$lock-tile-bg: $_lock-tile-bg-rgba; +$lock-tile-border: $_lock-tile-border-rgba; +$lock-tile-hover-bg: $_lock-tile-hover-bg-rgba; +$lock-tile-hover-border: $_lock-tile-hover-border-rgba; +$lock-tile-hover-shadow: $_lock-tile-hover-shadow; +$lock-frame-bg: $_lock-frame-bg-rgba; +$lock-frame-border: $_lock-frame-border-rgba; +$lock-frame-shadow: $_lock-frame-shadow; +$lock-status-clocked: var(--fp-lock-status-clocked, $_lock-status-clocked-hex); +$lock-status-pin: var(--fp-lock-status-pin, $_lock-status-pin-hex); +$lock-pulse-dot-border: $_lock-pulse-dot-border-hex; diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/scss/tablet_lock.scss b/fusion_plating/fusion_plating_shopfloor/static/src/scss/tablet_lock.scss index d6897828..2b5b00e8 100644 --- a/fusion_plating/fusion_plating_shopfloor/static/src/scss/tablet_lock.scss +++ b/fusion_plating/fusion_plating_shopfloor/static/src/scss/tablet_lock.scss @@ -1,96 +1,227 @@ -// ============================================================================= +// ===================================================================== // FpTabletLock — lock screen with tile grid + PIN pad overlay -// ============================================================================= - -$o-webclient-color-scheme: bright !default; - -$_lock-bg-hex: #f3f4f6; -$_lock-card-hex: #ffffff; -$_lock-border-hex: #d8dadd; -$_lock-ink-hex: #1d1d1f; - -@if $o-webclient-color-scheme == dark { - $_lock-bg-hex: #1a1d21 !global; - $_lock-card-hex: #22262d !global; - $_lock-border-hex: #424245 !global; - $_lock-ink-hex: #f5f5f7 !global; -} +// 2026-05-24 redesign: hybrid Industrial Bold + Premium Glassmorphism +// Spec: docs/superpowers/specs/2026-05-24-tablet-lock-screen-redesign-design.md +// Depends on _tablet_lock_tokens.scss being loaded first. +// ===================================================================== .o_fp_tablet_lock { position: fixed; inset: 0; - background: $_lock-bg-hex; - color: $_lock-ink-hex; + color: $lock-text; display: flex; flex-direction: column; align-items: center; - padding: 2rem; + padding: 28px 20px; + gap: 22px; z-index: 9000; overflow-y: auto; + background: + radial-gradient(ellipse at top, $lock-accent-1, transparent 50%), + radial-gradient(ellipse at bottom, $lock-accent-2, transparent 50%), + linear-gradient(135deg, $lock-bg-top 0%, $lock-bg-bottom 100%); } -.o_fp_tablet_lock_header { - h1 { - font-size: 1.4rem; - font-weight: 600; - margin-bottom: 1.5rem; - display: flex; - align-items: center; - gap: 0.6rem; +// === Logo block ===================================================== +.o_fp_lock_logo_block { + text-align: center; + animation: lockLogoEnter 0.5s ease-out; +} + +.o_fp_lock_logo_frame { + display: inline-flex; align-items: center; justify-content: center; + width: 84px; height: 84px; + border-radius: 20px; + margin-bottom: 12px; + padding: 14px; + box-sizing: border-box; + background: $lock-frame-bg; + border: 1px solid $lock-frame-border; + box-shadow: $lock-frame-shadow; + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + + img { + max-width: 100%; max-height: 100%; + object-fit: contain; } } -.o_fp_tablet_lock_loading, .o_fp_tablet_lock_empty { - margin: 2rem auto; - color: var(--text-secondary, #666); +.o_fp_lock_logo_placeholder { + width: 100%; height: 100%; border-radius: 14px; + background: linear-gradient(135deg, #f0a500, #ff6b00); + display: inline-flex; align-items: center; justify-content: center; + font-size: 32px; font-weight: 900; color: #1a1d21; } -.o_fp_tablet_lock_tiles { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); - gap: 1rem; - max-width: 900px; - width: 100%; +.o_fp_lock_logo_text { + font-size: 19px; font-weight: 700; letter-spacing: -0.01em; + color: $lock-text; } -.o_fp_tablet_lock_tile { - background: $_lock-card-hex; - border: 2px solid $_lock-border-hex; - border-radius: 12px; - padding: 1rem; - cursor: pointer; - display: flex; - flex-direction: column; - align-items: center; - gap: 0.5rem; - transition: border-color 0.1s ease, transform 0.05s ease; - - &:hover { border-color: #0071e3; } - &:active { transform: scale(0.98); } +.o_fp_lock_logo_sub { + font-size: 11px; + text-transform: uppercase; letter-spacing: 0.15em; + margin-top: 4px; + color: $lock-muted; } -.o_fp_tablet_lock_tile_avatar { - width: 80px; - height: 80px; - border-radius: 50%; - object-fit: cover; -} - -.o_fp_tablet_lock_tile_name { - font-weight: 600; +// === Clock block ==================================================== +.o_fp_lock_clock_block { text-align: center; + font-variant-numeric: tabular-nums; + animation: lockClockEnter 0.5s ease-out 0.1s both; } -.o_fp_tablet_lock_tile_clocked { - color: #34c759; - font-size: 0.75rem; +.o_fp_lock_clock { + font-size: 40px; font-weight: 800; + letter-spacing: -0.03em; line-height: 1; + color: $lock-text; } -.o_fp_tablet_lock_tile_nopin { - color: #ff9f0a; - font-size: 0.75rem; +.o_fp_lock_clock_date { + font-size: 12px; + text-transform: uppercase; letter-spacing: 0.12em; + margin-top: 4px; + color: $lock-muted; } -.o_fp_tablet_lock_pinwrap { - margin-top: 2rem; +// === Prompt pill ==================================================== +.o_fp_lock_prompt { + font-size: 13px; font-weight: 700; + text-transform: uppercase; letter-spacing: 0.18em; + color: $lock-prompt; + display: inline-flex; align-items: center; gap: 8px; + padding: 6px 16px; + border-radius: 999px; + background: $lock-prompt-bg; + border: 1px solid $lock-prompt-border; + animation: lockClockEnter 0.5s ease-out 0.2s both; +} + +// === Tile grid ====================================================== +.o_fp_lock_tiles { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; + width: 100%; + max-width: 480px; +} + +.o_fp_lock_tile { + background: $lock-tile-bg; + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid $lock-tile-border; + border-radius: 14px; + padding: 14px 8px 12px; + text-align: center; + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; + animation: lockTileEnter 0.4s cubic-bezier(0.4, 0, 0.2, 1) both; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.04); + color: inherit; + font-family: inherit; + + &:hover, &:focus-visible { + background: $lock-tile-hover-bg; + border-color: $lock-tile-hover-border; + transform: translateY(-3px); + box-shadow: $lock-tile-hover-shadow; + outline: none; + } + &:focus-visible { + outline: 2px solid $lock-tile-hover-border; + outline-offset: 2px; + } + &:active { + transform: scale(0.97); + transition: transform 0.05s; + } +} + +.o_fp_lock_avatar { + width: 52px; height: 52px; border-radius: 50%; + display: inline-flex; align-items: center; justify-content: center; + font-size: 21px; font-weight: 700; color: #fff; + margin-bottom: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + position: relative; + overflow: hidden; + + img { + width: 100%; height: 100%; + object-fit: cover; + border-radius: 50%; + } + + &.is-clocked::after { + content: ''; position: absolute; + width: 12px; height: 12px; border-radius: 50%; + background: $lock-status-clocked; + bottom: 0; right: 0; + border: 2px solid $lock-pulse-dot-border; + animation: lockPulseDot 2s ease-in-out infinite; + } +} + +.o_fp_lock_name { + font-size: 12px; font-weight: 600; + line-height: 1.3; + color: $lock-text; +} + +.o_fp_lock_status { + font-size: 10px; + margin-top: 4px; + font-weight: 500; + + &.status-clocked { color: $lock-status-clocked; } + &.status-pin { color: $lock-status-pin; } +} + +// === Empty / loading states ========================================= +.o_fp_lock_loading, +.o_fp_lock_empty { + margin: 2rem auto; + color: $lock-muted; + font-size: 14px; +} + +// === PIN pad wrap =================================================== +.o_fp_lock_pinwrap { + margin-top: 8px; + animation: lockClockEnter 0.3s ease-out; +} + +// === Animations ===================================================== +@keyframes lockLogoEnter { + from { opacity: 0; transform: translateY(-12px); } + to { opacity: 1; transform: translateY(0); } +} +@keyframes lockClockEnter { + from { opacity: 0; transform: translateY(8px); } + to { opacity: 1; transform: translateY(0); } +} +@keyframes lockTileEnter { + from { opacity: 0; transform: translateY(16px) scale(0.96); } + to { opacity: 1; transform: translateY(0) scale(1); } +} +@keyframes lockPulseDot { + 0%, 100% { box-shadow: 0 0 0 0 rgba(52, 199, 89, 0.6); } + 50% { box-shadow: 0 0 0 6px rgba(52, 199, 89, 0); } +} + +// === Reduced-motion override (accessibility) ======================== +@media (prefers-reduced-motion: reduce) { + .o_fp_tablet_lock, + .o_fp_lock_logo_block, + .o_fp_lock_clock_block, + .o_fp_lock_prompt, + .o_fp_lock_tile, + .o_fp_lock_avatar.is-clocked::after, + .o_fp_lock_pinwrap { + animation: none !important; + transition: none !important; + } } diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/xml/tablet_lock.xml b/fusion_plating/fusion_plating_shopfloor/static/src/xml/tablet_lock.xml index 98cc3f79..e9626953 100644 --- a/fusion_plating/fusion_plating_shopfloor/static/src/xml/tablet_lock.xml +++ b/fusion_plating/fusion_plating_shopfloor/static/src/xml/tablet_lock.xml @@ -4,35 +4,66 @@
-
-

Tap your name to unlock

+ + +
+
+ +
+
+
+
-
+ + +
+
+
+
+ + +
+ Tap your name +
+ + +
Loading…
-
+
-
+
No operators configured.
-
-
+