feat(fusion_clock): NFC kiosk Enroll Mode UI
Replaces the Task 18 stub renderEnroll with the full four-phase implementation (password numpad → employee picker → tap-to-enroll → result), adds _onEnrollTap wired to the NFC reading event, and exposes it via window.__nfcKiosk. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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) {
|
function renderEnroll(payload) {
|
||||||
// Full implementation lands in Task 18; this stub keeps the state machine valid.
|
const phase = (payload && payload.phase) || "password";
|
||||||
stateContainer.innerHTML = `<div class="nfc-kiosk__processing">Enroll mode (filled in by Task 18)</div>`;
|
resetEnrollIdleTimer();
|
||||||
|
|
||||||
|
if (phase === "password") {
|
||||||
|
const masked = "•".repeat(enrollPassword.length);
|
||||||
|
stateContainer.innerHTML = `
|
||||||
|
<div class="nfc-kiosk__enroll-overlay">
|
||||||
|
<div class="nfc-kiosk__enroll-panel">
|
||||||
|
<h2>Enter Enroll Mode Password</h2>
|
||||||
|
<div class="pin-display">${masked}</div>
|
||||||
|
<div class="numpad">
|
||||||
|
${[1,2,3,4,5,6,7,8,9].map(n => `<button data-n="${n}">${n}</button>`).join("")}
|
||||||
|
<button data-n="back">⌫</button>
|
||||||
|
<button data-n="0">0</button>
|
||||||
|
<button data-n="ok">OK</button>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="cancel" id="enroll_cancel">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
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 = `
|
||||||
|
<div class="nfc-kiosk__enroll-overlay">
|
||||||
|
<div class="nfc-kiosk__enroll-panel">
|
||||||
|
<h2>Pick the employee to enroll</h2>
|
||||||
|
<input class="employee-search" id="enroll_search" placeholder="Search by name…" autocomplete="off"/>
|
||||||
|
<div class="employee-list" id="enroll_list"></div>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="cancel" id="enroll_cancel">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
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 =>
|
||||||
|
`<div class="employee-row" data-id="${e.id}" data-name="${escapeHtml(e.name)}">${escapeHtml(e.name)}<small style="opacity:.6"> · ${escapeHtml(e.department || "")}</small></div>`
|
||||||
|
).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 = `
|
||||||
|
<div class="nfc-kiosk__enroll-overlay">
|
||||||
|
<div class="nfc-kiosk__enroll-panel" style="text-align:center">
|
||||||
|
<h2>Now tap ${escapeHtml(enrollSelectedEmployee.name)}'s card</h2>
|
||||||
|
<div class="nfc-kiosk__icon" style="font-size:5rem">⌐■</div>
|
||||||
|
<p style="color:#9ba3ad">Hold the card to the back of the tablet</p>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="cancel" id="enroll_cancel">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
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 = `
|
||||||
|
<div class="nfc-kiosk__enroll-overlay">
|
||||||
|
<div class="nfc-kiosk__enroll-panel" style="text-align:center">
|
||||||
|
<h2 style="color:${ok ? "#18a957" : "#d9374e"}">${msg}</h2>
|
||||||
|
<div class="actions" style="justify-content:center">
|
||||||
|
<button class="confirm" id="enroll_another">Enroll another</button>
|
||||||
|
<button class="cancel" id="enroll_done">Done</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
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) {
|
function escapeHtml(s) {
|
||||||
@@ -234,6 +396,6 @@
|
|||||||
|
|
||||||
window.__nfcKiosk = {
|
window.__nfcKiosk = {
|
||||||
setState, STATE, photoRequired, debugEnabled, locationConfigured,
|
setState, STATE, photoRequired, debugEnabled, locationConfigured,
|
||||||
handleTap, // exposed for mock-tap debug (Task 19)
|
handleTap, _onEnrollTap, // handleTap for mock-tap debug (Task 19)
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user