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 = ` +