1839 lines
76 KiB
JavaScript
1839 lines
76 KiB
JavaScript
/** @odoo-module **/
|
|
// Fusion Claims - Google Address Autocomplete for Contacts
|
|
// Copyright 2024-2025 Nexa Systems Inc.
|
|
// License OPL-1
|
|
|
|
import { FormController } from "@web/views/form/form_controller";
|
|
import { useService } from "@web/core/utils/hooks";
|
|
import { onMounted, onWillUnmount } from "@odoo/owl";
|
|
import { patch } from "@web/core/utils/patch";
|
|
|
|
// Store for autocomplete instances and services
|
|
let googleMapsLoaded = false;
|
|
let googleMapsLoading = false;
|
|
let googleMapsApiKey = null;
|
|
let globalOrm = null;
|
|
const autocompleteInstances = new Map();
|
|
|
|
/**
|
|
* Load Google Maps API dynamically
|
|
*/
|
|
async function loadGoogleMapsApi(apiKey) {
|
|
if (googleMapsLoaded) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
if (googleMapsLoading) {
|
|
return new Promise((resolve) => {
|
|
const checkLoaded = setInterval(() => {
|
|
if (googleMapsLoaded) {
|
|
clearInterval(checkLoaded);
|
|
resolve();
|
|
}
|
|
}, 100);
|
|
});
|
|
}
|
|
|
|
googleMapsLoading = true;
|
|
|
|
return new Promise((resolve, reject) => {
|
|
window.initGoogleMapsAutocomplete = () => {
|
|
googleMapsLoaded = true;
|
|
googleMapsLoading = false;
|
|
resolve();
|
|
};
|
|
|
|
const script = document.createElement('script');
|
|
script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places&callback=initGoogleMapsAutocomplete`;
|
|
script.async = true;
|
|
script.defer = true;
|
|
script.onerror = () => {
|
|
googleMapsLoading = false;
|
|
reject(new Error('Failed to load Google Maps API'));
|
|
};
|
|
document.head.appendChild(script);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get API key from Odoo config
|
|
*/
|
|
async function getGoogleMapsApiKey(orm) {
|
|
if (googleMapsApiKey) {
|
|
return googleMapsApiKey;
|
|
}
|
|
|
|
try {
|
|
const result = await orm.call(
|
|
'ir.config_parameter',
|
|
'get_param',
|
|
['fusion_claims.google_maps_api_key']
|
|
);
|
|
googleMapsApiKey = result || null;
|
|
return googleMapsApiKey;
|
|
} catch (error) {
|
|
console.warn('Could not fetch Google Maps API key:', error);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize autocomplete on a street input field
|
|
*/
|
|
function initAutocompleteOnField(input, formModel) {
|
|
if (!input || !window.google || !window.google.maps || !window.google.maps.places) {
|
|
return null;
|
|
}
|
|
|
|
if (autocompleteInstances.has(input)) {
|
|
return autocompleteInstances.get(input);
|
|
}
|
|
|
|
const autocomplete = new google.maps.places.Autocomplete(input, {
|
|
componentRestrictions: { country: 'ca' },
|
|
types: ['address'],
|
|
fields: ['address_components', 'formatted_address', 'geometry']
|
|
});
|
|
|
|
autocomplete.addListener('place_changed', async () => {
|
|
const place = autocomplete.getPlace();
|
|
if (!place.address_components) {
|
|
return;
|
|
}
|
|
|
|
// Parse address components
|
|
let streetNumber = '';
|
|
let streetName = '';
|
|
let unitNumber = ''; // For apartment/unit/suite
|
|
let city = '';
|
|
let province = '';
|
|
let postalCode = '';
|
|
let countryCode = '';
|
|
|
|
for (const component of place.address_components) {
|
|
const types = component.types;
|
|
|
|
if (types.includes('street_number')) {
|
|
streetNumber = component.long_name;
|
|
} else if (types.includes('route')) {
|
|
streetName = component.long_name;
|
|
} else if (types.includes('subpremise')) {
|
|
// Unit, apartment, suite number
|
|
unitNumber = component.long_name;
|
|
} else if (types.includes('floor')) {
|
|
// Floor number
|
|
if (!unitNumber) unitNumber = 'Floor ' + component.long_name;
|
|
} else if (types.includes('locality')) {
|
|
city = component.long_name;
|
|
} else if (types.includes('sublocality_level_1') && !city) {
|
|
city = component.long_name;
|
|
} else if (types.includes('administrative_area_level_1')) {
|
|
province = component.short_name;
|
|
} else if (types.includes('postal_code')) {
|
|
postalCode = component.long_name;
|
|
} else if (types.includes('country')) {
|
|
countryCode = component.short_name;
|
|
}
|
|
}
|
|
|
|
const street = streetNumber ? `${streetNumber} ${streetName}` : streetName;
|
|
|
|
console.log('[GooglePlaces] Parsed address:', { street, unitNumber, city, province, postalCode, countryCode });
|
|
|
|
if (!formModel || !formModel.root) {
|
|
console.warn('[GooglePlaces] No form model available');
|
|
return;
|
|
}
|
|
|
|
const record = formModel.root;
|
|
|
|
// First, lookup country and state IDs
|
|
let countryId = null;
|
|
let stateId = null;
|
|
|
|
if (globalOrm && countryCode) {
|
|
try {
|
|
// Find country
|
|
const countries = await globalOrm.searchRead(
|
|
'res.country',
|
|
[['code', '=', countryCode]],
|
|
['id'],
|
|
{ limit: 1 }
|
|
);
|
|
|
|
if (countries.length) {
|
|
countryId = countries[0].id;
|
|
console.log('[GooglePlaces] Found country ID:', countryId);
|
|
|
|
// Find state
|
|
if (province) {
|
|
const states = await globalOrm.searchRead(
|
|
'res.country.state',
|
|
[['code', '=', province], ['country_id', '=', countryId]],
|
|
['id'],
|
|
{ limit: 1 }
|
|
);
|
|
|
|
if (states.length) {
|
|
stateId = states[0].id;
|
|
console.log('[GooglePlaces] Found state ID:', stateId);
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('[GooglePlaces] Error looking up country/state:', error);
|
|
}
|
|
}
|
|
|
|
// Build complete update payload
|
|
const updatePayload = {};
|
|
if (street) updatePayload.street = street;
|
|
if (unitNumber) updatePayload.street2 = unitNumber;
|
|
if (city) updatePayload.city = city;
|
|
if (postalCode) updatePayload.zip = postalCode;
|
|
if (countryId !== null) updatePayload.country_id = countryId;
|
|
if (stateId !== null) updatePayload.state_id = stateId;
|
|
|
|
console.log('[GooglePlaces] Full update payload:', updatePayload);
|
|
console.log('[GooglePlaces] Record ID:', record.resId);
|
|
console.log('[GooglePlaces] Country ID to set:', countryId);
|
|
console.log('[GooglePlaces] State ID to set:', stateId);
|
|
|
|
// If record is saved (has ID), use ORM write directly
|
|
if (record.resId && globalOrm) {
|
|
try {
|
|
console.log('[GooglePlaces] Using ORM write for saved record');
|
|
|
|
// First write country and text fields
|
|
const firstWrite = { ...updatePayload };
|
|
delete firstWrite.state_id; // Remove state from first write
|
|
|
|
await globalOrm.write('res.partner', [record.resId], firstWrite);
|
|
console.log('[GooglePlaces] First write (country + text) successful');
|
|
|
|
// Then write state separately after a small delay
|
|
if (stateId !== null) {
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
await globalOrm.write('res.partner', [record.resId], { state_id: stateId });
|
|
console.log('[GooglePlaces] Second write (state) successful');
|
|
}
|
|
|
|
// Reload the record to show updated values
|
|
await record.load();
|
|
console.log('[GooglePlaces] Record reloaded');
|
|
} catch (error) {
|
|
console.error('[GooglePlaces] ORM write failed:', error);
|
|
}
|
|
} else {
|
|
// For new records - use DOM manipulation approach
|
|
console.log('[GooglePlaces] New record - using DOM approach');
|
|
|
|
try {
|
|
// Update text fields through record.update (these work)
|
|
const textUpdate = {};
|
|
if (street) textUpdate.street = street;
|
|
if (unitNumber) textUpdate.street2 = unitNumber;
|
|
if (city) textUpdate.city = city;
|
|
if (postalCode) textUpdate.zip = postalCode;
|
|
|
|
if (Object.keys(textUpdate).length > 0) {
|
|
await record.update(textUpdate);
|
|
console.log('[GooglePlaces] Text fields updated');
|
|
}
|
|
|
|
// For Many2one fields, we need to simulate user interaction
|
|
const formEl = input.closest('.o_form_view') || input.closest('.o_content') || document.body;
|
|
|
|
if (countryId !== null && globalOrm) {
|
|
// Get country and state names in parallel for speed
|
|
const [countryData, stateData] = await Promise.all([
|
|
globalOrm.read('res.country', [countryId], ['display_name']),
|
|
stateId ? globalOrm.read('res.country.state', [stateId], ['display_name']) : Promise.resolve([])
|
|
]);
|
|
|
|
const countryName = countryData[0]?.display_name || 'Canada';
|
|
const stateName = stateData[0]?.display_name || province;
|
|
|
|
// Find and update country field via DOM
|
|
await simulateMany2OneSelection(formEl, 'country_id', countryId, countryName);
|
|
|
|
// Wait for country onchange to complete (reduced time)
|
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
|
|
// Now set state
|
|
if (stateId !== null) {
|
|
await simulateMany2OneSelection(formEl, 'state_id', stateId, stateName);
|
|
}
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('[GooglePlaces] New record update failed:', error);
|
|
}
|
|
}
|
|
});
|
|
|
|
autocompleteInstances.set(input, autocomplete);
|
|
return autocomplete;
|
|
}
|
|
|
|
/**
|
|
* Simulate Many2One field selection by finding the widget and triggering its update
|
|
*/
|
|
async function simulateMany2OneSelection(formEl, fieldName, valueId, displayName) {
|
|
console.log(`[GooglePlaces] Simulating selection for ${fieldName}: ${valueId} - ${displayName}`);
|
|
|
|
// Find the field container
|
|
const fieldSelectors = [
|
|
`[name="${fieldName}"]`,
|
|
`.o_field_widget[name="${fieldName}"]`,
|
|
`div[name="${fieldName}"]`,
|
|
];
|
|
|
|
let fieldContainer = null;
|
|
for (const selector of fieldSelectors) {
|
|
fieldContainer = formEl.querySelector(selector);
|
|
if (fieldContainer) break;
|
|
}
|
|
|
|
if (!fieldContainer) {
|
|
console.log(`[GooglePlaces] Field container not found for ${fieldName}`);
|
|
return false;
|
|
}
|
|
|
|
console.log(`[GooglePlaces] Found field container for ${fieldName}`);
|
|
|
|
// Try to find the OWL component instance
|
|
// In Odoo 17+, OWL components store their instance in __owl__
|
|
let owlComponent = null;
|
|
let el = fieldContainer;
|
|
while (el && !owlComponent) {
|
|
if (el.__owl__) {
|
|
owlComponent = el.__owl__;
|
|
break;
|
|
}
|
|
el = el.parentElement;
|
|
}
|
|
|
|
if (owlComponent && owlComponent.component) {
|
|
console.log(`[GooglePlaces] Found OWL component for ${fieldName}`);
|
|
|
|
// Try to call the component's update method
|
|
const component = owlComponent.component;
|
|
|
|
if (component.props && component.props.record) {
|
|
try {
|
|
// Try updating through the props record
|
|
await component.props.record.update({ [fieldName]: valueId });
|
|
console.log(`[GooglePlaces] Updated via props.record for ${fieldName}`);
|
|
return true;
|
|
} catch (e) {
|
|
console.log(`[GooglePlaces] props.record.update failed for ${fieldName}:`, e.message);
|
|
}
|
|
}
|
|
|
|
if (typeof component.updateValue === 'function') {
|
|
try {
|
|
await component.updateValue(valueId);
|
|
console.log(`[GooglePlaces] Updated via updateValue for ${fieldName}`);
|
|
return true;
|
|
} catch (e) {
|
|
console.log(`[GooglePlaces] updateValue failed for ${fieldName}:`, e.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback: Try to find and manipulate the input directly
|
|
const inputEl = fieldContainer.querySelector('input:not([type="hidden"])');
|
|
if (inputEl) {
|
|
// Focus the input first
|
|
inputEl.focus();
|
|
|
|
// Clear and set value
|
|
inputEl.value = '';
|
|
inputEl.value = displayName;
|
|
|
|
// Trigger input event to open dropdown
|
|
inputEl.dispatchEvent(new InputEvent('input', {
|
|
bubbles: true,
|
|
cancelable: true,
|
|
data: displayName,
|
|
inputType: 'insertText'
|
|
}));
|
|
|
|
console.log(`[GooglePlaces] Triggered input event for ${fieldName}, waiting for dropdown...`);
|
|
|
|
// Wait for dropdown to appear and search results to load (reduced)
|
|
await new Promise(resolve => setTimeout(resolve, 250));
|
|
|
|
// Look for dropdown items with various selectors used by Odoo
|
|
const dropdownSelectors = [
|
|
'.o-autocomplete--dropdown-menu .o-autocomplete--dropdown-item',
|
|
'.o_m2o_dropdown_option',
|
|
'.dropdown-menu .dropdown-item',
|
|
'.o-autocomplete .dropdown-item',
|
|
'ul.ui-autocomplete li',
|
|
'.o_field_many2one_selection li',
|
|
];
|
|
|
|
let found = false;
|
|
for (const selector of dropdownSelectors) {
|
|
const dropdownItems = document.querySelectorAll(selector);
|
|
console.log(`[GooglePlaces] Checking selector "${selector}": found ${dropdownItems.length} items`);
|
|
|
|
for (const item of dropdownItems) {
|
|
const itemText = item.textContent.trim();
|
|
if (itemText.includes(displayName) || displayName.includes(itemText)) {
|
|
// Click the item
|
|
item.click();
|
|
console.log(`[GooglePlaces] Clicked dropdown item for ${fieldName}: "${itemText}"`);
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (found) break;
|
|
}
|
|
|
|
if (!found) {
|
|
console.log(`[GooglePlaces] No matching dropdown item found for ${fieldName}`);
|
|
|
|
// Try pressing Enter to select the first option
|
|
inputEl.dispatchEvent(new KeyboardEvent('keydown', {
|
|
key: 'Enter',
|
|
code: 'Enter',
|
|
keyCode: 13,
|
|
bubbles: true
|
|
}));
|
|
console.log(`[GooglePlaces] Sent Enter key for ${fieldName}`);
|
|
|
|
// Also try Tab to confirm selection
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
inputEl.dispatchEvent(new KeyboardEvent('keydown', {
|
|
key: 'Tab',
|
|
code: 'Tab',
|
|
keyCode: 9,
|
|
bubbles: true
|
|
}));
|
|
}
|
|
|
|
// Blur to finalize
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
inputEl.blur();
|
|
|
|
return found;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Initialize company name autocomplete (searches for businesses)
|
|
*/
|
|
function initCompanyAutocomplete(input, formModel) {
|
|
if (!input || !window.google || !window.google.maps || !window.google.maps.places) {
|
|
return null;
|
|
}
|
|
|
|
const instanceKey = 'company_' + input.id;
|
|
if (autocompleteInstances.has(instanceKey)) {
|
|
return autocompleteInstances.get(instanceKey);
|
|
}
|
|
|
|
const autocomplete = new google.maps.places.Autocomplete(input, {
|
|
componentRestrictions: { country: 'ca' },
|
|
types: ['establishment'], // Search for businesses
|
|
fields: ['place_id', 'name', 'address_components', 'formatted_address', 'formatted_phone_number', 'international_phone_number', 'website', 'geometry']
|
|
});
|
|
|
|
autocomplete.addListener('place_changed', async () => {
|
|
let place = autocomplete.getPlace();
|
|
if (!place.name && !place.place_id) {
|
|
return;
|
|
}
|
|
|
|
console.log('[GooglePlaces] Company selected:', place.name);
|
|
console.log('[GooglePlaces] Initial place data:', place);
|
|
|
|
// If phone/website not in initial response, fetch place details
|
|
if (place.place_id && (!place.formatted_phone_number && !place.website)) {
|
|
try {
|
|
const service = new google.maps.places.PlacesService(document.createElement('div'));
|
|
const detailsPromise = new Promise((resolve, reject) => {
|
|
service.getDetails(
|
|
{
|
|
placeId: place.place_id,
|
|
fields: ['formatted_phone_number', 'international_phone_number', 'website', 'name', 'address_components']
|
|
},
|
|
(result, status) => {
|
|
if (status === google.maps.places.PlacesServiceStatus.OK) {
|
|
resolve(result);
|
|
} else {
|
|
reject(new Error('Place details failed: ' + status));
|
|
}
|
|
}
|
|
);
|
|
});
|
|
|
|
const details = await detailsPromise;
|
|
console.log('[GooglePlaces] Fetched place details:', details);
|
|
|
|
// Merge details into place
|
|
if (details.formatted_phone_number) place.formatted_phone_number = details.formatted_phone_number;
|
|
if (details.international_phone_number) place.international_phone_number = details.international_phone_number;
|
|
if (details.website) place.website = details.website;
|
|
} catch (e) {
|
|
console.log('[GooglePlaces] Could not fetch place details:', e.message);
|
|
}
|
|
}
|
|
|
|
console.log('[GooglePlaces] Final place data:', place);
|
|
|
|
// Parse address components
|
|
let streetNumber = '';
|
|
let streetName = '';
|
|
let unitNumber = ''; // For apartment/unit/suite
|
|
let city = '';
|
|
let province = '';
|
|
let postalCode = '';
|
|
let countryCode = '';
|
|
|
|
if (place.address_components) {
|
|
for (const component of place.address_components) {
|
|
const types = component.types;
|
|
|
|
if (types.includes('street_number')) {
|
|
streetNumber = component.long_name;
|
|
} else if (types.includes('route')) {
|
|
streetName = component.long_name;
|
|
} else if (types.includes('subpremise')) {
|
|
// Unit, apartment, suite number
|
|
unitNumber = component.long_name;
|
|
} else if (types.includes('floor')) {
|
|
// Floor number - add to unit if no unit already
|
|
if (!unitNumber) unitNumber = 'Floor ' + component.long_name;
|
|
} else if (types.includes('locality')) {
|
|
city = component.long_name;
|
|
} else if (types.includes('sublocality_level_1') && !city) {
|
|
city = component.long_name;
|
|
} else if (types.includes('administrative_area_level_1')) {
|
|
province = component.short_name;
|
|
} else if (types.includes('postal_code')) {
|
|
postalCode = component.long_name;
|
|
} else if (types.includes('country')) {
|
|
countryCode = component.short_name;
|
|
}
|
|
}
|
|
}
|
|
|
|
const street = streetNumber ? `${streetNumber} ${streetName}` : streetName;
|
|
console.log('[GooglePlaces] Parsed: street=', street, 'unit=', unitNumber, 'city=', city);
|
|
|
|
if (!formModel || !formModel.root) {
|
|
return;
|
|
}
|
|
|
|
const record = formModel.root;
|
|
|
|
// Lookup country and state IDs
|
|
let countryId = null;
|
|
let stateId = null;
|
|
|
|
if (globalOrm && countryCode) {
|
|
try {
|
|
const [countryData, stateData] = await Promise.all([
|
|
globalOrm.searchRead('res.country', [['code', '=', countryCode]], ['id'], { limit: 1 }),
|
|
province ? globalOrm.searchRead('res.country.state', [['code', '=', province], ['country_id.code', '=', countryCode]], ['id'], { limit: 1 }) : Promise.resolve([])
|
|
]);
|
|
|
|
if (countryData.length) countryId = countryData[0].id;
|
|
if (stateData.length) stateId = stateData[0].id;
|
|
} catch (error) {
|
|
console.error('[GooglePlaces] Error looking up country/state:', error);
|
|
}
|
|
}
|
|
|
|
// Get phone number (prefer formatted, fallback to international)
|
|
const phoneNumber = place.formatted_phone_number || place.international_phone_number || '';
|
|
|
|
// Build update payload
|
|
const updatePayload = {
|
|
name: place.name,
|
|
};
|
|
if (street) updatePayload.street = street;
|
|
if (unitNumber) updatePayload.street2 = unitNumber;
|
|
if (city) updatePayload.city = city;
|
|
if (postalCode) updatePayload.zip = postalCode;
|
|
if (phoneNumber) updatePayload.phone = phoneNumber;
|
|
if (place.website) updatePayload.website = place.website;
|
|
if (countryId) updatePayload.country_id = countryId;
|
|
if (stateId) updatePayload.state_id = stateId;
|
|
|
|
console.log('[GooglePlaces] Phone:', phoneNumber, 'Website:', place.website, 'Unit:', unitNumber);
|
|
|
|
console.log('[GooglePlaces] Company update payload:', updatePayload);
|
|
|
|
// For saved records, use ORM write
|
|
if (record.resId && globalOrm) {
|
|
try {
|
|
const writePayload = { ...updatePayload };
|
|
delete writePayload.state_id; // Write state separately
|
|
|
|
await globalOrm.write('res.partner', [record.resId], writePayload);
|
|
|
|
if (stateId) {
|
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
await globalOrm.write('res.partner', [record.resId], { state_id: stateId });
|
|
}
|
|
|
|
await record.load();
|
|
console.log('[GooglePlaces] Company record updated');
|
|
} catch (error) {
|
|
console.error('[GooglePlaces] Company ORM write failed:', error);
|
|
}
|
|
} else {
|
|
// For new records, update through the form
|
|
try {
|
|
// Text fields
|
|
const textFields = {};
|
|
if (place.name) textFields.name = place.name;
|
|
if (street) textFields.street = street;
|
|
if (unitNumber) textFields.street2 = unitNumber;
|
|
if (city) textFields.city = city;
|
|
if (postalCode) textFields.zip = postalCode;
|
|
if (phoneNumber) textFields.phone = phoneNumber;
|
|
if (place.website) textFields.website = place.website;
|
|
|
|
console.log('[GooglePlaces] New record text fields:', textFields);
|
|
|
|
await record.update(textFields);
|
|
|
|
// Country and state via DOM simulation
|
|
const formEl = input.closest('.o_form_view') || input.closest('.o_content') || document.body;
|
|
|
|
if (countryId && globalOrm) {
|
|
const [countryInfo, stateInfo] = await Promise.all([
|
|
globalOrm.read('res.country', [countryId], ['display_name']),
|
|
stateId ? globalOrm.read('res.country.state', [stateId], ['display_name']) : Promise.resolve([])
|
|
]);
|
|
|
|
await simulateMany2OneSelection(formEl, 'country_id', countryId, countryInfo[0]?.display_name || 'Canada');
|
|
|
|
if (stateId) {
|
|
await new Promise(resolve => setTimeout(resolve, 300));
|
|
await simulateMany2OneSelection(formEl, 'state_id', stateId, stateInfo[0]?.display_name || province);
|
|
}
|
|
}
|
|
|
|
console.log('[GooglePlaces] Company new record updated');
|
|
} catch (error) {
|
|
console.error('[GooglePlaces] Company update failed:', error);
|
|
}
|
|
}
|
|
});
|
|
|
|
autocompleteInstances.set(instanceKey, autocomplete);
|
|
return autocomplete;
|
|
}
|
|
|
|
/**
|
|
* Setup autocomplete for partner form
|
|
*/
|
|
async function setupPartnerAutocomplete(el, model, orm) {
|
|
globalOrm = orm;
|
|
|
|
const apiKey = await getGoogleMapsApiKey(orm);
|
|
if (!apiKey) {
|
|
console.log('[GooglePlaces] API key not configured');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await loadGoogleMapsApi(apiKey);
|
|
} catch (error) {
|
|
console.warn('[GooglePlaces] Failed to load API:', error);
|
|
return;
|
|
}
|
|
|
|
// Find street input field for address autocomplete
|
|
const streetInputSelectors = [
|
|
'input[name="street"]',
|
|
'.o_field_widget[name="street"] input',
|
|
'[name="street"] input',
|
|
'div[name="street"] input',
|
|
'.o_address_street input'
|
|
];
|
|
|
|
let streetInput = null;
|
|
for (const selector of streetInputSelectors) {
|
|
streetInput = el.querySelector(selector);
|
|
if (streetInput) break;
|
|
}
|
|
|
|
if (streetInput && !autocompleteInstances.has(streetInput)) {
|
|
initAutocompleteOnField(streetInput, model);
|
|
|
|
// Add visual indicator
|
|
streetInput.style.backgroundImage = 'url("data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 24 24\' fill=\'%234CAF50\'%3E%3Cpath d=\'M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z\'/%3E%3C/svg%3E")';
|
|
streetInput.style.backgroundRepeat = 'no-repeat';
|
|
streetInput.style.backgroundPosition = 'right 8px center';
|
|
streetInput.style.backgroundSize = '20px';
|
|
streetInput.style.paddingRight = '35px';
|
|
|
|
console.log('[GooglePlaces] Address autocomplete initialized on street field');
|
|
}
|
|
|
|
// Find company name input field for company autocomplete
|
|
const nameInputSelectors = [
|
|
'input[name="name"]',
|
|
'.o_field_widget[name="name"] input',
|
|
'[name="name"] input',
|
|
'input.o_input[placeholder*="Lumber"]', // Odoo's placeholder for company name
|
|
'.o_field_char[name="name"] input',
|
|
];
|
|
|
|
let nameInput = null;
|
|
for (const selector of nameInputSelectors) {
|
|
nameInput = el.querySelector(selector);
|
|
if (nameInput) {
|
|
console.log('[GooglePlaces] Found name input with selector:', selector);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Check if company type is selected - try multiple ways
|
|
let isCompany = false;
|
|
|
|
// Method 1: Check radio buttons
|
|
const companyRadio = el.querySelector('input[name="company_type"][value="company"]');
|
|
if (companyRadio && companyRadio.checked) {
|
|
isCompany = true;
|
|
console.log('[GooglePlaces] Company detected via radio button');
|
|
}
|
|
|
|
// Method 2: Check for company-specific elements (building icon visible)
|
|
if (!isCompany) {
|
|
const companyIcon = el.querySelector('.fa-building, .o_field_partner_type .fa-building-o');
|
|
if (companyIcon) {
|
|
isCompany = true;
|
|
console.log('[GooglePlaces] Company detected via building icon');
|
|
}
|
|
}
|
|
|
|
// Method 3: Check data attribute or class
|
|
if (!isCompany) {
|
|
const companyTypeField = el.querySelector('[name="company_type"]');
|
|
if (companyTypeField) {
|
|
const selectedOption = companyTypeField.querySelector('.active, .selected, [aria-checked="true"]');
|
|
if (selectedOption && selectedOption.textContent.toLowerCase().includes('company')) {
|
|
isCompany = true;
|
|
console.log('[GooglePlaces] Company detected via active option');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method 4: Check the model data
|
|
if (!isCompany && model && model.root && model.root.data) {
|
|
const companyType = model.root.data.company_type;
|
|
if (companyType === 'company') {
|
|
isCompany = true;
|
|
console.log('[GooglePlaces] Company detected via model data');
|
|
}
|
|
}
|
|
|
|
console.log('[GooglePlaces] isCompany:', isCompany, 'nameInput:', !!nameInput);
|
|
|
|
if (nameInput) {
|
|
const instanceKey = 'company_' + (nameInput.id || 'default');
|
|
|
|
if (isCompany && !autocompleteInstances.has(instanceKey)) {
|
|
initCompanyAutocomplete(nameInput, model);
|
|
|
|
// Add visual indicator (building icon)
|
|
nameInput.style.backgroundImage = 'url("data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 24 24\' fill=\'%232196F3\'%3E%3Cpath d=\'M12 7V3H2v18h20V7H12zM6 19H4v-2h2v2zm0-4H4v-2h2v2zm0-4H4V9h2v2zm0-4H4V5h2v2zm4 12H8v-2h2v2zm0-4H8v-2h2v2zm0-4H8V9h2v2zm0-4H8V5h2v2zm10 12h-8v-2h2v-2h-2v-2h2v-2h-2V9h8v10zm-2-8h-2v2h2v-2zm0 4h-2v2h2v-2z\'/%3E%3C/svg%3E")';
|
|
nameInput.style.backgroundRepeat = 'no-repeat';
|
|
nameInput.style.backgroundPosition = 'right 8px center';
|
|
nameInput.style.backgroundSize = '20px';
|
|
nameInput.style.paddingRight = '35px';
|
|
|
|
console.log('[GooglePlaces] Company autocomplete initialized on name field');
|
|
} else if (!isCompany && autocompleteInstances.has(instanceKey)) {
|
|
// Remove company autocomplete when switched to Person
|
|
autocompleteInstances.delete(instanceKey);
|
|
nameInput.style.backgroundImage = '';
|
|
nameInput.style.paddingRight = '';
|
|
console.log('[GooglePlaces] Company autocomplete removed (switched to Person)');
|
|
}
|
|
}
|
|
|
|
// Listen for company_type changes - try multiple selectors
|
|
const companyTypeSelectors = [
|
|
'input[name="company_type"]',
|
|
'[name="company_type"] input',
|
|
'.o_field_radio[name="company_type"] input',
|
|
'.o_field_widget[name="company_type"] input',
|
|
];
|
|
|
|
for (const selector of companyTypeSelectors) {
|
|
const inputs = el.querySelectorAll(selector);
|
|
inputs.forEach(input => {
|
|
if (!input.dataset.googlePlacesListener) {
|
|
input.dataset.googlePlacesListener = 'true';
|
|
input.addEventListener('change', () => {
|
|
console.log('[GooglePlaces] company_type changed, re-initializing...');
|
|
setTimeout(() => {
|
|
setupPartnerAutocomplete(el, model, orm);
|
|
}, 200);
|
|
});
|
|
console.log('[GooglePlaces] Added listener to company_type input');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Also listen for clicks on the company type labels/buttons
|
|
const companyTypeContainer = el.querySelector('[name="company_type"]');
|
|
if (companyTypeContainer && !companyTypeContainer.dataset.googlePlacesListener) {
|
|
companyTypeContainer.dataset.googlePlacesListener = 'true';
|
|
companyTypeContainer.addEventListener('click', () => {
|
|
console.log('[GooglePlaces] company_type container clicked, re-initializing...');
|
|
setTimeout(() => {
|
|
setupPartnerAutocomplete(el, model, orm);
|
|
}, 300);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Cleanup autocomplete instances
|
|
*/
|
|
function cleanupAutocomplete(el) {
|
|
if (!el) return;
|
|
|
|
const inputs = el.querySelectorAll('input');
|
|
inputs.forEach(input => {
|
|
if (autocompleteInstances.has(input)) {
|
|
autocompleteInstances.delete(input);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Setup autocomplete for technician task form (address_street field)
|
|
*/
|
|
async function setupTaskAutocomplete(el, model, orm) {
|
|
globalOrm = orm;
|
|
|
|
const apiKey = await getGoogleMapsApiKey(orm);
|
|
if (!apiKey) {
|
|
console.log('[GooglePlaces Task] API key not configured');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await loadGoogleMapsApi(apiKey);
|
|
} catch (error) {
|
|
console.warn('[GooglePlaces Task] Failed to load API:', error);
|
|
return;
|
|
}
|
|
|
|
// Find address_street input
|
|
const streetSelectors = [
|
|
'div[name="address_street"] input',
|
|
'.o_field_widget[name="address_street"] input',
|
|
'[name="address_street"] input',
|
|
];
|
|
|
|
let streetInput = null;
|
|
for (const selector of streetSelectors) {
|
|
streetInput = el.querySelector(selector);
|
|
if (streetInput) break;
|
|
}
|
|
|
|
if (!streetInput || autocompleteInstances.has(streetInput)) {
|
|
return;
|
|
}
|
|
|
|
_attachTaskAutocomplete(streetInput, el, model);
|
|
}
|
|
|
|
/**
|
|
* Attach Google Places autocomplete to a task address_street input.
|
|
* Separated so it can be re-called after OWL re-renders the input.
|
|
*/
|
|
function _attachTaskAutocomplete(streetInput, el, model) {
|
|
if (!streetInput || !window.google?.maps?.places) return;
|
|
if (autocompleteInstances.has(streetInput)) return;
|
|
|
|
console.log('[GooglePlaces Task] Attaching autocomplete on address_street');
|
|
|
|
const autocomplete = new google.maps.places.Autocomplete(streetInput, {
|
|
componentRestrictions: { country: 'ca' },
|
|
types: ['address'],
|
|
fields: ['address_components', 'formatted_address', 'geometry'],
|
|
});
|
|
|
|
autocomplete.addListener('place_changed', async () => {
|
|
const place = autocomplete.getPlace();
|
|
if (!place.address_components) return;
|
|
|
|
let streetNumber = '', streetName = '', unitNumber = '';
|
|
let city = '', province = '', postalCode = '', countryCode = '';
|
|
let lat = 0, lng = 0;
|
|
|
|
for (const c of place.address_components) {
|
|
const t = c.types;
|
|
if (t.includes('street_number')) streetNumber = c.long_name;
|
|
else if (t.includes('route')) streetName = c.long_name;
|
|
else if (t.includes('subpremise')) unitNumber = c.long_name;
|
|
else if (t.includes('floor') && !unitNumber) unitNumber = 'Floor ' + c.long_name;
|
|
else if (t.includes('locality')) city = c.long_name;
|
|
else if (t.includes('sublocality_level_1') && !city) city = c.long_name;
|
|
else if (t.includes('administrative_area_level_1')) province = c.short_name;
|
|
else if (t.includes('postal_code')) postalCode = c.long_name;
|
|
else if (t.includes('country')) countryCode = c.short_name;
|
|
}
|
|
|
|
if (place.geometry && place.geometry.location) {
|
|
lat = place.geometry.location.lat();
|
|
lng = place.geometry.location.lng();
|
|
}
|
|
|
|
const streetOnly = streetNumber ? `${streetNumber} ${streetName}` : streetName;
|
|
// Use the full formatted address from Google for the Street field
|
|
// so the user sees the complete address in one shot.
|
|
const fullAddress = place.formatted_address || streetOnly;
|
|
console.log('[GooglePlaces Task] Parsed:', { fullAddress, streetOnly, unitNumber, city, province, postalCode, lat, lng });
|
|
|
|
if (!model || !model.root) return;
|
|
const record = model.root;
|
|
|
|
// Look up state ID
|
|
let stateId = false;
|
|
if (province && globalOrm) {
|
|
try {
|
|
const states = await globalOrm.searchRead(
|
|
'res.country.state',
|
|
[['code', '=', province], ['country_id.code', '=', countryCode || 'CA']],
|
|
['id'],
|
|
{ limit: 1 }
|
|
);
|
|
if (states.length) stateId = states[0].id;
|
|
} catch (e) {
|
|
console.warn('[GooglePlaces Task] State lookup failed:', e);
|
|
}
|
|
}
|
|
|
|
// Update through the form model
|
|
// address_street gets the FULL formatted address so the user can see it.
|
|
// Hidden fields still get parsed components for data/geocoding/travel.
|
|
try {
|
|
const update = {};
|
|
update.address_street = fullAddress;
|
|
if (unitNumber) update.address_street2 = unitNumber;
|
|
if (city) update.address_city = city;
|
|
if (postalCode) update.address_zip = postalCode;
|
|
if (lat) update.address_lat = lat;
|
|
if (lng) update.address_lng = lng;
|
|
if (stateId) update.address_state_id = stateId;
|
|
|
|
await record.update(update);
|
|
console.log('[GooglePlaces Task] Address fields updated via model');
|
|
} catch (err) {
|
|
console.error('[GooglePlaces Task] Update failed:', err);
|
|
}
|
|
|
|
// After record.update(), OWL may re-render and replace the input element.
|
|
// Re-attach autocomplete on the (potentially new) input after a short delay.
|
|
setTimeout(() => {
|
|
_reattachTaskAutocomplete(el, model);
|
|
}, 400);
|
|
});
|
|
|
|
autocompleteInstances.set(streetInput, autocomplete);
|
|
|
|
// Visual indicator
|
|
streetInput.style.backgroundImage = 'url("data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 24 24\' fill=\'%234CAF50\'%3E%3Cpath d=\'M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z\'/%3E%3C/svg%3E")';
|
|
streetInput.style.backgroundRepeat = 'no-repeat';
|
|
streetInput.style.backgroundPosition = 'right 8px center';
|
|
streetInput.style.backgroundSize = '20px';
|
|
streetInput.style.paddingRight = '35px';
|
|
|
|
console.log('[GooglePlaces Task] Autocomplete ready on address_street');
|
|
}
|
|
|
|
/**
|
|
* Re-attach autocomplete after OWL re-renders the task form input.
|
|
* Finds the current address_street input and attaches if not already done.
|
|
*/
|
|
function _reattachTaskAutocomplete(el, model) {
|
|
const streetSelectors = [
|
|
'div[name="address_street"] input',
|
|
'.o_field_widget[name="address_street"] input',
|
|
'[name="address_street"] input',
|
|
];
|
|
|
|
let streetInput = null;
|
|
for (const selector of streetSelectors) {
|
|
streetInput = el.querySelector(selector);
|
|
if (streetInput) break;
|
|
}
|
|
|
|
if (streetInput && !autocompleteInstances.has(streetInput)) {
|
|
console.log('[GooglePlaces Task] Re-attaching autocomplete after re-render');
|
|
_attachTaskAutocomplete(streetInput, el, model);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Attach Google Places autocomplete to a simple Char field (address only, no parsing).
|
|
* Sets the full formatted address string in the input.
|
|
*/
|
|
function initSimpleAddressAutocomplete(input) {
|
|
if (!input || !window.google || !window.google.maps || !window.google.maps.places) return;
|
|
if (autocompleteInstances.has(input)) return;
|
|
|
|
const autocomplete = new google.maps.places.Autocomplete(input, {
|
|
componentRestrictions: { country: 'ca' },
|
|
types: ['address'],
|
|
fields: ['formatted_address'],
|
|
});
|
|
|
|
autocomplete.addListener('place_changed', () => {
|
|
const place = autocomplete.getPlace();
|
|
if (place && place.formatted_address) {
|
|
// Use native setter for OWL reactivity
|
|
const nativeSetter = Object.getOwnPropertyDescriptor(
|
|
window.HTMLInputElement.prototype, 'value'
|
|
).set;
|
|
nativeSetter.call(input, place.formatted_address);
|
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
console.log('[GooglePlaces] Simple address set:', place.formatted_address);
|
|
}
|
|
});
|
|
|
|
autocompleteInstances.set(input, autocomplete);
|
|
|
|
// Visual indicator
|
|
input.style.backgroundImage = 'url("data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 24 24\' fill=\'%234CAF50\'%3E%3Cpath d=\'M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z\'/%3E%3C/svg%3E")';
|
|
input.style.backgroundRepeat = 'no-repeat';
|
|
input.style.backgroundPosition = 'right 8px center';
|
|
input.style.backgroundSize = '20px';
|
|
input.style.paddingRight = '35px';
|
|
|
|
console.log('[GooglePlaces] Simple address autocomplete attached');
|
|
}
|
|
|
|
/**
|
|
* Setup autocomplete on simple address Char fields by name.
|
|
* Works for res.users (x_fc_start_address) and res.config.settings (fc_technician_start_address).
|
|
*/
|
|
async function setupSimpleAddressFields(el, orm) {
|
|
const apiKey = await getGoogleMapsApiKey(orm);
|
|
if (!apiKey) return;
|
|
try { await loadGoogleMapsApi(apiKey); } catch (e) { return; }
|
|
|
|
const fieldNames = ['x_fc_start_address', 'fc_technician_start_address'];
|
|
for (const name of fieldNames) {
|
|
const selectors = [
|
|
`div[name="${name}"] input`,
|
|
`.o_field_widget[name="${name}"] input`,
|
|
`[name="${name}"] input`,
|
|
];
|
|
for (const sel of selectors) {
|
|
const inp = el.querySelector(sel);
|
|
if (inp) {
|
|
initSimpleAddressAutocomplete(inp);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Setup autocomplete for LTC Facility form.
|
|
* Attaches establishment search on the name field and address search on street.
|
|
*/
|
|
async function setupFacilityAutocomplete(el, model, orm) {
|
|
globalOrm = orm;
|
|
|
|
const apiKey = await getGoogleMapsApiKey(orm);
|
|
if (!apiKey) return;
|
|
try { await loadGoogleMapsApi(apiKey); } catch (e) { return; }
|
|
|
|
// --- Name field: establishment autocomplete ---
|
|
const nameSelectors = [
|
|
'.oe_title [name="name"] input',
|
|
'div[name="name"] input',
|
|
'.o_field_widget[name="name"] input',
|
|
'[name="name"] input',
|
|
];
|
|
|
|
let nameInput = null;
|
|
for (const sel of nameSelectors) {
|
|
nameInput = el.querySelector(sel);
|
|
if (nameInput) break;
|
|
}
|
|
|
|
if (nameInput && !autocompleteInstances.has('facility_name_' + (nameInput.id || 'default'))) {
|
|
_attachFacilityNameAutocomplete(nameInput, el, model);
|
|
}
|
|
|
|
// --- Street field: address autocomplete ---
|
|
const streetSelectors = [
|
|
'div[name="street"] input',
|
|
'.o_field_widget[name="street"] input',
|
|
'[name="street"] input',
|
|
];
|
|
|
|
let streetInput = null;
|
|
for (const sel of streetSelectors) {
|
|
streetInput = el.querySelector(sel);
|
|
if (streetInput) break;
|
|
}
|
|
|
|
if (streetInput && !autocompleteInstances.has(streetInput)) {
|
|
_attachFacilityAddressAutocomplete(streetInput, el, model);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Attach establishment (business) autocomplete on facility name field.
|
|
* Selecting a business fills name, address, phone, email, and website.
|
|
*/
|
|
function _attachFacilityNameAutocomplete(input, el, model) {
|
|
if (!input || !window.google?.maps?.places) return;
|
|
|
|
const instanceKey = 'facility_name_' + (input.id || 'default');
|
|
if (autocompleteInstances.has(instanceKey)) return;
|
|
|
|
const autocomplete = new google.maps.places.Autocomplete(input, {
|
|
componentRestrictions: { country: 'ca' },
|
|
types: ['establishment'],
|
|
fields: [
|
|
'place_id', 'name', 'address_components', 'formatted_address',
|
|
'formatted_phone_number', 'international_phone_number', 'website',
|
|
],
|
|
});
|
|
|
|
autocomplete.addListener('place_changed', async () => {
|
|
let place = autocomplete.getPlace();
|
|
if (!place.name && !place.place_id) return;
|
|
|
|
if (place.place_id && !place.formatted_phone_number && !place.website) {
|
|
try {
|
|
const service = new google.maps.places.PlacesService(document.createElement('div'));
|
|
const details = await new Promise((resolve, reject) => {
|
|
service.getDetails(
|
|
{
|
|
placeId: place.place_id,
|
|
fields: ['formatted_phone_number', 'international_phone_number', 'website'],
|
|
},
|
|
(result, status) => {
|
|
if (status === google.maps.places.PlacesServiceStatus.OK) resolve(result);
|
|
else reject(new Error(status));
|
|
}
|
|
);
|
|
});
|
|
if (details.formatted_phone_number) place.formatted_phone_number = details.formatted_phone_number;
|
|
if (details.international_phone_number) place.international_phone_number = details.international_phone_number;
|
|
if (details.website) place.website = details.website;
|
|
} catch (_) { /* ignore */ }
|
|
}
|
|
|
|
let streetNumber = '', streetName = '', unitNumber = '';
|
|
let city = '', province = '', postalCode = '', countryCode = '';
|
|
|
|
if (place.address_components) {
|
|
for (const c of place.address_components) {
|
|
const t = c.types;
|
|
if (t.includes('street_number')) streetNumber = c.long_name;
|
|
else if (t.includes('route')) streetName = c.long_name;
|
|
else if (t.includes('subpremise')) unitNumber = c.long_name;
|
|
else if (t.includes('floor') && !unitNumber) unitNumber = 'Floor ' + c.long_name;
|
|
else if (t.includes('locality')) city = c.long_name;
|
|
else if (t.includes('sublocality_level_1') && !city) city = c.long_name;
|
|
else if (t.includes('administrative_area_level_1')) province = c.short_name;
|
|
else if (t.includes('postal_code')) postalCode = c.long_name;
|
|
else if (t.includes('country')) countryCode = c.short_name;
|
|
}
|
|
}
|
|
|
|
const street = streetNumber ? `${streetNumber} ${streetName}` : streetName;
|
|
const phone = place.formatted_phone_number || place.international_phone_number || '';
|
|
|
|
if (!model?.root) return;
|
|
const record = model.root;
|
|
|
|
let countryId = null, stateId = null;
|
|
if (globalOrm && countryCode) {
|
|
try {
|
|
const [countries, states] = await Promise.all([
|
|
globalOrm.searchRead('res.country', [['code', '=', countryCode]], ['id'], { limit: 1 }),
|
|
province
|
|
? globalOrm.searchRead('res.country.state', [['code', '=', province], ['country_id.code', '=', countryCode]], ['id'], { limit: 1 })
|
|
: Promise.resolve([]),
|
|
]);
|
|
if (countries.length) countryId = countries[0].id;
|
|
if (states.length) stateId = states[0].id;
|
|
} catch (_) { /* ignore */ }
|
|
}
|
|
|
|
if (record.resId && globalOrm) {
|
|
try {
|
|
const firstWrite = {};
|
|
if (place.name) firstWrite.name = place.name;
|
|
if (street) firstWrite.street = street;
|
|
if (unitNumber) firstWrite.street2 = unitNumber;
|
|
if (city) firstWrite.city = city;
|
|
if (postalCode) firstWrite.zip = postalCode;
|
|
if (phone) firstWrite.phone = phone;
|
|
if (place.website) firstWrite.website = place.website;
|
|
if (countryId) firstWrite.country_id = countryId;
|
|
|
|
await globalOrm.write('fusion.ltc.facility', [record.resId], firstWrite);
|
|
|
|
if (stateId) {
|
|
await new Promise(r => setTimeout(r, 100));
|
|
await globalOrm.write('fusion.ltc.facility', [record.resId], { state_id: stateId });
|
|
}
|
|
|
|
await record.load();
|
|
} catch (err) {
|
|
console.error('[GooglePlaces Facility] Name autocomplete ORM write failed:', err);
|
|
}
|
|
} else {
|
|
try {
|
|
const textUpdate = {};
|
|
if (place.name) textUpdate.name = place.name;
|
|
if (street) textUpdate.street = street;
|
|
if (unitNumber) textUpdate.street2 = unitNumber;
|
|
if (city) textUpdate.city = city;
|
|
if (postalCode) textUpdate.zip = postalCode;
|
|
if (phone) textUpdate.phone = phone;
|
|
if (place.website) textUpdate.website = place.website;
|
|
|
|
await record.update(textUpdate);
|
|
|
|
if (countryId && globalOrm) {
|
|
const formEl = input.closest('.o_form_view') || input.closest('.o_content') || document.body;
|
|
const [countryData, stateData] = await Promise.all([
|
|
globalOrm.read('res.country', [countryId], ['display_name']),
|
|
stateId ? globalOrm.read('res.country.state', [stateId], ['display_name']) : Promise.resolve([]),
|
|
]);
|
|
|
|
await simulateMany2OneSelection(formEl, 'country_id', countryId, countryData[0]?.display_name || 'Canada');
|
|
await new Promise(r => setTimeout(r, 300));
|
|
|
|
if (stateId && stateData.length) {
|
|
await simulateMany2OneSelection(formEl, 'state_id', stateId, stateData[0]?.display_name || province);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('[GooglePlaces Facility] Name autocomplete update failed:', err);
|
|
}
|
|
}
|
|
});
|
|
|
|
autocompleteInstances.set(instanceKey, autocomplete);
|
|
|
|
input.style.backgroundImage = 'url("data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 24 24\' fill=\'%232196F3\'%3E%3Cpath d=\'M12 7V3H2v18h20V7H12zM6 19H4v-2h2v2zm0-4H4v-2h2v2zm0-4H4V9h2v2zm0-4H4V5h2v2zm4 12H8v-2h2v2zm0-4H8v-2h2v2zm0-4H8V9h2v2zm0-4H8V5h2v2zm10 12h-8v-2h2v-2h-2v-2h2v-2h-2V9h8v10zm-2-8h-2v2h2v-2zm0 4h-2v2h2v-2z\'/%3E%3C/svg%3E")';
|
|
input.style.backgroundRepeat = 'no-repeat';
|
|
input.style.backgroundPosition = 'right 8px center';
|
|
input.style.backgroundSize = '20px';
|
|
input.style.paddingRight = '35px';
|
|
}
|
|
|
|
/**
|
|
* Attach address autocomplete on facility street field.
|
|
* Fills street, street2, city, state, zip, and country.
|
|
*/
|
|
function _attachFacilityAddressAutocomplete(input, el, model) {
|
|
if (!input || !window.google?.maps?.places) return;
|
|
if (autocompleteInstances.has(input)) return;
|
|
|
|
const autocomplete = new google.maps.places.Autocomplete(input, {
|
|
componentRestrictions: { country: 'ca' },
|
|
types: ['address'],
|
|
fields: ['address_components', 'formatted_address'],
|
|
});
|
|
|
|
autocomplete.addListener('place_changed', async () => {
|
|
const place = autocomplete.getPlace();
|
|
if (!place.address_components) return;
|
|
|
|
let streetNumber = '', streetName = '', unitNumber = '';
|
|
let city = '', province = '', postalCode = '', countryCode = '';
|
|
|
|
for (const c of place.address_components) {
|
|
const t = c.types;
|
|
if (t.includes('street_number')) streetNumber = c.long_name;
|
|
else if (t.includes('route')) streetName = c.long_name;
|
|
else if (t.includes('subpremise')) unitNumber = c.long_name;
|
|
else if (t.includes('floor') && !unitNumber) unitNumber = 'Floor ' + c.long_name;
|
|
else if (t.includes('locality')) city = c.long_name;
|
|
else if (t.includes('sublocality_level_1') && !city) city = c.long_name;
|
|
else if (t.includes('administrative_area_level_1')) province = c.short_name;
|
|
else if (t.includes('postal_code')) postalCode = c.long_name;
|
|
else if (t.includes('country')) countryCode = c.short_name;
|
|
}
|
|
|
|
const street = streetNumber ? `${streetNumber} ${streetName}` : streetName;
|
|
|
|
if (!model?.root) return;
|
|
const record = model.root;
|
|
|
|
let countryId = null, stateId = null;
|
|
if (globalOrm && countryCode) {
|
|
try {
|
|
const [countries, states] = await Promise.all([
|
|
globalOrm.searchRead('res.country', [['code', '=', countryCode]], ['id'], { limit: 1 }),
|
|
province
|
|
? globalOrm.searchRead('res.country.state', [['code', '=', province], ['country_id.code', '=', countryCode]], ['id'], { limit: 1 })
|
|
: Promise.resolve([]),
|
|
]);
|
|
if (countries.length) countryId = countries[0].id;
|
|
if (states.length) stateId = states[0].id;
|
|
} catch (_) { /* ignore */ }
|
|
}
|
|
|
|
if (record.resId && globalOrm) {
|
|
try {
|
|
const firstWrite = {};
|
|
if (street) firstWrite.street = street;
|
|
if (unitNumber) firstWrite.street2 = unitNumber;
|
|
if (city) firstWrite.city = city;
|
|
if (postalCode) firstWrite.zip = postalCode;
|
|
if (countryId) firstWrite.country_id = countryId;
|
|
|
|
await globalOrm.write('fusion.ltc.facility', [record.resId], firstWrite);
|
|
|
|
if (stateId) {
|
|
await new Promise(r => setTimeout(r, 100));
|
|
await globalOrm.write('fusion.ltc.facility', [record.resId], { state_id: stateId });
|
|
}
|
|
|
|
await record.load();
|
|
} catch (err) {
|
|
console.error('[GooglePlaces Facility] Address ORM write failed:', err);
|
|
}
|
|
} else {
|
|
try {
|
|
const textUpdate = {};
|
|
if (street) textUpdate.street = street;
|
|
if (unitNumber) textUpdate.street2 = unitNumber;
|
|
if (city) textUpdate.city = city;
|
|
if (postalCode) textUpdate.zip = postalCode;
|
|
|
|
await record.update(textUpdate);
|
|
|
|
if (countryId && globalOrm) {
|
|
const formEl = input.closest('.o_form_view') || input.closest('.o_content') || document.body;
|
|
const [countryData, stateData] = await Promise.all([
|
|
globalOrm.read('res.country', [countryId], ['display_name']),
|
|
stateId ? globalOrm.read('res.country.state', [stateId], ['display_name']) : Promise.resolve([]),
|
|
]);
|
|
|
|
await simulateMany2OneSelection(formEl, 'country_id', countryId, countryData[0]?.display_name || 'Canada');
|
|
await new Promise(r => setTimeout(r, 300));
|
|
|
|
if (stateId && stateData.length) {
|
|
await simulateMany2OneSelection(formEl, 'state_id', stateId, stateData[0]?.display_name || province);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('[GooglePlaces Facility] Address autocomplete update failed:', err);
|
|
}
|
|
}
|
|
|
|
setTimeout(() => { _reattachFacilityAutocomplete(el, model); }, 400);
|
|
});
|
|
|
|
autocompleteInstances.set(input, autocomplete);
|
|
|
|
input.style.backgroundImage = 'url("data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 24 24\' fill=\'%234CAF50\'%3E%3Cpath d=\'M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z\'/%3E%3C/svg%3E")';
|
|
input.style.backgroundRepeat = 'no-repeat';
|
|
input.style.backgroundPosition = 'right 8px center';
|
|
input.style.backgroundSize = '20px';
|
|
input.style.paddingRight = '35px';
|
|
}
|
|
|
|
/**
|
|
* Re-attach facility autocomplete after OWL re-renders inputs.
|
|
*/
|
|
function _reattachFacilityAutocomplete(el, model) {
|
|
const streetSelectors = [
|
|
'div[name="street"] input',
|
|
'.o_field_widget[name="street"] input',
|
|
'[name="street"] input',
|
|
];
|
|
for (const sel of streetSelectors) {
|
|
const inp = el.querySelector(sel);
|
|
if (inp && !autocompleteInstances.has(inp)) {
|
|
_attachFacilityAddressAutocomplete(inp, el, model);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Patch FormController to add Google autocomplete for partner forms and dialog detection
|
|
*/
|
|
patch(FormController.prototype, {
|
|
setup() {
|
|
super.setup();
|
|
|
|
this.orm = useService("orm");
|
|
|
|
onMounted(() => {
|
|
// Store ORM globally for dialog watcher
|
|
globalOrm = this.orm;
|
|
|
|
// Direct partner form
|
|
if (this.props.resModel === 'res.partner') {
|
|
setTimeout(() => {
|
|
if (this.rootRef && this.rootRef.el) {
|
|
setupPartnerAutocomplete(this.rootRef.el, this.model, this.orm);
|
|
}
|
|
}, 800);
|
|
|
|
if (this.rootRef && this.rootRef.el) {
|
|
this._addressObserver = new MutationObserver((mutations) => {
|
|
const hasNewInputs = mutations.some(m =>
|
|
m.addedNodes.length > 0 &&
|
|
Array.from(m.addedNodes).some(n =>
|
|
n.nodeType === 1 && (n.tagName === 'INPUT' || n.querySelector?.('input'))
|
|
)
|
|
);
|
|
if (hasNewInputs) {
|
|
setTimeout(() => {
|
|
setupPartnerAutocomplete(this.rootRef.el, this.model, this.orm);
|
|
}, 300);
|
|
}
|
|
});
|
|
|
|
this._addressObserver.observe(this.rootRef.el, {
|
|
childList: true,
|
|
subtree: true
|
|
});
|
|
}
|
|
}
|
|
|
|
// Technician task form
|
|
if (this.props.resModel === 'fusion.technician.task') {
|
|
setTimeout(() => {
|
|
if (this.rootRef && this.rootRef.el) {
|
|
setupTaskAutocomplete(this.rootRef.el, this.model, this.orm);
|
|
}
|
|
}, 800);
|
|
|
|
if (this.rootRef && this.rootRef.el) {
|
|
this._taskAddressObserver = new MutationObserver((mutations) => {
|
|
const hasNewInputs = mutations.some(m =>
|
|
m.addedNodes.length > 0 &&
|
|
Array.from(m.addedNodes).some(n =>
|
|
n.nodeType === 1 && (n.tagName === 'INPUT' || n.querySelector?.('input'))
|
|
)
|
|
);
|
|
if (hasNewInputs) {
|
|
setTimeout(() => {
|
|
setupTaskAutocomplete(this.rootRef.el, this.model, this.orm);
|
|
}, 300);
|
|
}
|
|
});
|
|
|
|
this._taskAddressObserver.observe(this.rootRef.el, {
|
|
childList: true,
|
|
subtree: true,
|
|
});
|
|
|
|
// "Calculate Travel" button -- handle via JS to prevent dialog close
|
|
const calcBtn = this.rootRef.el.querySelector('.o_fc_calculate_travel');
|
|
if (calcBtn) {
|
|
const formModel = this.model;
|
|
const orm = this.orm;
|
|
calcBtn.addEventListener('click', async (ev) => {
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
const resId = formModel.root.resId;
|
|
if (!resId) {
|
|
// New unsaved record -- save first
|
|
const saved = await formModel.root.save();
|
|
if (!saved) return;
|
|
} else {
|
|
// Existing record -- save pending changes
|
|
if (formModel.root.isDirty) {
|
|
const saved = await formModel.root.save();
|
|
if (!saved) return;
|
|
}
|
|
}
|
|
const id = formModel.root.resId;
|
|
if (!id) return;
|
|
// Show loading state on button
|
|
const origText = calcBtn.textContent;
|
|
calcBtn.disabled = true;
|
|
calcBtn.textContent = ' Calculating...';
|
|
try {
|
|
await orm.call(
|
|
'fusion.technician.task',
|
|
'action_calculate_travel_times',
|
|
[id],
|
|
);
|
|
// Reload form data to show updated travel fields
|
|
await formModel.root.load();
|
|
} catch (e) {
|
|
console.error('[CalcTravel] Error:', e);
|
|
} finally {
|
|
calcBtn.disabled = false;
|
|
calcBtn.textContent = origText || ' Calculate Travel';
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// LTC Facility form
|
|
if (this.props.resModel === 'fusion.ltc.facility') {
|
|
setTimeout(() => {
|
|
if (this.rootRef && this.rootRef.el) {
|
|
setupFacilityAutocomplete(this.rootRef.el, this.model, this.orm);
|
|
}
|
|
}, 800);
|
|
|
|
if (this.rootRef && this.rootRef.el) {
|
|
this._facilityAddrObserver = new MutationObserver((mutations) => {
|
|
const hasNewInputs = mutations.some(m =>
|
|
m.addedNodes.length > 0 &&
|
|
Array.from(m.addedNodes).some(n =>
|
|
n.nodeType === 1 && (n.tagName === 'INPUT' || n.querySelector?.('input'))
|
|
)
|
|
);
|
|
if (hasNewInputs) {
|
|
setTimeout(() => {
|
|
setupFacilityAutocomplete(this.rootRef.el, this.model, this.orm);
|
|
}, 300);
|
|
}
|
|
});
|
|
this._facilityAddrObserver.observe(this.rootRef.el, {
|
|
childList: true,
|
|
subtree: true,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Simple address autocomplete: res.partner, res.users, res.config.settings
|
|
if (this.props.resModel === 'res.partner' || this.props.resModel === 'res.users' || this.props.resModel === 'res.config.settings') {
|
|
setTimeout(() => {
|
|
if (this.rootRef && this.rootRef.el) {
|
|
setupSimpleAddressFields(this.rootRef.el, this.orm);
|
|
}
|
|
}, 800);
|
|
|
|
if (this.rootRef && this.rootRef.el) {
|
|
this._simpleAddrObserver = new MutationObserver((mutations) => {
|
|
const hasNewInputs = mutations.some(m =>
|
|
m.addedNodes.length > 0 &&
|
|
Array.from(m.addedNodes).some(n =>
|
|
n.nodeType === 1 && (n.tagName === 'INPUT' || n.querySelector?.('input'))
|
|
)
|
|
);
|
|
if (hasNewInputs) {
|
|
setTimeout(() => {
|
|
setupSimpleAddressFields(this.rootRef.el, this.orm);
|
|
}, 300);
|
|
}
|
|
});
|
|
this._simpleAddrObserver.observe(this.rootRef.el, { childList: true, subtree: true });
|
|
}
|
|
}
|
|
|
|
// Start global dialog watcher once
|
|
_startDialogWatcher(this.orm);
|
|
});
|
|
|
|
onWillUnmount(() => {
|
|
if (this._addressObserver) {
|
|
this._addressObserver.disconnect();
|
|
}
|
|
if (this._taskAddressObserver) {
|
|
this._taskAddressObserver.disconnect();
|
|
}
|
|
if (this._facilityAddrObserver) {
|
|
this._facilityAddrObserver.disconnect();
|
|
}
|
|
if (this._simpleAddrObserver) {
|
|
this._simpleAddrObserver.disconnect();
|
|
}
|
|
if (this.rootRef && this.rootRef.el) {
|
|
cleanupAutocomplete(this.rootRef.el);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Start the global dialog watcher (runs once, watches forever)
|
|
*/
|
|
let _dialogWatcherStarted = false;
|
|
let _dialogCheckTimer = null;
|
|
const _processedDialogs = new WeakSet();
|
|
|
|
function _startDialogWatcher(orm) {
|
|
if (_dialogWatcherStarted) return;
|
|
_dialogWatcherStarted = true;
|
|
|
|
console.log('[GooglePlaces] Starting global dialog watcher...');
|
|
|
|
// Pre-load the API key and Google Maps
|
|
getGoogleMapsApiKey(orm).then(apiKey => {
|
|
if (!apiKey) {
|
|
console.log('[GooglePlaces] No API key, dialog watcher disabled');
|
|
return;
|
|
}
|
|
|
|
loadGoogleMapsApi(apiKey).then(() => {
|
|
console.log('[GooglePlaces] API loaded, dialog watcher active');
|
|
|
|
// Watch for ANY DOM change on body
|
|
const observer = new MutationObserver(() => {
|
|
if (_dialogCheckTimer) clearTimeout(_dialogCheckTimer);
|
|
_dialogCheckTimer = setTimeout(_checkDialogsForPartnerForms, 600);
|
|
});
|
|
|
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
|
|
}).catch(err => console.warn('[GooglePlaces] API load failed:', err));
|
|
}).catch(err => console.warn('[GooglePlaces] API key fetch failed:', err));
|
|
}
|
|
|
|
/**
|
|
* Check all open dialogs for partner address forms
|
|
*/
|
|
function _checkDialogsForPartnerForms() {
|
|
// Get all visible modals/dialogs
|
|
const modals = document.querySelectorAll(
|
|
'.o_dialog, .modal, .o_FormViewDialog, [role="dialog"]'
|
|
);
|
|
|
|
for (const modal of modals) {
|
|
// Skip already processed
|
|
if (_processedDialogs.has(modal)) continue;
|
|
|
|
// Skip hidden modals
|
|
if (modal.offsetParent === null && !modal.classList.contains('show')) continue;
|
|
|
|
// Check if this dialog has a street field (partner form)
|
|
const streetInput = modal.querySelector(
|
|
'[name="street"] input, ' +
|
|
'.o_address_street input, ' +
|
|
'div[name="street"] input, ' +
|
|
'.o_field_widget[name="street"] input'
|
|
);
|
|
|
|
if (!streetInput) continue;
|
|
|
|
// Mark as processed
|
|
_processedDialogs.add(modal);
|
|
|
|
console.log('[GooglePlaces] Found partner dialog with street field!');
|
|
|
|
// Get form element and model
|
|
const formEl = modal.querySelector('.o_form_view') || modal;
|
|
let formModel = null;
|
|
|
|
// Walk through ALL elements to find OWL component with model
|
|
const allEls = formEl.querySelectorAll('*');
|
|
for (const el of allEls) {
|
|
if (el.__owl__?.component?.model) {
|
|
formModel = el.__owl__.component.model;
|
|
console.log('[GooglePlaces] Found form model via child element');
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Also walk up from formEl
|
|
if (!formModel) {
|
|
let node = formEl;
|
|
while (node) {
|
|
if (node.__owl__?.component?.model) {
|
|
formModel = node.__owl__.component.model;
|
|
console.log('[GooglePlaces] Found form model via parent');
|
|
break;
|
|
}
|
|
node = node.parentElement;
|
|
}
|
|
}
|
|
|
|
// If still no model, try the modal itself and its controller
|
|
if (!formModel) {
|
|
let node = modal;
|
|
while (node) {
|
|
const comp = node.__owl__?.component;
|
|
if (comp) {
|
|
// Check if this component has a model property
|
|
if (comp.model) {
|
|
formModel = comp.model;
|
|
console.log('[GooglePlaces] Found form model via modal OWL component');
|
|
break;
|
|
}
|
|
// Check props for FormViewDialog
|
|
if (comp.props?.record) {
|
|
formModel = { root: comp.props.record };
|
|
console.log('[GooglePlaces] Found form model via dialog props.record');
|
|
break;
|
|
}
|
|
}
|
|
node = node.parentElement;
|
|
}
|
|
}
|
|
|
|
if (!formModel) {
|
|
console.log('[GooglePlaces] No form model found - will use DOM-only approach for dialog');
|
|
}
|
|
|
|
// Initialize autocomplete on the street field, with a special dialog handler
|
|
_initDialogAutocomplete(streetInput, formEl, formModel, modal);
|
|
|
|
// Watch for re-renders inside the dialog
|
|
let innerTimer = null;
|
|
const innerObs = new MutationObserver(() => {
|
|
if (innerTimer) clearTimeout(innerTimer);
|
|
innerTimer = setTimeout(() => {
|
|
const newStreet = modal.querySelector(
|
|
'[name="street"] input, .o_address_street input, div[name="street"] input'
|
|
);
|
|
if (newStreet && !autocompleteInstances.has(newStreet)) {
|
|
_initDialogAutocomplete(newStreet, formEl, formModel, modal);
|
|
}
|
|
}, 400);
|
|
});
|
|
innerObs.observe(modal, { childList: true, subtree: true });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialize autocomplete specifically for dialog forms (handles DOM-only updates)
|
|
*/
|
|
function _initDialogAutocomplete(streetInput, formEl, formModel, modal) {
|
|
if (!streetInput || !window.google?.maps?.places) return;
|
|
if (autocompleteInstances.has(streetInput)) return;
|
|
|
|
const autocomplete = new google.maps.places.Autocomplete(streetInput, {
|
|
componentRestrictions: { country: 'ca' },
|
|
types: ['address'],
|
|
fields: ['address_components', 'formatted_address']
|
|
});
|
|
|
|
autocomplete.addListener('place_changed', async () => {
|
|
const place = autocomplete.getPlace();
|
|
if (!place.address_components) return;
|
|
|
|
// Parse address components
|
|
let streetNumber = '', streetName = '', unitNumber = '';
|
|
let city = '', province = '', postalCode = '', countryCode = '';
|
|
|
|
for (const c of place.address_components) {
|
|
const t = c.types;
|
|
if (t.includes('street_number')) streetNumber = c.long_name;
|
|
else if (t.includes('route')) streetName = c.long_name;
|
|
else if (t.includes('subpremise')) unitNumber = c.long_name;
|
|
else if (t.includes('floor') && !unitNumber) unitNumber = 'Floor ' + c.long_name;
|
|
else if (t.includes('locality')) city = c.long_name;
|
|
else if (t.includes('sublocality_level_1') && !city) city = c.long_name;
|
|
else if (t.includes('administrative_area_level_1')) province = c.short_name;
|
|
else if (t.includes('postal_code')) postalCode = c.long_name;
|
|
else if (t.includes('country')) countryCode = c.short_name;
|
|
}
|
|
|
|
const street = streetNumber ? `${streetNumber} ${streetName}` : streetName;
|
|
console.log('[GooglePlaces Dialog] Parsed:', { street, unitNumber, city, province, postalCode, countryCode });
|
|
|
|
// Try record.update first (works if formModel is available)
|
|
if (formModel?.root) {
|
|
try {
|
|
const textUpdate = {};
|
|
if (street) textUpdate.street = street;
|
|
if (unitNumber) textUpdate.street2 = unitNumber;
|
|
if (city) textUpdate.city = city;
|
|
if (postalCode) textUpdate.zip = postalCode;
|
|
await formModel.root.update(textUpdate);
|
|
console.log('[GooglePlaces Dialog] Text fields updated via model');
|
|
|
|
// Handle country and state via model
|
|
if (countryCode && globalOrm) {
|
|
const countries = await globalOrm.searchRead('res.country', [['code', '=', countryCode]], ['id'], { limit: 1 });
|
|
if (countries.length) {
|
|
await formModel.root.update({ country_id: countries[0].id });
|
|
console.log('[GooglePlaces Dialog] Country set via model');
|
|
|
|
if (province) {
|
|
await new Promise(r => setTimeout(r, 300));
|
|
const states = await globalOrm.searchRead('res.country.state', [['code', '=', province], ['country_id', '=', countries[0].id]], ['id'], { limit: 1 });
|
|
if (states.length) {
|
|
await formModel.root.update({ state_id: states[0].id });
|
|
console.log('[GooglePlaces Dialog] State set via model');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
} catch (err) {
|
|
console.log('[GooglePlaces Dialog] Model update failed, falling back to DOM:', err.message);
|
|
}
|
|
}
|
|
|
|
// DOM fallback - directly set input values in the dialog
|
|
console.log('[GooglePlaces Dialog] Using DOM fallback');
|
|
|
|
function setFieldValue(container, fieldName, value) {
|
|
if (!value) return;
|
|
const field = container.querySelector(`[name="${fieldName}"] input, div[name="${fieldName}"] input`);
|
|
if (field) {
|
|
// Set value and trigger input event for OWL reactivity
|
|
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
|
|
nativeInputValueSetter.call(field, value);
|
|
field.dispatchEvent(new Event('input', { bubbles: true }));
|
|
field.dispatchEvent(new Event('change', { bubbles: true }));
|
|
console.log(`[GooglePlaces Dialog] Set ${fieldName} = ${value}`);
|
|
} else {
|
|
console.log(`[GooglePlaces Dialog] Field ${fieldName} input not found`);
|
|
}
|
|
}
|
|
|
|
// Set text fields via DOM
|
|
setFieldValue(modal, 'street', street);
|
|
setFieldValue(modal, 'street2', unitNumber);
|
|
setFieldValue(modal, 'city', city);
|
|
setFieldValue(modal, 'zip', postalCode);
|
|
|
|
// Set country and state via Many2one simulation
|
|
if (countryCode && globalOrm) {
|
|
try {
|
|
const countries = await globalOrm.searchRead('res.country', [['code', '=', countryCode]], ['id', 'display_name'], { limit: 1 });
|
|
if (countries.length) {
|
|
await simulateMany2OneSelection(modal, 'country_id', countries[0].id, countries[0].display_name);
|
|
|
|
if (province) {
|
|
await new Promise(r => setTimeout(r, 500));
|
|
const states = await globalOrm.searchRead('res.country.state', [['code', '=', province], ['country_id', '=', countries[0].id]], ['id', 'display_name'], { limit: 1 });
|
|
if (states.length) {
|
|
await simulateMany2OneSelection(modal, 'state_id', states[0].id, states[0].display_name);
|
|
}
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('[GooglePlaces Dialog] Country/state lookup failed:', err);
|
|
}
|
|
}
|
|
});
|
|
|
|
autocompleteInstances.set(streetInput, autocomplete);
|
|
|
|
// Add visual indicator
|
|
streetInput.style.backgroundImage = 'url("data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 24 24\' fill=\'%234CAF50\'%3E%3Cpath d=\'M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z\'/%3E%3C/svg%3E")';
|
|
streetInput.style.backgroundRepeat = 'no-repeat';
|
|
streetInput.style.backgroundPosition = 'right 8px center';
|
|
streetInput.style.backgroundSize = '20px';
|
|
streetInput.style.paddingRight = '35px';
|
|
|
|
console.log('[GooglePlaces Dialog] Autocomplete initialized on dialog street field');
|
|
}
|