/** @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"], });