loko/streetup/interventions/static/interventions/add/autocomplete.js
2026-07-22 14:48:40 +02:00

733 lines
26 KiB
JavaScript

// Autocomplete pour les localisations et adresses //
// ----------------------------------------------- //
const MAX_RESULTS = 50;
function formatWithNotice(items, mapFn) {
const hasMore = Array.isArray(items) && items.length === MAX_RESULTS;
const formatted = items.map(mapFn);
if (hasMore) {
formatted.push({
label: "…",
value: "",
response_type: "notice",
isNotice: true,
disabled: true
});
}
return formatted;
}
function fetchAndFormat(url, term, mapFn) {
return $.getJSON(url, { q: term })
.then(items => Array.isArray(items) ? formatWithNotice(items, mapFn) : [])
.catch(() => []); // en cas d'erreur, on retourne un tableau vide pour déclencher le fallback
}
// Mappers réutilisables
const mapLocation = e => ({
label: e.label,
location_id: e.value.obj,
location_ct: e.value.ct,
location_label: e.label,
lon: e.lon,
lat: e.lat,
parent_ref_id: e.parent_ref_id,
parent_ref_label: e.parent_ref_label,
response_type: 'location'
});
const mapAddress = e => ({
label: e.label,
address_id: e.id,
lon: e.lon,
lat: e.lat,
response_type: 'address'
});
/**
* Lance la recherche primaire; si vide → fallback.
* Appelle `response()` une seule fois avec le premier résultat non vide (ou vide si rien trouvé).
*/
function searchWithFallback(primaryUrl, primaryMapper, secondaryUrl, secondaryMapper, term, response) {
Promise.all([
fetchAndFormat(primaryUrl, term, primaryMapper),
fetchAndFormat(secondaryUrl, term, secondaryMapper)
]).then(([primary, secondary]) => {
const hasNotice = primary.some(item => item.isNotice) || secondary.some(item => item.isNotice);
const primaryClean = primary.filter(item => !item.isNotice);
const secondaryClean = secondary.filter(item => !item.isNotice);
let combined = [...primaryClean, ...secondaryClean];
if (combined.length > MAX_RESULTS) {
combined = combined.slice(0, MAX_RESULTS);
combined.push({
label: "…",
value: "",
response_type: "notice",
isNotice: true,
disabled: true
});
} else if (hasNotice) {
combined.push({
label: "…",
value: "",
response_type: "notice",
isNotice: true,
disabled: true
});
}
response(combined);
});
}
const selectedEquipments = window.selectedEquipments;
function equipmentKey(ct, id) {
return `${ct}:${id}`;
}
function removeEquipment(ct, id) {
const key = equipmentKey(String(ct), String(id));
const idx = selectedEquipments.findIndex(eq => eq.key === key);
if (idx !== -1) {
selectedEquipments.splice(idx, 1);
updateSelectedEquipmentsUI();
}
}
function clearAllEquipments() {
if (selectedEquipments.length > 0) {
selectedEquipments.length = 0;
}
updateSelectedEquipmentsUI();
}
function updateSelectedEquipmentsUI() {
const summary = document.getElementById('equipment_result_summary');
if (summary) {
summary.innerHTML = ''; // Clear existing content
selectedEquipments.forEach(eq => {
const badge = document.createElement('span');
badge.className = 'badge text-bg-primary d-inline-flex align-items-center';
badge.style.padding = '0.5em 0.75em';
badge.innerHTML = `
<span class="me-2">${eq.display_label}</span>
<button type="button"
class="btn-close btn-close-white p-0"
style="font-size: 0.75em;"
data-ct="${eq.equipment_ct}"
data-id="${eq.equipment_id}"
title="${gettext('Supprimer cet équipement')}">
</button>
`;
// Gestionnaire d'événements pour le bouton de suppression
const deleteBtn = badge.querySelector('button');
deleteBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
removeEquipment(eq.equipment_ct, eq.equipment_id);
});
summary.appendChild(badge);
});
}
const primaryLabel = document.getElementById('equipment_primary_label');
if (primaryLabel) {
const first = selectedEquipments[0];
primaryLabel.textContent = first ? first.display_label : '';
}
const assetsField = document.getElementById('assets-field');
if (assetsField) {
const payload = [];
selectedEquipments.forEach(eq => {
const ctNum = parseInt(eq.equipment_ct, 10);
const objNum = parseInt(eq.equipment_id, 10);
if (!Number.isNaN(ctNum) && !Number.isNaN(objNum)) {
payload.push({ ct: ctNum, obj: objNum });
}
});
assetsField.value = JSON.stringify(payload);
}
// Clear custom validation error on location-input since equipment was selected/updated
const locInput = document.getElementById('location-input');
if (locInput && selectedEquipments.length > 0) {
locInput.classList.remove('is-invalid');
const fb = locInput.parentElement?.querySelector('.invalid-feedback');
if (fb) {
fb.textContent = '';
fb.classList.remove('d-block');
}
}
// Clear equipment input alert style as soon as one equipment is selected.
const equipmentInput = document.getElementById('equipment-input');
if (equipmentInput && selectedEquipments.length > 0) {
equipmentInput.classList.remove('is-invalid');
const fb = equipmentInput.parentElement?.querySelector('.invalid-feedback');
if (fb) {
fb.textContent = '';
fb.classList.remove('d-block');
}
}
const first = selectedEquipments[0];
$('#equipment_id').val(first ? first.equipment_id : '').trigger('change');
$('#equipment_ct').val(first ? first.equipment_ct : '').trigger('change');
$('#equipment_type').val(first ? (first.equipment_type || '') : '').trigger('change');
updateTitle();
if (typeof refreshInterventionBadges === 'function') {
refreshInterventionBadges();
}
}
function applyEquipmentLocation(item) {
if (!item) {
return;
}
const locId = item.location_id;
const locCt = item.location_ct;
const locLabel = item.location_label;
$('#location_id').val(locId || '');
$('#location_ct').val(locCt || '');
$('#location_result').text(locLabel || '');
if (locLabel) {
$('#location-input').val(locLabel || '');
}
maybeClear(document.getElementById('location_id'), el => el.value.trim() !== '');
maybeClear(document.getElementById('location_result'), el => el.textContent.trim().length > 0);
if (thematic === 'structures') {
$('#structure_result').text(item.parent_ref_label || '');
}
else if (thematic === 'controlcenters') {
$('#controlcenter_result').text(item.parent_ref_label || '');
}
else if (thematic === 'publiclighting') {
if (!locId) {
fetchNearestLocation(item.lon, item.lat, "/assets/publiclighting/streets/get-nearest/");
}
}
else if (thematic === 'trafficlights') {
if (!locId) {
fetchNearestLocation(item.lon, item.lat, "/assets/trafficlights/intersections/get-nearest/");
}
}
else if (thematic === 'roads') {
if (!locId) {
fetchNearestLocation(item.lon, item.lat, "/assets/roads/streets/get-nearest/");
}
}
else if (thematic === 'sign') {
if (!locId) {
fetchNearestLocation(item.lon, item.lat, "/assets/sign/streets/get-nearest/");
}
}
else if (thematic === 'its') {
if (!locId) {
fetchNearestLocation(item.lon, item.lat, "/assets/its/locations/get-nearest/");
}
}
else if (thematic === 'nature') {
if (!locId) {
fetchNearestLocation(item.lon, item.lat, "/assets/nature/locations/get-nearest/");
}
}
else if (thematic === 'clean') {
if (!locId) {
fetchNearestLocation(item.lon, item.lat, "/assets/clean/locations/get-nearest/");
}
}
if (item.lon && item.lat) {
$('#id_lon').val(item.lon);
$('#id_lat').val(item.lat);
centerMap();
}
}
function addEquipmentSelection(item, options = {}) {
if (!item || item.equipment_id === undefined || item.equipment_ct === undefined || item.equipment_id === null || item.equipment_ct === null) {
return;
}
if (options.replaceExisting === true) {
selectedEquipments.length = 0;
}
const id = String(item.equipment_id);
const ct = String(item.equipment_ct);
const key = equipmentKey(ct, id);
if (!selectedEquipments.some(eq => eq.key === key)) {
const label = item.label || item.equipment_label || item.display_label || id;
selectedEquipments.push({
key,
equipment_id: id,
equipment_ct: ct,
equipment_type: item.equipment_type || '',
display_label: label
});
}
updateSelectedEquipmentsUI();
applyEquipmentLocation(item);
}
$(function() {
let selectionMade = false;
function isGiepActive() {
if (inspectionConfigId) return true;
if (thematic !== 'nature') return false;
const catField = document.getElementById('id_asset_category');
if (catField && catField.selectedIndex >= 0) {
const text = catField.options[catField.selectedIndex].text.toLowerCase();
if (text.includes('giep') || text.includes('rwi')) return true;
}
const subCatField = document.getElementById('id_interv_sub_category');
if (subCatField && subCatField.selectedIndex >= 0) {
const text = subCatField.options[subCatField.selectedIndex].text.toLowerCase();
if (text.includes('giep') || text.includes('rwi')) return true;
}
return false;
}
$("#location-input").autocomplete({
minLength: 2,
source: function(request, response) {
let locationsUrl = autocomplete_locations_url;
if (isGiepActive() && !locationsUrl.includes('prioritize=')) {
locationsUrl += (locationsUrl.includes('?') ? '&' : '?') + 'prioritize=giep';
}
if (['structures', 'trafficlights', 'controlcenters'].includes(thematic) || isGiepActive()) {
// Localisations d'abord, puis adresses
searchWithFallback(
locationsUrl, mapLocation,
autocomplete_street_url, mapAddress,
request.term, response
);
} else {
// Adresses d'abord, puis localisations
searchWithFallback(
autocomplete_street_url, mapAddress,
locationsUrl, mapLocation,
request.term, response
);
}
},
select: function(event, ui) {
if (ui.item.isNotice || ui.item.response_type === "notice") {
event.preventDefault(); // ne rien insérer si on clique la notice
return;
}
// Si une sélection a été faite, on cache le bouton 'Localiser'
selectionMade = true;
$("#geocode-btn").addClass("d-none");
$("#location-input").val(ui.item.label);
$('#id_lon').val(ui.item.lon);
$('#id_lat').val(ui.item.lat);
// Clear custom validation error on location-input
const locInput = document.getElementById('location-input');
if (locInput) {
locInput.classList.remove('is-invalid');
const fb = locInput.parentElement?.querySelector('.invalid-feedback');
if (fb) {
fb.textContent = '';
fb.classList.remove('d-block');
}
}
// Réinitialiser les équipements sélectionnés si on choisit une localisation
clearAllEquipments();
$('#equipment-input').val('');
if (ui.item.response_type === 'location') {
$('#location_id').val(ui.item.location_id);
$('#location_ct').val(ui.item.location_ct);
$('#location_result').text(ui.item.location_label);
maybeClear(document.getElementById('location_id'), el => el.value.trim() !== '');
maybeClear(document.getElementById('location_result'), el => el.textContent.trim().length > 0);
if (thematic == 'structures') {
$('#structure_result').text(ui.item.parent_ref_label);
}
else if (['publiclighting', 'roads', 'sign', 'nature', 'clean'].includes(thematic)) {
$('#address_result').text(ui.item.location_label);
}
else if (thematic == 'controlcenters') {
$('#controlcenter_result').text(ui.item.parent_ref_label);
}
}
else if (ui.item.response_type === 'address') {
$('#address_id').val(ui.item.label);
$('#address_result').text(ui.item.label);
update_locations({lon: ui.item.lon, lat: ui.item.lat, update_address: false});
}
centerMap();
// Mettre à jour le titre
updateTitle();
// Retirer le focus pour éviter la réouverture immédiate
$(this).blur();
}
});
// À chaque nouvelle frappe, on recache le bouton et on “oublie” la sélection
$("#location-input").on("input", function () {
selectionMade = false;
$("#geocode-btn").addClass("d-none");
});
// Quand l'autocomplete a une réponse, on sait combien d'items il y a
$("#location-input").on("autocompleteresponse", function (event, ui) {
const term = $(this).val().trim();
const hasResults = ui.content && ui.content.length > 0;
const enoughChars = term.length >= 5;
if (!selectionMade && enoughChars && !hasResults) {
$("#geocode-btn").removeClass("d-none"); // montrer le bouton si 0 résultat
} else {
$("#geocode-btn").addClass("d-none"); // sinon le cacher
}
});
});
// Autocomplete pour les équipements
$(function() {
let shouldOpenEquipmentAutocomplete = false;
$("#equipment-input").autocomplete({
minLength: 0,
autoFocus: false,
source: function(request, response) {
// Ne pas ouvrir si pas d'interaction utilisateur explicite
if (!shouldOpenEquipmentAutocomplete) {
response([]);
return;
}
const rawTerm = request.term || "";
const trimmedTerm = rawTerm.trim();
const isSingleSpace = rawTerm === " ";
// if (!isSingleSpace && trimmedTerm.length < 2) {
// response([]);
// return;
// }
// On prépare les paramètres
let params = { q: rawTerm };
let locId = $('#location_id').val();
if (locId) {
params.location_id = locId; // on n'ajoute que si non vide
}
const categoryId = (typeof window._getEffectiveInterventionCategoryId === 'function')
? window._getEffectiveInterventionCategoryId()
: ($('#id_interv_sub_category').val() || $('#id_asset_category').val() || '');
if (categoryId) {
params.asset_category = categoryId;
}
$.getJSON(autocomplete_assets_url, params, function (equipments) {
const maxResults = 100;
const hasMore = Array.isArray(equipments) && equipments.length === maxResults;
// Formatage
const formattedEquipments = equipments.map(e => ({
label: e.label, // pour l'affichage dans la liste
equipment_id: e.id,
equipment_ct: e.equipment_ct,
equipment_label: e.equipment_label,
equipment_type: e.equipment_type,
location_id: e.location_id,
location_ct: e.location_ct,
location_label: e.location_label,
parent_ref_id: e.parent_ref_id,
parent_ref_label: e.parent_ref_label,
lon: e.lon,
lat: e.lat
}));
// Ajout notice si limite atteinte
if (hasMore) {
formattedEquipments.push({
label: " … " + gettext("Limité à 100 résultats, affinez votre recherche"),
value: "",
response_type: "notice",
isNotice: true,
disabled: true
});
}
response(formattedEquipments);
});
},
select: function(event, ui) {
if (ui.item.isNotice || ui.item.response_type === "notice") {
event.preventDefault();
return;
}
event.preventDefault();
addEquipmentSelection(ui.item);
$('#equipment-input').val('').trigger('input');
// Retirer le focus pour éviter la réouverture immédiate
$('#equipment-input').blur();
}
})
// Rendu personnalisé pour la notice
.autocomplete("instance")._renderItem = function (ul, item) {
if (item.isNotice || item.response_type === "notice") {
return $("<li>").addClass("ui-state-disabled").text(item.label).appendTo(ul);
}
return $("<li>").append($("<div>").text(item.label)).appendTo(ul);
};
// Déclencher la recherche au clic pour afficher les équipements disponibles
$("#equipment-input").on('click', function() {
shouldOpenEquipmentAutocomplete = true;
$(this).autocomplete('search', $(this).val());
});
// Réinitialiser le flag quand l'autocomplete se ferme
$("#equipment-input").on('autocompleteclose', function() {
shouldOpenEquipmentAutocomplete = false;
});
});
// Utilitaire générique : réinitialise quand l'input devient vide
function wireResetOnEmpty({ input, clearVals = [], clearTexts = [], extra }) {
$(input).on("input change", function () {
if (this.value.trim() === "") {
clearVals.forEach(sel => $(sel).val(""));
clearTexts.forEach(sel => $(sel).text(""));
if (typeof extra === "function") extra();
}
});
}
// --- Localisations ---
wireResetOnEmpty({
input: "#location-input",
clearVals: [
"#location_id",
"#equipment-input",
"#id_lon",
"#id_lat"
],
clearTexts: [
"#location_result",
"#structure_result", // thematic "structures"
"#controlcenter_result" // thematic "controlcenters"
],
extra: function () {
clearAllEquipments();
if (typeof window.clearMap === "function") clearMap(); // optionnel
}
});
function update_locations({lon, lat, update_address=true, update_location=true, clean_address=false, clean_location=false, clean_equipment=false}) {
if (clean_equipment) {
clearAllEquipments();
}
if (update_address && ['publiclighting', 'nature', 'roads', 'sign', 'its', 'clean'].includes(thematic)) {
getNearestAddress(lon, lat);
}
if (update_location) {
// Récupérer le code de la rue pour l'éclairage public
if (thematic == 'publiclighting') {
// Appeler la fonction pour récupérer la rue
fetchNearestLocation(lon, lat, "/assets/publiclighting/streets/get-nearest/", false);
}
else if (thematic == 'trafficlights') {
// Appeler la fonction pour récupérer l'intersection
fetchNearestLocation(lon, lat, "/assets/trafficlights/intersections/get-nearest/", true);
}
else if (thematic == 'artworks') {
// Appeler la fonction pour récupérer l'asset
fetchNearestAsset(lon, lat, "/assets/artworks/assets/get-nearest/", true);
}
else if (thematic == 'roads') {
fetchNearestLocation(lon, lat, "/assets/roads/streets/get-nearest/");
}
else if (thematic == 'sign') {
fetchNearestLocation(lon, lat, "/assets/sign/streets/get-nearest/");
}
else if (thematic == 'nature') {
fetchNearestLocation(lon, lat, "/assets/nature/locations/get-nearest/");
}
else if (thematic == 'structures') {
fetchNearestLocation(lon, lat, "/assets/structures/locations/get-nearest/");
}
else if (thematic == 'clean') {
fetchNearestLocation(lon, lat, "/assets/clean/locations/get-nearest/");
}
}
}
// Fonction pour récupérer l'adresse la plus proche
function getNearestAddress(lon, lat) {
$.ajax({
url: "/common/address/get-nearest-address/",
data: { lon: lon, lat: lat },
success: function(response) {
if (response.label) {
$('#address_result').text(response.label);
$("#address_id").val(response.label); // Mettre à jour le champ caché avec l'adresse sélectionnée
updateTitle(); // Mettre à jour le titre avec l'adresse trouvée
} else {
console.warn(gettext("Aucune adresse trouvée pour ces coordonnées."));
$('#address_result').text("");
$("#address_id").val("");
updateTitle();
}
},
error: function(xhr) {
console.warn(gettext("Adresse non trouvée :"), xhr.responseJSON?.error || gettext("Erreur"));
$('#address_result').text("");
$("#address_id").val("");
updateTitle();
}
});
}
// Fonction pour récupérer la localisation
function fetchNearestLocation(lon, lat, url, update_coords=false) {
$.ajax({
url: url,
data: { lon: lon, lat: lat },
success: function(response) {
if (response.location_id) {
$('#location_result').text(response.location_label);
$('#location_id').val(response.location_id);
$('#location_ct').val(response.location_ct);
maybeClear(document.getElementById('location_id'), el => el.value.trim() !== '');
maybeClear(document.getElementById('location_result'), el => el.textContent.trim().length > 0);
if (update_coords) {
$('#location-input').val('');
var lng = parseFloat(response.lon);
var lat = parseFloat(response.lat);
marker.setLngLat([lng, lat]);
updateLatLon(lng, lat);
}
updateTitle();
if (thematic === 'structures') {
$('#structure_result').text(response.parent_ref_label || '');
}
}
else {
console.warn(gettext("Aucune localisation trouvée pour ces coordonnées."));
$('#location_result').text('');
$('#location_id').val('');
$('#location-input').val('');
}
},
error: function(xhr) {
console.warn(gettext("Localisation non trouvée :"), xhr.responseJSON?.error || gettext("Erreur"));
$('#location_result').text('');
$('#location_id').val('');
$('#location-input').val('');
}
});
}
// Fonction pour récupérer l'asset
function fetchNearestAsset(lon, lat, url, update_coords=false) {
$.ajax({
url: url,
data: { lon: lon, lat: lat },
success: function(response) {
if (response.equipment_id) {
const item = {
equipment_id: response.equipment_id,
equipment_ct: response.equipment_ct,
equipment_type: response.equipment_type,
label: response.equipment_label,
equipment_label: response.equipment_label,
location_id: response.location_id,
location_ct: response.location_ct,
location_label: response.location_label,
parent_ref_label: response.parent_ref_label,
lon: response.lon,
lat: response.lat
};
addEquipmentSelection(item, { replaceExisting: true });
$('#equipment-input').val('');
if (update_coords) {
$('#location-input').val('');
const lng = parseFloat(response.lon);
const latVal = parseFloat(response.lat);
if (!Number.isNaN(lng) && !Number.isNaN(latVal)) {
marker.setLngLat([lng, latVal]);
updateLatLon(lng, latVal);
}
}
}
else {
console.warn(gettext("Aucun équipement trouvé pour ces coordonnées."));
clearAllEquipments();
}
},
error: function(xhr) {
console.warn(gettext("Equipement non trouvé :"), xhr.responseJSON?.error || gettext("Erreur"));
clearAllEquipments();
}
});
}
// Documents
window.selectedEquipments = selectedEquipments;
window.equipmentKey = equipmentKey;
window.removeEquipment = removeEquipment;
window.clearAllEquipments = clearAllEquipments;
window.updateSelectedEquipmentsUI = updateSelectedEquipmentsUI;
window.applyEquipmentLocation = applyEquipmentLocation;
window.addEquipmentSelection = addEquipmentSelection;
window.wireResetOnEmpty = wireResetOnEmpty;
window.update_locations = update_locations;
window.getNearestAddress = getNearestAddress;
window.fetchNearestLocation = fetchNearestLocation;
window.fetchNearestAsset = fetchNearestAsset;