feat(fusion_clock): NFC kiosk — enrollment, manager page, sounds, lock, profile photos
Kiosk work across this session (19.0.3.6.0 -> 19.0.3.10.0): - Program-from-unknown-tap: amber prompt -> Manager PIN -> pick/create employee -> binds the captured UID (no re-tap). Reassign moves a card between employees. - Manager page (gear, when unlocked): search employees + tag status; assign/re-tag, clear tag, archive employee, + new employee. Server-gated by the enroll password. - Screen lock: kiosk starts locked (tap-only); Unlock -> Manager PIN, Lock button; PIN remembered for the session so the gear never re-prompts. - Sounds: pleasant + loud sine chimes (rising in / descending out) + a low "denied" tone for wrong/unknown taps. Gated by fusion_clock.enable_sounds. - Guided profile-photo capture for employees with no picture (clock-in or enroll): live camera + oval face guide -> capture -> preview -> save to hr.employee. - PIN no longer re-renders per digit; centered result card; 12h time; clock-out shows "Worked Xh Ym this shift"; modern clock idle icon; faster animations/result timers; session keep-alive so the kiosk login never expires. - New endpoints: create_employee, clear_tag, delete_employee (archive), verify_pin, save_profile_photo; enroll gains force-reassign. - Docs: fusion_clock is now developed in Claude Code (dropped Cursor references). Spec/plan under fusion_clock/docs/superpowers/. Deployed live on entech (odoo-entech / LXC 111 on pve-worker5), v19.0.3.10.0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
# NFC Kiosk — Enrollment UX / PIN fix / Speed / Clock-out Hours — Implementation Plan
|
||||
|
||||
> **For agentic workers:** Steps use checkbox (`- [ ]`) syntax. Executed inline this session.
|
||||
|
||||
**Goal:** Make NFC-tag enrollment programmable from an unknown tap (with create-new-employee), fix the per-digit PIN re-render, speed up clock-in/out for lines, and clearly show shift hours on clock-out.
|
||||
|
||||
**Architecture:** Extend the existing IIFE kiosk state machine (`fusion_clock_nfc_kiosk.js`) — no Interaction migration. Add one sudo controller endpoint for kiosk employee-create. SCSS-only changes for animation timing. Spec: `docs/superpowers/specs/2026-05-30-nfc-kiosk-enroll-speed-design.md`.
|
||||
|
||||
**Tech Stack:** Odoo 19 HTTP controller (jsonrpc), vanilla JS IIFE, SCSS. Verify: `pyflakes`, `xmllint`, manifest `ast.literal_eval`, on-device deploy on entech (LXC 111 / pve-worker5).
|
||||
|
||||
**XSS note:** the kiosk uses `innerHTML`; every dynamic value (employee names, the typed new-employee name, errors) MUST go through the existing `escapeHtml()`. The new-employee name is user input — escape it everywhere it renders.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Backend — `nfc_create_employee` endpoint
|
||||
|
||||
**Files:**
|
||||
- Modify: `controllers/clock_nfc_kiosk.py` (add route after `nfc_enroll`)
|
||||
- Test: `tests/test_clock_nfc_kiosk.py` (add a method)
|
||||
|
||||
- [ ] **Step 1: Add the endpoint.** Manager/Kiosk-Operator gated (`_is_kiosk_operator`) + password gated (`_check_enroll_password`). Create `hr.employee` via sudo with name + `x_fclk_enable_clock=True` + `company_id`. Return `{employee_id, employee_name}` or `{error}`.
|
||||
|
||||
```python
|
||||
@http.route('/fusion_clock/kiosk/nfc/create_employee', type='jsonrpc', auth='user', methods=['POST'])
|
||||
def nfc_create_employee(self, name='', enroll_password='', **kw):
|
||||
"""Create a minimal hr.employee from the kiosk (manager+password gated)."""
|
||||
user = request.env.user
|
||||
if not _is_kiosk_operator(user):
|
||||
return {'error': 'access_denied'}
|
||||
if not self._check_enroll_password(request.env, enroll_password):
|
||||
return {'error': 'invalid_password'}
|
||||
clean = (name or '').strip()
|
||||
if len(clean) < 2:
|
||||
return {'error': 'invalid_name'}
|
||||
employee = request.env['hr.employee'].sudo().create({
|
||||
'name': clean,
|
||||
'x_fclk_enable_clock': True,
|
||||
'company_id': request.env.company.id,
|
||||
})
|
||||
return {'employee_id': employee.id, 'employee_name': employee.name}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add a unit test** (runs when a test env is available; mirrors existing tests in the file).
|
||||
|
||||
```python
|
||||
def test_nfc_create_employee_creates_clock_enabled(self):
|
||||
Ctrl = self._controller() # follow existing pattern in this file for instantiating
|
||||
# password gate: wrong password rejected
|
||||
bad = Ctrl.nfc_create_employee(name='Test Person', enroll_password='wrong')
|
||||
self.assertEqual(bad.get('error'), 'invalid_password')
|
||||
# happy path (set the configured password in the test env first)
|
||||
self.env['ir.config_parameter'].sudo().set_param('fusion_clock.nfc_enroll_password', '1120')
|
||||
res = Ctrl.nfc_create_employee(name='Test Person', enroll_password='1120')
|
||||
emp = self.env['hr.employee'].browse(res['employee_id'])
|
||||
self.assertTrue(emp.exists())
|
||||
self.assertTrue(emp.x_fclk_enable_clock)
|
||||
```
|
||||
> If the existing test file doesn't instantiate controllers directly, adapt to its harness (or assert via model behaviour). Keep parity with existing tests.
|
||||
|
||||
- [ ] **Step 3: Verify.** `docker exec ... pyflakes controllers/clock_nfc_kiosk.py` (locally: `python3 -m pyflakes`). Expected: clean. Unit test runs in the next test invocation / on a Community dev box.
|
||||
|
||||
---
|
||||
|
||||
### Task 2: JS — reusable fixed PIN-pad component (fixes per-digit re-render)
|
||||
|
||||
**Files:** Modify `static/src/js/fusion_clock_nfc_kiosk.js`
|
||||
|
||||
- [ ] **Step 1:** Add a `mountPinPad({title, onOk, onCancel})` helper that sets `stateContainer.innerHTML` **once** (title, `.pin-display`, numpad, cancel), keeps a local `let pin = ""`, and on digit/back/ok updates **only** `displayEl.textContent = "•".repeat(pin.length)` — never re-renders the panel. `ok` calls `onOk(pin)`; cancel calls `onCancel()`. Resets the enroll idle timer on each press.
|
||||
- [ ] **Step 2:** Rewrite `renderEnroll(phase:"password")` to call `mountPinPad({title:"Enter Manager PIN", onOk:(pin)=>{enrollPassword=pin; renderEnroll({phase:"search"});}, onCancel:exitEnrollMode})`. Remove the old per-digit `renderEnroll(...)` rebuild.
|
||||
- [ ] **Step 3: Verify.** Manual on device: digits append with no flicker/screen refresh; backspace works; OK advances.
|
||||
|
||||
---
|
||||
|
||||
### Task 3: JS+SCSS — program-a-tag from an unknown tap (with create-new-employee)
|
||||
|
||||
**Files:** Modify `fusion_clock_nfc_kiosk.js`, `static/src/scss/nfc_kiosk.scss`
|
||||
|
||||
- [ ] **Step 1:** Add module var `let pendingEnrollUid = null;`. In `handleTap`, when `result.error === "card_unknown"`, call `renderUnknownCard(uid)` instead of the generic error result.
|
||||
- [ ] **Step 2:** `renderUnknownCard(uid)` renders an **amber** panel: "This card isn't programmed yet" + buttons "Program this card" / "Cancel". Auto-cancel to IDLE after 8s. "Program this card" → `pendingEnrollUid = uid; enrollPassword=""; setState(STATE.ENROLL,{phase:"program_pin"})`.
|
||||
- [ ] **Step 3:** Add enroll phases:
|
||||
- `program_pin` → `mountPinPad({title:"Manager PIN", onOk:(pin)=>{enrollPassword=pin; renderEnroll({phase:"employee"});}, onCancel:exitEnrollMode})`.
|
||||
- `employee` → search box (reuse existing `employee_search` debounced fetch) + a **"+ New employee"** button. Picking an existing row → `assignPendingCard(emp)`. "+ New employee" → `renderEnroll({phase:"new_employee"})`.
|
||||
- `new_employee` → a name input + "Create & assign" / back. On submit → POST `create_employee` {name, enroll_password}; on success → `assignPendingCard({id, name})`; on error → inline message (escape).
|
||||
- [ ] **Step 4:** `assignPendingCard(emp)`: POST `nfc/enroll` {employee_id: emp.id, card_uid: pendingEnrollUid, enroll_password}. Render enroll `result` phase (reuse existing). On done/another → reset `pendingEnrollUid`, back to IDLE.
|
||||
- [ ] **Step 5:** SCSS — add `.nfc-kiosk__result--warn` (amber: `#e0a83e`-ish border/glow) and a `.employee-create` styling block (reuse `.nfc-kiosk__enroll-panel` patterns). Escape all dynamic strings.
|
||||
- [ ] **Step 6: Verify.** `xmllint`/sass compile via deploy; device: unknown tap → program existing + new employee, card binds with no re-tap.
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Speed — "Fast" timers + animation durations
|
||||
|
||||
**Files:** Modify `fusion_clock_nfc_kiosk.js`, `static/src/scss/nfc_kiosk.scss`
|
||||
|
||||
- [ ] **Step 1 (JS):** In `renderResult`: success `setTimeout(... , 3000)` → `1800`; error `4000` → `3000`.
|
||||
- [ ] **Step 2 (SCSS):** `nfc-state-in` 400ms→200ms (the `#nfc_state_container > *` rule + keyframe usages); `.nfc-kiosk__result--success` `nfc-success-burst` 700ms→350ms; `.nfc-kiosk__avatar` `nfc-avatar-in` 600ms→300ms. Leave idle wave/chip + mesh drift unchanged. Keep `prefers-reduced-motion` block.
|
||||
- [ ] **Step 3: Verify.** Device: noticeably snappier; result clears ~1.8s.
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Clock-out shift hours — prominent + correct label
|
||||
|
||||
**Files:** Modify `fusion_clock_nfc_kiosk.js`, `static/src/scss/nfc_kiosk.scss`
|
||||
|
||||
- [ ] **Step 1 (JS):** In `renderResult` success branch, for `action === "clock_out"`: compute `const mins = Math.round((payload.net_hours_today || 0) * 60); const h = Math.floor(mins/60); const m = mins%60;` and always render `<div class="hours">Worked ${h}h ${m}m this shift</div>` (show even at 0). Clock-in: no hours line.
|
||||
- [ ] **Step 2 (SCSS):** Bump `.nfc-kiosk__result-text .hours` prominence (e.g. `font-size: 1.35rem; opacity: 0.9; margin-top: 0.6rem;`).
|
||||
- [ ] **Step 3: Verify.** Device: clock-out shows "Worked Xh Ym this shift".
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Version bump + deploy + verify
|
||||
|
||||
- [ ] **Step 1:** Bump `__manifest__.py` `version` `19.0.3.6.0` → `19.0.3.7.0` (assets changed).
|
||||
- [ ] **Step 2:** Local pre-flight: `pyflakes` controller, `xmllint`? (JS has no linter here — read carefully), manifest `ast.literal_eval`.
|
||||
- [ ] **Step 3:** Deploy to entech (backup → push 4 files → `-u fusion_clock` stop/upgrade/start). Bump asset cache (version bump handles it; `DELETE FROM ir_attachment WHERE url LIKE '/web/assets/%'` + restart if the bundle doesn't refresh).
|
||||
- [ ] **Step 4:** Verify: service active, version 19.0.3.7.0, manifest route 200. On tablet (hard refresh): PIN no flicker; unknown tap → program (existing + new); faster; clock-out hours.
|
||||
|
||||
---
|
||||
|
||||
## Self-review
|
||||
- **Spec coverage:** PIN fix (T2), unknown-tap+create-new (T1,T3), speed (T4), clock-out hours (T5), deploy (T6). All covered.
|
||||
- **Placeholders:** none (test harness instantiation noted as adapt-to-existing — acceptable, file-specific).
|
||||
- **Consistency:** `pendingEnrollUid`, `enrollPassword`, `mountPinPad`, `assignPendingCard`, `_is_kiosk_operator`, `_check_enroll_password`, `net_hours_today` used consistently with the existing code read.
|
||||
Reference in New Issue
Block a user