feat(fusion_plating_shopfloor): FpPinSetup client action for self-service PIN (P6.2.6)
Registers fp_tablet_pin_setup as an ir.actions.client tag. Triggered from res.users preferences via action_open_tablet_pin_setup (added to res_users.py in P6.1.1). Three-stage flow: loading → check if user has existing PIN via search_count old → enter current PIN (skipped if first-time) new → choose new PIN confirm → enter new PIN again done → success toast + auto-close 1.5s later Each stage reuses FpPinPad with a different onSubmit + title. On mismatch / server error, resets to the first stage with a notification. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -94,6 +94,8 @@ Copyright (c) 2026 Nexa Systems Inc. All rights reserved.
|
||||
'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',
|
||||
'fusion_plating_shopfloor/static/src/xml/components/pin_setup.xml',
|
||||
'fusion_plating_shopfloor/static/src/js/components/pin_setup.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',
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/** @odoo-module **/
|
||||
// =============================================================================
|
||||
// Fusion Plating — FpPinSetup (client action `fp_tablet_pin_setup`)
|
||||
//
|
||||
// Modal flow for setting OR changing the user's tablet PIN. Triggered
|
||||
// from res.users preferences via action_open_tablet_pin_setup. Three
|
||||
// stages: (1) old PIN (only if has_pin), (2) new PIN, (3) confirm new.
|
||||
// =============================================================================
|
||||
|
||||
import { Component, useState, onMounted } from "@odoo/owl";
|
||||
import { rpc } from "@web/core/network/rpc";
|
||||
import { registry } from "@web/core/registry";
|
||||
import { useService } from "@web/core/utils/hooks";
|
||||
import { user } from "@web/core/user";
|
||||
import { FpPinPad } from "./pin_pad";
|
||||
|
||||
export class FpPinSetup extends Component {
|
||||
static template = "fusion_plating_shopfloor.PinSetup";
|
||||
static components = { FpPinPad };
|
||||
static props = ["*"];
|
||||
|
||||
setup() {
|
||||
this.notification = useService("notification");
|
||||
this.action = useService("action");
|
||||
this.state = useState({
|
||||
stage: "loading", // 'loading' | 'old' | 'new' | 'confirm' | 'done'
|
||||
newPin: "",
|
||||
hasExistingPin: false,
|
||||
});
|
||||
onMounted(() => this._init());
|
||||
}
|
||||
|
||||
async _init() {
|
||||
// Cheap probe: search_count on the user's own record filtered
|
||||
// by pin_set_date. Non-manager users can read their own set_date
|
||||
// (not the hash). If the count is 1, they have a PIN; 0 = no PIN.
|
||||
try {
|
||||
const has = await rpc("/web/dataset/call_kw", {
|
||||
model: "res.users",
|
||||
method: "search_count",
|
||||
args: [[
|
||||
["id", "=", user.userId],
|
||||
["x_fc_tablet_pin_set_date", "!=", false],
|
||||
]],
|
||||
kwargs: {},
|
||||
});
|
||||
this.state.hasExistingPin = has > 0;
|
||||
} catch (e) {
|
||||
this.state.hasExistingPin = false;
|
||||
}
|
||||
this.state.stage = this.state.hasExistingPin ? "old" : "new";
|
||||
}
|
||||
|
||||
async onOldPinSubmit(pin) {
|
||||
// Stash for the final call; set_pin verifies it server-side
|
||||
this._oldPin = pin;
|
||||
this.state.stage = "new";
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async onNewPinSubmit(pin) {
|
||||
this.state.newPin = pin;
|
||||
this.state.stage = "confirm";
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async onConfirmPinSubmit(pin) {
|
||||
if (pin !== this.state.newPin) {
|
||||
return { ok: false, error: "PINs don't match. Try again." };
|
||||
}
|
||||
const params = { new_pin: this.state.newPin };
|
||||
if (this._oldPin) params.old_pin = this._oldPin;
|
||||
const res = await rpc("/fp/tablet/set_pin", params);
|
||||
if (res && res.ok) {
|
||||
this.notification.add("Tablet PIN updated.", { type: "success" });
|
||||
this.state.stage = "done";
|
||||
setTimeout(() => this._close(), 1500);
|
||||
return { ok: true };
|
||||
}
|
||||
// Reset back to start on hard error so user can retry cleanly
|
||||
this.notification.add((res && res.error) || "Failed to set PIN", { type: "danger" });
|
||||
this._oldPin = null;
|
||||
this.state.newPin = "";
|
||||
this.state.stage = this.state.hasExistingPin ? "old" : "new";
|
||||
return { ok: false, error: (res && res.error) || "Failed" };
|
||||
}
|
||||
|
||||
_close() {
|
||||
this.action.doAction({ type: "ir.actions.act_window_close" });
|
||||
}
|
||||
|
||||
onCancel() {
|
||||
this._close();
|
||||
}
|
||||
}
|
||||
|
||||
registry.category("actions").add("fp_tablet_pin_setup", FpPinSetup);
|
||||
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<templates xml:space="preserve">
|
||||
|
||||
<t t-name="fusion_plating_shopfloor.PinSetup">
|
||||
<div class="o_fp_pin_setup">
|
||||
<div t-if="state.stage === 'loading'" class="o_fp_pin_setup_loading">
|
||||
<i class="fa fa-spinner fa-spin"/> Loading…
|
||||
</div>
|
||||
<FpPinPad t-if="state.stage === 'old'"
|
||||
onSubmit.bind="onOldPinSubmit"
|
||||
title="'Enter your current PIN'"
|
||||
onCancel.bind="onCancel"/>
|
||||
<FpPinPad t-if="state.stage === 'new'"
|
||||
onSubmit.bind="onNewPinSubmit"
|
||||
title="'Choose a new 4-digit PIN'"
|
||||
onCancel.bind="onCancel"/>
|
||||
<FpPinPad t-if="state.stage === 'confirm'"
|
||||
onSubmit.bind="onConfirmPinSubmit"
|
||||
title="'Confirm your new PIN'"
|
||||
subtitle="'Enter it again to confirm'"
|
||||
onCancel.bind="onCancel"/>
|
||||
<div t-if="state.stage === 'done'" class="o_fp_pin_setup_done">
|
||||
<i class="fa fa-check-circle text-success fa-3x"/>
|
||||
<h3>PIN updated</h3>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
|
||||
</templates>
|
||||
Reference in New Issue
Block a user