feat: add favorite filters feature with UI, modals, and localizations for unified index view
This commit is contained in:
parent
91b2ca79ad
commit
1e80d14a89
15 changed files with 884 additions and 1 deletions
Binary file not shown.
|
|
@ -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."
|
||||
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:"
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -744,3 +744,84 @@ msgstr "Instructions & points of attention"
|
|||
|
||||
msgid "Centrer sur la 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"
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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."
|
||||
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 :"
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -749,3 +749,84 @@ msgstr "Consignes & points d'attention"
|
|||
|
||||
msgid "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)"
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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."
|
||||
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:"
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -746,3 +746,84 @@ msgstr "Instructies & aandachtspunten"
|
|||
|
||||
msgid "Centrer sur la 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"
|
||||
|
|
|
|||
|
|
@ -1346,6 +1346,7 @@ $(document).ready(function () {
|
|||
refreshData();
|
||||
updateFilterBadge();
|
||||
schedulePreferencesSave();
|
||||
checkActiveFavoriteMatch();
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('filterModal'));
|
||||
if (modal) modal.hide();
|
||||
});
|
||||
|
|
@ -1373,6 +1374,7 @@ $(document).ready(function () {
|
|||
refreshData();
|
||||
updateFilterBadge();
|
||||
schedulePreferencesSave();
|
||||
checkActiveFavoriteMatch();
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('filterModal'));
|
||||
if (modal) modal.hide();
|
||||
});
|
||||
|
|
@ -1438,6 +1440,7 @@ $(document).ready(function () {
|
|||
renderModelFilters();
|
||||
renderConditionalFilters();
|
||||
updateFilterBadge();
|
||||
initFavoriteFilters();
|
||||
|
||||
if (activeThematics.size > 0) {
|
||||
refreshData();
|
||||
|
|
@ -4615,3 +4618,457 @@ function 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;
|
||||
|
|
|
|||
|
|
@ -398,12 +398,36 @@
|
|||
height: 1.25rem;
|
||||
padding: 0 4px !important;
|
||||
line-height: 1.25rem;
|
||||
z-index: 5 !important;
|
||||
}
|
||||
#filter-badge:not(.d-none) {
|
||||
display: inline-flex !important;
|
||||
align-items: 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 {
|
||||
font-size: 1.35rem;
|
||||
line-height: 1;
|
||||
|
|
@ -949,6 +973,42 @@
|
|||
</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 ────────────────────────────────────────────────────── #}
|
||||
<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="d-flex align-items-center flex-wrap gap-2">
|
||||
<!-- 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"
|
||||
title="{% translate 'Filtres' %}">
|
||||
<i class="bi bi-funnel"></i> {% translate "Filtres" %}
|
||||
|
|
@ -1211,6 +1271,30 @@
|
|||
</span>
|
||||
</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 -->
|
||||
<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' %}">
|
||||
|
|
@ -1463,6 +1547,18 @@
|
|||
</form>
|
||||
{% 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 %}
|
||||
|
||||
{% block script %}
|
||||
|
|
@ -1470,6 +1566,9 @@
|
|||
<script src="{% static 'common/layer-switcher-control.js' %}?v=20261009_basemap"></script>
|
||||
<script>
|
||||
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:'' }}";
|
||||
var selected_locations_set = new Set({{ selected_locations_ids|safe }});
|
||||
const initial_selected_assets = {{ selected_assets_json|safe }};
|
||||
|
|
|
|||
|
|
@ -1259,5 +1259,8 @@ def unified_assets_index(request):
|
|||
'link_assets_url': link_assets_url,
|
||||
'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 ''),
|
||||
'favorite_filters_json': json.dumps(
|
||||
(user_config.favorite_filters or {}).get('assets', []) if user_config else []
|
||||
),
|
||||
}
|
||||
return render(request, 'assets/unified_index.html', context)
|
||||
|
|
|
|||
Loading…
Reference in a new issue