/** @odoo-module **/ import { Component, useState, onWillStart, onMounted, onWillUnmount } from "@odoo/owl"; import { rpc } from "@web/core/network/rpc"; import { registry } from "@web/core/registry"; import { useService } from "@web/core/utils/hooks"; export class FusionClockFAB extends Component { static props = {}; static template = "fusion_clock.ClockFAB"; setup() { this.notification = useService("notification"); this.state = useState({ isCheckedIn: false, isDisplayed: false, expanded: false, checkInTime: null, locationName: "", timerDisplay: "00:00:00", todayHours: "0.0", weekHours: "0.0", loading: false, error: "", }); this._timerInterval = null; onWillStart(async () => { await this._fetchStatus(); }); onMounted(() => { if (this.state.isCheckedIn) { this._startTimer(); } // Poll every 15s to stay in sync with portal clock-outs this._pollInterval = setInterval(() => this._fetchStatus(), 15000); // Re-sync immediately when browser tab regains focus this._onFocus = () => this._fetchStatus(); window.addEventListener("focus", this._onFocus); // Close panel when clicking outside this._onDocClick = (ev) => { if (!this.state.expanded) return; const el = ev.target.closest(".fclk-fab-wrapper"); if (!el) this.state.expanded = false; }; document.addEventListener("click", this._onDocClick, true); }); onWillUnmount(() => { this._stopTimer(); if (this._pollInterval) clearInterval(this._pollInterval); if (this._onDocClick) document.removeEventListener("click", this._onDocClick, true); if (this._onFocus) window.removeEventListener("focus", this._onFocus); }); } togglePanel() { this.state.expanded = !this.state.expanded; this.state.error = ""; // Always re-fetch when opening the panel if (this.state.expanded) { this._fetchStatus(); } } async _fetchStatus() { try { const result = await rpc("/fusion_clock/get_status", {}); if (result.error) { this.state.isDisplayed = false; return; } this.state.isDisplayed = result.enable_clock !== false; this.state.isCheckedIn = result.is_checked_in; this.state.locationName = result.location_name || ""; this.state.todayHours = (result.today_hours || 0).toFixed(1); this.state.weekHours = (result.week_hours || 0).toFixed(1); if (result.is_checked_in && result.check_in) { this.state.checkInTime = new Date(result.check_in + "Z"); this._startTimer(); } else { this.state.checkInTime = null; this._stopTimer(); this.state.timerDisplay = "00:00:00"; } } catch (e) { this.state.isDisplayed = false; } } async onClockAction() { this.state.loading = true; this.state.error = ""; try { let lat = 0, lng = 0, acc = 0; if (navigator.geolocation) { try { const pos = await new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition(resolve, reject, { enableHighAccuracy: true, timeout: 15000, maximumAge: 0, }); }); lat = pos.coords.latitude; lng = pos.coords.longitude; acc = pos.coords.accuracy; } catch (geoErr) { this.state.error = "Location access denied."; this.state.loading = false; return; } } const result = await rpc("/fusion_clock/clock_action", { latitude: lat, longitude: lng, accuracy: acc, source: "backend_fab", }); if (result.error) { this.state.error = result.error; this.state.loading = false; return; } if (result.action === "clock_in") { this.state.isCheckedIn = true; this.state.checkInTime = new Date(result.check_in + "Z"); this.state.locationName = result.location_name || ""; this._startTimer(); this.notification.add(result.message || "Clocked in!", { type: "success" }); } else if (result.action === "clock_out") { this.state.isCheckedIn = false; this.state.checkInTime = null; this._stopTimer(); this.state.timerDisplay = "00:00:00"; this.notification.add(result.message || "Clocked out!", { type: "success" }); await this._fetchStatus(); } } catch (e) { this.state.error = e.message || "Clock action failed."; } this.state.loading = false; } _startTimer() { this._stopTimer(); this._updateTimer(); this._timerInterval = setInterval(() => this._updateTimer(), 1000); } _stopTimer() { if (this._timerInterval) { clearInterval(this._timerInterval); this._timerInterval = null; } } _updateTimer() { if (!this.state.checkInTime) return; const now = new Date(); let diff = Math.max(0, Math.floor((now - this.state.checkInTime) / 1000)); const h = Math.floor(diff / 3600); const m = Math.floor((diff % 3600) / 60); const s = diff % 60; const pad = (n) => (n < 10 ? "0" + n : "" + n); this.state.timerDisplay = pad(h) + ":" + pad(m) + ":" + pad(s); } } registry.category("main_components").add("FusionClockFAB", { Component: FusionClockFAB, });