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 8a82aad0..4ec5503d 100644
--- a/fusion_clock/static/src/js/fusion_clock_nfc_kiosk.js
+++ b/fusion_clock/static/src/js/fusion_clock_nfc_kiosk.js
@@ -82,9 +82,171 @@
}
}
+ // ──────────────────────────────────────────────────────────────
+ // Enroll Mode
+ // ──────────────────────────────────────────────────────────────
+ let enrollPassword = "";
+ let enrollSelectedEmployee = null;
+ let enrollIdleTimer = null;
+
+ function resetEnrollIdleTimer() {
+ if (enrollIdleTimer) clearTimeout(enrollIdleTimer);
+ enrollIdleTimer = setTimeout(() => {
+ // 60s of inactivity in Enroll Mode → exit
+ exitEnrollMode();
+ }, 60000);
+ }
+
+ function exitEnrollMode() {
+ if (enrollIdleTimer) clearTimeout(enrollIdleTimer);
+ enrollIdleTimer = null;
+ enrollPassword = "";
+ enrollSelectedEmployee = null;
+ setState(STATE.IDLE);
+ }
+
function renderEnroll(payload) {
- // Full implementation lands in Task 18; this stub keeps the state machine valid.
- stateContainer.innerHTML = `
Enroll mode (filled in by Task 18)
`;
+ const phase = (payload && payload.phase) || "password";
+ resetEnrollIdleTimer();
+
+ if (phase === "password") {
+ const masked = "•".repeat(enrollPassword.length);
+ stateContainer.innerHTML = `
+
+
+
Enter Enroll Mode Password
+
${masked}
+
+ ${[1,2,3,4,5,6,7,8,9].map(n => ``).join("")}
+
+
+
+
+
+
+
+
+
+ `;
+ stateContainer.querySelectorAll(".numpad button").forEach(btn => {
+ btn.addEventListener("click", async () => {
+ resetEnrollIdleTimer();
+ const n = btn.dataset.n;
+ if (n === "back") enrollPassword = enrollPassword.slice(0, -1);
+ else if (n === "ok") {
+ if (enrollPassword.length === 0) return;
+ renderEnroll({ phase: "search" });
+ return;
+ }
+ else enrollPassword += n;
+ renderEnroll({ phase: "password" });
+ });
+ });
+ document.getElementById("enroll_cancel").addEventListener("click", exitEnrollMode);
+ return;
+ }
+
+ if (phase === "search") {
+ stateContainer.innerHTML = `
+
+
+
Pick the employee to enroll
+
+
+
+
+
+
+
+ `;
+ const searchEl = document.getElementById("enroll_search");
+ const listEl = document.getElementById("enroll_list");
+ let debounceTimer = null;
+ searchEl.addEventListener("input", () => {
+ resetEnrollIdleTimer();
+ if (debounceTimer) clearTimeout(debounceTimer);
+ debounceTimer = setTimeout(async () => {
+ const result = await postJson("/fusion_clock/kiosk/nfc/employee_search", { query: searchEl.value });
+ listEl.innerHTML = (result.employees || []).map(e =>
+ `${escapeHtml(e.name)} · ${escapeHtml(e.department || "")}
`
+ ).join("");
+ listEl.querySelectorAll(".employee-row").forEach(row => {
+ row.addEventListener("click", () => {
+ enrollSelectedEmployee = { id: parseInt(row.dataset.id, 10), name: row.dataset.name };
+ renderEnroll({ phase: "tap" });
+ });
+ });
+ }, 200);
+ });
+ searchEl.focus();
+ document.getElementById("enroll_cancel").addEventListener("click", exitEnrollMode);
+ return;
+ }
+
+ if (phase === "tap") {
+ stateContainer.innerHTML = `
+
+
+
Now tap ${escapeHtml(enrollSelectedEmployee.name)}'s card
+
⌐■
+
Hold the card to the back of the tablet
+
+
+
+
+
+ `;
+ document.getElementById("enroll_cancel").addEventListener("click", exitEnrollMode);
+ return;
+ }
+
+ if (phase === "result") {
+ const ok = !payload.error;
+ const msg = ok
+ ? `✓ Card ${escapeHtml(payload.card_uid)} enrolled to ${escapeHtml(payload.employee_name)}`
+ : (payload.error === "invalid_password"
+ ? "Wrong password. Try again."
+ : payload.error === "card_already_assigned"
+ ? `This card is already assigned to ${escapeHtml(payload.existing_employee || "another employee")}.`
+ : `Enroll failed: ${escapeHtml(payload.error)}`);
+ stateContainer.innerHTML = `
+
+
+
${msg}
+
+
+
+
+
+
+ `;
+ document.getElementById("enroll_another").addEventListener("click", () => {
+ enrollSelectedEmployee = null;
+ renderEnroll({ phase: ok ? "search" : "password" });
+ });
+ document.getElementById("enroll_done").addEventListener("click", exitEnrollMode);
+ }
+ }
+
+ async function _onEnrollTap(uid) {
+ if (!enrollSelectedEmployee) return;
+ const result = await postJson("/fusion_clock/kiosk/nfc/enroll", {
+ employee_id: enrollSelectedEmployee.id,
+ card_uid: uid,
+ enroll_password: enrollPassword,
+ });
+ renderEnroll({ phase: "result", ...result });
+ }
+
+ // ⚙ button → enter Enroll Mode
+ const settingsBtn = document.getElementById("nfc_settings_btn");
+ if (settingsBtn) {
+ settingsBtn.addEventListener("click", () => {
+ if (currentState !== STATE.IDLE) return;
+ enrollPassword = "";
+ enrollSelectedEmployee = null;
+ setState(STATE.ENROLL, { phase: "password" });
+ });
}
function escapeHtml(s) {
@@ -234,6 +396,6 @@
window.__nfcKiosk = {
setState, STATE, photoRequired, debugEnabled, locationConfigured,
- handleTap, // exposed for mock-tap debug (Task 19)
+ handleTap, _onEnrollTap, // handleTap for mock-tap debug (Task 19)
};
})();