feat: add favorite filters feature for user configurations and interventions module
This commit is contained in:
parent
e4d182596b
commit
3d76855d0e
27 changed files with 1277 additions and 6 deletions
Binary file not shown.
|
|
@ -560,3 +560,19 @@ msgstr "Reset"
|
|||
msgid "Format de fond de plan invalide"
|
||||
msgstr "Invalid basemap format"
|
||||
|
||||
#: common/views.py:863
|
||||
msgid "Module invalide"
|
||||
msgstr "Invalid module"
|
||||
|
||||
#: common/views.py:874
|
||||
msgid "Le nom du filtre est obligatoire"
|
||||
msgstr "Filter name is required"
|
||||
|
||||
#: common/views.py:937
|
||||
msgid "Identifiant de filtre manquant"
|
||||
msgstr "Missing filter ID"
|
||||
|
||||
#: common/models.py:382
|
||||
msgid "Filtres favoris"
|
||||
msgstr "Favorite filters"
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -546,3 +546,19 @@ msgstr "Réinitialiser"
|
|||
msgid "Format de fond de plan invalide"
|
||||
msgstr "Format de fond de plan invalide"
|
||||
|
||||
#: common/views.py:863
|
||||
msgid "Module invalide"
|
||||
msgstr "Module invalide"
|
||||
|
||||
#: common/views.py:874
|
||||
msgid "Le nom du filtre est obligatoire"
|
||||
msgstr "Le nom du filtre est obligatoire"
|
||||
|
||||
#: common/views.py:937
|
||||
msgid "Identifiant de filtre manquant"
|
||||
msgstr "Identifiant de filtre manquant"
|
||||
|
||||
#: common/models.py:382
|
||||
msgid "Filtres favoris"
|
||||
msgstr "Filtres favoris"
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -546,3 +546,19 @@ msgstr "Herstellen"
|
|||
msgid "Format de fond de plan invalide"
|
||||
msgstr "Ongeldig basiskaartformaat"
|
||||
|
||||
#: common/views.py:863
|
||||
msgid "Module invalide"
|
||||
msgstr "Ongeldige module"
|
||||
|
||||
#: common/views.py:874
|
||||
msgid "Le nom du filtre est obligatoire"
|
||||
msgstr "Filternaam is verplicht"
|
||||
|
||||
#: common/views.py:937
|
||||
msgid "Identifiant de filtre manquant"
|
||||
msgstr "Filter-ID ontbreekt"
|
||||
|
||||
#: common/models.py:382
|
||||
msgid "Filtres favoris"
|
||||
msgstr "Favoriete filters"
|
||||
|
||||
|
|
|
|||
18
loko/common/migrations/0021_userconfig_favorite_filters.py
Normal file
18
loko/common/migrations/0021_userconfig_favorite_filters.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 6.0.7 on 2026-10-12 12:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('common', '0020_userconfig_can_view_all_posts_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userconfig',
|
||||
name='favorite_filters',
|
||||
field=models.JSONField(blank=True, default=dict, help_text='Filtres favoris enregistrés par module (interventions, observations, assets, etc.).', verbose_name='Filtres favoris'),
|
||||
),
|
||||
]
|
||||
|
|
@ -377,6 +377,12 @@ class UserConfig(models.Model):
|
|||
verbose_name="Préférences du calendrier",
|
||||
help_text="Préférences de la page calendrier (mode d'affichage, zoom, filtres actifs, état d'expansion du backlog).",
|
||||
)
|
||||
favorite_filters = models.JSONField(
|
||||
default=dict,
|
||||
blank=True,
|
||||
verbose_name="Filtres favoris",
|
||||
help_text="Filtres favoris enregistrés par module (interventions, observations, assets, etc.).",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"Configuration de {self.user.username}"
|
||||
|
|
|
|||
|
|
@ -665,6 +665,81 @@ class HelpDocumentTests(TestCase):
|
|||
self.assertNotIn("StreetUp", rendered_nl)
|
||||
|
||||
|
||||
class FavoriteFiltersAPITests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(username="fav_tester", password="password123")
|
||||
self.config = UserConfig.objects.create(user=self.user)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_save_and_update_favorite_filter(self):
|
||||
# 1. Create a new favorite filter
|
||||
url = reverse("common:save_favorite_filter", kwargs={"module": "interventions"})
|
||||
payload = {
|
||||
"name": "Urgences Voirie",
|
||||
"filters": {
|
||||
"thematic": ["road"],
|
||||
"priority": "high",
|
||||
"status": ["in_progress"],
|
||||
},
|
||||
}
|
||||
response = self.client.post(url, json.dumps(payload), content_type="application/json")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
data = response.json()
|
||||
self.assertTrue(data["success"])
|
||||
fav = data["favorite"]
|
||||
self.assertEqual(fav["name"], "Urgences Voirie")
|
||||
self.assertIn("id", fav)
|
||||
self.assertEqual(len(data["favorites"]), 1)
|
||||
|
||||
# Verify persistence in UserConfig
|
||||
self.config.refresh_from_db()
|
||||
interventions_favs = self.config.favorite_filters.get("interventions", [])
|
||||
self.assertEqual(len(interventions_favs), 1)
|
||||
self.assertEqual(interventions_favs[0]["name"], "Urgences Voirie")
|
||||
|
||||
# 2. Update the existing favorite filter
|
||||
fav_id = fav["id"]
|
||||
update_payload = {
|
||||
"id": fav_id,
|
||||
"name": "Urgences Voirie Modifiées",
|
||||
"filters": {
|
||||
"thematic": ["road"],
|
||||
"priority": "high",
|
||||
"status": ["in_progress", "assigned"],
|
||||
},
|
||||
}
|
||||
update_response = self.client.post(url, json.dumps(update_payload), content_type="application/json")
|
||||
self.assertEqual(update_response.status_code, 200)
|
||||
update_data = update_response.json()
|
||||
self.assertEqual(update_data["favorite"]["name"], "Urgences Voirie Modifiées")
|
||||
self.assertEqual(len(update_data["favorites"]), 1)
|
||||
|
||||
# 3. Create a second favorite
|
||||
second_payload = {
|
||||
"name": "Espaces verts",
|
||||
"filters": {"thematic": ["green"]},
|
||||
}
|
||||
self.client.post(url, json.dumps(second_payload), content_type="application/json")
|
||||
self.config.refresh_from_db()
|
||||
self.assertEqual(len(self.config.favorite_filters["interventions"]), 2)
|
||||
|
||||
# 4. Delete the first favorite
|
||||
delete_url = reverse("common:delete_favorite_filter", kwargs={"module": "interventions"})
|
||||
delete_response = self.client.post(delete_url, json.dumps({"id": fav_id}), content_type="application/json")
|
||||
self.assertEqual(delete_response.status_code, 200)
|
||||
delete_data = delete_response.json()
|
||||
self.assertEqual(len(delete_data["favorites"]), 1)
|
||||
self.assertEqual(delete_data["favorites"][0]["name"], "Espaces verts")
|
||||
|
||||
self.config.refresh_from_db()
|
||||
self.assertEqual(len(self.config.favorite_filters["interventions"]), 1)
|
||||
|
||||
def test_invalid_module_rejected(self):
|
||||
url = reverse("common:save_favorite_filter", kwargs={"module": "unknown_module"})
|
||||
response = self.client.post(url, json.dumps({"name": "Test", "filters": {}}), content_type="application/json")
|
||||
self.assertEqual(response.status_code, 400)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -20,5 +20,7 @@ urlpatterns = [
|
|||
path("save-assets-index-preferences/", views.save_assets_index_preferences, name="save_assets_index_preferences"),
|
||||
path("save-calendar-preferences/", views.save_calendar_preferences, name="save_calendar_preferences"),
|
||||
path("save-project-list-preferences/", views.save_project_list_preferences, name="save_project_list_preferences"),
|
||||
path("favorite-filters/<str:module>/save/", views.save_favorite_filter, name="save_favorite_filter"),
|
||||
path("favorite-filters/<str:module>/delete/", views.delete_favorite_filter, name="delete_favorite_filter"),
|
||||
path("help/<str:doc_name>/", views.get_help_document, name="get_help_document"),
|
||||
]
|
||||
|
|
@ -167,6 +167,7 @@ def copy_user_config(source_user_or_config: Any, target_user_or_config: Any, cop
|
|||
target_config.calendar_preferences = (
|
||||
dict(source_config.calendar_preferences) if source_config.calendar_preferences is not None else None
|
||||
)
|
||||
target_config.favorite_filters = dict(source_config.favorite_filters or {})
|
||||
|
||||
# Permissions spécifiques (eSign, documents, panoramax, transport exceptionnel, contrats)
|
||||
target_config.can_add_unique_tag_documents = source_config.can_add_unique_tag_documents
|
||||
|
|
|
|||
|
|
@ -852,5 +852,119 @@ def set_language(request):
|
|||
return response
|
||||
|
||||
|
||||
ALLOWED_FAVORITE_MODULES = [
|
||||
'interventions',
|
||||
'observations',
|
||||
'assets',
|
||||
'controls',
|
||||
'inspections',
|
||||
'projects',
|
||||
]
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def save_favorite_filter(request, module):
|
||||
"""API endpoint to save or update a favorite filter for a specific module."""
|
||||
import json
|
||||
import uuid
|
||||
from django.utils import timezone
|
||||
|
||||
if module not in ALLOWED_FAVORITE_MODULES:
|
||||
return JsonResponse({'error': _('Module invalide')}, status=400)
|
||||
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
name = (data.get('name') or '').strip()
|
||||
filters = data.get('filters', {})
|
||||
favorite_id = data.get('id')
|
||||
except (json.JSONDecodeError, AttributeError):
|
||||
return JsonResponse({'error': _('Données invalides')}, status=400)
|
||||
|
||||
if not name:
|
||||
return JsonResponse({'error': _('Le nom du filtre est obligatoire')}, status=400)
|
||||
|
||||
if not isinstance(filters, dict):
|
||||
return JsonResponse({'error': _('Format de filtres invalide')}, status=400)
|
||||
|
||||
try:
|
||||
user_config, _created = UserConfig.objects.get_or_create(user=request.user)
|
||||
all_favorites = dict(user_config.favorite_filters or {})
|
||||
module_favorites = list(all_favorites.get(module, []))
|
||||
|
||||
now_str = timezone.now().isoformat()
|
||||
saved_item = None
|
||||
|
||||
if favorite_id:
|
||||
# Update existing
|
||||
for item in module_favorites:
|
||||
if item.get('id') == favorite_id:
|
||||
item['name'] = name
|
||||
item['filters'] = filters
|
||||
item['updated_at'] = now_str
|
||||
saved_item = item
|
||||
break
|
||||
|
||||
if not saved_item:
|
||||
# Create new
|
||||
new_id = f"fav_{uuid.uuid4().hex[:12]}"
|
||||
saved_item = {
|
||||
'id': new_id,
|
||||
'name': name,
|
||||
'filters': filters,
|
||||
'created_at': now_str,
|
||||
}
|
||||
module_favorites.append(saved_item)
|
||||
|
||||
all_favorites[module] = module_favorites
|
||||
user_config.favorite_filters = all_favorites
|
||||
user_config.save(update_fields=['favorite_filters'])
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'favorite': saved_item,
|
||||
'favorites': module_favorites,
|
||||
})
|
||||
except Exception as e:
|
||||
return JsonResponse({'error': str(e)}, status=500)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_POST
|
||||
def delete_favorite_filter(request, module):
|
||||
"""API endpoint to delete a favorite filter for a specific module."""
|
||||
import json
|
||||
|
||||
if module not in ALLOWED_FAVORITE_MODULES:
|
||||
return JsonResponse({'error': _('Module invalide')}, status=400)
|
||||
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
favorite_id = data.get('id')
|
||||
except (json.JSONDecodeError, AttributeError):
|
||||
return JsonResponse({'error': _('Données invalides')}, status=400)
|
||||
|
||||
if not favorite_id:
|
||||
return JsonResponse({'error': _('Identifiant de filtre manquant')}, status=400)
|
||||
|
||||
try:
|
||||
user_config, _created = UserConfig.objects.get_or_create(user=request.user)
|
||||
all_favorites = dict(user_config.favorite_filters or {})
|
||||
module_favorites = list(all_favorites.get(module, []))
|
||||
|
||||
updated_favorites = [item for item in module_favorites if item.get('id') != favorite_id]
|
||||
|
||||
all_favorites[module] = updated_favorites
|
||||
user_config.favorite_filters = all_favorites
|
||||
user_config.save(update_fields=['favorite_filters'])
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'favorites': updated_favorites,
|
||||
})
|
||||
except Exception as e:
|
||||
return JsonResponse({'error': str(e)}, status=500)
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -8431,5 +8431,32 @@ msgstr "-- Select a role… --"
|
|||
msgid "-- Choisir une personne… --"
|
||||
msgstr "-- Choose a person… --"
|
||||
|
||||
msgid "— Sélectionner un favori —"
|
||||
msgstr "— Select a favorite —"
|
||||
|
||||
msgid "Enregistrer en favori"
|
||||
msgstr "Save as favorite"
|
||||
|
||||
msgid "Enregistrer comme favori"
|
||||
msgstr "Save as favorite"
|
||||
|
||||
msgid "Nom du filtre favori"
|
||||
msgstr "Favorite filter name"
|
||||
|
||||
msgid "Ex. Mes urgences voirie"
|
||||
msgstr "Ex. My road emergencies"
|
||||
|
||||
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:"
|
||||
|
||||
msgid "Mes filtres favoris"
|
||||
msgstr "My favorite filters"
|
||||
|
||||
msgid "Enregistrer le filtre actuel..."
|
||||
msgstr "Save current filter..."
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1207,6 +1207,97 @@ msgstr "Error while saving"
|
|||
msgid "Erreur lors de la mise à jour"
|
||||
msgstr "Error during update"
|
||||
|
||||
msgid "Favoris"
|
||||
msgstr "Favorites"
|
||||
|
||||
msgid "Aucun favori enregistré"
|
||||
msgstr "No saved favorites"
|
||||
|
||||
msgid "Mettre à jour avec les filtres actuels"
|
||||
msgstr "Update with current filters"
|
||||
|
||||
msgid "Sélectionner un favori"
|
||||
msgstr "Select a favorite"
|
||||
|
||||
msgid "Aucun filtre sélectionné (toutes les interventions)."
|
||||
msgstr "No filter selected (all interventions)."
|
||||
|
||||
msgid "Thématique(s)"
|
||||
msgstr "Thematic(s)"
|
||||
|
||||
msgid "Statut(s)"
|
||||
msgstr "Status(es)"
|
||||
|
||||
msgid "Priorité"
|
||||
msgstr "Priority"
|
||||
|
||||
msgid "Type maintenance"
|
||||
msgstr "Maintenance type"
|
||||
|
||||
msgid "Catégories"
|
||||
msgstr "Categories"
|
||||
|
||||
msgid "sélectionnée(s)"
|
||||
msgstr "selected"
|
||||
|
||||
msgid "Date début"
|
||||
msgstr "Start date"
|
||||
|
||||
msgid "Date fin"
|
||||
msgstr "End date"
|
||||
|
||||
msgid "Non traitées (en retard)"
|
||||
msgstr "Pending (overdue)"
|
||||
|
||||
msgid "Suivies uniquement"
|
||||
msgstr "Followed only"
|
||||
|
||||
msgid "Marché bloqué : Oui"
|
||||
msgstr "Contract blocked: Yes"
|
||||
|
||||
msgid "Marché bloqué : Non"
|
||||
msgstr "Contract blocked: No"
|
||||
|
||||
msgid "Localisations"
|
||||
msgstr "Locations"
|
||||
|
||||
msgid "Filtres divers appliqués."
|
||||
msgstr "Various filters applied."
|
||||
|
||||
msgid "Filtre favori enregistré avec succès."
|
||||
msgstr "Favorite filter saved successfully."
|
||||
|
||||
msgid "Erreur lors de la sauvegarde du favori."
|
||||
msgstr "Error saving favorite filter."
|
||||
|
||||
msgid "Erreur de communication avec le serveur."
|
||||
msgstr "Communication error with server."
|
||||
|
||||
#, javascript-format
|
||||
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 deleting favorite filter."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Mettre à jour le favori '%s' avec les filtres actuellement actifs ?"
|
||||
msgstr "Update favorite '%s' with currently active filters?"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Favori '%s' mis à jour."
|
||||
msgstr "Favorite '%s' updated."
|
||||
|
||||
msgid "Erreur lors de la mise à jour du favori."
|
||||
msgstr "Error updating favorite filter."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Filtre '%s' appliqué."
|
||||
msgstr "Filter '%s' applied."
|
||||
|
||||
#~ msgid "Veuillez entrer un entier de maximum 6 chiffres"
|
||||
#~ msgstr "Please enter an integer with maximum 6 digits"
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -8538,5 +8538,32 @@ msgstr "-- Sélectionner un rôle… --"
|
|||
msgid "-- Choisir une personne… --"
|
||||
msgstr "-- Choisir une personne… --"
|
||||
|
||||
msgid "— Sélectionner un favori —"
|
||||
msgstr "— Sélectionner un favori —"
|
||||
|
||||
msgid "Enregistrer en favori"
|
||||
msgstr "Enregistrer en favori"
|
||||
|
||||
msgid "Enregistrer comme favori"
|
||||
msgstr "Enregistrer comme favori"
|
||||
|
||||
msgid "Nom du filtre favori"
|
||||
msgstr "Nom du filtre favori"
|
||||
|
||||
msgid "Ex. Mes urgences voirie"
|
||||
msgstr "Ex. Mes urgences 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 :"
|
||||
|
||||
msgid "Mes filtres favoris"
|
||||
msgstr "Mes filtres favoris"
|
||||
|
||||
msgid "Enregistrer le filtre actuel..."
|
||||
msgstr "Enregistrer le filtre actuel..."
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1214,6 +1214,97 @@ msgstr "Erreur lors de la sauvegarde"
|
|||
msgid "Erreur lors de la mise à jour"
|
||||
msgstr "Erreur lors de la mise à jour"
|
||||
|
||||
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 "Sélectionner un favori"
|
||||
msgstr "Sélectionner un favori"
|
||||
|
||||
msgid "Aucun filtre sélectionné (toutes les interventions)."
|
||||
msgstr "Aucun filtre sélectionné (toutes les interventions)."
|
||||
|
||||
msgid "Thématique(s)"
|
||||
msgstr "Thématique(s)"
|
||||
|
||||
msgid "Statut(s)"
|
||||
msgstr "Statut(s)"
|
||||
|
||||
msgid "Priorité"
|
||||
msgstr "Priorité"
|
||||
|
||||
msgid "Type maintenance"
|
||||
msgstr "Type maintenance"
|
||||
|
||||
msgid "Catégories"
|
||||
msgstr "Catégories"
|
||||
|
||||
msgid "sélectionnée(s)"
|
||||
msgstr "sélectionnée(s)"
|
||||
|
||||
msgid "Date début"
|
||||
msgstr "Date début"
|
||||
|
||||
msgid "Date fin"
|
||||
msgstr "Date fin"
|
||||
|
||||
msgid "Non traitées (en retard)"
|
||||
msgstr "Non traitées (en retard)"
|
||||
|
||||
msgid "Suivies uniquement"
|
||||
msgstr "Suivies uniquement"
|
||||
|
||||
msgid "Marché bloqué : Oui"
|
||||
msgstr "Marché bloqué : Oui"
|
||||
|
||||
msgid "Marché bloqué : Non"
|
||||
msgstr "Marché bloqué : Non"
|
||||
|
||||
msgid "Localisations"
|
||||
msgstr "Localisations"
|
||||
|
||||
msgid "Filtres divers appliqués."
|
||||
msgstr "Filtres divers appliqués."
|
||||
|
||||
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."
|
||||
|
||||
#, javascript-format
|
||||
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."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Mettre à jour le favori '%s' avec les filtres actuellement actifs ?"
|
||||
msgstr "Mettre à jour le favori '%s' avec les filtres actuellement actifs ?"
|
||||
|
||||
#, javascript-format
|
||||
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."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Filtre '%s' appliqué."
|
||||
msgstr "Filtre '%s' appliqué."
|
||||
|
||||
#~ msgid "Veuillez entrer un entier de maximum 6 chiffres"
|
||||
#~ msgstr "Veuillez entrer un entier de maximum 6 chiffres"
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -8472,4 +8472,31 @@ msgstr "-- Selecteer een rol… --"
|
|||
msgid "-- Choisir une personne… --"
|
||||
msgstr "-- Kies een persoon… --"
|
||||
|
||||
msgid "— Sélectionner un favori —"
|
||||
msgstr "— Selecteer een favoriet —"
|
||||
|
||||
msgid "Enregistrer en favori"
|
||||
msgstr "Opslaan als favoriet"
|
||||
|
||||
msgid "Enregistrer comme favori"
|
||||
msgstr "Opslaan als favoriet"
|
||||
|
||||
msgid "Nom du filtre favori"
|
||||
msgstr "Naam van favoriet filter"
|
||||
|
||||
msgid "Ex. Mes urgences voirie"
|
||||
msgstr "Bijv. Mijn wegongevallen"
|
||||
|
||||
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:"
|
||||
|
||||
msgid "Mes filtres favoris"
|
||||
msgstr "Mijn favoriete filters"
|
||||
|
||||
msgid "Enregistrer le filtre actuel..."
|
||||
msgstr "Huidige filter opslaan..."
|
||||
|
||||
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1206,6 +1206,97 @@ msgstr "Fout bij het opslaan"
|
|||
msgid "Erreur lors de la mise à jour"
|
||||
msgstr "Fout bij het bijwerken"
|
||||
|
||||
msgid "Favoris"
|
||||
msgstr "Favorieten"
|
||||
|
||||
msgid "Aucun favori enregistré"
|
||||
msgstr "Geen opgeslagen favorieten"
|
||||
|
||||
msgid "Mettre à jour avec les filtres actuels"
|
||||
msgstr "Bijwerken met huidige filters"
|
||||
|
||||
msgid "Sélectionner un favori"
|
||||
msgstr "Selecteer een favoriet"
|
||||
|
||||
msgid "Aucun filtre sélectionné (toutes les interventions)."
|
||||
msgstr "Geen filter geselecteerd (alle interventies)."
|
||||
|
||||
msgid "Thématique(s)"
|
||||
msgstr "Thema('s)"
|
||||
|
||||
msgid "Statut(s)"
|
||||
msgstr "Status(sen)"
|
||||
|
||||
msgid "Priorité"
|
||||
msgstr "Prioriteit"
|
||||
|
||||
msgid "Type maintenance"
|
||||
msgstr "Onderhoudstype"
|
||||
|
||||
msgid "Catégories"
|
||||
msgstr "Categorieën"
|
||||
|
||||
msgid "sélectionnée(s)"
|
||||
msgstr "geselecteerd"
|
||||
|
||||
msgid "Date début"
|
||||
msgstr "Startdatum"
|
||||
|
||||
msgid "Date fin"
|
||||
msgstr "Einddatum"
|
||||
|
||||
msgid "Non traitées (en retard)"
|
||||
msgstr "Niet behandeld (achterstallig)"
|
||||
|
||||
msgid "Suivies uniquement"
|
||||
msgstr "Alleen gevolgde"
|
||||
|
||||
msgid "Marché bloqué : Oui"
|
||||
msgstr "Overeenkomst geblokkeerd: Ja"
|
||||
|
||||
msgid "Marché bloqué : Non"
|
||||
msgstr "Overeenkomst geblokkeerd: Nee"
|
||||
|
||||
msgid "Localisations"
|
||||
msgstr "Locaties"
|
||||
|
||||
msgid "Filtres divers appliqués."
|
||||
msgstr "Diverse filters toegepast."
|
||||
|
||||
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 het favoriete filter."
|
||||
|
||||
msgid "Erreur de communication avec le serveur."
|
||||
msgstr "Communicatiefout met server."
|
||||
|
||||
#, javascript-format
|
||||
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 het favoriete filter."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Mettre à jour le favori '%s' avec les filtres actuellement actifs ?"
|
||||
msgstr "Favoriet '%s' bijwerken met momenteel actieve filters?"
|
||||
|
||||
#, javascript-format
|
||||
msgid "Favori '%s' mis à jour."
|
||||
msgstr "Favoriet '%s' bijgewerkt."
|
||||
|
||||
msgid "Erreur lors de la mise à jour du favori."
|
||||
msgstr "Fout bij het bijwerken van het favoriete filter."
|
||||
|
||||
#, javascript-format
|
||||
msgid "Filtre '%s' appliqué."
|
||||
msgstr "Filter '%s' toegepast."
|
||||
|
||||
#~ msgid "Veuillez entrer un entier de maximum 6 chiffres"
|
||||
#~ msgstr "Voer een geheel getal van maximaal 6 cijfers in"
|
||||
|
||||
|
|
|
|||
|
|
@ -314,6 +314,10 @@ document.getElementById('apply-filters').addEventListener('click', async functio
|
|||
if (filterModal) {
|
||||
filterModal.hide();
|
||||
}
|
||||
|
||||
if (typeof checkActiveFavoriteMatch === 'function') {
|
||||
checkActiveFavoriteMatch();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -361,6 +365,10 @@ document.getElementById('reset-filters').addEventListener('click', async functio
|
|||
// Update filter badge after URL change
|
||||
updateFilterBadge();
|
||||
|
||||
if (typeof checkActiveFavoriteMatch === 'function') {
|
||||
checkActiveFavoriteMatch();
|
||||
}
|
||||
|
||||
// Clear search input too
|
||||
$('#interventions-search-input').val('');
|
||||
$('#interventions-search-clear').css('visibility', 'hidden');
|
||||
|
|
@ -2414,7 +2422,7 @@ function countActiveFilters() {
|
|||
'provider', 'contract', 'assigned_team', 'assigned_member',
|
||||
'assigned_controller', 'intervention_manager', 'inspector', 'pause_reason',
|
||||
'begin_date_min', 'begin_date_max', 'end_date_min', 'end_date_max', 'locations',
|
||||
'overdue', 'to_be_planned_soon'
|
||||
'overdue', 'to_be_planned_soon', 'followed_only', 'is_blocked_due_to_contract'
|
||||
];
|
||||
|
||||
filterParams.forEach(param => {
|
||||
|
|
@ -2459,7 +2467,7 @@ function savePreferencesFromUrl() {
|
|||
'provider', 'contract', 'assigned_team', 'assigned_member',
|
||||
'assigned_controller', 'intervention_manager', 'inspector', 'pause_reason',
|
||||
'begin_date_min', 'begin_date_max', 'end_date_min', 'end_date_max', 'locations',
|
||||
'overdue', 'to_be_planned_soon'
|
||||
'overdue', 'to_be_planned_soon', 'followed_only', 'is_blocked_due_to_contract'
|
||||
];
|
||||
|
||||
filterParams.forEach(param => {
|
||||
|
|
@ -2513,7 +2521,7 @@ async function savePreferencesFromForm(urlParams) {
|
|||
'provider', 'contract', 'assigned_team', 'assigned_member',
|
||||
'assigned_controller', 'intervention_manager', 'inspector', 'pause_reason',
|
||||
'begin_date_min', 'begin_date_max', 'end_date_min', 'end_date_max', 'locations',
|
||||
'overdue', 'to_be_planned_soon'
|
||||
'overdue', 'to_be_planned_soon', 'followed_only', 'is_blocked_due_to_contract'
|
||||
];
|
||||
|
||||
filterParams.forEach(param => {
|
||||
|
|
@ -2566,6 +2574,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
updateFilterBadge();
|
||||
initDateFilterPresets();
|
||||
initRolePersonFilter();
|
||||
initFavoriteFilters();
|
||||
|
||||
// Save preferences when table sort changes (user action only)
|
||||
const $table = $('#interventions-table');
|
||||
|
|
@ -2579,6 +2588,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
// Update badge after page fully loads (including after filter redirect)
|
||||
window.addEventListener('load', function() {
|
||||
updateFilterBadge();
|
||||
checkActiveFavoriteMatch();
|
||||
|
||||
// Restore search input from URL if present
|
||||
const _urlParams = new URLSearchParams(window.location.search);
|
||||
|
|
@ -3109,3 +3119,547 @@ function initDateFilterPresets() {
|
|||
|
||||
updateDateFilterTimelinePreview();
|
||||
}
|
||||
|
||||
// ==================== Favorite Filters Management ====================
|
||||
|
||||
function initFavoriteFilters() {
|
||||
renderFavoriteFiltersUI();
|
||||
checkActiveFavoriteMatch();
|
||||
|
||||
// Open save modal buttons
|
||||
const btnOpenSave = document.getElementById('btn-open-save-favorite-modal');
|
||||
if (btnOpenSave) {
|
||||
btnOpenSave.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
openSaveFavoriteModal();
|
||||
});
|
||||
}
|
||||
|
||||
const btnModalSave = document.getElementById('modal-save-favorite-btn');
|
||||
if (btnModalSave) {
|
||||
btnModalSave.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
openSaveFavoriteModal();
|
||||
});
|
||||
}
|
||||
|
||||
// Modal favorite select change
|
||||
const modalSelect = document.getElementById('modal-favorite-filter-select');
|
||||
if (modalSelect) {
|
||||
modalSelect.addEventListener('change', function() {
|
||||
if (this.value) {
|
||||
applyFavoriteFilter(this.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 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');
|
||||
const modalSelect = document.getElementById('modal-favorite-filter-select');
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if (modalSelect) {
|
||||
const currentVal = modalSelect.value;
|
||||
let selectHtml = `<option value="">— ${gettext('Sélectionner un favori')} —</option>`;
|
||||
if (typeof userFavoriteFilters !== 'undefined' && Array.isArray(userFavoriteFilters)) {
|
||||
userFavoriteFilters.forEach(fav => {
|
||||
selectHtml += `<option value="${fav.id}">${fav.name}</option>`;
|
||||
});
|
||||
}
|
||||
modalSelect.innerHTML = selectHtml;
|
||||
modalSelect.value = currentVal;
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentFilterValues() {
|
||||
const form = document.getElementById('filter-form');
|
||||
const filters = {};
|
||||
if (!form) return filters;
|
||||
const formData = new FormData(form);
|
||||
|
||||
const multiFields = [
|
||||
'thematic', 'status', 'priority', 'maintain_type', 'category', 'type',
|
||||
'provider', 'contract', 'assigned_team', 'assigned_member', 'assigned_control_team',
|
||||
'pause_reason'
|
||||
];
|
||||
|
||||
multiFields.forEach(field => {
|
||||
const values = formData.getAll(field).filter(v => v.trim() !== '');
|
||||
if (values.length > 0) {
|
||||
filters[field] = values;
|
||||
}
|
||||
});
|
||||
|
||||
const singleFields = [
|
||||
'assigned_controller', 'intervention_manager', 'inspector',
|
||||
'begin_date_min', 'begin_date_max', 'end_date_min', 'end_date_max',
|
||||
'overdue', 'followed_only', 'to_be_planned_soon', 'is_blocked_due_to_contract'
|
||||
];
|
||||
|
||||
singleFields.forEach(field => {
|
||||
const val = formData.get(field);
|
||||
if (val && val.trim() !== '') {
|
||||
filters[field] = val.trim();
|
||||
}
|
||||
});
|
||||
|
||||
// Locations
|
||||
if (typeof selectedLocations !== 'undefined' && selectedLocations.length > 0) {
|
||||
filters['locations'] = selectedLocations.map(x => ({ ct: x.ct, obj: x.obj }));
|
||||
}
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
function areFilterObjectsEqual(f1, f2) {
|
||||
if (!f1 || !f2) return false;
|
||||
const keys1 = Object.keys(f1).filter(k => f1[k] !== undefined && f1[k] !== null && f1[k] !== '' && (!Array.isArray(f1[k]) || f1[k].length > 0)).sort();
|
||||
const keys2 = Object.keys(f2).filter(k => f2[k] !== undefined && f2[k] !== null && f2[k] !== '' && (!Array.isArray(f2[k]) || f2[k].length > 0)).sort();
|
||||
|
||||
if (keys1.length !== keys2.length) return false;
|
||||
for (let i = 0; i < keys1.length; i++) {
|
||||
const k = keys1[i];
|
||||
if (k !== keys2[i]) return false;
|
||||
let v1 = f1[k];
|
||||
let v2 = f2[k];
|
||||
if (Array.isArray(v1) || Array.isArray(v2)) {
|
||||
const arr1 = (Array.isArray(v1) ? v1 : [v1]).map(String).sort();
|
||||
const arr2 = (Array.isArray(v2) ? v2 : [v2]).map(String).sort();
|
||||
if (arr1.length !== arr2.length) return false;
|
||||
for (let j = 0; j < arr1.length; j++) {
|
||||
if (arr1[j] !== arr2[j]) return false;
|
||||
}
|
||||
} else if (k === 'locations') {
|
||||
if (JSON.stringify(v1) !== JSON.stringify(v2)) return false;
|
||||
} else {
|
||||
if (String(v1) !== String(v2)) 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');
|
||||
const modalSelect = document.getElementById('modal-favorite-filter-select');
|
||||
|
||||
// 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';
|
||||
}
|
||||
if (modalSelect) {
|
||||
modalSelect.value = matchedFav.id;
|
||||
}
|
||||
} else {
|
||||
if (activeLabel) {
|
||||
activeLabel.textContent = gettext('Favoris');
|
||||
}
|
||||
if (modalSelect) {
|
||||
modalSelect.value = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function generateFilterSummary(filters) {
|
||||
if (!filters || Object.keys(filters).length === 0) {
|
||||
return `<span class="text-muted fst-italic">${gettext('Aucun filtre sélectionné (toutes les interventions).')}</span>`;
|
||||
}
|
||||
const badges = [];
|
||||
|
||||
if (filters.thematic && filters.thematic.length) {
|
||||
const names = filters.thematic.map(code => {
|
||||
const lbl = document.querySelector(`label[for="thematic_${code}"]`);
|
||||
return lbl ? lbl.textContent.trim() : code;
|
||||
});
|
||||
badges.push(`<strong>${gettext('Thématique(s)')} :</strong> ${names.join(', ')}`);
|
||||
}
|
||||
|
||||
if (filters.status && filters.status.length) {
|
||||
const names = filters.status.map(s => {
|
||||
const lbl = document.querySelector(`label[for="status_${s}"]`);
|
||||
return lbl ? lbl.textContent.trim() : s;
|
||||
});
|
||||
badges.push(`<strong>${gettext('Statut(s)')} :</strong> ${names.join(', ')}`);
|
||||
}
|
||||
|
||||
if (filters.priority && filters.priority.length) {
|
||||
badges.push(`<strong>${gettext('Priorité')} :</strong> ${filters.priority.join(', ')}`);
|
||||
}
|
||||
|
||||
if (filters.maintain_type && filters.maintain_type.length) {
|
||||
badges.push(`<strong>${gettext('Type maintenance')} :</strong> ${filters.maintain_type.join(', ')}`);
|
||||
}
|
||||
|
||||
if (filters.category && filters.category.length) {
|
||||
badges.push(`<strong>${gettext('Catégories')} :</strong> ${filters.category.length} ${gettext('sélectionnée(s)')}`);
|
||||
}
|
||||
|
||||
if (filters.begin_date_min || filters.begin_date_max) {
|
||||
badges.push(`<strong>${gettext('Date début')} :</strong> [${filters.begin_date_min || '—'} → ${filters.begin_date_max || '—'}]`);
|
||||
}
|
||||
if (filters.end_date_min || filters.end_date_max) {
|
||||
badges.push(`<strong>${gettext('Date fin')} :</strong> [${filters.end_date_min || '—'} → ${filters.end_date_max || '—'}]`);
|
||||
}
|
||||
|
||||
if (filters.overdue === '1') {
|
||||
badges.push(`<strong>${gettext('Non traitées (en retard)')}</strong>`);
|
||||
}
|
||||
if (filters.followed_only === '1') {
|
||||
badges.push(`<strong>${gettext('Suivies uniquement')}</strong>`);
|
||||
}
|
||||
if (filters.is_blocked_due_to_contract === '1') {
|
||||
badges.push(`<strong>${gettext('Marché bloqué : Oui')}</strong>`);
|
||||
} else if (filters.is_blocked_due_to_contract === '0') {
|
||||
badges.push(`<strong>${gettext('Marché bloqué : Non')}</strong>`);
|
||||
}
|
||||
|
||||
if (filters.locations && filters.locations.length) {
|
||||
badges.push(`<strong>${gettext('Localisations')} :</strong> ${filters.locations.length}`);
|
||||
}
|
||||
|
||||
if (badges.length === 0) {
|
||||
return `<span class="text-muted fst-italic">${gettext('Filtres divers appliqués.')}</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': getCookie('csrftoken')
|
||||
},
|
||||
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': getCookie('csrftoken')
|
||||
},
|
||||
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': getCookie('csrftoken')
|
||||
},
|
||||
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.'));
|
||||
}
|
||||
}
|
||||
|
||||
async function applyFavoriteFilter(favoriteId) {
|
||||
if (!favoriteId) return;
|
||||
const fav = userFavoriteFilters.find(f => f.id === favoriteId);
|
||||
if (!fav) return;
|
||||
|
||||
const form = document.getElementById('filter-form');
|
||||
if (!form) return;
|
||||
|
||||
// 1. Reset all fields first
|
||||
form.reset();
|
||||
form.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
|
||||
checkbox.checked = false;
|
||||
});
|
||||
|
||||
document.getElementById('begin_date_min').value = '';
|
||||
document.getElementById('begin_date_max').value = '';
|
||||
document.getElementById('end_date_min').value = '';
|
||||
document.getElementById('end_date_max').value = '';
|
||||
|
||||
const blockedSelect = document.getElementById('is_blocked_due_to_contract');
|
||||
if (blockedSelect) blockedSelect.value = '';
|
||||
|
||||
selectedLocations = [];
|
||||
|
||||
// 2. Restore values from fav.filters
|
||||
const filters = fav.filters || {};
|
||||
for (const [key, value] of Object.entries(filters)) {
|
||||
if (key === 'locations') {
|
||||
try {
|
||||
let locs = value;
|
||||
if (typeof locs === 'string') {
|
||||
locs = JSON.parse(locs);
|
||||
}
|
||||
if (Array.isArray(locs)) {
|
||||
selectedLocations = locs;
|
||||
}
|
||||
} catch (e) {
|
||||
selectedLocations = [];
|
||||
}
|
||||
} else if (['begin_date_min', 'begin_date_max', 'end_date_min', 'end_date_max'].includes(key)) {
|
||||
const input = document.getElementById(key);
|
||||
if (input) input.value = value;
|
||||
} else if (key === 'is_blocked_due_to_contract') {
|
||||
if (blockedSelect) blockedSelect.value = value;
|
||||
} else {
|
||||
// Checkboxes
|
||||
const valList = Array.isArray(value) ? value.map(String) : [String(value)];
|
||||
form.querySelectorAll(`input[type="checkbox"][name="${key}"]`).forEach(cb => {
|
||||
cb.checked = valList.includes(String(cb.value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderLocationChips();
|
||||
togglePauseReasonFilter();
|
||||
filterCategoriesByThematics();
|
||||
filterProvidersAndContracts();
|
||||
updateDateFilterTimelinePreview();
|
||||
|
||||
// 3. Build URL params & save preferences
|
||||
const formData = new FormData(form);
|
||||
const params = new URLSearchParams();
|
||||
for (const [k, v] of formData.entries()) {
|
||||
if (v.trim() !== '') params.append(k, v);
|
||||
}
|
||||
if (selectedLocations.length) {
|
||||
const payload = selectedLocations.map(x => ({ ct: x.ct, obj: x.obj }));
|
||||
params.set('locations', JSON.stringify(payload));
|
||||
} else {
|
||||
params.delete('locations');
|
||||
}
|
||||
|
||||
// Save as current active preferences
|
||||
await savePreferencesFromForm(params);
|
||||
|
||||
// Preserve search input if present
|
||||
const _activeQ = $('#interventions-search-input').val().trim();
|
||||
const _activeMode = document.querySelector('input[name="search-mode"]:checked')?.value || 'and';
|
||||
if (_activeQ) {
|
||||
params.set('q', _activeQ);
|
||||
params.set('search_mode', _activeMode);
|
||||
}
|
||||
|
||||
// Update URL & filter badge
|
||||
const newUrl = `${window.location.pathname}?${params.toString()}`;
|
||||
window.history.pushState({}, '', newUrl);
|
||||
updateFilterBadge();
|
||||
|
||||
// Reload table and map data
|
||||
handleGeojsonMapLoading(false, interventionsgeojsonloader, 'interventions');
|
||||
interventionsgeojsonloader.loadGeojson(interventionsgeojsonUrl + '?' + params);
|
||||
|
||||
// Update active label and stars
|
||||
checkActiveFavoriteMatch();
|
||||
|
||||
// Close modal if open
|
||||
const filterModalEl = document.getElementById('filterModal');
|
||||
if (filterModalEl) {
|
||||
const filterModal = bootstrap.Modal.getInstance(filterModalEl);
|
||||
if (filterModal) filterModal.hide();
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -45,6 +45,29 @@
|
|||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.popup-scrollable {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
|
|
@ -555,6 +578,20 @@
|
|||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Fermer"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- Barre de sélection rapide de favoris -->
|
||||
<div class="d-flex flex-wrap align-items-center justify-content-between bg-light p-2 rounded mb-3 border gap-2">
|
||||
<div class="d-flex align-items-center gap-2 flex-grow-1" style="min-width: 220px;">
|
||||
<i class="bi bi-bookmark-star text-warning flex-shrink-0"></i>
|
||||
<label for="modal-favorite-filter-select" class="form-label small fw-semibold mb-0 text-nowrap">{% translate "Favori :" %}</label>
|
||||
<select id="modal-favorite-filter-select" class="form-select form-select-sm">
|
||||
<option value="">{% translate "— Sélectionner un favori —" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary text-nowrap" id="modal-save-favorite-btn">
|
||||
<i class="bi bi-star me-1"></i>{% translate "Enregistrer en favori" %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Formulaire de filtre -->
|
||||
<form method="get" id="filter-form" class="mb-4">
|
||||
<!-- Thématique, Statut et Catégorie -->
|
||||
|
|
@ -1098,6 +1135,42 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modale 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. Mes urgences 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>
|
||||
|
||||
<div class="position-relative">
|
||||
<!-- Layout selection buttons -->
|
||||
<div class="position-absolute top-0 end-0 mt-2" style="z-index: 10;">
|
||||
|
|
@ -1225,18 +1298,40 @@
|
|||
|
||||
<div id="interventions-table-container" style="display: none; flex: 1; min-height: 0; overflow: hidden;">
|
||||
|
||||
<!-- Bouton de filtre -->
|
||||
<div id="filter-dropdown-wrapper" class="float-left mb-2">
|
||||
<!-- Bouton de filtre et Favoris -->
|
||||
<div id="filter-dropdown-wrapper" class="float-left mb-2 d-flex align-items-center gap-1">
|
||||
<button class="btn btn-light position-relative" type="button" data-bs-toggle="modal" data-bs-target="#filterModal">
|
||||
<i class="bi bi-funnel"></i> {% translate "Filtre" %}
|
||||
<span id="filter-badge" class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-black d-none">
|
||||
<span id="filter-count">0</span>
|
||||
</span>
|
||||
</button>
|
||||
<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>
|
||||
<!-- Alerte limite atteinte (cachée par défaut) -->
|
||||
<span id="filter-limit-alert" class="alert alert-info py-1 px-2 ms-2 d-none align-middle" role="alert">
|
||||
<i class="bi bi-info-circle me-1"></i>
|
||||
{% translate "Contenu limité à 5 000 éléments." %}"
|
||||
{% translate "Contenu limité à 5 000 éléments." %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
|
@ -1480,6 +1575,9 @@
|
|||
const executionReportDownloadBaseUrl = "{% url 'interventions:download_bulk_execution_report' token='__TOKEN__' %}";
|
||||
const saveLayoutUrl = "{% url 'common:save_intervention_list_layout' %}";
|
||||
const savePreferencesUrl = "{% url 'common:save_intervention_list_preferences' %}";
|
||||
const saveFavoriteUrl = "{% url 'common:save_favorite_filter' module='interventions' %}";
|
||||
const deleteFavoriteUrl = "{% url 'common:delete_favorite_filter' module='interventions' %}";
|
||||
let userFavoriteFilters = {{ favorite_filters_json|default:"[]"|safe }};
|
||||
|
||||
var selected_interventions_set = new Set(JSON.parse('{{ selected_interventions_ids|default:"[]"|escapejs }}'));
|
||||
const PRESELECTED_LOCATIONS = JSON.parse('{{ preselected_locations|default:"[]"|escapejs }}');
|
||||
|
|
|
|||
|
|
@ -544,6 +544,7 @@ def index(request):
|
|||
'saved_columns': json.dumps(user_config.intervention_list_columns, ensure_ascii=False),
|
||||
'is_admin': user_config.roles.filter(name='admin').exists(),
|
||||
'selected_is_blocked': request.GET.get('is_blocked') or request.GET.get('is_blocked_due_to_contract') or request.GET.get('blocked') or '',
|
||||
'favorite_filters_json': json.dumps((user_config.favorite_filters or {}).get('interventions', []) if isinstance((user_config.favorite_filters or {}).get('interventions'), list) else [], ensure_ascii=False),
|
||||
}
|
||||
return render(request, "interventions/interventions_index.html", context)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue