This commit is contained in:
gsinghpal
2026-02-23 00:32:20 -05:00
parent d6bac8e623
commit e8e554de95
549 changed files with 1330 additions and 124935 deletions

View File

@@ -1,300 +0,0 @@
/** @odoo-module **/
/**
* Fusion Clock - Location Widgets (Odoo 19)
*
* 1) fclk_places_address - CharField with Google Places Autocomplete.
* Selecting a place auto-fills lat/lng on the record (no manual geocode).
*
* 2) fclk_map - Interactive map view widget.
* Reacts live to lat/lng/radius changes on the record.
* Dragging the pin updates the record directly (no separate save).
*/
import { Component, onMounted, onWillUnmount, useRef, useState, useEffect } from "@odoo/owl";
import { CharField, charField } from "@web/views/fields/char/char_field";
import { registry } from "@web/core/registry";
import { rpc } from "@web/core/network/rpc";
import { useService } from "@web/core/utils/hooks";
import { standardWidgetProps } from "@web/views/widgets/standard_widget_props";
// ---------------------------------------------------------------------------
// Google Maps JS API singleton loader
// ---------------------------------------------------------------------------
let _gmapsPromise = null;
function loadGoogleMapsAPI(apiKey) {
if (window.google?.maps?.places) {
return Promise.resolve();
}
if (_gmapsPromise) {
return _gmapsPromise;
}
_gmapsPromise = new Promise((resolve, reject) => {
window.__fclkGmapsReady = () => {
delete window.__fclkGmapsReady;
resolve();
};
const s = document.createElement("script");
s.src =
"https://maps.googleapis.com/maps/api/js?key=" +
encodeURIComponent(apiKey) +
"&libraries=places&callback=__fclkGmapsReady";
s.async = true;
s.onerror = () => reject(new Error("Failed to load Google Maps JS API"));
document.head.appendChild(s);
});
return _gmapsPromise;
}
// ===========================================================================
// 1) fclk_map - Interactive map view widget
// ===========================================================================
export class FclkLocationMap extends Component {
static template = "fusion_clock.LocationMap";
static props = { ...standardWidgetProps };
setup() {
this.mapContainerRef = useRef("mapContainer");
this.searchRef = useRef("searchInput");
this.notification = useService("notification");
this.state = useState({
loaded: false,
error: null,
});
this.map = null;
this.marker = null;
this.circle = null;
this._skipSync = false;
onMounted(() => this._init());
onWillUnmount(() => this._cleanup());
// React to record data changes (lat, lng, radius)
useEffect(
() => {
this._syncMapToRecord();
},
() => {
const d = this.props.record.data;
return [d.latitude, d.longitude, d.radius];
}
);
}
get record() {
return this.props.record;
}
// ------------------------------------------------------------------
// Initialise
// ------------------------------------------------------------------
async _init() {
try {
const settings = await rpc("/fusion_clock/get_settings");
const apiKey = settings?.google_maps_api_key;
if (!apiKey) {
this.state.error =
"Google Maps API key not configured. Go to Fusion Clock Settings.";
return;
}
await loadGoogleMapsAPI(apiKey);
this.state.loaded = true;
await new Promise((r) => requestAnimationFrame(r));
this._buildMap();
} catch (e) {
this.state.error = "Could not load Google Maps: " + (e.message || e);
}
}
// ------------------------------------------------------------------
// Build the map + marker + circle + search
// ------------------------------------------------------------------
_buildMap() {
const lat = this.record.data.latitude || 43.65;
const lng = this.record.data.longitude || -79.38;
const radius = this.record.data.radius || 100;
const hasCoords =
this.record.data.latitude !== 0 || this.record.data.longitude !== 0;
const el = this.mapContainerRef.el;
if (!el) return;
this.map = new google.maps.Map(el, {
center: { lat, lng },
zoom: hasCoords ? 17 : 12,
mapTypeControl: true,
streetViewControl: false,
fullscreenControl: true,
});
this.marker = new google.maps.Marker({
position: { lat, lng },
map: this.map,
draggable: !this.props.readonly,
animation: google.maps.Animation.DROP,
title: "Drag to fine-tune location",
});
this.circle = new google.maps.Circle({
map: this.map,
center: { lat, lng },
radius,
fillColor: "#10B981",
fillOpacity: 0.12,
strokeColor: "#10B981",
strokeWeight: 2,
strokeOpacity: 0.45,
});
// Pin drag -> update record directly (saves with form Save button)
this.marker.addListener("dragend", () => {
const pos = this.marker.getPosition();
this.circle.setCenter(pos);
this._skipSync = true;
this.record.update({
latitude: pos.lat(),
longitude: pos.lng(),
});
});
// Places Autocomplete search inside the map widget
if (this.searchRef.el && !this.props.readonly) {
const autocomplete = new google.maps.places.Autocomplete(
this.searchRef.el,
{ types: ["establishment", "geocode"] }
);
autocomplete.bindTo("bounds", this.map);
autocomplete.addListener("place_changed", () => {
const place = autocomplete.getPlace();
if (!place.geometry?.location) return;
const loc = place.geometry.location;
this.map.setCenter(loc);
this.map.setZoom(17);
this.marker.setPosition(loc);
this.circle.setCenter(loc);
this._skipSync = true;
const updates = {
latitude: loc.lat(),
longitude: loc.lng(),
};
if (place.formatted_address) {
updates.address = place.formatted_address;
}
this.record.update(updates);
});
}
}
// ------------------------------------------------------------------
// Sync map when record lat/lng/radius changes externally
// (e.g. from the address field's Places autocomplete)
// ------------------------------------------------------------------
_syncMapToRecord() {
if (!this.map || !this.marker || !this.circle) return;
// Skip if this change originated from our own map interaction
if (this._skipSync) {
this._skipSync = false;
return;
}
const lat = this.record.data.latitude || 43.65;
const lng = this.record.data.longitude || -79.38;
const radius = this.record.data.radius || 100;
const hasCoords =
this.record.data.latitude !== 0 || this.record.data.longitude !== 0;
const pos = { lat, lng };
this.map.setCenter(pos);
if (hasCoords) {
this.map.setZoom(17);
}
this.marker.setPosition(pos);
this.circle.setCenter(pos);
this.circle.setRadius(radius);
}
// ------------------------------------------------------------------
// Cleanup
// ------------------------------------------------------------------
_cleanup() {
this.map = null;
this.marker = null;
this.circle = null;
}
}
registry.category("view_widgets").add("fclk_map", {
component: FclkLocationMap,
});
// ===========================================================================
// 2) fclk_places_address - CharField with Google Places Autocomplete
// ===========================================================================
export class FclkPlacesAddress extends CharField {
static template = "fusion_clock.PlacesAddress";
setup() {
super.setup();
this._autocomplete = null;
onMounted(() => this._attachPlaces());
onWillUnmount(() => this._detachPlaces());
}
async _attachPlaces() {
if (this.props.readonly) return;
try {
const settings = await rpc("/fusion_clock/get_settings");
const apiKey = settings?.google_maps_api_key;
if (!apiKey) return;
await loadGoogleMapsAPI(apiKey);
const inputEl = this.input?.el;
if (!inputEl) return;
this._autocomplete = new google.maps.places.Autocomplete(inputEl, {
types: ["establishment", "geocode"],
});
this._autocomplete.addListener("place_changed", () => {
const place = this._autocomplete.getPlace();
if (!place.geometry?.location) return;
const lat = place.geometry.location.lat();
const lng = place.geometry.location.lng();
const addr = place.formatted_address || place.name || "";
// Auto-fill address + coordinates on the record
this.props.record.update({
[this.props.name]: addr,
latitude: lat,
longitude: lng,
});
});
} catch (e) {
// Silently degrade - Places is non-critical
}
}
_detachPlaces() {
if (this._autocomplete) {
google.maps.event.clearInstanceListeners(this._autocomplete);
this._autocomplete = null;
}
}
}
const fclkPlacesAddress = {
...charField,
component: FclkPlacesAddress,
};
registry.category("fields").add("fclk_places_address", fclkPlacesAddress);

View File

@@ -34,9 +34,6 @@ export class FusionClockPortal extends Interaction {
this.selectedLocationId = this.locations[0].id;
}
// Auto-detect nearest location in background
this._autoSelectNearestLocation();
// Restore localStorage state
this._restoreState();
@@ -226,7 +223,7 @@ export class FusionClockPortal extends Interaction {
if (btn) btn.classList.remove("fclk-clock-btn-out");
if (playIcon) playIcon.style.display = "block";
if (stopIcon) stopIcon.style.display = "none";
if (timer) timer.textContent = "00:00:00";
if (timer) timer.textContent = "00 : 00 : 00";
if (data.net_hours !== undefined) {
const todayEl = document.getElementById("fclk-today-hours");
@@ -271,7 +268,7 @@ export class FusionClockPortal extends Interaction {
const pad = (n) => (n < 10 ? "0" + n : "" + n);
const timerEl = document.getElementById("fclk-timer");
if (timerEl) {
timerEl.textContent = pad(h) + ":" + pad(m) + ":" + pad(s);
timerEl.textContent = pad(h) + " : " + pad(m) + " : " + pad(s);
}
}
@@ -300,53 +297,6 @@ export class FusionClockPortal extends Interaction {
el.textContent = h + ":" + (m < 10 ? "0" : "") + m + " " + ampm;
}
// =========================================================================
// Auto-detect nearest location
// =========================================================================
_autoSelectNearestLocation() {
if (this.locations.length < 1) return;
if (!navigator.geolocation) return;
navigator.geolocation.getCurrentPosition(
(pos) => {
const userLat = pos.coords.latitude;
const userLng = pos.coords.longitude;
let nearest = this.locations[0];
let minDist = Infinity;
for (const loc of this.locations) {
const d = this._haversine(userLat, userLng, loc.latitude, loc.longitude);
if (d < minDist) {
minDist = d;
nearest = loc;
}
}
this.selectedLocationId = nearest.id;
const nameEl = document.getElementById("fclk-location-name");
const addrEl = document.getElementById("fclk-location-address");
if (nameEl) nameEl.textContent = nearest.name;
if (addrEl) addrEl.textContent = nearest.address || "";
},
() => {
// Silently fall back to the first location
},
{ enableHighAccuracy: false, timeout: 5000, maximumAge: 60000 }
);
}
_haversine(lat1, lon1, lat2, lon2) {
const R = 6371000;
const toRad = (v) => (v * Math.PI) / 180;
const dLat = toRad(lat2 - lat1);
const dLon = toRad(lon2 - lon1);
const a =
Math.sin(dLat / 2) ** 2 +
Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
// =========================================================================
// Sound Effects
// =========================================================================