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 66f8e817..54fd3d0a 100644 --- a/fusion_clock/static/src/js/fusion_clock_nfc_kiosk.js +++ b/fusion_clock/static/src/js/fusion_clock_nfc_kiosk.js @@ -112,20 +112,99 @@ // ────────────────────────────────────────────────────────────── // Setup wizard // ────────────────────────────────────────────────────────────── + // ────────────────────────────────────────────────────────────── + // Web NFC reader + // ────────────────────────────────────────────────────────────── + let ndefReader = null; + let nfcReady = false; + + async function startNfcReader() { + if (!("NDEFReader" in window)) { + throw new Error("Web NFC not supported on this browser/device. Use Chrome on Android."); + } + ndefReader = new NDEFReader(); + await ndefReader.scan(); + ndefReader.addEventListener("reading", onNfcReading); + ndefReader.addEventListener("readingerror", () => { + console.warn("[nfc-kiosk] reading error; reader still active"); + }); + nfcReady = true; + } + + function onNfcReading(event) { + // event.serialNumber is the card UID — works for raw MIFARE access cards + const uid = (event.serialNumber || "").toUpperCase(); + if (!uid) return; + if (currentState === STATE.ENROLL) { + // Enroll Mode handles taps differently (wired up in Task 18) + window.__nfcKiosk._onEnrollTap && window.__nfcKiosk._onEnrollTap(uid); + return; + } + if (currentState !== STATE.IDLE) return; // ignore taps mid-result + handleTap(uid); + } + + async function handleTap(uid) { + setState(STATE.PROCESSING); + let photoB64 = ""; + try { + photoB64 = await capturePhoto(); + } catch (e) { + console.warn("[nfc-kiosk] camera capture failed", e); + // Server enforces photo_required if needed + } + try { + const result = await postJson("/fusion_clock/kiosk/nfc/tap", { card_uid: uid, photo_b64: photoB64 }); + if (result.error === "debounce") { + // silent — back to IDLE + setState(STATE.IDLE); + return; + } + setState(STATE.RESULT, result); + } catch (e) { + setState(STATE.RESULT, { error: "network", message: "No connection. Please try again." }); + } + } + + async function postJson(url, params) { + const res = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ jsonrpc: "2.0", method: "call", params }), + }); + const json = await res.json(); + return json.result || {}; + } + + // ────────────────────────────────────────────────────────────── + // Camera capture (real implementation in Task 17, stub for now) + // ────────────────────────────────────────────────────────────── + async function capturePhoto() { + return ""; // overridden in Task 17 + } + + // ────────────────────────────────────────────────────────────── + // Setup wizard activation + // ────────────────────────────────────────────────────────────── const setupBtn = document.getElementById("nfc_setup_start"); if (setupBtn) { setupBtn.addEventListener("click", async () => { - // Web NFC + camera activation lives in Tasks 16 + 17 - setState(STATE.IDLE); + try { + await startNfcReader(); + setState(STATE.IDLE); + } catch (e) { + stateContainer.innerHTML = ` +
${escapeHtml(e.message)}
+