/** @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"; /** * Google Places Autocomplete widget for the address field. * Automatically geocodes the selected place and updates lat/lng on the record. */ export class FusionClockPlacesAutocomplete extends Component { static template = "fusion_clock.PlacesAutocomplete"; static props = { ...standardFieldProps }; setup() { this.inputRef = useRef("addressInput"); this.autocomplete = null; this._apiReady = false; this.state = useState({ value: this.props.record.data[this.props.name] || "", }); onMounted(() => this._init()); onWillUnmount(() => this._cleanup()); } get isReadonly() { return this.props.readonly; } 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 (e) { return ""; } } async _waitForGoogleMaps() { if (window.google && window.google.maps && window.google.maps.places) { return true; } return new Promise((resolve) => { let attempts = 0; const check = setInterval(() => { attempts++; if (window.google && window.google.maps && window.google.maps.places) { clearInterval(check); resolve(true); } if (attempts > 50) { clearInterval(check); resolve(false); } }, 100); }); } async _loadGoogleMaps(apiKey) { if (window.google && window.google.maps) return; if (document.querySelector('script[src*="maps.googleapis.com"]')) { await this._waitForGoogleMaps(); return; } return new Promise((resolve, reject) => { const script = document.createElement("script"); script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places&callback=__fclkPlacesInit`; script.async = true; script.defer = true; window.__fclkPlacesInit = () => { delete window.__fclkPlacesInit; resolve(); }; script.onerror = () => reject(new Error("Failed to load Google Maps")); document.head.appendChild(script); }); } async _init() { if (this.isReadonly) return; const apiKey = await this._getApiKey(); if (!apiKey) return; try { await this._loadGoogleMaps(apiKey); } catch (e) { return; } await this._waitForGoogleMaps(); if (!this.inputRef.el || !window.google || !window.google.maps.places) return; this.autocomplete = new google.maps.places.Autocomplete(this.inputRef.el, { types: ["geocode", "establishment"], fields: ["formatted_address", "geometry", "name"], }); this.autocomplete.addListener("place_changed", () => { const place = this.autocomplete.getPlace(); if (!place || !place.geometry) return; const lat = place.geometry.location.lat(); const lng = place.geometry.location.lng(); const address = place.formatted_address || place.name || ""; this.state.value = address; this.props.record.update({ [this.props.name]: address, latitude: Math.round(lat * 10000000) / 10000000, longitude: Math.round(lng * 10000000) / 10000000, }); }); } onInput(ev) { this.state.value = ev.target.value; } onChange(ev) { this.props.record.update({ [this.props.name]: ev.target.value }); } _cleanup() { if (this.autocomplete) { google.maps.event.clearInstanceListeners(this.autocomplete); this.autocomplete = null; } const containers = document.querySelectorAll(".pac-container"); containers.forEach((c) => c.remove()); } } FusionClockPlacesAutocomplete.template = "fusion_clock.PlacesAutocomplete"; registry.category("fields").add("fclk_places_autocomplete", { component: FusionClockPlacesAutocomplete, supportedTypes: ["char"], });