From 29821bd54150746f8450ed6d2a7bb9703276629b Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sat, 23 May 2026 00:29:24 -0400 Subject: [PATCH] feat(fusion_plating_shopfloor): FpTabletLock outer wrapper component (P6.2.4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Top-level wrapper that renders lock screen (tile grid + PIN pad) when no tech is signed in, and renders otherwise. Drives the auto-lock countdown via the activity_tracker service + sends a /fp/tablet/ping heartbeat every 60s while a tech is signed in. Tiles fetch from /fp/tablet/tiles using the localStorage station id (set by ShopfloorLanding on QR pair / station picker selection). State machine for the lock screen body: loadingTiles → tiles list → tile tapped → PinPad → unlock RPC ↑ onPinCancel → back to tiles Co-Authored-By: Claude Opus 4.7 (1M context) --- .../fusion_plating_shopfloor/__manifest__.py | 3 + .../static/src/js/tablet_lock.js | 135 ++++++++++++++++++ .../static/src/scss/tablet_lock.scss | 96 +++++++++++++ .../static/src/xml/tablet_lock.xml | 50 +++++++ 4 files changed, 284 insertions(+) create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/js/tablet_lock.js create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/scss/tablet_lock.scss create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/xml/tablet_lock.xml diff --git a/fusion_plating/fusion_plating_shopfloor/__manifest__.py b/fusion_plating/fusion_plating_shopfloor/__manifest__.py index 89c9381c..a100a1e5 100644 --- a/fusion_plating/fusion_plating_shopfloor/__manifest__.py +++ b/fusion_plating/fusion_plating_shopfloor/__manifest__.py @@ -91,6 +91,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', + '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', # ---- Job Workspace (Phase 1 — tablet redesign) ---- 'fusion_plating_shopfloor/static/src/scss/job_workspace.scss', 'fusion_plating_shopfloor/static/src/xml/job_workspace.xml', 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 new file mode 100644 index 00000000..032b508b --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/js/tablet_lock.js @@ -0,0 +1,135 @@ +/** @odoo-module **/ +// ============================================================================= +// Fusion Plating — FpTabletLock (top-level wrapper) +// +// Mounted by Landing / Workspace / Manager Dashboard as their outermost +// element. Renders the lock screen (tile grid + PIN pad) when no tech +// is signed in; renders (the wrapped client +// action) otherwise. Also drives the auto-lock countdown + idle warning. +// +// Usage in a parent template: +// +// +// +//
...your existing tree...
+//
+//
+// +// ============================================================================= + +import { Component, useState, onMounted, onWillUnmount } from "@odoo/owl"; +import { rpc } from "@web/core/network/rpc"; +import { useService } from "@web/core/utils/hooks"; +import { FpPinPad } from "./components/pin_pad"; +import { FpIdleWarning } from "./components/idle_warning"; + +export class FpTabletLock extends Component { + static template = "fusion_plating_shopfloor.TabletLock"; + static components = { FpPinPad, FpIdleWarning }; + static props = { + slots: { type: Object, optional: true }, + }; + + setup() { + this.techStore = useService("fp_shopfloor_tech_store"); + this.activity = useService("fp_shopfloor_activity"); + this.notification = useService("notification"); + + this.state = useState({ + tiles: [], + selectedTileUserId: null, + idleSecondsRemaining: null, + loadingTiles: false, + }); + + onMounted(async () => { + await this._loadTiles(); + this._tick = setInterval(() => this._checkIdle(), 1000); + // Heartbeat ping every 60s — for forensic visibility + this._ping = setInterval(() => { + if (this.techStore.currentTechId) { + rpc("/fp/tablet/ping", { current_tech_id: this.techStore.currentTechId }) + .catch(() => {}); + } + }, 60000); + }); + + onWillUnmount(() => { + if (this._tick) clearInterval(this._tick); + if (this._ping) clearInterval(this._ping); + }); + } + + get isLocked() { + return this.techStore.isLocked; + } + + async _loadTiles() { + this.state.loadingTiles = true; + try { + 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; + } + } catch (err) { + // Quiet fail — tile grid stays empty; user gets prompted + } finally { + this.state.loadingTiles = false; + } + } + + _checkIdle() { + if (!this.techStore.currentTechId) { + this.state.idleSecondsRemaining = null; + return; + } + const remaining = this.activity.getSecondsUntilLock(); + const warnThreshold = this.activity.getWarnThresholdSec(); + if (remaining <= 0) { + this.handOff(); + } else if (remaining <= warnThreshold) { + this.state.idleSecondsRemaining = remaining; + } else if (this.state.idleSecondsRemaining !== null) { + this.state.idleSecondsRemaining = null; + } + } + + onTileClick(userId) { + this.state.selectedTileUserId = userId; + } + + _selectedTileName() { + const tile = this.state.tiles.find(t => t.user_id === this.state.selectedTileUserId); + return tile ? tile.name : ""; + } + + async unlock(pin) { + try { + const res = await rpc("/fp/tablet/unlock", { + user_id: this.state.selectedTileUserId, + pin, + }); + if (res && res.ok) { + this.techStore.setTech(res.current_tech_id, res.current_tech_name); + this.activity.bump(); + this.state.selectedTileUserId = null; + return { ok: true }; + } + return { ok: false, error: (res && res.error) || "Unlock failed" }; + } catch (err) { + return { ok: false, error: err.message || String(err) }; + } + } + + onPinCancel() { + this.state.selectedTileUserId = null; + } + + handOff() { + this.techStore.lock(); + this.state.selectedTileUserId = null; + this.state.idleSecondsRemaining = null; + this._loadTiles(); + } +} 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 new file mode 100644 index 00000000..d6897828 --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/scss/tablet_lock.scss @@ -0,0 +1,96 @@ +// ============================================================================= +// 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; +} + +.o_fp_tablet_lock { + position: fixed; + inset: 0; + background: $_lock-bg-hex; + color: $_lock-ink-hex; + display: flex; + flex-direction: column; + align-items: center; + padding: 2rem; + z-index: 9000; + overflow-y: auto; +} + +.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; + } +} + +.o_fp_tablet_lock_loading, .o_fp_tablet_lock_empty { + margin: 2rem auto; + color: var(--text-secondary, #666); +} + +.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_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_tablet_lock_tile_avatar { + width: 80px; + height: 80px; + border-radius: 50%; + object-fit: cover; +} + +.o_fp_tablet_lock_tile_name { + font-weight: 600; + text-align: center; +} + +.o_fp_tablet_lock_tile_clocked { + color: #34c759; + font-size: 0.75rem; +} + +.o_fp_tablet_lock_tile_nopin { + color: #ff9f0a; + font-size: 0.75rem; +} + +.o_fp_tablet_lock_pinwrap { + margin-top: 2rem; +} 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 new file mode 100644 index 00000000..98cc3f79 --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/xml/tablet_lock.xml @@ -0,0 +1,50 @@ + + + + + +
+
+

Tap your name to unlock

+
+
+ Loading… +
+
+ +
+ No operators configured. +
+
+ + + +
+
+ +
+
+
+ + + + + + +