feat: add favorite filters feature with UI, modals, and localizations for unified index view

This commit is contained in:
kdeterme 2026-10-12 15:16:01 +02:00
parent 91b2ca79ad
commit 1e80d14a89
15 changed files with 884 additions and 1 deletions

View file

@ -8764,3 +8764,30 @@ msgstr "Management zone documents"
msgid "Si coché, cet équipement est exclu de la zone même si sa géométrie intersecte le périmètre." msgid "Si coché, cet équipement est exclu de la zone même si sa géométrie intersecte le périmètre."
msgstr "If checked, this asset is excluded from the zone even if its geometry intersects the perimeter." msgstr "If checked, this asset is excluded from the zone even if its geometry intersects the perimeter."
msgid "Filtres favoris"
msgstr "Favorite filters"
msgid "Favoris"
msgstr "Favorites"
msgid "Mes filtres favoris"
msgstr "My favorite filters"
msgid "Enregistrer le filtre actuel..."
msgstr "Save current filter..."
msgid "Enregistrer comme favori"
msgstr "Save as favorite"
msgid "Nom du filtre favori"
msgstr "Favorite filter name"
msgid "Ex. Mobilier urbain et voirie"
msgstr "Ex. Street furniture and roads"
msgid "Donnez un nom clair pour retrouver facilement cette sélection de filtres."
msgstr "Give a clear name to easily find this selection of filters."
msgid "Critères enregistrés :"
msgstr "Saved criteria:"

View file

@ -744,3 +744,84 @@ msgstr "Instructions & points of attention"
msgid "Centrer sur la zone" msgid "Centrer sur la zone"
msgstr "Center on zone" msgstr "Center on zone"
msgid "Favoris"
msgstr "Favorites"
msgid "Aucun favori enregistré"
msgstr "No favorite saved"
msgid "Mettre à jour avec les filtres actuels"
msgstr "Update with current filters"
msgid "Supprimer"
msgstr "Delete"
msgid "Filtre favori enregistré avec succès."
msgstr "Favorite filter saved successfully."
msgid "Erreur lors de la sauvegarde du favori."
msgstr "Error while saving favorite."
msgid "Erreur de communication avec le serveur."
msgstr "Server communication error."
msgid "Voulez-vous vraiment supprimer le favori '%s' ?"
msgstr "Are you sure you want to delete favorite '%s'?"
msgid "Filtre favori supprimé."
msgstr "Favorite filter deleted."
msgid "Erreur lors de la suppression du favori."
msgstr "Error while deleting favorite."
msgid "Mettre à jour le favori '%s' avec les filtres actuellement actifs ?"
msgstr "Update favorite '%s' with currently active filters?"
msgid "Favori '%s' mis à jour."
msgstr "Favorite '%s' updated."
msgid "Erreur lors de la mise à jour du favori."
msgstr "Error while updating favorite."
msgid "Filtre '%s' appliqué."
msgstr "Filter '%s' applied."
msgid "Aucun filtre sélectionné."
msgstr "No filter selected."
msgid "Thématique(s) :"
msgstr "Thematic(s):"
msgid "Type d'élément :"
msgstr "Element type:"
msgid "Assets"
msgstr "Assets"
msgid "Localisations"
msgstr "Locations"
msgid "Catégorie(s) :"
msgstr "Category(ies):"
msgid "Modèle(s) :"
msgstr "Model(s):"
msgid "Statut(s) :"
msgstr "Status(es):"
msgid "Validation"
msgstr "Validation"
msgid "Sans géométrie :"
msgstr "Without geometry:"
msgid "Exclus"
msgstr "Excluded"
msgid "Filtres par défaut."
msgstr "Default filters."
msgid "sélectionnée(s)"
msgstr "selected"

View file

@ -8817,3 +8817,30 @@ msgstr "Documents de zones de gestion"
msgid "Si coché, cet équipement est exclu de la zone même si sa géométrie intersecte le périmètre." msgid "Si coché, cet équipement est exclu de la zone même si sa géométrie intersecte le périmètre."
msgstr "Si coché, cet équipement est exclu de la zone même si sa géométrie intersecte le périmètre." msgstr "Si coché, cet équipement est exclu de la zone même si sa géométrie intersecte le périmètre."
msgid "Filtres favoris"
msgstr "Filtres favoris"
msgid "Favoris"
msgstr "Favoris"
msgid "Mes filtres favoris"
msgstr "Mes filtres favoris"
msgid "Enregistrer le filtre actuel..."
msgstr "Enregistrer le filtre actuel..."
msgid "Enregistrer comme favori"
msgstr "Enregistrer comme favori"
msgid "Nom du filtre favori"
msgstr "Nom du filtre favori"
msgid "Ex. Mobilier urbain et voirie"
msgstr "Ex. Mobilier urbain et voirie"
msgid "Donnez un nom clair pour retrouver facilement cette sélection de filtres."
msgstr "Donnez un nom clair pour retrouver facilement cette sélection de filtres."
msgid "Critères enregistrés :"
msgstr "Critères enregistrés :"

View file

@ -749,3 +749,84 @@ msgstr "Consignes & points d'attention"
msgid "Centrer sur la zone" msgid "Centrer sur la zone"
msgstr "Centrer sur la zone" msgstr "Centrer sur la zone"
msgid "Favoris"
msgstr "Favoris"
msgid "Aucun favori enregistré"
msgstr "Aucun favori enregistré"
msgid "Mettre à jour avec les filtres actuels"
msgstr "Mettre à jour avec les filtres actuels"
msgid "Supprimer"
msgstr "Supprimer"
msgid "Filtre favori enregistré avec succès."
msgstr "Filtre favori enregistré avec succès."
msgid "Erreur lors de la sauvegarde du favori."
msgstr "Erreur lors de la sauvegarde du favori."
msgid "Erreur de communication avec le serveur."
msgstr "Erreur de communication avec le serveur."
msgid "Voulez-vous vraiment supprimer le favori '%s' ?"
msgstr "Voulez-vous vraiment supprimer le favori '%s' ?"
msgid "Filtre favori supprimé."
msgstr "Filtre favori supprimé."
msgid "Erreur lors de la suppression du favori."
msgstr "Erreur lors de la suppression du favori."
msgid "Mettre à jour le favori '%s' avec les filtres actuellement actifs ?"
msgstr "Mettre à jour le favori '%s' avec les filtres actuellement actifs ?"
msgid "Favori '%s' mis à jour."
msgstr "Favori '%s' mis à jour."
msgid "Erreur lors de la mise à jour du favori."
msgstr "Erreur lors de la mise à jour du favori."
msgid "Filtre '%s' appliqué."
msgstr "Filtre '%s' appliqué."
msgid "Aucun filtre sélectionné."
msgstr "Aucun filtre sélectionné."
msgid "Thématique(s) :"
msgstr "Thématique(s) :"
msgid "Type d'élément :"
msgstr "Type d'élément :"
msgid "Assets"
msgstr "Assets"
msgid "Localisations"
msgstr "Localisations"
msgid "Catégorie(s) :"
msgstr "Catégorie(s) :"
msgid "Modèle(s) :"
msgstr "Modèle(s) :"
msgid "Statut(s) :"
msgstr "Statut(s) :"
msgid "Validation"
msgstr "Validation"
msgid "Sans géométrie :"
msgstr "Sans géométrie :"
msgid "Exclus"
msgstr "Exclus"
msgid "Filtres par défaut."
msgstr "Filtres par défaut."
msgid "sélectionnée(s)"
msgstr "sélectionnée(s)"

View file

@ -8765,3 +8765,30 @@ msgstr "Beheerszonedocumenten"
msgid "Si coché, cet équipement est exclu de la zone même si sa géométrie intersecte le périmètre." msgid "Si coché, cet équipement est exclu de la zone même si sa géométrie intersecte le périmètre."
msgstr "Indien aangevinkt, wordt dit asset uitgesloten van de zone, zelfs als de geometrie de perimeter doorsnijdt." msgstr "Indien aangevinkt, wordt dit asset uitgesloten van de zone, zelfs als de geometrie de perimeter doorsnijdt."
msgid "Filtres favoris"
msgstr "Favoriete filters"
msgid "Favoris"
msgstr "Favorieten"
msgid "Mes filtres favoris"
msgstr "Mijn favoriete filters"
msgid "Enregistrer le filtre actuel..."
msgstr "Huidige filter opslaan..."
msgid "Enregistrer comme favori"
msgstr "Opslaan als favoriet"
msgid "Nom du filtre favori"
msgstr "Naam van favoriet filter"
msgid "Ex. Mobilier urbain et voirie"
msgstr "Bijv. Straatmeubilair en wegen"
msgid "Donnez un nom clair pour retrouver facilement cette sélection de filtres."
msgstr "Geef een duidelijke naam om deze filterselectie gemakkelijk terug te vinden."
msgid "Critères enregistrés :"
msgstr "Opgeslagen criteria:"

View file

@ -746,3 +746,84 @@ msgstr "Instructies & aandachtspunten"
msgid "Centrer sur la zone" msgid "Centrer sur la zone"
msgstr "Centreren op zone" msgstr "Centreren op zone"
msgid "Favoris"
msgstr "Favorieten"
msgid "Aucun favori enregistré"
msgstr "Geen favoriet opgeslagen"
msgid "Mettre à jour avec les filtres actuels"
msgstr "Bijwerken met huidige filters"
msgid "Supprimer"
msgstr "Verwijderen"
msgid "Filtre favori enregistré avec succès."
msgstr "Favoriet filter succesvol opgeslagen."
msgid "Erreur lors de la sauvegarde du favori."
msgstr "Fout bij het opslaan van favoriet."
msgid "Erreur de communication avec le serveur."
msgstr "Communicatiefout met de server."
msgid "Voulez-vous vraiment supprimer le favori '%s' ?"
msgstr "Weet u zeker dat u favoriet '%s' wilt verwijderen?"
msgid "Filtre favori supprimé."
msgstr "Favoriet filter verwijderd."
msgid "Erreur lors de la suppression du favori."
msgstr "Fout bij het verwijderen van favoriet."
msgid "Mettre à jour le favori '%s' avec les filtres actuellement actifs ?"
msgstr "Favoriet '%s' bijwerken met momenteel actieve filters?"
msgid "Favori '%s' mis à jour."
msgstr "Favoriet '%s' bijgewerkt."
msgid "Erreur lors de la mise à jour du favori."
msgstr "Fout bij het bijwerken van favoriet."
msgid "Filtre '%s' appliqué."
msgstr "Filter '%s' toegepast."
msgid "Aucun filtre sélectionné."
msgstr "Geen filter geselecteerd."
msgid "Thématique(s) :"
msgstr "Thema('s):"
msgid "Type d'élément :"
msgstr "Elementtype:"
msgid "Assets"
msgstr "Assets"
msgid "Localisations"
msgstr "Locaties"
msgid "Catégorie(s) :"
msgstr "Categorie(ën):"
msgid "Modèle(s) :"
msgstr "Model(len):"
msgid "Statut(s) :"
msgstr "Status(sen):"
msgid "Validation"
msgstr "Validatie"
msgid "Sans géométrie :"
msgstr "Zonder geometrie:"
msgid "Exclus"
msgstr "Uitgesloten"
msgid "Filtres par défaut."
msgstr "Standaardfilters."
msgid "sélectionnée(s)"
msgstr "geselecteerd"

View file

@ -1346,6 +1346,7 @@ $(document).ready(function () {
refreshData(); refreshData();
updateFilterBadge(); updateFilterBadge();
schedulePreferencesSave(); schedulePreferencesSave();
checkActiveFavoriteMatch();
const modal = bootstrap.Modal.getInstance(document.getElementById('filterModal')); const modal = bootstrap.Modal.getInstance(document.getElementById('filterModal'));
if (modal) modal.hide(); if (modal) modal.hide();
}); });
@ -1373,6 +1374,7 @@ $(document).ready(function () {
refreshData(); refreshData();
updateFilterBadge(); updateFilterBadge();
schedulePreferencesSave(); schedulePreferencesSave();
checkActiveFavoriteMatch();
const modal = bootstrap.Modal.getInstance(document.getElementById('filterModal')); const modal = bootstrap.Modal.getInstance(document.getElementById('filterModal'));
if (modal) modal.hide(); if (modal) modal.hide();
}); });
@ -1438,6 +1440,7 @@ $(document).ready(function () {
renderModelFilters(); renderModelFilters();
renderConditionalFilters(); renderConditionalFilters();
updateFilterBadge(); updateFilterBadge();
initFavoriteFilters();
if (activeThematics.size > 0) { if (activeThematics.size > 0) {
refreshData(); refreshData();
@ -4615,3 +4618,457 @@ function initTotalsOverlay() {
} }
document.addEventListener('DOMContentLoaded', initTotalsOverlay); document.addEventListener('DOMContentLoaded', initTotalsOverlay);
// ==================== Favorite Filters Management ====================
function showToast(message, type = 'success') {
const toastEl = document.getElementById('app-toast');
const toastBody = document.getElementById('app-toast-body');
if (!toastEl || !toastBody) return;
toastEl.className = 'toast align-items-center text-white border-0';
toastEl.classList.add(`bg-${type}`);
toastBody.textContent = message;
const toast = new bootstrap.Toast(toastEl);
toast.show();
}
function initFavoriteFilters() {
renderFavoriteFiltersUI();
checkActiveFavoriteMatch();
// Open save modal button
const btnOpenSave = document.getElementById('btn-open-save-favorite-modal');
if (btnOpenSave) {
btnOpenSave.addEventListener('click', function(e) {
e.preventDefault();
openSaveFavoriteModal();
});
}
// Confirm save button
const btnConfirmSave = document.getElementById('btn-confirm-save-favorite');
if (btnConfirmSave) {
btnConfirmSave.addEventListener('click', function() {
confirmSaveFavorite();
});
}
// Submit on Enter in save favorite modal
const saveNameInput = document.getElementById('save-favorite-name');
if (saveNameInput) {
saveNameInput.addEventListener('keydown', function(e) {
if (e.key === 'Enter') {
e.preventDefault();
confirmSaveFavorite();
}
});
}
}
function renderFavoriteFiltersUI() {
const countBadge = document.getElementById('favorite-filters-count');
const itemsContainer = document.getElementById('favorite-filters-items');
if (countBadge) {
countBadge.textContent = (typeof userFavoriteFilters !== 'undefined' && Array.isArray(userFavoriteFilters)) ? userFavoriteFilters.length : 0;
}
if (itemsContainer) {
if (!userFavoriteFilters || !userFavoriteFilters.length) {
itemsContainer.innerHTML = `
<li class="px-3 py-2 text-muted small text-center fst-italic">
${gettext('Aucun favori enregistré')}
</li>
`;
} else {
let html = '';
userFavoriteFilters.forEach(fav => {
html += `
<li class="dropdown-item-favorite d-flex align-items-center justify-content-between px-3 py-1-5" id="fav-item-${fav.id}">
<div class="d-flex align-items-center gap-2 flex-grow-1 overflow-hidden" role="button" onclick="applyFavoriteFilter('${fav.id}')" style="cursor: pointer;" title="${fav.name}">
<i class="bi bi-star favorite-star-icon text-muted" id="fav-star-${fav.id}"></i>
<span class="text-truncate fav-name small fw-medium">${fav.name}</span>
</div>
<div class="favorite-actions d-flex align-items-center gap-1 ms-2">
<button type="button" class="btn btn-xs btn-outline-secondary py-0 px-1 border-0" title="${gettext('Mettre à jour avec les filtres actuels')}" onclick="updateFavoriteWithCurrentFilters('${fav.id}', event)">
<i class="bi bi-arrow-repeat"></i>
</button>
<button type="button" class="btn btn-xs btn-outline-danger py-0 px-1 border-0" title="${gettext('Supprimer')}" onclick="deleteFavoriteFilter('${fav.id}', event)">
<i class="bi bi-trash"></i>
</button>
</div>
</li>
`;
});
itemsContainer.innerHTML = html;
}
}
}
function getCurrentFilterValues() {
const statusFilters = Array.from(
document.querySelectorAll('[name="filter-status"]:checked')
).map(cb => cb.value).sort();
const validationStatusFilters = Array.from(
document.querySelectorAll('[name="filter-validation-status"]:checked')
).map(cb => cb.value).sort();
const includeNoGeomCheckbox = document.getElementById('include-no-geom');
const includeNoGeom = includeNoGeomCheckbox ? includeNoGeomCheckbox.checked : true;
return {
thematics: Array.from(activeThematics || []).sort(),
type_filter: getActiveTypeFilters().sort(),
include_no_geom: includeNoGeom,
status_filters: statusFilters,
validation_status_filters: validationStatusFilters,
categories: Array.from(activeCategories || []).sort(),
models: Array.from(activeModels || []).sort(),
conditional_filters: activeConditionalFilters || {}
};
}
function areFilterObjectsEqual(f1, f2) {
if (!f1 || !f2) return false;
const arrayKeys = ['thematics', 'type_filter', 'status_filters', 'validation_status_filters', 'categories', 'models'];
for (const key of arrayKeys) {
const arr1 = Array.isArray(f1[key]) ? f1[key].map(String).sort() : [];
const arr2 = Array.isArray(f2[key]) ? f2[key].map(String).sort() : [];
if (arr1.length !== arr2.length) return false;
for (let i = 0; i < arr1.length; i++) {
if (arr1[i] !== arr2[i]) return false;
}
}
const noGeom1 = f1.include_no_geom !== false;
const noGeom2 = f2.include_no_geom !== false;
if (noGeom1 !== noGeom2) return false;
const cf1 = f1.conditional_filters || {};
const cf2 = f2.conditional_filters || {};
if (JSON.stringify(cf1) !== JSON.stringify(cf2)) return false;
return true;
}
function checkActiveFavoriteMatch() {
const currentFilters = getCurrentFilterValues();
let matchedFav = null;
if (typeof userFavoriteFilters !== 'undefined' && Array.isArray(userFavoriteFilters)) {
for (const fav of userFavoriteFilters) {
if (areFilterObjectsEqual(fav.filters, currentFilters)) {
matchedFav = fav;
break;
}
}
}
const activeLabel = document.getElementById('active-favorite-label');
// Reset all star icons and active classes
document.querySelectorAll('.dropdown-item-favorite').forEach(el => el.classList.remove('active'));
document.querySelectorAll('.favorite-star-icon').forEach(icon => {
icon.className = 'bi bi-star favorite-star-icon text-muted';
});
if (matchedFav) {
if (activeLabel) {
activeLabel.textContent = matchedFav.name;
}
const activeItem = document.getElementById(`fav-item-${matchedFav.id}`);
if (activeItem) {
activeItem.classList.add('active');
}
const activeStar = document.getElementById(`fav-star-${matchedFav.id}`);
if (activeStar) {
activeStar.className = 'bi bi-star-fill favorite-star-icon text-warning';
}
} else {
if (activeLabel) {
activeLabel.textContent = gettext('Favoris');
}
}
}
function generateFilterSummary(filters) {
if (!filters || Object.keys(filters).length === 0) {
return `<span class="text-muted fst-italic">${gettext('Aucun filtre sélectionné.')}</span>`;
}
const badges = [];
if (filters.thematics && filters.thematics.length) {
const names = filters.thematics.map(code => {
return (THEMATIC_CONFIG && THEMATIC_CONFIG[code] && THEMATIC_CONFIG[code].name) || code;
});
badges.push(`<strong>${gettext('Thématique(s)')} :</strong> ${names.join(', ')}`);
}
if (filters.type_filter && filters.type_filter.length) {
const typeLabels = {
'assets': gettext('Assets'),
'locations': gettext('Localisations'),
'management_zones': gettext('Zones de gestion')
};
const names = filters.type_filter.map(t => typeLabels[t] || t);
badges.push(`<strong>${gettext("Type d'élément")} :</strong> ${names.join(', ')}`);
}
if (filters.categories && filters.categories.length) {
badges.push(`<strong>${gettext('Catégorie(s)')} :</strong> ${filters.categories.length} ${gettext('sélectionnée(s)')}`);
}
if (filters.models && filters.models.length) {
badges.push(`<strong>${gettext('Modèle(s)')} :</strong> ${filters.models.length} ${gettext('sélectionné(s)')}`);
}
if (filters.status_filters && filters.status_filters.length) {
badges.push(`<strong>${gettext('Statut(s)')} :</strong> ${filters.status_filters.join(', ')}`);
}
if (filters.validation_status_filters && filters.validation_status_filters.length) {
badges.push(`<strong>${gettext('Validation')} :</strong> ${filters.validation_status_filters.join(', ')}`);
}
if (filters.include_no_geom === false) {
badges.push(`<strong>${gettext('Sans géométrie')} :</strong> ${gettext('Exclus')}`);
}
if (badges.length === 0) {
return `<span class="text-muted fst-italic">${gettext('Filtres par défaut.')}</span>`;
}
return badges.map(b => `<div class="mb-1">${b}</div>`).join('');
}
function openSaveFavoriteModal(existingId = null) {
const currentFilters = getCurrentFilterValues();
const modalEl = document.getElementById('saveFavoriteModal');
if (!modalEl) return;
const idInput = document.getElementById('save-favorite-id');
const nameInput = document.getElementById('save-favorite-name');
const summaryEl = document.getElementById('save-favorite-summary');
if (existingId) {
const existing = userFavoriteFilters.find(f => f.id === existingId);
if (existing) {
idInput.value = existing.id;
nameInput.value = existing.name;
}
} else {
idInput.value = '';
nameInput.value = '';
}
if (summaryEl) {
summaryEl.innerHTML = generateFilterSummary(currentFilters);
}
nameInput.classList.remove('is-invalid');
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
modal.show();
setTimeout(() => {
nameInput.focus();
}, 400);
}
async function confirmSaveFavorite() {
const nameInput = document.getElementById('save-favorite-name');
const idInput = document.getElementById('save-favorite-id');
const name = nameInput.value.trim();
if (!name) {
nameInput.classList.add('is-invalid');
return;
}
nameInput.classList.remove('is-invalid');
const favoriteId = idInput.value || null;
const filters = getCurrentFilterValues();
try {
const response = await fetch(saveFavoriteUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify({
id: favoriteId,
name: name,
filters: filters
})
});
const data = await response.json();
if (data.success) {
userFavoriteFilters = data.favorites;
renderFavoriteFiltersUI();
checkActiveFavoriteMatch();
const modalEl = document.getElementById('saveFavoriteModal');
const modal = bootstrap.Modal.getInstance(modalEl);
if (modal) modal.hide();
showToast(gettext('Filtre favori enregistré avec succès.'), 'success');
} else {
alert(data.error || gettext('Erreur lors de la sauvegarde du favori.'));
}
} catch (err) {
alert(gettext('Erreur de communication avec le serveur.'));
}
}
async function deleteFavoriteFilter(favoriteId, event) {
if (event) event.stopPropagation();
const fav = userFavoriteFilters.find(f => f.id === favoriteId);
if (!fav) return;
const confirmMsg = gettext("Voulez-vous vraiment supprimer le favori '%s' ?").replace('%s', fav.name);
if (!confirm(confirmMsg)) return;
try {
const response = await fetch(deleteFavoriteUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify({ id: favoriteId })
});
const data = await response.json();
if (data.success) {
userFavoriteFilters = data.favorites;
renderFavoriteFiltersUI();
checkActiveFavoriteMatch();
showToast(gettext('Filtre favori supprimé.'), 'info');
} else {
alert(data.error || gettext('Erreur lors de la suppression du favori.'));
}
} catch (err) {
alert(gettext('Erreur de communication avec le serveur.'));
}
}
async function updateFavoriteWithCurrentFilters(favoriteId, event) {
if (event) event.stopPropagation();
const fav = userFavoriteFilters.find(f => f.id === favoriteId);
if (!fav) return;
const confirmMsg = gettext("Mettre à jour le favori '%s' avec les filtres actuellement actifs ?").replace('%s', fav.name);
if (!confirm(confirmMsg)) return;
const currentFilters = getCurrentFilterValues();
try {
const response = await fetch(saveFavoriteUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify({
id: favoriteId,
name: fav.name,
filters: currentFilters
})
});
const data = await response.json();
if (data.success) {
userFavoriteFilters = data.favorites;
renderFavoriteFiltersUI();
checkActiveFavoriteMatch();
showToast(gettext("Favori '%s' mis à jour.").replace('%s', fav.name), 'success');
} else {
alert(data.error || gettext('Erreur lors de la mise à jour du favori.'));
}
} catch (err) {
alert(gettext('Erreur de communication avec le serveur.'));
}
}
function applyFavoriteFilter(favoriteId) {
if (!favoriteId) return;
const fav = userFavoriteFilters.find(f => f.id === favoriteId);
if (!fav) return;
const filters = fav.filters || {};
// 1. Thematics
activeThematics.clear();
const favThematics = Array.isArray(filters.thematics) ? filters.thematics : [];
favThematics.forEach(t => {
if (t in THEMATIC_CONFIG) activeThematics.add(t);
});
document.querySelectorAll('.thematic-checkbox').forEach(cb => {
cb.checked = activeThematics.has(cb.value);
});
// 2. Types
const cbA = document.getElementById('type-cb-assets');
const cbL = document.getElementById('type-cb-locations');
const cbZ = document.getElementById('type-cb-management-zones');
const favTypes = Array.isArray(filters.type_filter) ? filters.type_filter : ['assets', 'locations', 'management_zones'];
if (cbA) cbA.checked = favTypes.includes('assets');
if (cbL) cbL.checked = favTypes.includes('locations');
if (cbZ) cbZ.checked = favTypes.includes('management_zones');
// 3. Include no geom
const includeNoGeomCheckbox = document.getElementById('include-no-geom');
if (includeNoGeomCheckbox) {
includeNoGeomCheckbox.checked = filters.include_no_geom !== false;
}
// 4. Status
const favStatus = Array.isArray(filters.status_filters) ? filters.status_filters : [];
document.querySelectorAll('[name="filter-status"]').forEach(cb => {
cb.checked = favStatus.includes(cb.value);
});
// 5. Validation status
const favValidation = Array.isArray(filters.validation_status_filters) ? filters.validation_status_filters : [];
document.querySelectorAll('[name="filter-validation-status"]').forEach(cb => {
cb.checked = favValidation.includes(cb.value);
});
// 6. Categories
activeCategories = new Set(Array.isArray(filters.categories) ? filters.categories : []);
// 7. Models
activeModels = new Set(Array.isArray(filters.models) ? filters.models : []);
// 8. Conditional filters
activeConditionalFilters = (filters.conditional_filters && typeof filters.conditional_filters === 'object')
? JSON.parse(JSON.stringify(filters.conditional_filters))
: {};
// 9. Re-render filters UI
renderCategoryFilters();
renderModelFilters();
renderConditionalFilters();
updateFilterBadge();
// 10. Save current active preferences
savePreferences();
// 11. Reload data
if (activeThematics.size > 0) {
refreshData();
} else {
showEmptyState();
}
// 12. Update active favorite display
checkActiveFavoriteMatch();
showToast(gettext("Filtre '%s' appliqué.").replace('%s', fav.name), 'info');
}
// Make functions globally available for inline onclicks
window.applyFavoriteFilter = applyFavoriteFilter;
window.deleteFavoriteFilter = deleteFavoriteFilter;
window.updateFavoriteWithCurrentFilters = updateFavoriteWithCurrentFilters;
window.openSaveFavoriteModal = openSaveFavoriteModal;

View file

@ -398,12 +398,36 @@
height: 1.25rem; height: 1.25rem;
padding: 0 4px !important; padding: 0 4px !important;
line-height: 1.25rem; line-height: 1.25rem;
z-index: 5 !important;
} }
#filter-badge:not(.d-none) { #filter-badge:not(.d-none) {
display: inline-flex !important; display: inline-flex !important;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.dropdown-item-favorite {
transition: background-color 0.15s ease;
border-radius: 4px;
margin: 2px 4px;
}
.dropdown-item-favorite:hover {
background-color: #f1f5f9;
}
.dropdown-item-favorite.active {
background-color: #e7f1ff;
}
.dropdown-item-favorite.active .fav-name {
color: #0c5cc9;
font-weight: 600 !important;
}
.favorite-actions button {
opacity: 0.6;
transition: opacity 0.15s;
}
.dropdown-item-favorite:hover .favorite-actions button {
opacity: 1;
}
#map .maplibregl-popup-close-button { #map .maplibregl-popup-close-button {
font-size: 1.35rem; font-size: 1.35rem;
line-height: 1; line-height: 1;
@ -949,6 +973,42 @@
</div> </div>
</div> </div>
{# ─── Modal pour enregistrer un filtre favori ─────────────────────────────── #}
<div class="modal fade" id="saveFavoriteModal" tabindex="-1" aria-labelledby="saveFavoriteModalLabel" aria-hidden="true" style="z-index: 1070;">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="saveFavoriteModalLabel">
<i class="bi bi-bookmark-star text-warning me-2"></i>{% translate "Enregistrer comme favori" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% translate 'Fermer' %}"></button>
</div>
<div class="modal-body">
<form id="save-favorite-form" onsubmit="return false;">
<input type="hidden" id="save-favorite-id" value="">
<div class="mb-3">
<label for="save-favorite-name" class="form-label fw-semibold">{% translate "Nom du filtre favori" %}</label>
<input type="text" class="form-control" id="save-favorite-name" placeholder="{% translate 'Ex. Mobilier urbain et voirie' %}" required maxlength="100">
<div class="form-text">{% translate "Donnez un nom clair pour retrouver facilement cette sélection de filtres." %}</div>
</div>
<div class="mb-2">
<label class="form-label small fw-semibold text-muted mb-1">{% translate "Critères enregistrés :" %}</label>
<div id="save-favorite-summary" class="small text-muted p-2 rounded bg-light border" style="max-height: 120px; overflow-y: auto;">
<!-- Résumé généré dynamiquement -->
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% translate "Annuler" %}</button>
<button type="button" class="btn btn-primary" id="btn-confirm-save-favorite">
<i class="bi bi-check-lg me-1"></i>{% translate "Enregistrer" %}
</button>
</div>
</div>
</div>
</div>
{# ─── Add chooser modal ────────────────────────────────────────────────────── #} {# ─── Add chooser modal ────────────────────────────────────────────────────── #}
<div class="modal fade" id="unifiedAddChooserModal" tabindex="-1" aria-labelledby="unifiedAddChooserModalLabel" aria-hidden="true"> <div class="modal fade" id="unifiedAddChooserModal" tabindex="-1" aria-labelledby="unifiedAddChooserModalLabel" aria-hidden="true">
@ -1202,7 +1262,7 @@
<div class="assets-toolbar d-flex justify-content-between align-items-center flex-wrap gap-2 mb-2"> <div class="assets-toolbar d-flex justify-content-between align-items-center flex-wrap gap-2 mb-2">
<div class="d-flex align-items-center flex-wrap gap-2"> <div class="d-flex align-items-center flex-wrap gap-2">
<!-- Bouton Filtres avec badge --> <!-- Bouton Filtres avec badge -->
<button class="btn btn-light position-relative filter-trigger-btn" type="button" <button class="btn btn-light position-relative filter-trigger-btn me-1" type="button"
data-bs-toggle="modal" data-bs-target="#filterModal" data-bs-toggle="modal" data-bs-target="#filterModal"
title="{% translate 'Filtres' %}"> title="{% translate 'Filtres' %}">
<i class="bi bi-funnel"></i> {% translate "Filtres" %} <i class="bi bi-funnel"></i> {% translate "Filtres" %}
@ -1211,6 +1271,30 @@
</span> </span>
</button> </button>
<!-- Dropdown Favoris -->
<div class="dropdown d-inline-block" id="favorite-filters-dropdown-container">
<button class="btn btn-light dropdown-toggle d-flex align-items-center gap-1" type="button" id="favoriteFiltersDropdown" data-bs-toggle="dropdown" aria-expanded="false" title="{% translate 'Filtres favoris' %}">
<i class="bi bi-bookmark-star text-warning"></i>
<span id="active-favorite-label" class="d-none d-md-inline text-truncate" style="max-width: 180px;">{% translate "Favoris" %}</span>
</button>
<ul class="dropdown-menu shadow-sm" id="favorite-filters-menu" aria-labelledby="favoriteFiltersDropdown" style="min-width: 280px; max-height: 400px; overflow-y: auto;">
<li class="dropdown-header small text-uppercase fw-bold text-muted d-flex justify-content-between align-items-center py-1">
<span>{% translate "Mes filtres favoris" %}</span>
<span class="badge bg-secondary" id="favorite-filters-count">0</span>
</li>
<div id="favorite-filters-items">
<!-- Peuplé dynamiquement par JS -->
</div>
<li><hr class="dropdown-divider my-1"></li>
<li>
<button type="button" class="dropdown-item d-flex align-items-center gap-2 text-primary py-2" id="btn-open-save-favorite-modal">
<i class="bi bi-plus-circle"></i>
<span>{% translate "Enregistrer le filtre actuel..." %}</span>
</button>
</li>
</ul>
</div>
<!-- Dropdown Sélecteur de Colonnes --> <!-- Dropdown Sélecteur de Colonnes -->
<div class="dropdown d-inline-block"> <div class="dropdown d-inline-block">
<button type="button" class="btn btn-light dropdown-toggle" id="columns-dropdown-btn" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false" title="{% translate 'Choisir les colonnes à afficher' %}"> <button type="button" class="btn btn-light dropdown-toggle" id="columns-dropdown-btn" data-bs-toggle="dropdown" data-bs-auto-close="outside" aria-expanded="false" title="{% translate 'Choisir les colonnes à afficher' %}">
@ -1463,6 +1547,18 @@
</form> </form>
{% endif %} {% endif %}
{# Toast container #}
<div class="toast-container position-fixed bottom-0 end-0 p-3" style="z-index: 1090;">
<div id="app-toast" class="toast align-items-center text-white bg-success border-0" role="alert" aria-live="polite" aria-atomic="true">
<div class="d-flex">
<div id="app-toast-body" class="toast-body">
<!-- Contenu dynamique -->
</div>
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="{% translate 'Fermer' %}"></button>
</div>
</div>
</div>
{% endblock content %} {% endblock content %}
{% block script %} {% block script %}
@ -1470,6 +1566,9 @@
<script src="{% static 'common/layer-switcher-control.js' %}?v=20261009_basemap"></script> <script src="{% static 'common/layer-switcher-control.js' %}?v=20261009_basemap"></script>
<script> <script>
window.TERRITORY_MASK_URL = "{% url 'common:get_territory_mask_geojson' %}"; window.TERRITORY_MASK_URL = "{% url 'common:get_territory_mask_geojson' %}";
const saveFavoriteUrl = "{% url 'common:save_favorite_filter' module='assets' %}";
const deleteFavoriteUrl = "{% url 'common:delete_favorite_filter' module='assets' %}";
let userFavoriteFilters = {{ favorite_filters_json|default:"[]"|safe }};
const preventive_draft_id = "{{ preventive_draft_id|default:'' }}"; const preventive_draft_id = "{{ preventive_draft_id|default:'' }}";
var selected_locations_set = new Set({{ selected_locations_ids|safe }}); var selected_locations_set = new Set({{ selected_locations_ids|safe }});
const initial_selected_assets = {{ selected_assets_json|safe }}; const initial_selected_assets = {{ selected_assets_json|safe }};

View file

@ -1259,5 +1259,8 @@ def unified_assets_index(request):
'link_assets_url': link_assets_url, 'link_assets_url': link_assets_url,
'assets_index_prefs_json': json.dumps(user_config.assets_index_preferences or {}), 'assets_index_prefs_json': json.dumps(user_config.assets_index_preferences or {}),
'default_thematic_code': (user_config.default_thematic.code if user_config.default_thematic_id else ''), 'default_thematic_code': (user_config.default_thematic.code if user_config.default_thematic_id else ''),
'favorite_filters_json': json.dumps(
(user_config.favorite_filters or {}).get('assets', []) if user_config else []
),
} }
return render(request, 'assets/unified_index.html', context) return render(request, 'assets/unified_index.html', context)