diff --git a/loko/controls/locale/en/LC_MESSAGES/django.mo b/loko/controls/locale/en/LC_MESSAGES/django.mo index 4f19d34..27c95bc 100644 Binary files a/loko/controls/locale/en/LC_MESSAGES/django.mo and b/loko/controls/locale/en/LC_MESSAGES/django.mo differ diff --git a/loko/controls/locale/en/LC_MESSAGES/django.po b/loko/controls/locale/en/LC_MESSAGES/django.po index 670dba7..12099a2 100644 --- a/loko/controls/locale/en/LC_MESSAGES/django.po +++ b/loko/controls/locale/en/LC_MESSAGES/django.po @@ -1414,3 +1414,64 @@ msgstr "Error generating the PDF" #~ msgid "Mise à jour" #~ msgstr "Last update" + +#: controls/templates/controls/controls_index.html +msgid "Sélectionnez les critères à afficher sur la carte et dans le tableau" +msgstr "Select criteria to display on the map and in the table" + +#: controls/templates/controls/controls_index.html +msgid "Colonnes" +msgstr "Columns" + +#: controls/templates/controls/controls_index.html +msgid "Choisir les colonnes à afficher" +msgstr "Choose columns to display" + +#: controls/templates/controls/controls_index.html +msgid "Toutes" +msgstr "All" + +#: controls/templates/controls/controls_index.html +msgid "Défaut" +msgstr "Default" + +#: controls/templates/controls/controls_index.html +msgid "Afficher par" +msgstr "Show by" + +#: controls/templates/controls/controls_index.html +msgid "Filtres actifs" +msgstr "Active filters" + +#: controls/templates/controls/controls_index.html +msgid "Supprimer ce critère" +msgstr "Remove this criterion" + +#: controls/templates/controls/controls_index.html +msgid "Tout effacer" +msgstr "Clear all" + +#: controls/templates/controls/controls_index.html +msgid "Aucun contrôle trouvé" +msgstr "No controls found" + +#: controls/templates/controls/controls_index.html +msgid "Aucun contrôle ne correspond à vos critères de recherche actuels." +msgstr "No controls match your current search criteria." + +#: controls/templates/controls/controls_index.html +msgid "Réinitialiser les filtres" +msgstr "Reset filters" + +#: controls/templates/controls/controls_index.html +msgid "Affichage de %(start)s à %(end)s sur %(total)s contrôle(s)" +msgstr "Showing %(start)s to %(end)s of %(total)s control(s)" + +#: controls/templates/controls/controls_index.html +msgid "Pagination des contrôles" +msgstr "Controls pagination" + +#: controls/templates/controls/controls_index.html +msgid "Excel" +msgstr "Excel" + diff --git a/loko/controls/locale/fr/LC_MESSAGES/django.mo b/loko/controls/locale/fr/LC_MESSAGES/django.mo index 74cd42c..f5a76b4 100644 Binary files a/loko/controls/locale/fr/LC_MESSAGES/django.mo and b/loko/controls/locale/fr/LC_MESSAGES/django.mo differ diff --git a/loko/controls/locale/fr/LC_MESSAGES/django.po b/loko/controls/locale/fr/LC_MESSAGES/django.po index 31fc86e..9dbed2b 100644 --- a/loko/controls/locale/fr/LC_MESSAGES/django.po +++ b/loko/controls/locale/fr/LC_MESSAGES/django.po @@ -1419,3 +1419,64 @@ msgstr "Erreur lors de la génération du PDF" #~ msgid "Mise à jour" #~ msgstr "Mise à jour" + +#: controls/templates/controls/controls_index.html +msgid "Sélectionnez les critères à afficher sur la carte et dans le tableau" +msgstr "Sélectionnez les critères à afficher sur la carte et dans le tableau" + +#: controls/templates/controls/controls_index.html +msgid "Colonnes" +msgstr "Colonnes" + +#: controls/templates/controls/controls_index.html +msgid "Choisir les colonnes à afficher" +msgstr "Choisir les colonnes à afficher" + +#: controls/templates/controls/controls_index.html +msgid "Toutes" +msgstr "Toutes" + +#: controls/templates/controls/controls_index.html +msgid "Défaut" +msgstr "Défaut" + +#: controls/templates/controls/controls_index.html +msgid "Afficher par" +msgstr "Afficher par" + +#: controls/templates/controls/controls_index.html +msgid "Filtres actifs" +msgstr "Filtres actifs" + +#: controls/templates/controls/controls_index.html +msgid "Supprimer ce critère" +msgstr "Supprimer ce critère" + +#: controls/templates/controls/controls_index.html +msgid "Tout effacer" +msgstr "Tout effacer" + +#: controls/templates/controls/controls_index.html +msgid "Aucun contrôle trouvé" +msgstr "Aucun contrôle trouvé" + +#: controls/templates/controls/controls_index.html +msgid "Aucun contrôle ne correspond à vos critères de recherche actuels." +msgstr "Aucun contrôle ne correspond à vos critères de recherche actuels." + +#: controls/templates/controls/controls_index.html +msgid "Réinitialiser les filtres" +msgstr "Réinitialiser les filtres" + +#: controls/templates/controls/controls_index.html +msgid "Affichage de %(start)s à %(end)s sur %(total)s contrôle(s)" +msgstr "Affichage de %(start)s à %(end)s sur %(total)s contrôle(s)" + +#: controls/templates/controls/controls_index.html +msgid "Pagination des contrôles" +msgstr "Pagination des contrôles" + +#: controls/templates/controls/controls_index.html +msgid "Excel" +msgstr "Excel" + diff --git a/loko/controls/locale/nl/LC_MESSAGES/django.mo b/loko/controls/locale/nl/LC_MESSAGES/django.mo index bee8147..c2e8dbf 100644 Binary files a/loko/controls/locale/nl/LC_MESSAGES/django.mo and b/loko/controls/locale/nl/LC_MESSAGES/django.mo differ diff --git a/loko/controls/locale/nl/LC_MESSAGES/django.po b/loko/controls/locale/nl/LC_MESSAGES/django.po index 292a97b..d3d0e6d 100644 --- a/loko/controls/locale/nl/LC_MESSAGES/django.po +++ b/loko/controls/locale/nl/LC_MESSAGES/django.po @@ -1417,3 +1417,64 @@ msgstr "Fout bij het genereren van de PDF" #~ msgid "Mise à jour" #~ msgstr "Laatste update" + +#: controls/templates/controls/controls_index.html +msgid "Sélectionnez les critères à afficher sur la carte et dans le tableau" +msgstr "Selecteer de criteria om weer te geven op de kaart en in de tabel" + +#: controls/templates/controls/controls_index.html +msgid "Colonnes" +msgstr "Kolommen" + +#: controls/templates/controls/controls_index.html +msgid "Choisir les colonnes à afficher" +msgstr "Kies de weer te geven kolommen" + +#: controls/templates/controls/controls_index.html +msgid "Toutes" +msgstr "Alle" + +#: controls/templates/controls/controls_index.html +msgid "Défaut" +msgstr "Standaard" + +#: controls/templates/controls/controls_index.html +msgid "Afficher par" +msgstr "Weergeven per" + +#: controls/templates/controls/controls_index.html +msgid "Filtres actifs" +msgstr "Actieve filters" + +#: controls/templates/controls/controls_index.html +msgid "Supprimer ce critère" +msgstr "Dit criterium verwijderen" + +#: controls/templates/controls/controls_index.html +msgid "Tout effacer" +msgstr "Alles wissen" + +#: controls/templates/controls/controls_index.html +msgid "Aucun contrôle trouvé" +msgstr "Geen controles gevonden" + +#: controls/templates/controls/controls_index.html +msgid "Aucun contrôle ne correspond à vos critères de recherche actuels." +msgstr "Geen controles komen overeen met uw huidige zoekcriteria." + +#: controls/templates/controls/controls_index.html +msgid "Réinitialiser les filtres" +msgstr "Filters resetten" + +#: controls/templates/controls/controls_index.html +msgid "Affichage de %(start)s à %(end)s sur %(total)s contrôle(s)" +msgstr "Weergave van %(start)s tot %(end)s van %(total)s controle(s)" + +#: controls/templates/controls/controls_index.html +msgid "Pagination des contrôles" +msgstr "Paginering van controles" + +#: controls/templates/controls/controls_index.html +msgid "Excel" +msgstr "Excel" + diff --git a/loko/controls/static/controls/controls_index.css b/loko/controls/static/controls/controls_index.css index 5ed25ff..e17b5c0 100644 --- a/loko/controls/static/controls/controls_index.css +++ b/loko/controls/static/controls/controls_index.css @@ -1,8 +1,15 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;600&display=swap'); + /* ========================================================================== Controls Index CSS ========================================================================== */ -/* Supprime le contour bleu Bootstrap au focus sur l'input de recherche */ +/* Typography & Global Layout */ +.controls-layout { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; +} + +/* Supprime le contour bleu Bootstrap au focus sur les inputs */ #controls-search-input:focus, #project-autocomplete-input:focus { outline: none; @@ -28,86 +35,220 @@ } } -/* Styles pour la modale de filtre */ +/* ─── Airbnb-style Filter Modal ────────────────────────────────────── */ #filterModal .modal-dialog { - max-width: 95%; + max-width: 820px; + margin: 1.5rem auto; } + #filterModal .modal-content { + border: 1px solid #e2e8f0; + border-radius: 18px; + box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); display: flex; flex-direction: column; - max-height: 90vh; + max-height: 88vh; + overflow: hidden; + background-color: #ffffff; } + +#filterModal .modal-header { + padding: 1.25rem 1.5rem; + border-bottom: 1px solid #f1f5f9; + background-color: #ffffff; +} + +#filterModal .modal-title { + font-size: 1.25rem; + font-weight: 700; + color: #0f172a; + letter-spacing: -0.02em; +} + +#filterModal .btn-close { + font-size: 0.85rem; + padding: 0.5rem; + opacity: 0.5; + transition: opacity 0.15s ease; +} +#filterModal .btn-close:hover { + opacity: 1; +} + #filterModal .modal-body { - flex: 1; + padding: 1.5rem; overflow-y: auto; + background-color: #ffffff; } + #filterModal .modal-footer { - flex-shrink: 0; + padding: 1rem 1.5rem; + border-top: 1px solid #f1f5f9; + background-color: #ffffff; + display: flex; + justify-content: space-between; + align-items: center; } +/* Filter Sections */ .filter-section { - margin-bottom: 1rem; + margin-bottom: 1.5rem; +} + +.filter-section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; +} + +.filter-section-header label { + font-size: 0.9rem; + font-weight: 700; + color: #0f172a; + letter-spacing: -0.01em; + margin: 0; +} + +.filter-section-buttons { + display: flex; + gap: 0.4rem; +} + +.filter-section-buttons .btn { + font-size: 0.75rem; + font-weight: 500; + padding: 0.2rem 0.6rem; + border-radius: 6px; + border: 1px solid #e2e8f0; + background-color: #ffffff; + color: #64748b; + transition: all 0.15s ease; +} + +.filter-section-buttons .btn:hover { + background-color: #f1f5f9; + color: #0f172a; + border-color: #cbd5e1; +} + +/* Filter Options (Pills) */ +.filter-options { + display: flex; + flex-wrap: wrap; + gap: 0.45rem; + align-items: center; +} + +.filter-option { + display: inline-flex; + margin: 0; + position: relative; +} + +.filter-option input[type="checkbox"] { + position: absolute; + opacity: 0; + width: 0; + height: 0; + pointer-events: none; } -/* Default badge styling */ .filter-option label { display: inline-flex; align-items: center; - justify-content: center; - margin: 0; + gap: 0.35rem; + padding: 0.35rem 0.85rem; + font-size: 0.78rem; font-weight: 500; + color: #334155; + background-color: #ffffff; + border: 1.5px solid #e2e8f0; + border-radius: 9999px; cursor: pointer; - background-color: #e7f1ff; - color: #0c5cc9; - border-radius: 1rem; - padding: 0.4rem 0.5rem; - font-size: 0.75rem; - border: 1px solid #b8d4f1; - transition: all 0.2s ease; - width: 140px; - flex: 1 1 auto; - text-align: center; + transition: all 0.15s ease; + user-select: none; white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + margin: 0; } -/* Contextual color badges - inactive state */ -.filter-option label.context-badge { - border-color: currentColor; - color: inherit; - background-color: transparent; +.filter-option label:hover { + background-color: #f8fafc; + border-color: #94a3b8; + color: #0f172a; } -.filter-option label.btn-outline-primary { border-color: #0c5cc9; color: #0c5cc9; } -.filter-option label.btn-outline-danger { border-color: #dc3545; color: #dc3545; } -.filter-option label.btn-outline-warning { border-color: #ffc107; color: #c69500; } -.filter-option label.btn-outline-success { border-color: #28a745; color: #28a745; } -.filter-option label.btn-outline-info { border-color: #17a2b8; color: #17a2b8; } -.filter-option label.btn-outline-dark { border-color: #343a40; color: #343a40; } -.filter-option label.btn-outline-secondary { border-color: #6c757d; color: #6c757d; } - -/* Default badges - checked state */ +/* Standard Checked State */ .filter-option input[type="checkbox"]:checked + label:not(.context-badge) { - background-color: #0c5cc9; color: white; border-color: #0c5cc9; + background-color: #0f172a !important; + border-color: #0f172a !important; + color: #ffffff !important; + font-weight: 600; + box-shadow: 0 2px 6px rgba(15, 23, 42, 0.16); } +/* Contextual badges - inactive state (outline) */ +.filter-option label.context-badge { + background-color: #ffffff; + border-width: 1.5px; + border-style: solid; + font-weight: 600; +} + +.filter-option label.btn-outline-primary, +.filter-option label.status-primary { border-color: #0c5cc9; color: #0c5cc9; } +.filter-option label.btn-outline-danger, +.filter-option label.status-danger { border-color: #dc3545; color: #dc3545; } +.filter-option label.btn-outline-warning, +.filter-option label.status-warning { border-color: #f59e0b; color: #d97706; } +.filter-option label.btn-outline-success, +.filter-option label.status-success { border-color: #198754; color: #198754; } +.filter-option label.btn-outline-info, +.filter-option label.status-info { border-color: #0284c7; color: #0284c7; } +.filter-option label.btn-outline-dark, +.filter-option label.status-dark { border-color: #334155; color: #334155; } +.filter-option label.btn-outline-secondary, +.filter-option label.status-secondary { border-color: #64748b; color: #64748b; } + +/* Contextual badges - hover state */ +.filter-option input[type="checkbox"]:not(:checked):hover + label.btn-outline-primary { background-color: #eff6ff; } +.filter-option input[type="checkbox"]:not(:checked):hover + label.btn-outline-danger { background-color: #fef2f2; } +.filter-option input[type="checkbox"]:not(:checked):hover + label.btn-outline-warning { background-color: #fffbeb; } +.filter-option input[type="checkbox"]:not(:checked):hover + label.btn-outline-success { background-color: #f0fdf4; } +.filter-option input[type="checkbox"]:not(:checked):hover + label.btn-outline-info { background-color: #f0f9ff; } +.filter-option input[type="checkbox"]:not(:checked):hover + label.btn-outline-dark { background-color: #f8fafc; } +.filter-option input[type="checkbox"]:not(:checked):hover + label.btn-outline-secondary { background-color: #f8fafc; } + /* Contextual badges - checked state */ -.filter-option input[type="checkbox"]:checked + label.context-badge { color: white; } -.filter-option input[type="checkbox"]:checked + label.btn-outline-primary { background-color: #0c5cc9; border-color: #0c5cc9; } -.filter-option input[type="checkbox"]:checked + label.btn-outline-danger { background-color: #dc3545; border-color: #dc3545; } -.filter-option input[type="checkbox"]:checked + label.btn-outline-warning { background-color: #ffc107; border-color: #ffc107; color: #212529 !important; } -.filter-option input[type="checkbox"]:checked + label.btn-outline-success { background-color: #28a745; border-color: #28a745; } -.filter-option input[type="checkbox"]:checked + label.btn-outline-info { background-color: #17a2b8; border-color: #17a2b8; } -.filter-option input[type="checkbox"]:checked + label.btn-outline-dark { background-color: #343a40; border-color: #343a40; } -.filter-option input[type="checkbox"]:checked + label.btn-outline-secondary { background-color: #6c757d; border-color: #6c757d; } - -/* Default badges - hover */ -.filter-option input[type="checkbox"]:hover + label:not(.context-badge) { background-color: #cfe2f3; } +.filter-option input[type="checkbox"]:checked + label.context-badge { + color: #ffffff !important; +} +.filter-option input[type="checkbox"]:checked + label.btn-outline-primary { + background-color: #0c5cc9 !important; border-color: #0c5cc9 !important; box-shadow: 0 2px 6px rgba(12, 92, 201, 0.2); +} +.filter-option input[type="checkbox"]:checked + label.btn-outline-danger { + background-color: #dc3545 !important; border-color: #dc3545 !important; box-shadow: 0 2px 6px rgba(220, 53, 69, 0.2); +} +.filter-option input[type="checkbox"]:checked + label.btn-outline-warning { + background-color: #f59e0b !important; border-color: #f59e0b !important; box-shadow: 0 2px 6px rgba(245, 158, 11, 0.2); +} +.filter-option input[type="checkbox"]:checked + label.btn-outline-success { + background-color: #198754 !important; border-color: #198754 !important; box-shadow: 0 2px 6px rgba(25, 135, 84, 0.2); +} +.filter-option input[type="checkbox"]:checked + label.btn-outline-info { + background-color: #0284c7 !important; border-color: #0284c7 !important; box-shadow: 0 2px 6px rgba(2, 132, 199, 0.2); +} +.filter-option input[type="checkbox"]:checked + label.btn-outline-dark { + background-color: #1e293b !important; border-color: #1e293b !important; box-shadow: 0 2px 6px rgba(30, 41, 59, 0.2); +} +.filter-option input[type="checkbox"]:checked + label.btn-outline-secondary { + background-color: #64748b !important; border-color: #64748b !important; box-shadow: 0 2px 6px rgba(100, 116, 139, 0.2); +} +/* Subcategories */ .filter-option.subcategory-option label { - font-size: 0.875em; - color: #555; + font-size: 0.76rem; + color: #64748b; } .filter-option-break { flex-basis: 100%; @@ -118,74 +259,34 @@ align-items: center; gap: 0.25rem; } - -.filter-section-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 0.75rem; - gap: 1rem; -} -.filter-section-header label { - font-weight: 600; - color: #333; - margin: 0; - flex-shrink: 0; -} -.filter-section-buttons { - display: flex; - gap: 0.5rem; - margin-left: auto; - flex-shrink: 0; -} -.filter-section-buttons button { - padding: 0.25rem 0.5rem; - font-size: 0.75rem; - white-space: nowrap; -} - -.filter-options { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; - min-height: 2.5rem; - padding: 0.5rem; - background-color: #f8f9fa; - border-radius: 0.375rem; - border: 1px solid #dee2e6; -} - -.filter-option { - display: flex; - align-items: center; -} - -.filter-option input[type="checkbox"] { - position: absolute; - opacity: 0; - width: 0; - height: 0; - cursor: pointer; +.filter-subcat-controls button { + font-size: 0.72rem; + padding: 0.15rem 0.5rem; + border-radius: 5px; + border: 1px solid #e2e8f0; + background: #fff; + color: #64748b; } /* Project Autocomplete & Chips */ .project-chip { font-size: 0.8rem; font-weight: 500; - border-radius: 1rem !important; + border-radius: 9999px !important; + background-color: #ffffff !important; + border: 1px solid #cbd5e1 !important; + color: #1e293b !important; transition: background-color 0.15s ease; - border-color: #ced4da !important; } .project-chip:hover { - background-color: #f1f3f5 !important; + background-color: #f1f5f9 !important; } .project-chip .remove-project-btn { opacity: 0.6; transition: opacity 0.15s ease; } - .project-chip .remove-project-btn:hover { opacity: 1; } @@ -200,43 +301,417 @@ #project-autocomplete-dropdown .list-group-item:hover, #project-autocomplete-dropdown .list-group-item.active-item { - background-color: #e9ecef; + background-color: #f1f5f9; } #project-autocomplete-dropdown .list-group-item.selected-project { - background-color: #e7f1ff; + background-color: #eff6ff; color: #0c5cc9; } -/* Limites de largeur pour les colonnes Code et Thématiques */ -#controls-table th[data-field="control_code"], -#controls-table td.col-code { +/* ─── Header Tabs & New Button ─────────────────────────────────────── */ +.controls-tabs { + border-bottom: 2px solid #e2e8f0; + margin-bottom: 0; +} + +.controls-tabs .nav-link { + font-size: 0.92rem; + font-weight: 600; + color: #64748b; + border: none; + border-bottom: 2px solid transparent; + margin-bottom: -2px; + padding: 0.5rem 1rem; + background: transparent; + transition: all 0.15s ease; + display: inline-flex; + align-items: center; + gap: 0.45rem; +} + +.controls-tabs .nav-link:hover { + color: #0f172a; + border-color: #cbd5e1; +} + +.controls-tabs .nav-link.active { + color: #0f172a; + border-bottom-color: #0f172a; + background: transparent; +} + +.tab-counter-badge { + font-size: 0.74rem; + font-weight: 600; + padding: 2px 7px; + border-radius: 9999px; + background-color: #f1f5f9; + color: #475569; + border: 1px solid #e2e8f0; +} + +.controls-tabs .nav-link.active .tab-counter-badge { + background-color: #0f172a; + color: #ffffff; + border-color: #0f172a; +} + +.btn-new-control { + background-color: #0f172a; + color: #ffffff; + border: 1px solid #0f172a; + border-radius: 8px; + padding: 0.42rem 0.85rem; + font-size: 0.84rem; + font-weight: 600; + display: inline-flex; + align-items: center; + gap: 0.45rem; + transition: all 0.15s ease; + text-decoration: none; + box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08); +} + +.btn-new-control:hover { + background-color: #1e293b; + color: #ffffff; + border-color: #1e293b; + transform: translateY(-1px); + box-shadow: 0 4px 6px -1px rgba(15, 23, 42, 0.12); +} + +/* ─── Active Filters Bar ───────────────────────────────────────────── */ +.active-filters-bar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.45rem; + padding: 0 0.15rem; +} + +.active-filter-label { + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: #94a3b8; + margin-right: 0.2rem; +} + +.active-filter-chip { + display: inline-flex; + align-items: center; + gap: 0.35rem; + padding: 0.2rem 0.6rem; + font-size: 0.76rem; + font-weight: 500; + border-radius: 9999px; + background-color: #f1f5f9; + border: 1px solid #e2e8f0; + color: #334155; + transition: all 0.12s ease; +} + +.active-filter-chip .remove-filter-btn { + border: none; + background: none; + padding: 0; + margin-left: 2px; + color: #94a3b8; + cursor: pointer; + display: inline-flex; + align-items: center; + font-size: 0.75rem; + line-height: 1; +} + +.active-filter-chip .remove-filter-btn:hover { + color: #dc2626; +} + +.btn-clear-all-filters { + font-size: 0.76rem; + color: #dc2626; + text-decoration: none; + font-weight: 600; + padding: 0.2rem 0.5rem; + border-radius: 6px; + transition: background-color 0.12s ease; +} + +.btn-clear-all-filters:hover { + background-color: #fef2f2; + color: #b91c1c; +} + +/* ─── Table Toolbar & Column Chooser ───────────────────────────────── */ +.controls-toolbar { + padding: 0.25rem 0; +} + +.filter-trigger-btn { + border: 1px solid #cbd5e1; + border-radius: 8px; + font-size: 0.82rem; + font-weight: 500; + padding: 0.32rem 0.75rem; + background-color: #ffffff; + color: #334155; + display: inline-flex; + align-items: center; + gap: 0.4rem; + transition: all 0.15s ease; +} +.filter-trigger-btn:hover { + background-color: #f8fafc; + border-color: #94a3b8; + color: #0f172a; +} + +#filter-badge { + background-color: #0f172a !important; + color: #ffffff !important; + font-size: 0.7rem; + font-weight: 700; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); + top: -5px !important; + right: -7px !important; + left: auto !important; + transform: none !important; + min-width: 1.25rem; + height: 1.25rem; + padding: 0 4px !important; + line-height: 1.25rem; +} +#filter-badge:not(.d-none) { + display: inline-flex !important; + align-items: center; + justify-content: center; +} + +#columns-dropdown-btn { + border: 1px solid #cbd5e1; + border-radius: 8px; + font-size: 0.82rem; + font-weight: 500; + padding: 0.32rem 0.75rem; + background-color: #ffffff; + color: #334155; +} +#columns-dropdown-btn:hover { + background-color: #f8fafc; + border-color: #94a3b8; + color: #0f172a; +} + +.columns-dropdown-menu { + border-radius: 12px; + border: 1px solid #e2e8f0; + box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.12), 0 8px 10px -6px rgba(0, 0, 0, 0.08); + min-width: 250px; + z-index: 1060; +} + +.columns-checkbox-list { + max-height: 280px; + overflow-y: auto; +} + +.columns-checkbox-list .form-check { + margin-bottom: 0.2rem; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.columns-checkbox-list .form-check-input { + cursor: pointer; + margin-top: 0; +} + +.columns-checkbox-list .form-check-label { + cursor: pointer; + font-size: 0.82rem; +} + +.page-size-selector { + font-size: 0.82rem; + color: #64748b; +} + +.page-size-select { + padding: 0.25rem 0.55rem; + font-size: 0.82rem; + border: 1px solid #cbd5e1; + border-radius: 7px; + background-color: #ffffff; + color: #334155; + cursor: pointer; + font-weight: 500; +} +.page-size-select:focus { + outline: none; + border-color: #2563eb; +} + +/* ─── Custom Controls Table Card ───────────────────────────────────── */ +.controls-table-card { + background-color: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 12px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03); + overflow: hidden; +} + +#controls-table-scrollable { + overflow-y: auto; + overflow-x: auto; +} + +/* Scrollbar discrète */ +#controls-table-scrollable::-webkit-scrollbar { + width: 6px; + height: 6px; +} +#controls-table-scrollable::-webkit-scrollbar-track { + background: #f8fafc; +} +#controls-table-scrollable::-webkit-scrollbar-thumb { + background: #cbd5e1; + border-radius: 4px; +} +#controls-table-scrollable::-webkit-scrollbar-thumb:hover { + background: #94a3b8; +} + +.controls-table { + width: 100%; + margin-bottom: 0; + border-collapse: separate; + border-spacing: 0; +} + +/* Table Headers (Sticky) */ +.controls-table thead th { + position: sticky; + top: 0; + z-index: 5; + background-color: #f8fafc !important; + color: #475569 !important; + font-size: 0.74rem !important; + font-weight: 600 !important; + text-transform: uppercase !important; + letter-spacing: 0.04em !important; + border-bottom: 1.5px solid #e2e8f0 !important; + padding: 0.55rem 0.75rem !important; + vertical-align: middle !important; + white-space: nowrap !important; + user-select: none; +} + +.controls-table thead th.sortable { + cursor: pointer; + transition: background-color 0.12s ease; +} + +.controls-table thead th.sortable:hover { + background-color: #f1f5f9 !important; + color: #0f172a !important; +} + +.sort-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.4rem; +} + +.sort-icon { + font-size: 0.75rem; +} + +/* Table Rows & Cells */ +.controls-table tbody td { + padding: 0.45rem 0.75rem !important; + vertical-align: middle !important; + font-size: 0.82rem !important; + color: #334155; + border-bottom: 1px solid #f1f5f9 !important; + line-height: 1.35 !important; +} + +.controls-table tbody tr { + transition: background-color 0.12s ease; +} + +.controls-table tbody tr:hover { + background-color: #f8fafc !important; +} + +.controls-table tbody tr.selected, +.controls-table tbody tr.rowhighlight { + background-color: #eff6ff !important; +} + +/* Code Link Badge in Table */ +.table-code-link { + font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace !important; + font-size: 0.79rem !important; + font-weight: 600 !important; + color: #0f172a !important; + text-decoration: none !important; + white-space: nowrap !important; + display: inline-block; + padding: 2px 7px; + border-radius: 5px; + background-color: #f8fafc; + border: 1px solid #e2e8f0; + transition: all 0.15s ease; +} +.table-code-link:hover { + color: #2563eb !important; + border-color: #93c5fd; + background-color: #eff6ff; +} + +/* Action button / Zoom button */ +.zoom-btn, +.table-action-btn { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + padding: 0; + border-radius: 6px; + color: #64748b; + background-color: transparent; + border: 1px solid transparent; + transition: all 0.15s ease; + text-decoration: none; +} +.zoom-btn:hover, +.table-action-btn:hover { + color: #0f172a; + background-color: #f1f5f9; + border-color: #cbd5e1; +} + +/* Limites de largeur pour colonnes */ +.controls-table th.col-control_code, +.controls-table td.col-control_code { max-width: 140px; width: 140px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; } -#controls-table td.col-code a { - display: inline-block; - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - vertical-align: middle; -} - -#controls-table th[data-field="thematic_names"], -#controls-table td.col-thematics { +.controls-table th.col-thematic_names, +.controls-table td.col-thematic_names { max-width: 180px; width: 180px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; } -#controls-table td.col-thematics .thematic-content { +.controls-table td.col-thematics .thematic-content { display: inline-block; max-width: 100%; overflow: hidden; @@ -245,29 +720,60 @@ vertical-align: middle; } -/* Layout helpers */ -.controls-layout #controls-table-container { - position: relative; - min-height: 420px; +/* Empty State */ +.table-empty-state { + padding: 2.5rem 1rem; + text-align: center; } -#filter-dropdown-wrapper { - position: absolute; - top: 0.5rem; - left: 0.5rem; - z-index: 10; +/* ─── Custom Pagination Bar ────────────────────────────────────────── */ +.controls-pagination-bar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.65rem 1rem; + border-top: 1px solid #e2e8f0; + background-color: #ffffff; + flex-wrap: wrap; + gap: 0.5rem; } -.filter-dropdown { - min-width: min(640px, calc(100vw - 4rem)); +.controls-pagination-bar .pagination-info { + font-size: 0.82rem; + color: #64748b; } +.controls-pagination-bar .pagination .page-item .page-link { + border-radius: 6px; + margin: 0 2px; + border-color: #e2e8f0; + color: #334155; + font-size: 0.8rem; + padding: 0.25rem 0.55rem; + text-decoration: none; +} + +.controls-pagination-bar .pagination .page-item.active .page-link { + background-color: #0f172a; + border-color: #0f172a; + color: #ffffff; +} + +.controls-pagination-bar .pagination .page-item.disabled .page-link { + color: #cbd5e1; + background-color: transparent; + border-color: #f1f5f9; +} + +/* ─── Map Container ────────────────────────────────────────────────── */ .controls-map { - height: calc(100vh - 210px); + height: calc(100vh - 190px); min-height: 360px; position: relative; width: 100%; - margin-top: 50px; + border-radius: 12px; + border: 1px solid #e2e8f0; + margin-top: 44px; } .controls-map-filter { @@ -276,6 +782,9 @@ right: 2.75rem; z-index: 5; box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.15); + border-radius: 8px; + font-weight: 500; + font-size: 0.8rem; } .popup-scrollable { @@ -289,16 +798,8 @@ } @media (max-width: 991.98px) { - .controls-layout #filter-dropdown-wrapper { - position: static; - margin-bottom: 1rem; - } - - .controls-layout #controls-table-container { - padding-top: 0; - } - - .filter-dropdown { - width: 100%; + .controls-layout .controls-toolbar { + flex-direction: column; + align-items: stretch; } } diff --git a/loko/controls/static/controls/controls_index.js b/loko/controls/static/controls/controls_index.js index befb256..0c60061 100644 --- a/loko/controls/static/controls/controls_index.js +++ b/loko/controls/static/controls/controls_index.js @@ -1,9 +1,32 @@ +/** + * Controls Index JavaScript + * Custom table implementation with client-side sorting, pagination, and column selection. + */ + let controls_geojson = {}; let allControls = []; +let filteredControls = []; let visibleRows = []; +let currentPage = 1; +let pageSize = 50; +let sortField = 'updated_at'; +let sortDir = 'desc'; +let hiddenColumns = new Set(); + const MAX_ITEMS = 5000; const DESCRIPTION_MAX_LENGTH = 60; +const CTRL_COLUMNS_KEY = 'controls_columns_hidden'; + +function escapeHtml(str) { + if (str === null || str === undefined) return ''; + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} function truncateText(text, limit) { if (!text || text.length <= limit) { @@ -12,6 +35,20 @@ function truncateText(text, limit) { return `${text.substring(0, limit - 3).trim()}...`; } +function dateFormatter(value) { + if (!value) return '-'; + try { + const date = new Date(value); + if (isNaN(date.getTime())) return '-'; + const lang = (document.documentElement.lang || 'fr').split('-')[0]; + const localeMap = { 'fr': 'fr-BE', 'nl': 'nl-BE', 'en': 'en-GB' }; + const locale = localeMap[lang] || 'fr-BE'; + return date.toLocaleDateString(locale, { day: '2-digit', month: '2-digit', year: 'numeric' }); + } catch (e) { + return '-'; + } +} + const STATUS_METADATA = { in_progress: { label: gettext('En cours'), badge: 'warning', color: '#ffc107' }, information: { label: gettext('Information'), badge: 'info', color: '#0dcaf0' }, @@ -36,6 +73,21 @@ function getControlTypeMetadata(type) { return CONTROL_TYPE_METADATA[type] || { label: type || '-', badge: 'secondary', icon: '' }; } +const AVAILABLE_COLUMNS = [ + { key: 'control_code', label: gettext('Code') }, + { key: 'thematic_names', label: gettext('Thématiques') }, + { key: 'category_names', label: gettext('Catégories') }, + { key: 'control_type_label', label: gettext('Type') }, + { key: 'status_label', label: gettext('Statut') }, + { key: 'project_name', label: gettext('Projet') }, + { key: 'location', label: gettext('Localisation') }, + { key: 'author', label: gettext('Auteur') }, + { key: 'remarks_count', label: gettext('Remarques') }, + { key: 'created_at', label: gettext('Création') }, + { key: 'updated_at', label: gettext('Mise à jour') }, + { key: 'description', label: gettext('Description') }, +]; + const controlsgeojsonloader = new GeojsonLoader(); const controls_layer_defs = [ { @@ -83,29 +135,32 @@ document.addEventListener('DOMContentLoaded', () => { setupFilterForm(); setupFilterZoneButton(); setupLayoutButtons(); + setupQuickFilterRemovers(); + updateFilterBadge(); const query = new URLSearchParams(window.location.search).toString(); const initialUrl = query ? `${controlsgeojsonUrl}?${query}` : controlsgeojsonUrl; controlsgeojsonloader.loadGeojson(initialUrl); - // Sync export button with initial URL filters _updateExportExcelUrl(query); }); controlsgeojsonloader.addEventListener('geojsonLoaded', (event) => { const geojsonData = event.detail; controls_geojson = geojsonData; - allControls = geojsonData.features || []; - visibleRows = [...allControls]; + const features = geojsonData.features || []; - loadDataIntoControlsTable(allControls); - fitMapToControls(allControls); - updateFilterLimitAlert(allControls.length); + loadDataIntoControlsTable(features); + fitMapToControls(features); + updateFilterLimitAlert(features.length); }); controlsgeojsonloader.addEventListener('geojsonError', () => { document.getElementById('controls-spinner')?.classList.add('d-none'); document.getElementById('controls-table-container')?.classList.remove('d-none'); + allControls = []; + filteredControls = []; + renderTable(); updateResultCount(0); }); @@ -195,7 +250,7 @@ function registerMapInteractions(event) { .setHTML(popupContent) .addTo(mapInstance); - highlightRow('#controls-table', 'id', props.id); + highlightControlRow(props.id); }); mapInstance.on('mouseenter', 'controls', () => { @@ -207,73 +262,155 @@ function registerMapInteractions(event) { }); } +// ─── Custom Table Core ─────────────────────────────────────────────── + +function getHiddenColumns() { + try { + const raw = localStorage.getItem(CTRL_COLUMNS_KEY); + if (!raw) return new Set(); + const parsed = JSON.parse(raw); + return new Set(Array.isArray(parsed) ? parsed : []); + } catch (e) { + return new Set(); + } +} + +function saveHiddenColumns(set) { + try { + localStorage.setItem(CTRL_COLUMNS_KEY, JSON.stringify([...set])); + } catch (e) { + // ignore + } +} + function initControlsTable() { - const $table = $('#controls-table'); - if (!$table.length) { - return; - } - - function calculateTableHeight() { - const windowHeight = $(window).height(); - const offset = 260; - return Math.max(360, windowHeight - offset); - } - - // Prepare initial sort from saved preferences - const initialSort = {}; - if (typeof SAVED_SORT !== 'undefined' && SAVED_SORT.sortName) { - initialSort.sortName = SAVED_SORT.sortName; - initialSort.sortOrder = SAVED_SORT.sortOrder || 'desc'; - } else { - initialSort.sortName = 'updated_at'; - initialSort.sortOrder = 'desc'; - } - - $table.bootstrapTable({ - columns: [ - { field: 'actions', title: '', formatter: 'actionsFormatter', class: 'text-nowrap' }, - { field: 'control_code', title: gettext('Code'), formatter: 'codeFormatter', class: 'text-nowrap col-code', sortable: true }, - { field: 'thematic_names', title: gettext('Thématiques'), formatter: 'thematicFormatter', class: 'col-thematics', sortable: true }, - { field: 'category_names', title: gettext('Catégories'), sortable: true }, - { field: 'control_type_label', title: gettext('Type'), formatter: 'controlTypeFormatter', class: 'text-nowrap', sortable: true }, - { field: 'status_label', title: gettext('Statut'), formatter: 'statusFormatter', class: 'text-nowrap', sortable: true }, - { field: 'project_name', title: gettext('Projet'), formatter: 'projectFormatter', sortable: true }, - { field: 'location', title: gettext('Localisation'), sortable: true }, - { field: 'author', title: gettext('Auteur'), class: 'text-nowrap', sortable: true }, - { field: 'remarks_count', title: gettext('Remarques'), formatter: 'remarksFormatter', class: 'text-center text-nowrap', sortable: true }, - { field: 'created_at', title: gettext('Création'), formatter: 'dateFormatter', class: 'text-nowrap', sortable: true }, - { field: 'updated_at', title: gettext('Mise à jour'), formatter: 'dateFormatter', class: 'text-nowrap', sortable: true }, - { field: 'description', title: gettext('Description'), formatter: 'descriptionFormatter', sortable: true } - ], - height: calculateTableHeight(), - ...initialSort - }); - // Restore saved column visibility - _restoreColumnVisibility($table); + hiddenColumns = getHiddenColumns(); - // Save column visibility when toggled - $table.on('column-switch.bs.table', function () { - setTimeout(function () { - _saveColumnVisibility($table); - $table.bootstrapTable('resetView'); - }, 0); + // Restore initial sort + if (typeof SAVED_SORT !== 'undefined' && SAVED_SORT && SAVED_SORT.sortName) { + sortField = SAVED_SORT.sortName; + sortDir = SAVED_SORT.sortOrder || 'desc'; + } else { + sortField = 'updated_at'; + sortDir = 'desc'; + } + + // Page size setup + const pageSizeSelect = document.getElementById('page-size-select'); + if (pageSizeSelect) { + pageSize = parseInt(pageSizeSelect.value, 10) || 50; + pageSizeSelect.addEventListener('change', function () { + pageSize = parseInt(this.value, 10) || 50; + currentPage = 1; + renderTable(); + }); + } + + // Columns chooser dropdown + initColumnChooser(); + + // Sortable headers click listeners + document.querySelectorAll('#controls-table thead th.sortable').forEach(th => { + th.addEventListener('click', function () { + const field = this.dataset.field; + if (!field) return; + handleHeaderSort(field); + }); }); - // Save sort preferences when sort changes - $table.on('sort.bs.table', function () { - setTimeout(savePreferencesFromUrl, 500); + // Pagination clicks delegation + document.getElementById('controls-pagination-list')?.addEventListener('click', (e) => { + const link = e.target.closest('a.page-link'); + if (!link) return; + e.preventDefault(); + const newPage = parseInt(link.dataset.page, 10); + if (!isNaN(newPage) && newPage >= 1 && newPage !== currentPage) { + currentPage = newPage; + renderTable(); + const scrollable = document.getElementById('controls-table-scrollable'); + if (scrollable) scrollable.scrollTo({ top: 0, behavior: 'smooth' }); + } }); - registerZoomAction('#controls-table', '.zoom-btn', map, 16); + // Zoom button click delegation in table + document.getElementById('controls-table')?.addEventListener('click', (e) => { + const btn = e.target.closest('.zoom-btn'); + if (!btn || !map) return; + const lon = parseFloat(btn.dataset.lon); + const lat = parseFloat(btn.dataset.lat); + if (!isNaN(lon) && !isNaN(lat)) { + map.flyTo({ + center: [lon, lat], + zoom: 16, + essential: true + }); + } + }); +} - $table.on('post-body.bs.table', () => { - visibleRows = $('#controls-table').bootstrapTable('getData'); - syncMapWithTable(); +function initColumnChooser() { + const container = document.getElementById('columns-list-container'); + if (!container) return; + + container.innerHTML = ''; + AVAILABLE_COLUMNS.forEach(col => { + const isVisible = !hiddenColumns.has(col.key); + const div = document.createElement('div'); + div.className = 'form-check py-1'; + div.innerHTML = ` + + + `; + container.appendChild(div); }); - $(window).on('resize', () => { - $table.bootstrapTable('resetView', { height: calculateTableHeight() }); + // Checkbox change listener + container.addEventListener('change', (e) => { + const cb = e.target.closest('.col-toggle-check'); + if (!cb) return; + const colKey = cb.dataset.column; + if (cb.checked) { + hiddenColumns.delete(colKey); + } else { + hiddenColumns.add(colKey); + } + saveHiddenColumns(hiddenColumns); + applyColumnVisibility(); + }); + + // Don't close dropdown on click inside + document.querySelector('.columns-dropdown-menu')?.addEventListener('click', (e) => { + e.stopPropagation(); + }); + + // Select all columns + document.getElementById('cols-select-all')?.addEventListener('click', (e) => { + e.preventDefault(); + hiddenColumns.clear(); + saveHiddenColumns(hiddenColumns); + container.querySelectorAll('.col-toggle-check').forEach(cb => { cb.checked = true; }); + applyColumnVisibility(); + }); + + // Reset columns to default + document.getElementById('cols-reset-default')?.addEventListener('click', (e) => { + e.preventDefault(); + hiddenColumns.clear(); + localStorage.removeItem(CTRL_COLUMNS_KEY); + container.querySelectorAll('.col-toggle-check').forEach(cb => { cb.checked = true; }); + applyColumnVisibility(); + }); + + applyColumnVisibility(); +} + +function applyColumnVisibility() { + AVAILABLE_COLUMNS.forEach(col => { + const isHidden = hiddenColumns.has(col.key); + document.querySelectorAll(`.col-${col.key}`).forEach(el => { + el.classList.toggle('d-none', isHidden); + }); }); } @@ -285,7 +422,6 @@ function loadDataIntoControlsTable(features) { const typeMeta = getControlTypeMetadata(props.control_type); return { - actions: '', id: props.id || feature.id, control_code: props.control_code || '', description: props.description || '', @@ -312,20 +448,292 @@ function loadDataIntoControlsTable(features) { }; }); - $('#controls-spinner').addClass('d-none'); - $('#controls-table-container').removeClass('d-none'); - $('#controls-table').bootstrapTable('load', tableData); + allControls = tableData; + filteredControls = [...allControls]; + sortData(filteredControls, sortField, sortDir); - visibleRows = tableData; + currentPage = 1; + renderTable(); + + document.getElementById('controls-spinner')?.classList.add('d-none'); + document.getElementById('controls-table-container')?.classList.remove('d-none'); + + visibleRows = filteredControls; syncMapWithTable(); - updateResultCount(tableData.length); + updateResultCount(filteredControls.length); + updateFilterBadge(); } +function sortData(data, field, dir) { + const multiplier = dir === 'asc' ? 1 : -1; + data.sort((a, b) => { + let valA = a[field]; + let valB = b[field]; + + if (valA === null || valA === undefined || valA === '') return 1 * multiplier; + if (valB === null || valB === undefined || valB === '') return -1 * multiplier; + + if (field === 'remarks_count') { + return (Number(valA) - Number(valB)) * multiplier; + } + if (field === 'created_at' || field === 'updated_at') { + return (new Date(valA).getTime() - new Date(valB).getTime()) * multiplier; + } + return String(valA).localeCompare(String(valB), undefined, { numeric: true, sensitivity: 'base' }) * multiplier; + }); +} + +function handleHeaderSort(field) { + if (sortField === field) { + sortDir = sortDir === 'asc' ? 'desc' : 'asc'; + } else { + sortField = field; + sortDir = ['updated_at', 'created_at', 'remarks_count'].includes(field) ? 'desc' : 'asc'; + } + + sortData(filteredControls, sortField, sortDir); + currentPage = 1; + renderTable(); + savePreferencesFromUrl(); +} + +function updateHeaderSortIcons() { + document.querySelectorAll('#controls-table thead th.sortable').forEach(th => { + const field = th.dataset.field; + const icon = th.querySelector('.sort-icon'); + if (!icon) return; + if (field === sortField) { + icon.className = `sort-icon bi ${sortDir === 'asc' ? 'bi-sort-up' : 'bi-sort-down'} text-primary`; + } else { + icon.className = 'sort-icon bi bi-arrow-down-up text-muted opacity-50'; + } + }); +} + +function renderTable() { + const tbody = document.getElementById('controls-table-tbody'); + if (!tbody) return; + + const totalItems = filteredControls.length; + const totalPages = Math.ceil(totalItems / pageSize) || 1; + if (currentPage > totalPages) currentPage = totalPages; + if (currentPage < 1) currentPage = 1; + + const startIndex = (currentPage - 1) * pageSize; + const endIndex = Math.min(startIndex + pageSize, totalItems); + const pageItems = filteredControls.slice(startIndex, endIndex); + + // État vide + if (totalItems === 0) { + tbody.innerHTML = ` + + +
+
+
${gettext('Aucun contrôle trouvé')}
+
${gettext('Aucun contrôle ne correspond à vos critères de recherche actuels.')}
+ +
+ + + `; + updatePaginationBar(0, 0, 0, 1); + updateHeaderSortIcons(); + applyColumnVisibility(); + return; + } + + // Rendu des lignes + let rowsHtml = ''; + for (let i = 0; i < pageItems.length; i++) { + rowsHtml += renderRowHtml(pageItems[i]); + } + tbody.innerHTML = rowsHtml; + + updatePaginationBar(startIndex + 1, endIndex, totalItems, totalPages); + updateHeaderSortIcons(); + applyColumnVisibility(); +} + +function renderRowHtml(row) { + const codeLabel = escapeHtml(row.control_code || `CTRL#${row.id}`); + const typeMeta = getControlTypeMetadata(row.control_type); + const statusMeta = getStatusMetadata(row.status); + const detailUrl = `${controlsIndexUrl}${row.id}/`; + + const zoomBtn = (row.lon && row.lat) + ? `` + : ``; + + const actionBtn = ``; + + const themIcon = row.thematic_icon ? `` : ''; + const typeIcon = typeMeta.icon ? `` : ''; + + const projectHtml = row.project_name && row.project_name !== '-' + ? (row.project_id && typeof projectsDetailBaseUrl !== 'undefined' + ? `${escapeHtml(row.project_name)}` + : `${escapeHtml(row.project_name)}`) + : '-'; + + const remarksCount = parseInt(row.remarks_count, 10) || 0; + const remarksBadge = remarksCount > 0 + ? `${remarksCount}` + : `0`; + + const descText = row.description ? truncateText(row.description, DESCRIPTION_MAX_LENGTH) : '-'; + const descTitle = row.description ? escapeHtml(row.description) : ''; + + return ` + + ${zoomBtn}${actionBtn} + + ${codeLabel} + + + ${themIcon}${escapeHtml(row.thematic_names || '-')} + + + ${escapeHtml(row.category_names || '-')} + + + ${typeIcon}${escapeHtml(row.control_type_label || typeMeta.label)} + + + ${escapeHtml(row.status_label || statusMeta.label)} + + + ${projectHtml} + + + ${escapeHtml(row.location || '-')} + + + ${escapeHtml(row.author || '-')} + + + ${remarksBadge} + + + ${dateFormatter(row.created_at)} + + + ${dateFormatter(row.updated_at)} + + + ${escapeHtml(descText)} + + + `; +} + +function updatePaginationBar(start, end, total, totalPages) { + const infoEl = document.getElementById('controls-pagination-info'); + const listEl = document.getElementById('controls-pagination-list'); + if (!infoEl || !listEl) return; + + if (total === 0) { + infoEl.textContent = gettext('Aucun contrôle chargé'); + listEl.innerHTML = ''; + return; + } + + infoEl.innerHTML = interpolate( + gettext('Affichage de %(start)s à %(end)s sur %(total)s contrôle(s)'), + { start, end, total }, + true + ); + + if (totalPages <= 1) { + listEl.innerHTML = ''; + return; + } + + let pagesHtml = ''; + + // Bouton Précédent + const prevDisabled = currentPage === 1 ? 'disabled' : ''; + pagesHtml += ` +
  • + + + +
  • + `; + + // Numéros de page + const pageNumbers = getPageRange(currentPage, totalPages); + pageNumbers.forEach(p => { + if (p === '...') { + pagesHtml += `
  • …
  • `; + } else { + const activeClass = p === currentPage ? 'active' : ''; + pagesHtml += ` +
  • + ${p} +
  • + `; + } + }); + + // Bouton Suivant + const nextDisabled = currentPage === totalPages ? 'disabled' : ''; + pagesHtml += ` +
  • + + + +
  • + `; + + listEl.innerHTML = pagesHtml; +} + +function getPageRange(current, total) { + if (total <= 7) { + return Array.from({ length: total }, (_, i) => i + 1); + } + if (current <= 4) { + return [1, 2, 3, 4, 5, '...', total]; + } + if (current >= total - 3) { + return [1, '...', total - 4, total - 3, total - 2, total - 1, total]; + } + return [1, '...', current - 1, current, current + 1, '...', total]; +} + +function highlightControlRow(id) { + const rowIndex = filteredControls.findIndex(row => String(row.id) === String(id)); + if (rowIndex === -1) return; + + const targetPage = Math.floor(rowIndex / pageSize) + 1; + if (currentPage !== targetPage) { + currentPage = targetPage; + renderTable(); + } + + setTimeout(() => { + document.querySelectorAll('#controls-table-tbody tr').forEach(tr => { + tr.classList.remove('selected', 'rowhighlight'); + }); + const targetTr = document.querySelector(`#controls-table-tbody tr[data-id="${id}"]`); + if (targetTr) { + targetTr.classList.add('selected', 'rowhighlight'); + targetTr.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + }, 60); +} + +// Alias global pour compatibilité avec base_script / popups +window.highlightRow = function (tableId, field, value) { + highlightControlRow(value); +}; + function updateFilterLimitAlert(count) { const alertEl = document.getElementById('filter-limit-alert'); - if (!alertEl) { - return; - } + if (!alertEl) return; const reached = count >= MAX_ITEMS; alertEl.classList.toggle('d-none', !reached); @@ -338,9 +746,9 @@ function updateFilterLimitAlert(count) { function updateResultCount(count) { const target = document.getElementById('controls-result-count'); - if (!target) { - return; - } + const badge = document.getElementById('controls-counter-badge'); + if (badge) badge.textContent = count; + if (!target) return; if (!count) { target.textContent = gettext('Aucun contrôle chargé'); @@ -352,9 +760,7 @@ function updateResultCount(count) { } function fitMapToControls(features) { - if (!map || !features.length) { - return; - } + if (!map || !features.length) return; const bounds = new maplibregl.LngLatBounds(); features.forEach((feature) => { @@ -369,33 +775,81 @@ function fitMapToControls(features) { } } -function setupFilterForm() { - const form = document.getElementById('filter-form'); - if (!form) { +function syncMapWithTable() { + if (!map || !map.getLayer('controls')) return; + + if (!visibleRows.length || visibleRows.length === allControls.length) { + map.setFilter('controls', null); return; } - // Apply filters button + const ids = visibleRows.map((row) => row.id); + if (!ids.length) { + map.setFilter('controls', ['==', ['get', 'id'], '__none__']); + return; + } + + map.setFilter('controls', ['match', ['get', 'id'], ['literal', ids], true, false]); +} + +function countActiveFilters() { + const form = document.getElementById('filter-form'); + if (!form) return 0; + let count = 0; + if (form.querySelectorAll('input[name="thematic"]:checked').length > 0) count++; + if (form.querySelectorAll('input[name="category"]:checked').length > 0) count++; + if (form.querySelectorAll('input[name="type"]:checked').length > 0) count++; + if (form.querySelectorAll('input[name="status"]:checked').length > 0) count++; + if (form.querySelectorAll('input[name="project"]').length > 0) count++; + const dateFrom = form.querySelector('input[name="date_from"]')?.value; + const dateTo = form.querySelector('input[name="date_to"]')?.value; + if (dateFrom || dateTo) count++; + if (form.querySelector('input[name="mine"]')?.checked) count++; + const searchVal = document.getElementById('controls-search-input')?.value.trim(); + if (searchVal) count++; + return count; +} + +function updateFilterBadge() { + const count = countActiveFilters(); + const badge = document.getElementById('filter-badge'); + const countSpan = document.getElementById('filter-count'); + if (badge && countSpan) { + if (count > 0) { + countSpan.textContent = count; + badge.classList.remove('d-none'); + } else { + badge.classList.add('d-none'); + } + } +} + +// ─── Filter Form & Preferences ─────────────────────────────────────── + +function setupFilterForm() { + const form = document.getElementById('filter-form'); + if (!form) return; + + // Bouton appliquer les filtres document.getElementById('apply-filters')?.addEventListener('click', () => { const params = buildFilterParams(new FormData(form)); _applySearchToParams(params); savePreferencesFromForm(params); refreshListing(params); + updateFilterBadge(); const filterModal = bootstrap.Modal.getInstance(document.getElementById('filterModal')); if (filterModal) filterModal.hide(); }); - // Reset filters button + // Bouton réinitialiser les filtres document.getElementById('reset-filters')?.addEventListener('click', () => { - // Uncheck filter checkboxes form.querySelectorAll('input[type="checkbox"][name="thematic"]').forEach(cb => { cb.checked = false; }); form.querySelectorAll('input[type="checkbox"][name="category"]').forEach(cb => { cb.checked = false; }); form.querySelectorAll('input[type="checkbox"][name="type"]').forEach(cb => { cb.checked = false; }); form.querySelectorAll('input[type="checkbox"][name="status"]').forEach(cb => { cb.checked = false; }); form.querySelectorAll('input[type="checkbox"][name="project"]').forEach(cb => { cb.checked = false; }); - // Reset dates and mine const dateFrom = form.querySelector('input[name="date_from"]'); const dateTo = form.querySelector('input[name="date_to"]'); if (dateFrom) dateFrom.value = ''; @@ -403,7 +857,6 @@ function setupFilterForm() { const mineCheck = form.querySelector('input[name="mine"]'); if (mineCheck) mineCheck.checked = false; - // Clear project chips const projContainer = document.getElementById('selected-projects-container'); if (projContainer) { projContainer.querySelectorAll('.project-chip').forEach(chip => chip.remove()); @@ -423,7 +876,6 @@ function setupFilterForm() { const projClear = document.getElementById('project-search-clear-btn'); if (projClear) projClear.style.display = 'none'; - // Reset category view document.querySelectorAll('.filter-options[data-field="category"] .filter-option[data-parent-id]:not([data-parent-id=""])').forEach(ch => { ch.style.display = 'none'; }); @@ -433,24 +885,24 @@ function setupFilterForm() { const params = buildFilterParams(new FormData(form)); _applySearchToParams(params); - // Save empty preferences on reset if (typeof savePreferencesUrl !== 'undefined') { fetch(savePreferencesUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRFToken': getCookie('csrftoken') }, - body: JSON.stringify({ filters: {}, sort: {} }) + body: JSON.stringify({ filters: {}, sort: { sortName: sortField, sortOrder: sortDir } }) }).catch(() => {}); } refreshListing(params); + updateFilterBadge(); const filterModal = bootstrap.Modal.getInstance(document.getElementById('filterModal')); if (filterModal) filterModal.hide(); }); - // Select-all / deselect-all buttons + // Boutons Tous / Aucun document.querySelectorAll('.select-all-btn').forEach(btn => { - btn.addEventListener('click', function(e) { + btn.addEventListener('click', function (e) { e.preventDefault(); const field = this.getAttribute('data-field'); const container = document.querySelector(`.filter-options[data-field="${field}"]`); @@ -463,7 +915,7 @@ function setupFilterForm() { }); document.querySelectorAll('.deselect-all-btn').forEach(btn => { - btn.addEventListener('click', function(e) { + btn.addEventListener('click', function (e) { e.preventDefault(); const field = this.getAttribute('data-field'); const container = document.querySelector(`.filter-options[data-field="${field}"]`); @@ -478,7 +930,7 @@ function setupFilterForm() { }); }); - // Thematic → category chain + // Chaînage Thématique → Catégorie document.querySelectorAll('input[name="thematic"]').forEach(cb => { cb.addEventListener('change', () => { filterCategoriesByThematics(); @@ -486,7 +938,7 @@ function setupFilterForm() { }); document.querySelectorAll('input[name="category"]').forEach(cb => { - cb.addEventListener('change', function() { + cb.addEventListener('change', function () { const option = this.closest('.filter-option'); const catId = option ? option.getAttribute('data-category-id') : null; const parentId = option ? option.getAttribute('data-parent-id') : null; @@ -519,7 +971,7 @@ function setupFilterForm() { }); }); - // Subcategory groups initialization + // Groupes de sous-catégories const catContainer = document.querySelector('.filter-options[data-field="category"]'); if (catContainer) { const parentIds = new Set(); @@ -572,7 +1024,7 @@ function setupFilterForm() { if (allChildren[0]) allChildren[0].style.marginLeft = '0.5rem'; - // Controls Tous/Aucun + // Contrôles Tous/Aucun const controls = document.createElement('div'); controls.className = 'filter-subcat-controls'; controls.setAttribute('data-for-parent', parentId); @@ -582,7 +1034,7 @@ function setupFilterForm() { ``; allChildren[allChildren.length - 1].insertAdjacentElement('afterend', controls); - controls.querySelector('.subcat-select-all').addEventListener('click', function(e) { + controls.querySelector('.subcat-select-all').addEventListener('click', function (e) { e.preventDefault(); catContainer.querySelectorAll(`.filter-option[data-parent-id="${parentId}"]`).forEach(opt => { if (opt.style.display !== 'none') { @@ -591,7 +1043,7 @@ function setupFilterForm() { } }); }); - controls.querySelector('.subcat-deselect-all').addEventListener('click', function(e) { + controls.querySelector('.subcat-deselect-all').addEventListener('click', function (e) { e.preventDefault(); catContainer.querySelectorAll(`.filter-option[data-parent-id="${parentId}"]`).forEach(opt => { const cb = opt.querySelector('input[type="checkbox"]'); @@ -648,7 +1100,7 @@ function setupProjectAutocomplete() { function addProjectChip(id, name) { const currentIds = getSelectedProjectIds(); if (currentIds.includes(String(id))) { - return; // already added + return; } const chip = document.createElement('div'); @@ -656,7 +1108,7 @@ function setupProjectAutocomplete() { chip.setAttribute('data-project-id', id); chip.innerHTML = ` - ${name} + ${escapeHtml(name)} `; @@ -665,7 +1117,6 @@ function setupProjectAutocomplete() { updateEmptyMessage(); } - // Remove project chip handler (delegated) container.addEventListener('click', (e) => { const removeBtn = e.target.closest('.remove-project-btn'); if (removeBtn) { @@ -677,14 +1128,12 @@ function setupProjectAutocomplete() { } }); - // Clear all projects button clearAllBtn?.addEventListener('click', (e) => { e.preventDefault(); container.querySelectorAll('.project-chip').forEach(chip => chip.remove()); updateEmptyMessage(); }); - // Autocomplete search input.addEventListener('input', () => { const q = input.value.trim(); if (clearBtn) clearBtn.style.display = q ? '' : 'none'; @@ -718,7 +1167,7 @@ function setupProjectAutocomplete() { item.setAttribute('data-project-name', p.name); item.setAttribute('data-index', idx); item.innerHTML = ` - ${p.label} + ${escapeHtml(p.label)} ${isSelected ? `${gettext('Ajouté')}` : ``} `; item.addEventListener('click', () => { @@ -735,7 +1184,6 @@ function setupProjectAutocomplete() { }, 200); }); - // Keyboard navigation in autocomplete input.addEventListener('keydown', (e) => { const items = dropdown.querySelectorAll('.list-group-item-action'); if (!items.length || dropdown.classList.contains('d-none')) { @@ -770,7 +1218,6 @@ function setupProjectAutocomplete() { input.focus(); }); - // Close on click outside document.addEventListener('click', (e) => { if (!input.contains(e.target) && !dropdown.contains(e.target)) { closeDropdown(); @@ -841,8 +1288,8 @@ function refreshListing(params) { _updateExportExcelUrl(queryString); - $('#controls-spinner').removeClass('d-none'); - $('#controls-table-container').addClass('d-none'); + document.getElementById('controls-spinner')?.classList.remove('d-none'); + document.getElementById('controls-table-container')?.classList.add('d-none'); handleGeojsonMapLoading(false, controlsgeojsonloader, 'controls'); controlsgeojsonloader.loadGeojson(targetUrl); @@ -854,7 +1301,30 @@ function _updateExportExcelUrl(queryString) { btn.href = queryString ? `${controlsExportExcelUrl}?${queryString}` : controlsExportExcelUrl; } -// === Search widget === +// ─── Suppression rapide des filtres actifs (chips) ─────────────────── + +function setupQuickFilterRemovers() { + document.querySelectorAll('.remove-filter-btn').forEach(btn => { + btn.addEventListener('click', function (e) { + e.preventDefault(); + const param = this.dataset.param; + const val = this.dataset.val; + const url = new URL(window.location.href); + + if (param === 'q' || param === 'date_from' || param === 'date_to' || param === 'mine') { + url.searchParams.delete(param); + } else { + const remaining = url.searchParams.getAll(param).filter(v => v !== val); + url.searchParams.delete(param); + remaining.forEach(v => url.searchParams.append(param, v)); + } + + window.location.href = url.toString(); + }); + }); +} + +// ─── Search Widget ─────────────────────────────────────────────────── function _applySearchToParams(params) { const q = $('#controls-search-input').val().trim(); @@ -915,111 +1385,17 @@ window.addEventListener('load', function () { function setupFilterZoneButton() { const button = document.getElementById('filterzone'); - if (!button) { - return; - } + if (!button) return; button.addEventListener('click', (event) => { event.preventDefault(); - if (!map || !controls_geojson.features) { - return; - } + if (!map || !controls_geojson.features) return; filterTableByMapBounds(map, controls_geojson, loadDataIntoControlsTable); }); } -function syncMapWithTable() { - if (!map || !map.getLayer('controls')) { - return; - } +// ─── Layout Switcher ───────────────────────────────────────────────── - if (!visibleRows.length || visibleRows.length === allControls.length) { - map.setFilter('controls', null); - return; - } - - const ids = visibleRows.map((row) => row.id); - if (!ids.length) { - map.setFilter('controls', ['==', ['get', 'id'], '__none__']); - return; - } - - map.setFilter('controls', ['match', ['get', 'id'], ['literal', ids], true, false]); -} - -// Table column formatters - -function actionsFormatter(value, row) { - let html = ''; - if (row.lon && row.lat) { - html += ` - `; - } - return html; -} - -function codeFormatter(value, row) { - const label = value || '-'; - return `${label}`; -} - -function descriptionFormatter(value, row) { - if (!value || !value.trim()) return '-'; - const safeValue = value.trim(); - const truncated = truncateText(safeValue, DESCRIPTION_MAX_LENGTH); - return `${truncated}`; -} - -function thematicFormatter(value, row) { - if (!value || value === '-') { - return '-'; - } - const icon = row.thematic_icon ? `` : ''; - return `${icon}${value}`; -} - -function statusFormatter(value, row) { - if (!value) { - return '-'; - } - const meta = getStatusMetadata(row.status); - const label = row.status_label || meta.label; - return `${label}`; -} - -function controlTypeFormatter(value, row) { - const meta = getControlTypeMetadata(row.control_type); - const label = value || meta.label; - const icon = row.control_type_icon ? `` : ''; - return `${icon}${label}`; -} - -function projectFormatter(value, row) { - if (!value || value === '-') return '-'; - if (row.project_id && typeof projectsDetailBaseUrl !== 'undefined') { - return `${value}`; - } - return `${value}`; -} - -function remarksFormatter(value, row) { - const count = parseInt(value, 10) || 0; - if (count === 0) return '0'; - return `${count}`; -} - -window.actionsFormatter = actionsFormatter; -window.codeFormatter = codeFormatter; -window.descriptionFormatter = descriptionFormatter; -window.thematicFormatter = thematicFormatter; -window.statusFormatter = statusFormatter; -window.controlTypeFormatter = controlTypeFormatter; -window.projectFormatter = projectFormatter; -window.remarksFormatter = remarksFormatter; - -// Layout switcher function setupLayoutButtons() { const btnTable = document.getElementById('layout-btn-table'); const btnBoth = document.getElementById('layout-btn-both'); @@ -1027,9 +1403,7 @@ function setupLayoutButtons() { const colLeft = document.getElementById('col-left'); const colRight = document.getElementById('col-right'); - if (!btnTable || !btnBoth || !btnMap || !colLeft || !colRight) { - return; - } + if (!btnTable || !btnBoth || !btnMap || !colLeft || !colRight) return; function setLayout(layout) { [btnTable, btnBoth, btnMap].forEach(btn => btn.classList.remove('text-primary')); @@ -1045,7 +1419,6 @@ function setupLayoutButtons() { colRight.classList.remove('d-none', 'col-md-6'); colRight.classList.add('col-12'); } else { - // both (default) btnBoth.classList.add('text-primary'); colLeft.classList.remove('d-none', 'col-12'); colLeft.classList.add('col-md-6'); @@ -1057,7 +1430,6 @@ function setupLayoutButtons() { setTimeout(() => { maploader.map?.resize(); - $('#controls-table').bootstrapTable('resetView'); }, 100); } @@ -1069,7 +1441,7 @@ function setupLayoutButtons() { setLayout(savedLayout); } -// Preferences Management +// ─── Preferences Management ────────────────────────────────────────── function getCookie(name) { let cookieValue = null; @@ -1086,36 +1458,6 @@ function getCookie(name) { return cookieValue; } -const CTRL_COLUMNS_KEY = 'controls_columns_hidden'; - -function _saveColumnVisibility($table) { - try { - const allCols = $table.bootstrapTable('getOptions').columns[0] || []; - const visibleFields = $table.bootstrapTable('getVisibleColumns').map((c) => c.field); - const hiddenFields = allCols - .filter((c) => c.field && c.field !== 'actions' && !visibleFields.includes(c.field)) - .map((c) => c.field); - localStorage.setItem(CTRL_COLUMNS_KEY, JSON.stringify(hiddenFields)); - } catch (e) { - // ignore - } -} - -function _restoreColumnVisibility($table) { - try { - const raw = localStorage.getItem(CTRL_COLUMNS_KEY); - if (!raw) return; - const hiddenFields = JSON.parse(raw); - if (Array.isArray(hiddenFields)) { - hiddenFields.forEach((field) => { - $table.bootstrapTable('hideColumn', field); - }); - } - } catch (e) { - // ignore - } -} - function savePreferencesFromUrl() { if (typeof savePreferencesUrl === 'undefined') return; @@ -1131,14 +1473,7 @@ function savePreferencesFromUrl() { } }); - const $table = $('#controls-table'); - let sort = {}; - if ($table.length && $table.data('bootstrap.table')) { - const options = $table.bootstrapTable('getOptions'); - if (options.sortName) { - sort = { sortName: options.sortName, sortOrder: options.sortOrder || 'desc' }; - } - } + const sort = { sortName: sortField, sortOrder: sortDir }; fetch(savePreferencesUrl, { method: 'POST', @@ -1161,14 +1496,7 @@ async function savePreferencesFromForm(urlParams) { } }); - const $table = $('#controls-table'); - let sort = {}; - if ($table.length && $table.data('bootstrap.table')) { - const options = $table.bootstrapTable('getOptions'); - if (options.sortName) { - sort = { sortName: options.sortName, sortOrder: options.sortOrder || 'desc' }; - } - } + const sort = { sortName: sortField, sortOrder: sortDir }; try { await fetch(savePreferencesUrl, { diff --git a/loko/controls/templates/controls/controls_index.html b/loko/controls/templates/controls/controls_index.html index 72a7216..a49f28c 100644 --- a/loko/controls/templates/controls/controls_index.html +++ b/loko/controls/templates/controls/controls_index.html @@ -8,24 +8,27 @@ {% block content %} - +