diff --git a/fusion_clock/__manifest__.py b/fusion_clock/__manifest__.py index 7dcc4ca7..6e5962f9 100644 --- a/fusion_clock/__manifest__.py +++ b/fusion_clock/__manifest__.py @@ -84,6 +84,7 @@ Integrates natively with Odoo's hr.attendance module for full payroll compatibil 'fusion_clock/static/src/scss/nfc_kiosk.scss', 'fusion_clock/static/src/js/fusion_clock_portal.js', 'fusion_clock/static/src/js/fusion_clock_kiosk.js', + 'fusion_clock/static/src/js/fusion_clock_nfc_kiosk.js', ], 'web.assets_backend': [ 'fusion_clock/static/src/scss/fusion_clock.scss', diff --git a/fusion_clock/static/src/js/fusion_clock_nfc_kiosk.js b/fusion_clock/static/src/js/fusion_clock_nfc_kiosk.js new file mode 100644 index 00000000..66f8e817 --- /dev/null +++ b/fusion_clock/static/src/js/fusion_clock_nfc_kiosk.js @@ -0,0 +1,131 @@ +/* @odoo-module */ + +// NFC Clock Kiosk — Web NFC + camera + state machine. +// Loaded as a frontend asset on /fusion_clock/kiosk/nfc only (the +// element #nfc_kiosk_root only exists on that page, so the module is +// inert elsewhere). + +(function() { + "use strict"; + + const root = document.getElementById("nfc_kiosk_root"); + if (!root) return; // not on the kiosk page + + const stateContainer = document.getElementById("nfc_state_container"); + const photoRequired = root.dataset.photoRequired === "1"; + const debugEnabled = root.dataset.debugEnabled === "1"; + const locationConfigured = root.dataset.locationConfigured === "1"; + + // ────────────────────────────────────────────────────────────── + // State machine + // ────────────────────────────────────────────────────────────── + const STATE = { SETUP: "setup", IDLE: "idle", PROCESSING: "processing", RESULT: "result", ENROLL: "enroll" }; + let currentState = STATE.SETUP; + + function setState(next, payload) { + currentState = next; + if (next === STATE.IDLE) renderIdle(); + else if (next === STATE.PROCESSING) renderProcessing(); + else if (next === STATE.RESULT) renderResult(payload); + else if (next === STATE.ENROLL) renderEnroll(payload); + } + + // ────────────────────────────────────────────────────────────── + // Rendering helpers + // ────────────────────────────────────────────────────────────── + function renderIdle() { + stateContainer.innerHTML = ` +
+
⌐■
+
Tap your card to clock in or out
+
+ `; + } + + function renderProcessing() { + stateContainer.innerHTML = ` +
Reading card…
+ `; + } + + function renderResult(payload) { + const isError = payload && payload.error; + const cls = isError ? "nfc-kiosk__result--error" : "nfc-kiosk__result--success"; + + if (isError) { + stateContainer.innerHTML = ` +
+
+
${escapeHtml(payload.message || "Error")}
+
+
+ `; + setTimeout(() => setState(STATE.IDLE), 4000); + } else { + const avatar = payload.employee_avatar_url || ""; + const action = payload.action === "clock_in" ? "CLOCKED IN" : "CLOCKED OUT"; + const hours = payload.action === "clock_out" && payload.net_hours_today + ? `${payload.net_hours_today.toFixed(1)}h today` + : ""; + const time = new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); + stateContainer.innerHTML = ` +
+
+
+
${escapeHtml(payload.employee_name)}
+
${action} at ${time}
+ ${hours ? `
${hours}
` : ""} +
+
+ `; + setTimeout(() => setState(STATE.IDLE), 3000); + } + } + + 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)
`; + } + + function escapeHtml(s) { + return String(s || "").replace(/[&<>"']/g, c => ({ + "&": "&", "<": "<", ">": ">", '"': """, "'": "'" + }[c])); + } + + // ────────────────────────────────────────────────────────────── + // Clock display (top-right time + date) + // ────────────────────────────────────────────────────────────── + function updateClock() { + const now = new Date(); + const hh = String(now.getHours()).padStart(2, "0"); + const mm = String(now.getMinutes()).padStart(2, "0"); + const dateStr = now.toLocaleDateString([], { weekday: "short", month: "short", day: "numeric" }); + const timeEl = document.getElementById("nfc_clock_time"); + const dateEl = document.getElementById("nfc_clock_date"); + if (timeEl) timeEl.textContent = `${hh}:${mm}`; + if (dateEl) dateEl.textContent = dateStr; + } + updateClock(); + setInterval(updateClock, 1000); + + // ────────────────────────────────────────────────────────────── + // Setup wizard + // ────────────────────────────────────────────────────────────── + 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); + }); + } + + // Expose a tiny API for later tasks + window.__nfcKiosk = { + setState, + STATE, + photoRequired, + debugEnabled, + locationConfigured, + }; +})();