diff --git a/fusion_clock/__manifest__.py b/fusion_clock/__manifest__.py index f17deed8..363704ac 100644 --- a/fusion_clock/__manifest__.py +++ b/fusion_clock/__manifest__.py @@ -5,7 +5,7 @@ { 'name': 'Fusion Clock', - 'version': '19.0.3.1.1', + 'version': '19.0.3.2.0', 'category': 'Human Resources/Attendances', 'summary': 'Complete Employee T&A with Geofencing, Shifts, Penalties, Overtime, Kiosk, Dashboard & Payroll Export', 'description': """ diff --git a/fusion_clock/static/src/js/fusion_clock_nfc_kiosk.js b/fusion_clock/static/src/js/fusion_clock_nfc_kiosk.js index 701279b0..65886a6c 100644 --- a/fusion_clock/static/src/js/fusion_clock_nfc_kiosk.js +++ b/fusion_clock/static/src/js/fusion_clock_nfc_kiosk.js @@ -414,6 +414,74 @@ debugLog("startNfcReader: listeners attached, nfcReady=true"); } + // ────────────────────────────────────────────────────────────── + // USB HID keyboard-wedge listener (works alongside Web NFC). + // Most USB NFC readers in HID mode type the UID as keystrokes and + // end with Enter. We buffer chars until Enter arrives (or 500ms + // pause), then route the UID through the same flow Web NFC uses. + // + // Critical: this listener fires the same handleTap()/_onEnrollTap() + // codepath as Web NFC, so penalty + photo + activity log all work + // identically regardless of which reader produced the UID. + // ────────────────────────────────────────────────────────────── + let _hidBuffer = ""; + let _hidLastKeyAt = 0; + let _hidFlushTimer = null; + const HID_RESET_MS = 500; // pause longer than this resets the buffer + const HID_FLUSH_MS = 600; // if no Enter arrives, flush this long after last char + const HID_MIN_LEN = 4; // shortest plausible UID + const HID_CHAR_RE = /^[0-9A-Fa-f:\-]$/; // hex digits + common separators + + function _flushHidBuffer() { + const uid = _hidBuffer.trim().toUpperCase(); + _hidBuffer = ""; + if (_hidFlushTimer) { clearTimeout(_hidFlushTimer); _hidFlushTimer = null; } + if (uid.length < HID_MIN_LEN) { + debugLog("HID flush: too short, ignored (" + JSON.stringify(uid) + ")"); + return; + } + debugLog("HID flush: uid=" + uid + " state=" + currentState); + if (currentState === STATE.ENROLL) { + window.__nfcKiosk && window.__nfcKiosk._onEnrollTap && window.__nfcKiosk._onEnrollTap(uid); + } else if (currentState === STATE.IDLE) { + handleTap(uid); + } else { + debugLog(" → IGNORED: state=" + currentState); + } + } + + function startUsbHidListener() { + document.addEventListener("keydown", (e) => { + // Don't capture keystrokes inside form inputs — preserves + // typing in enroll-mode search box, etc. + const t = e.target; + if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable)) { + return; + } + // Don't fight the existing Ctrl+Shift+T mock-tap shortcut. + if (e.ctrlKey || e.metaKey || e.altKey) return; + + const now = Date.now(); + if (now - _hidLastKeyAt > HID_RESET_MS) { + _hidBuffer = ""; + } + _hidLastKeyAt = now; + + if (e.key === "Enter") { + e.preventDefault(); + _flushHidBuffer(); + return; + } + if (HID_CHAR_RE.test(e.key)) { + _hidBuffer += e.key; + // Fallback flush if the reader doesn't emit Enter + if (_hidFlushTimer) clearTimeout(_hidFlushTimer); + _hidFlushTimer = setTimeout(_flushHidBuffer, HID_FLUSH_MS); + } + }); + debugLog("startUsbHidListener: listening for HID keystrokes ✓"); + } + function onNfcReading(event) { // event.serialNumber is the card UID — works for raw MIFARE access cards const rawSerial = event.serialNumber || ""; @@ -547,27 +615,42 @@ if (setupBtn) { setupBtn.addEventListener("click", async () => { debugLog("setup button clicked"); + // Try Web NFC, but don't fail if absent — USB HID reader is a + // first-class alternative (works on desktops/iOS too). + let webNfcOk = false; try { await startNfcReader(); - debugLog("setup: NFC ready, starting camera..."); - try { - await startCamera(); - debugLog("setup: camera ready ✓"); - } catch (camErr) { - debugLog("setup: camera failed: " + camErr.message); - if (photoRequired) throw camErr; - console.warn("[nfc-kiosk] camera unavailable, continuing (photo not required)", camErr); - } - await acquireWakeLock(); - setState(STATE.IDLE); - } catch (e) { - stateContainer.innerHTML = ` -
-

Setup failed

-

${escapeHtml(e.message)}

-
- `; + webNfcOk = true; + debugLog("setup: Web NFC ready ✓"); + } catch (webNfcErr) { + debugLog("setup: Web NFC unavailable, continuing with USB HID — " + webNfcErr.message); } + // USB HID listener: no permission needed, works on any platform. + startUsbHidListener(); + // Camera: best-effort unless photoRequired forces it. + try { + await startCamera(); + debugLog("setup: camera ready ✓"); + } catch (camErr) { + debugLog("setup: camera failed: " + camErr.message); + if (photoRequired) { + // Only THIS path is a hard fail. Use the existing error + // render to keep DOM patterns consistent with the rest + // of this file. + stateContainer.innerHTML = ` +
+

Setup failed

+

${escapeHtml(camErr.message)}

+

Camera is required but unavailable. Either plug in a webcam, or disable "Photo Required" in Fusion Clock settings.

+
+ `; + return; + } + console.warn("[nfc-kiosk] camera unavailable, continuing (photo not required)", camErr); + } + await acquireWakeLock(); + setState(STATE.IDLE); + debugLog("setup: IDLE — Web NFC: " + (webNfcOk ? "✓" : "✗") + " · USB HID: ✓"); }); }