From 9584953467a7607988f0d97b548a1669de040b2f Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sat, 23 May 2026 00:28:01 -0400 Subject: [PATCH] feat(fusion_plating_shopfloor): FpPinPad numeric keypad component (P6.2.2) Reusable 4-digit PIN pad. Auto-submits on the 4th digit via the onSubmit prop. On wrong PIN, shake animation + dots clear + error banner (caller controls the message via the returned {ok:false, error}). Used by FpTabletLock (unlock flow) and FpPinSetup (set/change flow). Dark-mode SCSS branch follows the same $o-webclient-color-scheme pattern as the rest of the shopfloor components. Also registers tech_store + activity_tracker services in the asset bundle (assets/web.assets_backend) before the pin_pad files, since the pin_pad/tablet_lock components consume them. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../fusion_plating_shopfloor/__manifest__.py | 6 ++ .../static/src/js/components/pin_pad.js | 72 +++++++++++++++ .../static/src/scss/components/_pin_pad.scss | 89 +++++++++++++++++++ .../static/src/xml/components/pin_pad.xml | 37 ++++++++ 4 files changed, 204 insertions(+) create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/js/components/pin_pad.js create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_pin_pad.scss create mode 100644 fusion_plating/fusion_plating_shopfloor/static/src/xml/components/pin_pad.xml diff --git a/fusion_plating/fusion_plating_shopfloor/__manifest__.py b/fusion_plating/fusion_plating_shopfloor/__manifest__.py index 0a8fa4aa..7b9f058e 100644 --- a/fusion_plating/fusion_plating_shopfloor/__manifest__.py +++ b/fusion_plating/fusion_plating_shopfloor/__manifest__.py @@ -82,6 +82,12 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved. 'fusion_plating_shopfloor/static/src/scss/components/_kanban_card.scss', 'fusion_plating_shopfloor/static/src/xml/components/kanban_card.xml', 'fusion_plating_shopfloor/static/src/js/components/kanban_card.js', + # ---- Phase 6.2 tablet PIN gate ---- + 'fusion_plating_shopfloor/static/src/js/services/tech_store.js', + 'fusion_plating_shopfloor/static/src/js/services/activity_tracker.js', + 'fusion_plating_shopfloor/static/src/scss/components/_pin_pad.scss', + 'fusion_plating_shopfloor/static/src/xml/components/pin_pad.xml', + 'fusion_plating_shopfloor/static/src/js/components/pin_pad.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/components/pin_pad.js b/fusion_plating/fusion_plating_shopfloor/static/src/js/components/pin_pad.js new file mode 100644 index 00000000..2ecf3228 --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/js/components/pin_pad.js @@ -0,0 +1,72 @@ +/** @odoo-module **/ +// ============================================================================= +// Fusion Plating — FpPinPad (shared OWL service) +// +// Numeric 4-digit PIN pad. Auto-submits on the 4th digit via onSubmit +// callback. Used by FpTabletLock unlock flow AND FpPinSetup change flow. +// +// Props: +// onSubmit : (pin: string) => Promise<{ok: boolean, error?: string}> +// title : optional header text +// subtitle : optional smaller text +// onCancel : optional cancel callback (e.g. close modal) +// ============================================================================= + +import { Component, useState } from "@odoo/owl"; + +export class FpPinPad extends Component { + static template = "fusion_plating_shopfloor.PinPad"; + static props = { + onSubmit: { type: Function }, + title: { type: String, optional: true }, + subtitle: { type: String, optional: true }, + onCancel: { type: Function, optional: true }, + }; + + setup() { + this.state = useState({ + pin: "", + submitting: false, + error: "", + shake: false, + }); + } + + async _press(digit) { + if (this.state.submitting) return; + if (this.state.pin.length >= 4) return; + this.state.pin = this.state.pin + digit; + this.state.error = ""; + if (this.state.pin.length === 4) { + await this._submit(); + } + } + + _clear() { + this.state.pin = ""; + this.state.error = ""; + } + + async _submit() { + this.state.submitting = true; + try { + const result = await this.props.onSubmit(this.state.pin); + if (result && !result.ok) { + this.state.error = result.error || "Incorrect PIN"; + this.state.shake = true; + setTimeout(() => { this.state.shake = false; }, 400); + this.state.pin = ""; + } + } catch (err) { + this.state.error = err.message || String(err); + this.state.pin = ""; + } finally { + this.state.submitting = false; + } + } + + get dots() { + // Render 4 dot slots: filled if typed, empty otherwise + return [0, 1, 2, 3].map((i) => this.state.pin.length > i); + } +} diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_pin_pad.scss b/fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_pin_pad.scss new file mode 100644 index 00000000..fcdaa00f --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/scss/components/_pin_pad.scss @@ -0,0 +1,89 @@ +// ============================================================================= +// FpPinPad — numeric keypad for tablet lock screen + PIN setup +// Dark-mode aware via $o-webclient-color-scheme branch. +// ============================================================================= + +$o-webclient-color-scheme: bright !default; + +$_pin-bg-hex: #ffffff; +$_pin-key-bg-hex: #f3f4f6; +$_pin-key-hover-hex: #e5e7eb; +$_pin-border-hex: #d8dadd; +$_pin-dot-hex: #d8dadd; +$_pin-dot-fill-hex: #1d1d1f; + +@if $o-webclient-color-scheme == dark { + $_pin-bg-hex: #22262d !global; + $_pin-key-bg-hex: #2d3138 !global; + $_pin-key-hover-hex: #3a3f48 !global; + $_pin-border-hex: #424245 !global; + $_pin-dot-fill-hex: #f5f5f7 !global; +} + +.o_fp_pin_pad { + background: $_pin-bg-hex; + border-radius: 12px; + padding: 1.5rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.8rem; + min-width: 280px; +} + +.o_fp_pin_title { font-size: 1.1rem; font-weight: 600; } +.o_fp_pin_subtitle { font-size: 0.85rem; color: var(--text-secondary, #666); text-align: center; } + +.o_fp_pin_dots { + display: flex; + gap: 0.8rem; + margin: 0.5rem 0; +} + +.o_fp_pin_dot { + width: 14px; + height: 14px; + border-radius: 50%; + background: $_pin-dot-hex; + transition: background 0.1s ease; + &.filled { background: $_pin-dot-fill-hex; } +} + +.o_fp_pin_error { + color: #ff3b30; + font-size: 0.85rem; + min-height: 1.2rem; +} + +.o_fp_pin_grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.5rem; + width: 100%; +} + +.o_fp_pin_key { + background: $_pin-key-bg-hex; + border: 1px solid $_pin-border-hex; + border-radius: 10px; + padding: 1rem 0; + font-size: 1.5rem; + font-weight: 500; + cursor: pointer; + transition: background 0.1s ease, transform 0.05s ease; + + &:hover { background: $_pin-key-hover-hex; } + &:active { transform: scale(0.97); } + &:disabled { opacity: 0.5; cursor: wait; } +} + +.o_fp_pin_key_clear { font-size: 0.95rem; color: var(--text-secondary, #666); } +.o_fp_pin_key_cancel { font-size: 0.95rem; color: var(--text-secondary, #666); } + +@keyframes o_fp_pin_shake_kf { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-8px); } + 50% { transform: translateX(8px); } + 75% { transform: translateX(-4px); } +} +.o_fp_pin_shake { animation: o_fp_pin_shake_kf 0.4s ease; } diff --git a/fusion_plating/fusion_plating_shopfloor/static/src/xml/components/pin_pad.xml b/fusion_plating/fusion_plating_shopfloor/static/src/xml/components/pin_pad.xml new file mode 100644 index 00000000..4ab95078 --- /dev/null +++ b/fusion_plating/fusion_plating_shopfloor/static/src/xml/components/pin_pad.xml @@ -0,0 +1,37 @@ + + + + +
+
+
+ +
+ + + +
+ +
+ +
+ + + + + + +
+
+ + +