Files
Odoo-Modules/fusion_clock/static/src/js/fusion_clock_location_map.js
gsinghpal b925766966 changes
2026-02-27 14:32:32 -05:00

248 lines
8.0 KiB
JavaScript

/** @odoo-module **/
import { Component, onMounted, onWillUnmount, useRef, useState } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { standardFieldProps } from "@web/views/fields/standard_field_props";
import { rpc } from "@web/core/network/rpc";
export class FusionClockLocationMap extends Component {
static template = "fusion_clock.LocationMap";
static props = { ...standardFieldProps };
setup() {
this.mapRef = useRef("mapContainer");
this.map = null;
this.marker = null;
this.circle = null;
this._suppress = false;
this._interval = null;
this._AdvancedMarkerElement = null;
this.state = useState({
loading: true,
error: "",
mapVisible: false,
});
onMounted(() => this._init());
onWillUnmount(() => this._cleanup());
}
get lat() { return this.props.record.data.latitude || 0; }
get lng() { return this.props.record.data.longitude || 0; }
get radius() { return this.props.record.data.radius || 100; }
get color() { return this.props.record.data.color || "#10B981"; }
get hasCoords() { return this.lat !== 0 || this.lng !== 0; }
async _init() {
const apiKey = await this._getApiKey();
if (!apiKey) {
this.state.loading = false;
this.state.error = "Google Maps API key not configured. Set it in Fusion Clock Settings.";
return;
}
try {
await this._loadScript(apiKey);
} catch {
this.state.loading = false;
this.state.error = "Failed to load Google Maps API.";
return;
}
try {
const { AdvancedMarkerElement } = await google.maps.importLibrary("marker");
this._AdvancedMarkerElement = AdvancedMarkerElement;
} catch {
this.state.loading = false;
this.state.error = "Failed to load marker library.";
return;
}
this.state.loading = false;
if (!this.hasCoords) {
this._startWatcher();
return;
}
this.state.mapVisible = true;
await new Promise((r) => requestAnimationFrame(r));
await new Promise((r) => requestAnimationFrame(r));
this._buildMap();
}
_buildMap() {
const el = this.mapRef.el;
if (!el || !window.google || !this._AdvancedMarkerElement) return;
const center = { lat: this.lat, lng: this.lng };
this.map = new google.maps.Map(el, {
center,
zoom: 17,
mapId: "DEMO_MAP_ID",
mapTypeControl: true,
mapTypeControlOptions: {
style: google.maps.MapTypeControlStyle.DROPDOWN_MENU,
position: google.maps.ControlPosition.TOP_RIGHT,
mapTypeIds: ["roadmap", "satellite", "hybrid"],
},
streetViewControl: false,
fullscreenControl: true,
zoomControl: true,
gestureHandling: "greedy",
});
this._placeMarker(center);
this._drawCircle(center);
if (!this.props.readonly) {
this.map.addListener("click", (e) => {
const pos = { lat: e.latLng.lat(), lng: e.latLng.lng() };
this._placeMarker(pos);
this._drawCircle(pos);
this._suppress = true;
this._saveCoords(pos.lat, pos.lng);
});
}
this._startWatcher();
}
_placeMarker(pos) {
if (this.marker) {
this.marker.position = pos;
return;
}
this.marker = new this._AdvancedMarkerElement({
map: this.map,
position: pos,
gmpDraggable: !this.props.readonly,
title: "Drag to fine-tune location",
});
if (!this.props.readonly) {
this.marker.addListener("dragend", () => {
const p = this.marker.position;
const newPos = { lat: p.lat, lng: p.lng };
this._drawCircle(newPos);
this._suppress = true;
this._saveCoords(newPos.lat, newPos.lng);
});
}
}
_drawCircle(center) {
if (this.circle) {
this.circle.setCenter(center);
this.circle.setRadius(this.radius);
this.circle.setOptions({ fillColor: this.color, strokeColor: this.color });
} else {
this.circle = new google.maps.Circle({
map: this.map,
center,
radius: this.radius,
fillColor: this.color,
fillOpacity: 0.15,
strokeColor: this.color,
strokeOpacity: 0.6,
strokeWeight: 2,
clickable: false,
});
}
}
async _saveCoords(lat, lng) {
if (this.props.readonly) return;
await this.props.record.update({
latitude: Math.round(lat * 10000000) / 10000000,
longitude: Math.round(lng * 10000000) / 10000000,
});
}
_startWatcher() {
if (this._interval) return;
this._lastLat = this.lat;
this._lastLng = this.lng;
this._lastRadius = this.radius;
this._interval = setInterval(() => {
const lat = this.lat;
const lng = this.lng;
const r = this.radius;
const moved = Math.abs(this._lastLat - lat) > 0.0000001
|| Math.abs(this._lastLng - lng) > 0.0000001;
const resized = Math.abs(this._lastRadius - r) > 0.5;
if (moved && this.map) {
this._lastLat = lat;
this._lastLng = lng;
if (this._suppress) { this._suppress = false; return; }
const pos = { lat, lng };
this._placeMarker(pos);
this._drawCircle(pos);
this.map.panTo(pos);
}
if (resized && this.circle) {
this._lastRadius = r;
this.circle.setRadius(r);
}
if (!this.map && this.hasCoords && !this.state.error && this._AdvancedMarkerElement) {
this.state.mapVisible = true;
requestAnimationFrame(() => {
requestAnimationFrame(() => {
this._buildMap();
});
});
}
}, 500);
}
async _getApiKey() {
try {
return await rpc("/web/dataset/call_kw", {
model: "ir.config_parameter",
method: "get_param",
args: ["fusion_clock.google_maps_api_key", ""],
kwargs: {},
}) || "";
} catch { return ""; }
}
async _loadScript(apiKey) {
if (window.google && window.google.maps) return;
return new Promise((resolve, reject) => {
if (document.querySelector('script[src*="maps.googleapis.com"]')) {
const t = setInterval(() => {
if (window.google && window.google.maps) { clearInterval(t); resolve(); }
}, 100);
setTimeout(() => { clearInterval(t); resolve(); }, 5000);
return;
}
const s = document.createElement("script");
s.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places&callback=__fclkMapCb`;
s.async = true;
s.defer = true;
window.__fclkMapCb = () => { delete window.__fclkMapCb; resolve(); };
s.onerror = () => reject(new Error("script load failed"));
document.head.appendChild(s);
});
}
_cleanup() {
if (this._interval) clearInterval(this._interval);
if (this.marker) { this.marker.map = null; this.marker = null; }
if (this.circle) { this.circle.setMap(null); this.circle = null; }
this.map = null;
}
}
registry.category("fields").add("fclk_location_map", {
component: FusionClockLocationMap,
supportedTypes: ["char"],
});