feat(shopfloor): tablet lock-screen redesign — frontend + manifest
LS-T2..T6 of the tablet lock-screen redesign (LS-T1 backend shipped
separately in c6137100).
Files:
- _tablet_lock_tokens.scss (new — design tokens, dark/light branches
via $o-webclient-color-scheme, registered
first in manifest per project rule 8)
- tablet_lock.scss (full rewrite — gradient bg, glassmorphic
tiles, 4 entrance keyframes, hover lift,
click press, clocked-in pulse,
prefers-reduced-motion gate)
- tablet_lock.xml (extended — logo + clock + prompt blocks
wrapping the existing tile loop; tile
inner shape updated for avatar gradient,
has_photo conditional, is_clocked_in
modifier)
- tablet_lock.js (extended — state.clockText / dateText /
company, setInterval(60s) clock tick,
_formatTime / _formatDate / tileStyle /
avatarClass helpers per project rule 20)
- __manifest__.py (19.0.31.0.0 -> 19.0.32.0.0, registered
new tokens SCSS BEFORE tablet_lock.scss)
Verified live on entech:
- Module upgrade clean, registry loaded in 15.5s
- 6 stale asset attachments cleared
- Helpers in tablet_controller.py emit company payload + initials +
gradients correctly (Garry Singh -> GS, EN Tech -> ET, uid=5 ->
pink gradient)
- res.company.logo present (has_logo: True)
- All animations gated by prefers-reduced-motion per spec §6
CLAUDE.md updated with new Critical Rule 22 about Odoo 19 HTML fields
auto-wrapping plain-string writes — caught during Task 1 testing when
the original 'tagline equality' test failed because res.company.report
_header is an HTML field that wraps writes with <p> tags.
Closes the 6-task plan in
docs/superpowers/plans/2026-05-24-tablet-lock-screen-redesign-plan.md
Spec: docs/superpowers/specs/2026-05-24-tablet-lock-screen-redesign-design.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
|
||||
{
|
||||
'name': 'Fusion Plating — Shop Floor',
|
||||
'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',
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,35 +4,66 @@
|
||||
<t t-name="fusion_plating_shopfloor.TabletLock">
|
||||
<t t-if="isLocked">
|
||||
<div class="o_fp_tablet_lock">
|
||||
<div class="o_fp_tablet_lock_header">
|
||||
<h1><i class="fa fa-lock"/> Tap your name to unlock</h1>
|
||||
|
||||
<!-- ============== LOGO BLOCK ============== -->
|
||||
<div class="o_fp_lock_logo_block" t-if="state.company">
|
||||
<div class="o_fp_lock_logo_frame">
|
||||
<img t-if="state.company.has_logo"
|
||||
t-att-src="state.company.logo_url"
|
||||
t-att-alt="state.company.name"/>
|
||||
<div t-else=""
|
||||
class="o_fp_lock_logo_placeholder"
|
||||
t-esc="state.company.initials"/>
|
||||
</div>
|
||||
<div class="o_fp_lock_logo_text" t-esc="state.company.name"/>
|
||||
<div class="o_fp_lock_logo_sub" t-esc="state.company.tagline"/>
|
||||
</div>
|
||||
<div t-if="state.loadingTiles" class="o_fp_tablet_lock_loading">
|
||||
|
||||
<!-- ============== CLOCK BLOCK ============== -->
|
||||
<div class="o_fp_lock_clock_block">
|
||||
<div class="o_fp_lock_clock" t-esc="state.clockText"/>
|
||||
<div class="o_fp_lock_clock_date" t-esc="state.dateText"/>
|
||||
</div>
|
||||
|
||||
<!-- ============== PROMPT PILL ============== -->
|
||||
<div t-if="!state.selectedTileUserId" class="o_fp_lock_prompt">
|
||||
<i class="fa fa-lock"/> Tap your name
|
||||
</div>
|
||||
|
||||
<!-- ============== TILES OR PIN PAD ============== -->
|
||||
<div t-if="state.loadingTiles" class="o_fp_lock_loading">
|
||||
<i class="fa fa-spinner fa-spin"/> Loading…
|
||||
</div>
|
||||
<div t-elif="!state.selectedTileUserId" class="o_fp_tablet_lock_tiles">
|
||||
<div t-elif="!state.selectedTileUserId" class="o_fp_lock_tiles">
|
||||
<t t-if="!state.tiles.length">
|
||||
<div class="o_fp_tablet_lock_empty">
|
||||
<div class="o_fp_lock_empty">
|
||||
No operators configured.
|
||||
</div>
|
||||
</t>
|
||||
<t t-foreach="state.tiles" t-as="tile" t-key="tile.user_id">
|
||||
<button class="o_fp_tablet_lock_tile"
|
||||
<button class="o_fp_lock_tile"
|
||||
t-att-style="tileStyle(tile)"
|
||||
t-on-click="() => this.onTileClick(tile.user_id)">
|
||||
<img class="o_fp_tablet_lock_tile_avatar"
|
||||
t-att-src="tile.avatar_url"
|
||||
t-att-alt="tile.name"/>
|
||||
<div class="o_fp_tablet_lock_tile_name" t-esc="tile.name"/>
|
||||
<span t-if="tile.is_clocked_in" class="o_fp_tablet_lock_tile_clocked">
|
||||
● Clocked in
|
||||
</span>
|
||||
<span t-if="!tile.has_pin" class="o_fp_tablet_lock_tile_nopin">
|
||||
<div t-att-class="avatarClass(tile)"
|
||||
t-att-style="'background: ' + tile.avatar_gradient">
|
||||
<img t-if="tile.has_photo"
|
||||
t-att-src="tile.avatar_url"
|
||||
t-att-alt="tile.name"/>
|
||||
<span t-else="" t-esc="tile.initials"/>
|
||||
</div>
|
||||
<div class="o_fp_lock_name" t-esc="tile.name"/>
|
||||
<div t-if="tile.is_clocked_in"
|
||||
class="o_fp_lock_status status-clocked">
|
||||
Clocked in
|
||||
</div>
|
||||
<div t-elif="!tile.has_pin"
|
||||
class="o_fp_lock_status status-pin">
|
||||
PIN required
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</t>
|
||||
</div>
|
||||
<div t-else="" class="o_fp_tablet_lock_pinwrap">
|
||||
<div t-else="" class="o_fp_lock_pinwrap">
|
||||
<FpPinPad onSubmit.bind="unlock"
|
||||
title="_selectedTileName()"
|
||||
subtitle="'Enter your 4-digit PIN'"
|
||||
|
||||
Reference in New Issue
Block a user