243 lines
9.3 KiB
JavaScript
243 lines
9.3 KiB
JavaScript
/** @odoo-module **/
|
|
|
|
import { Interaction } from "@web/public/interaction";
|
|
import { registry } from "@web/core/registry";
|
|
|
|
export class FusionClockKiosk extends Interaction {
|
|
static selector = "#fclk-kiosk";
|
|
|
|
setup() {
|
|
this.selectedEmployeeId = 0;
|
|
this.resetTimer = null;
|
|
this.searchTimeout = null;
|
|
|
|
const pinAttr = this.el.dataset.pinRequired;
|
|
this.pinRequired = pinAttr === "true" || pinAttr === "True";
|
|
|
|
this._startClock();
|
|
this._bindEvents();
|
|
}
|
|
|
|
_startClock() {
|
|
const el = document.getElementById("fclk-kiosk-time");
|
|
if (!el) return;
|
|
const update = () => {
|
|
el.textContent = new Date().toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
};
|
|
update();
|
|
setInterval(update, 1000);
|
|
}
|
|
|
|
_bindEvents() {
|
|
const queryInput = document.getElementById("fclk-kiosk-query");
|
|
if (queryInput) {
|
|
queryInput.addEventListener("input", (e) => this._onSearch(e.target.value));
|
|
}
|
|
|
|
const backBtn = document.getElementById("fclk-kiosk-back-btn");
|
|
if (backBtn) {
|
|
backBtn.addEventListener("click", () => this._resetKiosk());
|
|
}
|
|
|
|
const clockBtn = document.getElementById("fclk-kiosk-clock-btn");
|
|
if (clockBtn) {
|
|
clockBtn.addEventListener("click", () => this._onClock());
|
|
}
|
|
}
|
|
|
|
_resetKiosk() {
|
|
const search = document.getElementById("fclk-kiosk-search");
|
|
const pin = document.getElementById("fclk-kiosk-pin");
|
|
const result = document.getElementById("fclk-kiosk-result");
|
|
const error = document.getElementById("fclk-kiosk-error");
|
|
const query = document.getElementById("fclk-kiosk-query");
|
|
const results = document.getElementById("fclk-kiosk-results");
|
|
const pinInput = document.getElementById("fclk-kiosk-pin-input");
|
|
|
|
if (search) search.style.display = "";
|
|
if (pin) pin.style.display = "none";
|
|
if (result) result.style.display = "none";
|
|
if (error) error.style.display = "none";
|
|
if (query) query.value = "";
|
|
if (results) results.innerHTML = "";
|
|
if (pinInput) pinInput.value = "";
|
|
|
|
this.selectedEmployeeId = 0;
|
|
if (this.resetTimer) clearTimeout(this.resetTimer);
|
|
}
|
|
|
|
_showError(msg) {
|
|
const el = document.getElementById("fclk-kiosk-error");
|
|
if (el) {
|
|
el.textContent = msg;
|
|
el.style.display = "";
|
|
}
|
|
}
|
|
|
|
_onSearch(value) {
|
|
if (this.searchTimeout) clearTimeout(this.searchTimeout);
|
|
const q = value.trim();
|
|
if (q.length < 2) {
|
|
const container = document.getElementById("fclk-kiosk-results");
|
|
if (container) container.innerHTML = "";
|
|
return;
|
|
}
|
|
this.searchTimeout = setTimeout(async () => {
|
|
try {
|
|
const resp = await fetch("/fusion_clock/kiosk/search", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ jsonrpc: "2.0", method: "call", params: { query: q } }),
|
|
});
|
|
const data = await resp.json();
|
|
const employees = (data.result || {}).employees || [];
|
|
const container = document.getElementById("fclk-kiosk-results");
|
|
if (!container) return;
|
|
container.innerHTML = "";
|
|
for (const emp of employees) {
|
|
const item = document.createElement("a");
|
|
item.href = "#";
|
|
item.className = "list-group-item list-group-item-action d-flex justify-content-between";
|
|
const statusBadge = emp.is_checked_in ? "bg-success" : "bg-secondary";
|
|
const statusText = emp.is_checked_in ? "In" : "Out";
|
|
item.innerHTML =
|
|
`<span>${emp.name} <small class="text-muted">${emp.department}</small></span>` +
|
|
`<span class="badge ${statusBadge}">${statusText}</span>`;
|
|
item.addEventListener("click", (e) => {
|
|
e.preventDefault();
|
|
this._selectEmployee(emp);
|
|
});
|
|
container.appendChild(item);
|
|
}
|
|
} catch {
|
|
this._showError("Search failed.");
|
|
}
|
|
}, 300);
|
|
}
|
|
|
|
_selectEmployee(emp) {
|
|
this.selectedEmployeeId = emp.id;
|
|
const nameEl = document.getElementById("fclk-kiosk-emp-name");
|
|
if (nameEl) nameEl.textContent = emp.name;
|
|
|
|
const searchEl = document.getElementById("fclk-kiosk-search");
|
|
const pinEl = document.getElementById("fclk-kiosk-pin");
|
|
const errorEl = document.getElementById("fclk-kiosk-error");
|
|
if (searchEl) searchEl.style.display = "none";
|
|
if (pinEl) pinEl.style.display = "";
|
|
if (errorEl) errorEl.style.display = "none";
|
|
|
|
const clockBtn = document.getElementById("fclk-kiosk-clock-btn");
|
|
if (clockBtn) {
|
|
clockBtn.textContent = emp.is_checked_in ? "Clock Out" : "Clock In";
|
|
clockBtn.className = "btn btn-lg " + (emp.is_checked_in ? "btn-danger" : "btn-success");
|
|
}
|
|
}
|
|
|
|
async _onClock() {
|
|
if (!this.selectedEmployeeId) return;
|
|
|
|
const btn = document.getElementById("fclk-kiosk-clock-btn");
|
|
if (btn) btn.disabled = true;
|
|
|
|
const pinInput = document.getElementById("fclk-kiosk-pin-input");
|
|
const pin = pinInput ? pinInput.value : "";
|
|
|
|
if (this.pinRequired && pin.length === 0) {
|
|
this._showError("Please enter your PIN.");
|
|
if (btn) btn.disabled = false;
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (this.pinRequired) {
|
|
const vResp = await fetch("/fusion_clock/kiosk/verify_pin", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
method: "call",
|
|
params: { employee_id: this.selectedEmployeeId, pin },
|
|
}),
|
|
});
|
|
const vData = await vResp.json();
|
|
if (vData.result && vData.result.error) {
|
|
this._showError(vData.result.error);
|
|
if (btn) btn.disabled = false;
|
|
return;
|
|
}
|
|
}
|
|
|
|
let lat = 0;
|
|
let lng = 0;
|
|
try {
|
|
const pos = await new Promise((resolve, reject) => {
|
|
navigator.geolocation.getCurrentPosition(resolve, reject, {
|
|
timeout: 10000,
|
|
enableHighAccuracy: true,
|
|
});
|
|
});
|
|
lat = pos.coords.latitude;
|
|
lng = pos.coords.longitude;
|
|
} catch {
|
|
// Native GPS unavailable -- try IP geolocation
|
|
}
|
|
if (lat === 0 && lng === 0) {
|
|
try {
|
|
const ipResp = await fetch("https://ipapi.co/json/");
|
|
if (ipResp.ok) {
|
|
const ipData = await ipResp.json();
|
|
if (ipData.latitude && ipData.longitude) {
|
|
lat = ipData.latitude;
|
|
lng = ipData.longitude;
|
|
}
|
|
}
|
|
} catch {
|
|
// IP geolocation also unavailable
|
|
}
|
|
}
|
|
|
|
const resp = await fetch("/fusion_clock/kiosk/clock", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
method: "call",
|
|
params: { employee_id: this.selectedEmployeeId, latitude: lat, longitude: lng },
|
|
}),
|
|
});
|
|
const data = await resp.json();
|
|
const result = data.result || {};
|
|
|
|
if (result.error) {
|
|
this._showError(result.error);
|
|
if (btn) btn.disabled = false;
|
|
return;
|
|
}
|
|
|
|
const pinEl = document.getElementById("fclk-kiosk-pin");
|
|
const resultEl = document.getElementById("fclk-kiosk-result");
|
|
if (pinEl) pinEl.style.display = "none";
|
|
if (resultEl) resultEl.style.display = "";
|
|
|
|
const msgEl = document.getElementById("fclk-kiosk-result-msg");
|
|
if (msgEl) {
|
|
const icon = result.action === "clock_in" ? "fa-check-circle text-success" : "fa-hand-paper-o text-warning";
|
|
let html = `<div style="font-size:3rem"><i class="fa ${icon}"></i></div>`;
|
|
html += `<div class="mt-2">${result.message || "Done"}</div>`;
|
|
if (result.net_hours !== undefined) {
|
|
html += `<div class="text-muted mt-1">Net hours: ${result.net_hours}h</div>`;
|
|
}
|
|
msgEl.innerHTML = html;
|
|
}
|
|
|
|
this.resetTimer = setTimeout(() => this._resetKiosk(), 10000);
|
|
} catch {
|
|
this._showError("Operation failed.");
|
|
}
|
|
if (btn) btn.disabled = false;
|
|
}
|
|
}
|
|
|
|
registry.category("public.interactions").add("fusion_clock.kiosk", FusionClockKiosk);
|