changes
This commit is contained in:
@@ -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);
|
||||
@@ -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
|
||||
// =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user