Initial commit
This commit is contained in:
183
fusion_clock/static/src/js/fusion_clock_systray.js
Normal file
183
fusion_clock/static/src/js/fusion_clock_systray.js
Normal file
@@ -0,0 +1,183 @@
|
||||
/** @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,
|
||||
});
|
||||
Reference in New Issue
Block a user