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