updates
This commit is contained in:
@@ -1048,6 +1048,332 @@ async function setupSimpleAddressFields(el, orm) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@@ -1164,6 +1490,35 @@ patch(FormController.prototype, {
|
||||
}
|
||||
}
|
||||
|
||||
// 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(() => {
|
||||
@@ -1201,6 +1556,9 @@ patch(FormController.prototype, {
|
||||
if (this._taskAddressObserver) {
|
||||
this._taskAddressObserver.disconnect();
|
||||
}
|
||||
if (this._facilityAddrObserver) {
|
||||
this._facilityAddrObserver.disconnect();
|
||||
}
|
||||
if (this._simpleAddrObserver) {
|
||||
this._simpleAddrObserver.disconnect();
|
||||
}
|
||||
|
||||
@@ -869,4 +869,62 @@ html.dark, .o_dark {
|
||||
z-index: 100000 !important;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// LTC REPAIR KANBAN - Stage & priority color coding
|
||||
// Uses data-* attributes on <main> + CSS :has() to style the outer card.
|
||||
// =============================================================================
|
||||
|
||||
.o_kanban_view .o_kanban_record {
|
||||
|
||||
// --- Stage left border (on the full card) ---
|
||||
&:has(main[data-stage="info"]) {
|
||||
border-left: 3px solid #0dcaf0 !important;
|
||||
background-color: rgba(13, 202, 240, 0.04) !important;
|
||||
}
|
||||
&:has(main[data-stage="warning"]) {
|
||||
border-left: 3px solid #ffc107 !important;
|
||||
background-color: rgba(255, 193, 7, 0.04) !important;
|
||||
}
|
||||
&:has(main[data-stage="success"]) {
|
||||
border-left: 3px solid #198754 !important;
|
||||
background-color: rgba(25, 135, 84, 0.04) !important;
|
||||
}
|
||||
&:has(main[data-stage="danger"]) {
|
||||
border-left: 3px solid #dc3545 !important;
|
||||
background-color: rgba(220, 53, 69, 0.04) !important;
|
||||
}
|
||||
&:has(main[data-stage="secondary"]) {
|
||||
border-left: 3px solid #adb5bd !important;
|
||||
}
|
||||
|
||||
// --- Priority high: warm amber bottom accent ---
|
||||
&:has(main[data-priority="1"]) {
|
||||
box-shadow: inset 0 -2px 0 0 rgba(255, 152, 0, 0.4) !important;
|
||||
}
|
||||
|
||||
// --- Emergency: override with stronger red ---
|
||||
&:has(main[data-emergency="1"]) {
|
||||
border-left: 4px solid #dc3545 !important;
|
||||
background-color: rgba(220, 53, 69, 0.06) !important;
|
||||
box-shadow: inset 0 0 0 1px rgba(220, 53, 69, 0.15) !important;
|
||||
}
|
||||
|
||||
// Emergency + priority combined
|
||||
&:has(main[data-emergency="1"][data-priority="1"]) {
|
||||
box-shadow: inset 0 0 0 1px rgba(220, 53, 69, 0.15),
|
||||
inset 0 -2px 0 0 rgba(255, 152, 0, 0.4) !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Dark mode: slightly stronger tints
|
||||
html.dark, .o_dark {
|
||||
.o_kanban_view .o_kanban_record {
|
||||
&:has(main[data-stage="info"]) { background-color: rgba(13, 202, 240, 0.07) !important; }
|
||||
&:has(main[data-stage="warning"]) { background-color: rgba(255, 193, 7, 0.07) !important; }
|
||||
&:has(main[data-stage="success"]) { background-color: rgba(25, 135, 84, 0.07) !important; }
|
||||
&:has(main[data-stage="danger"]) { background-color: rgba(220, 53, 69, 0.07) !important; }
|
||||
&:has(main[data-emergency="1"]) { background-color: rgba(220, 53, 69, 0.1) !important; }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user