diff --git a/loko/interventions/locale/en/LC_MESSAGES/django.mo b/loko/interventions/locale/en/LC_MESSAGES/django.mo index 5252ef2..9e46f09 100644 Binary files a/loko/interventions/locale/en/LC_MESSAGES/django.mo and b/loko/interventions/locale/en/LC_MESSAGES/django.mo differ diff --git a/loko/interventions/locale/en/LC_MESSAGES/django.po b/loko/interventions/locale/en/LC_MESSAGES/django.po index e9d5ea4..27b9a03 100644 --- a/loko/interventions/locale/en/LC_MESSAGES/django.po +++ b/loko/interventions/locale/en/LC_MESSAGES/django.po @@ -8458,5 +8458,27 @@ msgstr "My favorite filters" msgid "Enregistrer le filtre actuel..." msgstr "Save current filter..." +msgid "Classification & Statut" +msgstr "Classification & Status" +msgid "Acteurs & Marchés" +msgstr "Actors & Contracts" + +msgid "Dates & Lieux" +msgstr "Dates & Locations" + +msgid "Choisir les colonnes à afficher" +msgstr "Choose columns to display" + +msgid "Toutes" +msgstr "All" + +msgid "Défaut" +msgstr "Default" + +msgid "Afficher par" +msgstr "Show by" + +msgid "Aucune intervention ne correspond aux critères de recherche." +msgstr "No interventions match the search criteria." diff --git a/loko/interventions/locale/en/LC_MESSAGES/djangojs.mo b/loko/interventions/locale/en/LC_MESSAGES/djangojs.mo index 7a77808..424e6f8 100644 Binary files a/loko/interventions/locale/en/LC_MESSAGES/djangojs.mo and b/loko/interventions/locale/en/LC_MESSAGES/djangojs.mo differ diff --git a/loko/interventions/locale/en/LC_MESSAGES/djangojs.po b/loko/interventions/locale/en/LC_MESSAGES/djangojs.po index 8a6fc48..518fbf4 100644 --- a/loko/interventions/locale/en/LC_MESSAGES/djangojs.po +++ b/loko/interventions/locale/en/LC_MESSAGES/djangojs.po @@ -1298,6 +1298,24 @@ msgstr "Error updating favorite filter." msgid "Filtre '%s' appliqué." msgstr "Filter '%s' applied." +msgid "Filtres actifs" +msgstr "Active filters" + +msgid "Supprimer ce critère" +msgstr "Remove this criterion" + +msgid "Tout effacer" +msgstr "Clear all" + +msgid "Rechercher dans les colonnes..." +msgstr "Search columns..." + +msgid "Lignes par page" +msgstr "Rows per page" + +msgid "Afficher par" +msgstr "Show by" + #~ msgid "Veuillez entrer un entier de maximum 6 chiffres" #~ msgstr "Please enter an integer with maximum 6 digits" diff --git a/loko/interventions/locale/fr/LC_MESSAGES/django.mo b/loko/interventions/locale/fr/LC_MESSAGES/django.mo index 54b4a0f..8473bcb 100644 Binary files a/loko/interventions/locale/fr/LC_MESSAGES/django.mo and b/loko/interventions/locale/fr/LC_MESSAGES/django.mo differ diff --git a/loko/interventions/locale/fr/LC_MESSAGES/django.po b/loko/interventions/locale/fr/LC_MESSAGES/django.po index f57b1d7..801ca76 100644 --- a/loko/interventions/locale/fr/LC_MESSAGES/django.po +++ b/loko/interventions/locale/fr/LC_MESSAGES/django.po @@ -8565,5 +8565,27 @@ msgstr "Mes filtres favoris" msgid "Enregistrer le filtre actuel..." msgstr "Enregistrer le filtre actuel..." +msgid "Classification & Statut" +msgstr "Classification & Statut" +msgid "Acteurs & Marchés" +msgstr "Acteurs & Marchés" + +msgid "Dates & Lieux" +msgstr "Dates & Lieux" + +msgid "Choisir les colonnes à afficher" +msgstr "Choisir les colonnes à afficher" + +msgid "Toutes" +msgstr "Toutes" + +msgid "Défaut" +msgstr "Défaut" + +msgid "Afficher par" +msgstr "Afficher par" + +msgid "Aucune intervention ne correspond aux critères de recherche." +msgstr "Aucune intervention ne correspond aux critères de recherche." diff --git a/loko/interventions/locale/fr/LC_MESSAGES/djangojs.mo b/loko/interventions/locale/fr/LC_MESSAGES/djangojs.mo index 238c680..f6679b7 100644 Binary files a/loko/interventions/locale/fr/LC_MESSAGES/djangojs.mo and b/loko/interventions/locale/fr/LC_MESSAGES/djangojs.mo differ diff --git a/loko/interventions/locale/fr/LC_MESSAGES/djangojs.po b/loko/interventions/locale/fr/LC_MESSAGES/djangojs.po index db25a87..2bd048f 100644 --- a/loko/interventions/locale/fr/LC_MESSAGES/djangojs.po +++ b/loko/interventions/locale/fr/LC_MESSAGES/djangojs.po @@ -1305,6 +1305,24 @@ msgstr "Erreur lors de la mise à jour du favori." msgid "Filtre '%s' appliqué." msgstr "Filtre '%s' appliqué." +msgid "Filtres actifs" +msgstr "Filtres actifs" + +msgid "Supprimer ce critère" +msgstr "Supprimer ce critère" + +msgid "Tout effacer" +msgstr "Tout effacer" + +msgid "Rechercher dans les colonnes..." +msgstr "Rechercher dans les colonnes..." + +msgid "Lignes par page" +msgstr "Lignes par page" + +msgid "Afficher par" +msgstr "Afficher par" + #~ msgid "Veuillez entrer un entier de maximum 6 chiffres" #~ msgstr "Veuillez entrer un entier de maximum 6 chiffres" diff --git a/loko/interventions/locale/nl/LC_MESSAGES/django.mo b/loko/interventions/locale/nl/LC_MESSAGES/django.mo index 0870108..5c7dce2 100644 Binary files a/loko/interventions/locale/nl/LC_MESSAGES/django.mo and b/loko/interventions/locale/nl/LC_MESSAGES/django.mo differ diff --git a/loko/interventions/locale/nl/LC_MESSAGES/django.po b/loko/interventions/locale/nl/LC_MESSAGES/django.po index 80a019d..7e3d375 100644 --- a/loko/interventions/locale/nl/LC_MESSAGES/django.po +++ b/loko/interventions/locale/nl/LC_MESSAGES/django.po @@ -8499,4 +8499,27 @@ msgstr "Mijn favoriete filters" msgid "Enregistrer le filtre actuel..." msgstr "Huidige filter opslaan..." +msgid "Classification & Statut" +msgstr "Classificatie & Status" + +msgid "Acteurs & Marchés" +msgstr "Betrokkenen & Contracten" + +msgid "Dates & Lieux" +msgstr "Datums & Locaties" + +msgid "Choisir les colonnes à afficher" +msgstr "Kies de weer te geven kolommen" + +msgid "Toutes" +msgstr "Alle" + +msgid "Défaut" +msgstr "Standaard" + +msgid "Afficher par" +msgstr "Weergeven per" + +msgid "Aucune intervention ne correspond aux critères de recherche." +msgstr "Geen interventies gevonden die voldoen aan de zoekcriteria." diff --git a/loko/interventions/locale/nl/LC_MESSAGES/djangojs.mo b/loko/interventions/locale/nl/LC_MESSAGES/djangojs.mo index 50b59f3..4776c6a 100644 Binary files a/loko/interventions/locale/nl/LC_MESSAGES/djangojs.mo and b/loko/interventions/locale/nl/LC_MESSAGES/djangojs.mo differ diff --git a/loko/interventions/locale/nl/LC_MESSAGES/djangojs.po b/loko/interventions/locale/nl/LC_MESSAGES/djangojs.po index 5f4ebae..ac9ccf6 100644 --- a/loko/interventions/locale/nl/LC_MESSAGES/djangojs.po +++ b/loko/interventions/locale/nl/LC_MESSAGES/djangojs.po @@ -1297,6 +1297,24 @@ msgstr "Fout bij het bijwerken van het favoriete filter." msgid "Filtre '%s' appliqué." msgstr "Filter '%s' toegepast." +msgid "Filtres actifs" +msgstr "Actieve filters" + +msgid "Supprimer ce critère" +msgstr "Dit criterium verwijderen" + +msgid "Tout effacer" +msgstr "Alles wissen" + +msgid "Rechercher dans les colonnes..." +msgstr "Kolommen zoeken..." + +msgid "Lignes par page" +msgstr "Rijen per pagina" + +msgid "Afficher par" +msgstr "Weergeven per" + #~ msgid "Veuillez entrer un entier de maximum 6 chiffres" #~ msgstr "Voer een geheel getal van maximaal 6 cijfers in" diff --git a/loko/interventions/static/interventions/interventions_index.css b/loko/interventions/static/interventions/interventions_index.css new file mode 100644 index 0000000..0eff3bc --- /dev/null +++ b/loko/interventions/static/interventions/interventions_index.css @@ -0,0 +1,954 @@ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;600&display=swap'); + +/* ========================================================================== + Interventions Index CSS (Modern Airbnb-style UI & Custom Table) + ========================================================================== */ + +/* Typography & Global Layout */ +.interventions-layout { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; +} + +/* Supprime le contour bleu Bootstrap au focus sur les inputs */ +#interventions-search-input:focus, +#location-autocomplete:focus, +#order-search:focus { + outline: none; + box-shadow: none; + border-color: #dee2e6; +} + +#interventions-search-widget { + max-width: calc(100% - 340px); +} + +@media (max-width: 767.98px) { + #interventions-search-widget { + position: static !important; + transform: none !important; + max-width: 100% !important; + margin-top: 0.5rem; + margin-bottom: 0.5rem; + pointer-events: auto !important; + } + #interventions-search-widget .input-group > div:first-child { + width: 100% !important; + } +} + +/* ─── Airbnb-style Filter Modal ────────────────────────────────────── */ +#filterModal .modal-dialog { + max-width: 880px; + 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: 88vh; + overflow: hidden; + background-color: #ffffff; +} + +#filterModal .modal-header { + padding: 1.25rem 1.5rem 0.5rem 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; +} + +/* Filter Modal Tabs */ +.filter-modal-tabs { + gap: 0.5rem; + border-bottom: 1px solid #e2e8f0; + margin-top: 0.75rem; +} + +.filter-modal-tabs .nav-link { + border: none; + border-bottom: 2.5px solid transparent; + color: #64748b; + font-size: 0.88rem; + font-weight: 500; + padding: 0.6rem 0.85rem; + background: transparent; + border-radius: 0; + display: inline-flex; + align-items: center; + gap: 0.4rem; + transition: all 0.15s ease; +} + +.filter-modal-tabs .nav-link:hover { + color: #0f172a; + border-bottom-color: #cbd5e1; +} + +.filter-modal-tabs .nav-link.active { + color: #0f172a; + font-weight: 700; + border-bottom-color: #0f172a; + background: transparent; +} + +.filter-modal-tabs .nav-link .badge { + font-size: 0.68rem; + padding: 2px 6px; + background-color: #0f172a !important; +} + +#filterModal .modal-body { + padding: 1.25rem 1.5rem; + overflow-y: auto; + background-color: #f8fafc; +} + +#filterModal .modal-footer { + padding: 1rem 1.5rem; + border-top: 1px solid #f1f5f9; + background-color: #ffffff; + display: flex; + justify-content: space-between; + align-items: center; +} + +/* Filter Section Cards inside Modal */ +.filter-section { + background-color: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 14px; + padding: 1.15rem; + margin-bottom: 1rem; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02); + transition: border-color 0.15s ease, box-shadow 0.15s ease; +} + +.filter-section:hover { + border-color: #cbd5e1; +} + +.filter-section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 0.75rem; + gap: 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.35rem; + margin-left: auto; +} + +.filter-section-buttons .btn, +.select-all-btn, +.deselect-all-btn { + font-size: 0.74rem; + font-weight: 500; + padding: 0.2rem 0.55rem; + border-radius: 6px; + border: 1px solid #e2e8f0; + background-color: #ffffff; + color: #64748b; + transition: all 0.15s ease; +} + +.filter-section-buttons .btn:hover, +.select-all-btn:hover, +.deselect-all-btn:hover { + background-color: #f1f5f9; + color: #0f172a; + border-color: #cbd5e1; +} + +/* Thematic Grid (Airbnb-style visual cards) */ +.thematic-grid { + display: grid !important; + grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)) !important; + gap: 0.65rem !important; + width: 100% !important; +} + +@media (max-width: 576px) { + .thematic-grid { + grid-template-columns: repeat(2, 1fr) !important; + } +} + +.thematic-tile { + position: relative; + display: flex !important; +} + +.thematic-tile input[type="checkbox"] { + position: absolute; + opacity: 0; + width: 0; + height: 0; + pointer-events: none; +} + +.thematic-tile label { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + width: 100%; + min-height: 90px; + padding: 0.75rem 0.5rem; + background-color: #ffffff; + border: 1.5px solid #e2e8f0; + border-radius: 12px; + cursor: pointer; + transition: all 0.18s cubic-bezier(0.16, 1, 0.3, 1); + user-select: none; + margin: 0; +} + +.thematic-tile .thematic-icon { + font-size: 1.75rem; + color: #475569; + margin-bottom: 0.4rem; + transition: transform 0.18s ease, color 0.18s ease; + line-height: 1; +} + +.thematic-tile .thematic-label { + font-size: 0.78rem; + font-weight: 600; + color: #334155; + line-height: 1.25; + transition: color 0.18s ease; +} + +.thematic-tile:hover label { + background-color: #f8fafc; + border-color: #94a3b8; + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); +} + +.thematic-tile:hover .thematic-icon { + color: #0f172a; + transform: scale(1.08); +} + +.thematic-tile input[type="checkbox"]:checked + label { + background-color: #0f172a !important; + border-color: #0f172a !important; + box-shadow: 0 4px 14px rgba(15, 23, 42, 0.22); + transform: translateY(-1px); +} + +.thematic-tile input[type="checkbox"]:checked + label .thematic-icon { + color: #ffffff !important; + transform: scale(1.05); +} + +.thematic-tile input[type="checkbox"]:checked + label .thematic-label { + color: #ffffff !important; +} + +/* Filter Options (Pills) */ +.filter-options { + display: flex; + flex-wrap: wrap; + gap: 0.45rem; + align-items: center; + background-color: transparent !important; + border: none !important; + padding: 0 !important; + min-height: auto !important; +} + +.filter-option { + display: inline-flex; + margin: 0; + position: relative; +} + +.filter-option input[type="checkbox"], +.filter-option input[type="radio"] { + position: absolute; + opacity: 0; + width: 0; + height: 0; + pointer-events: none; +} + +.filter-option label { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.35rem; + padding: 0.35rem 0.8rem; + font-size: 0.78rem; + font-weight: 500; + color: #334155; + background-color: #ffffff; + border: 1.5px solid #e2e8f0; + border-radius: 9999px; + cursor: pointer; + transition: all 0.15s ease; + user-select: none; + white-space: nowrap; + margin: 0; +} + +.filter-option label:hover { + background-color: #f8fafc; + border-color: #94a3b8; + color: #0f172a; +} + +/* Checked state for generic badges */ +.filter-option input[type="checkbox"]:checked + label:not(.context-badge) { + 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 (Statut, maintenance, stock) */ +.filter-option label.context-badge { + border-color: currentColor; + color: inherit; + background-color: transparent; +} + +.filter-option label.btn-outline-primary { border-color: #0d6efd; color: #0d6efd; } +.filter-option label.btn-outline-danger { border-color: #dc3545; color: #dc3545; } +.filter-option label.btn-outline-warning { border-color: #d97706; color: #b45309; } +.filter-option label.btn-outline-success { border-color: #16a34a; color: #15803d; } +.filter-option label.btn-outline-info { border-color: #0891b2; color: #0e7490; } +.filter-option label.btn-outline-dark { border-color: #334155; color: #1e293b; } +.filter-option label.btn-outline-secondary { border-color: #64748b; color: #475569; } + +.filter-option input[type="checkbox"]:checked + label.btn-outline-primary { + background-color: #0d6efd !important; + border-color: #0d6efd !important; + color: #ffffff !important; +} +.filter-option input[type="checkbox"]:checked + label.btn-outline-danger { + background-color: #dc3545 !important; + border-color: #dc3545 !important; + color: #ffffff !important; +} +.filter-option input[type="checkbox"]:checked + label.btn-outline-warning { + background-color: #d97706 !important; + border-color: #d97706 !important; + color: #ffffff !important; +} +.filter-option input[type="checkbox"]:checked + label.btn-outline-success { + background-color: #16a34a !important; + border-color: #16a34a !important; + color: #ffffff !important; +} +.filter-option input[type="checkbox"]:checked + label.btn-outline-info { + background-color: #0891b2 !important; + border-color: #0891b2 !important; + color: #ffffff !important; +} +.filter-option input[type="checkbox"]:checked + label.btn-outline-dark { + background-color: #0f172a !important; + border-color: #0f172a !important; + color: #ffffff !important; +} +.filter-option input[type="checkbox"]:checked + label.btn-outline-secondary { + background-color: #64748b !important; + border-color: #64748b !important; + color: #ffffff !important; +} + +/* Priority styling with modern colors */ +.filter-option label.btn-outline-priority-1 { border-color: #dc3545; color: #dc3545; } +.filter-option label.btn-outline-priority-2 { border-color: #f97316; color: #ea580c; } +.filter-option label.btn-outline-priority-3 { border-color: #0284c7; color: #0369a1; } +.filter-option label.btn-outline-priority-4 { border-color: #64748b; color: #475569; } + +.filter-option input[type="checkbox"]:checked + label.btn-outline-priority-1 { + background: linear-gradient(135deg, #dc3545 0%, #ef4444 100%) !important; + border-color: #dc3545 !important; + color: white !important; +} +.filter-option input[type="checkbox"]:checked + label.btn-outline-priority-2 { + background: linear-gradient(135deg, #f97316 0%, #fb923c 100%) !important; + border-color: #f97316 !important; + color: white !important; +} +.filter-option input[type="checkbox"]:checked + label.btn-outline-priority-3 { + background: linear-gradient(135deg, #0284c7 0%, #38bdf8 100%) !important; + border-color: #0284c7 !important; + color: white !important; +} +.filter-option input[type="checkbox"]:checked + label.btn-outline-priority-4 { + background: linear-gradient(135deg, #64748b 0%, #94a3b8 100%) !important; + border-color: #64748b !important; + color: white !important; +} + +.filter-option-break { + flex-basis: 100%; + height: 0; +} +.filter-option.subcategory-option { + margin-left: 0.75rem; +} + +/* Timeline Preview & Dates */ +.date-timeline-card { + background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%); + border: 1px solid #cbd5e1; +} + +.timeline-axis-line { + height: 3px; + background: linear-gradient(90deg, #cbd5e1 0%, #94a3b8 50%, #cbd5e1 100%); + border-radius: 2px; + position: relative; +} + +.timeline-bar-wrapper { + position: relative; + height: 12px; + border-radius: 6px; + overflow: hidden; + background-color: #e2e8f0; + border: 1px solid #cbd5e1; +} + +.timeline-track-bg { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: #e2e8f0; +} + +.timeline-bar { + position: absolute; + top: 0; + height: 100%; + border-radius: 5px; + transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1), width 0.3s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.2s ease; +} + +.timeline-bar-begin { + background: linear-gradient(90deg, #10b981, #059669); + box-shadow: 0 0 8px rgba(16, 185, 129, 0.35); +} + +.timeline-bar-end { + background: linear-gradient(90deg, #3b82f6, #1d4ed8); + box-shadow: 0 0 8px rgba(59, 130, 246, 0.35); +} + +.timeline-bar.is-all { + left: 0% !important; + width: 100% !important; + opacity: 0.3; + background: #94a3b8 !important; + box-shadow: none !important; +} + +/* Chips pour Localisations */ +.location-item { + background-color: #ffffff; + border: 1px solid #e2e8f0; + border-left: 3.5px solid #0d6efd !important; + border-radius: 8px; + padding: 0.4rem 0.65rem; + margin-bottom: 0.35rem; + transition: all 0.15s ease-in-out; +} + +.location-item:hover { + background-color: #f8fafc; + border-color: #cbd5e1; +} + +/* ─── Main Content & Toolbar ───────────────────────────────────────── */ +.interventions-tabs .nav-link { + font-size: 0.88rem; + font-weight: 600; + color: #475569; + border: none; + border-bottom: 2px solid transparent; + padding: 0.5rem 0.85rem; + display: inline-flex; + align-items: center; + gap: 0.4rem; +} + +.interventions-tabs .nav-link.active { + color: #0f172a; + border-bottom-color: #0f172a; + background: transparent; +} + +.tab-counter-badge { + font-size: 0.72rem; + font-weight: 700; + padding: 2px 7px; + border-radius: 9999px; + background-color: #f1f5f9; + color: #475569; + border: 1px solid #e2e8f0; +} + +.interventions-tabs .nav-link.active .tab-counter-badge { + background-color: #0f172a; + color: #ffffff; + border-color: #0f172a; +} + +.btn-new-intervention { + background-color: #0f172a; + color: #ffffff !important; + border: 1px solid #0f172a; + border-radius: 8px; + padding: 0.35rem 0.75rem; + font-size: 0.82rem; + font-weight: 600; + display: inline-flex; + align-items: center; + gap: 0.4rem; + transition: all 0.15s ease; + text-decoration: none; + box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08); + white-space: nowrap; +} + +.btn-new-intervention:hover { + background-color: #1e293b; + color: #ffffff !important; + 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; + margin-bottom: 0.45rem; +} + +.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; + background: none; + border: none; + text-decoration: none; + font-weight: 600; + padding: 0.2rem 0.5rem; + border-radius: 6px; + transition: background-color 0.12s ease; + cursor: pointer; +} + +.btn-clear-all-filters:hover { + background-color: #fef2f2; + color: #b91c1c; +} + +/* ─── Table Toolbar & Buttons ──────────────────────────────────────── */ +.interventions-toolbar { + padding: 0.25rem 0; +} + +.filter-trigger-btn, +#favoriteFiltersDropdown, +#columns-dropdown-btn, +#bulk-action-dropdown button { + 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; + text-decoration: none; +} + +.filter-trigger-btn:hover, +#favoriteFiltersDropdown:hover, +#columns-dropdown-btn:hover, +#bulk-action-dropdown button: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; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.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.25rem; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.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 Interventions Table Card ──────────────────────────────── */ +.interventions-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; +} + +#interventions-table-scrollable { + overflow-y: auto; + overflow-x: auto; +} + +/* Scrollbar discrète */ +#interventions-table-scrollable::-webkit-scrollbar { + width: 6px; + height: 6px; +} +#interventions-table-scrollable::-webkit-scrollbar-track { + background: #f8fafc; +} +#interventions-table-scrollable::-webkit-scrollbar-thumb { + background: #cbd5e1; + border-radius: 4px; +} +#interventions-table-scrollable::-webkit-scrollbar-thumb:hover { + background: #94a3b8; +} + +.interventions-table { + width: 100%; + margin-bottom: 0; + border-collapse: separate; + border-spacing: 0; +} + +/* Table Headers (Sticky) */ +.interventions-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: 2px solid #cbd5e1 !important; + padding: 0.55rem 0.75rem !important; + vertical-align: middle !important; + white-space: nowrap !important; + user-select: none; +} + +.interventions-table thead th.sortable { + cursor: pointer; + transition: background-color 0.12s ease; +} + +.interventions-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 */ +.interventions-table tbody td { + padding: 0.45rem 0.75rem !important; + vertical-align: middle !important; + font-size: 0.82rem !important; + color: #334155; + border-bottom: 1px solid #e2e8f0 !important; + line-height: 1.35 !important; +} + +.interventions-table tbody tr { + transition: background-color 0.12s ease; +} + +.interventions-table tbody tr:hover { + background-color: #f8fafc !important; +} + +.interventions-table tbody tr.selected, +.interventions-table tbody tr.rowhighlight { + background-color: #eff6ff !important; +} + +/* Monospace Code Link Badge */ +.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-flex; + align-items: center; + max-width: 145px; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: middle; + 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; +} + +/* Title cell (2-line clamp with tooltip) */ +.intervention-title-cell { + font-size: 0.82rem; + max-height: 2.8em; + line-height: 1.35; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + word-wrap: break-word; +} + +.intervention-title-cell[data-truncated="true"] { + cursor: help; +} + +/* Action button / Zoom button / Selection Toggle */ +.zoom-btn, +.intervention-btn-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 27px; + height: 27px; + padding: 0; + border-radius: 6px; + color: #64748b; + background-color: transparent; + border: 1px solid transparent; + transition: all 0.15s ease; +} + +.zoom-btn:hover, +.intervention-btn-toggle:hover { + color: #0f172a; + background-color: #f1f5f9; + border-color: #cbd5e1; +} + +/* Custom Pagination Bar */ +.interventions-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; +} + +.interventions-pagination-bar .pagination-info { + font-size: 0.82rem; + color: #64748b; +} + +.interventions-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; +} + +.interventions-pagination-bar .pagination .page-item.active .page-link { + background-color: #0f172a; + border-color: #0f172a; + color: #ffffff; +} + +.interventions-pagination-bar .pagination .page-item.disabled .page-link { + color: #cbd5e1; + background-color: transparent; + border-color: #f1f5f9; +} + +/* Map styling */ +.interventions-map { + height: calc(100vh - 190px); + min-height: 360px; + position: relative; + width: 100%; + border-radius: 12px; + border: 1px solid #e2e8f0; + margin-top: 44px; +} + +.interventions-map-filter { + position: absolute; + top: 0.75rem; + 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 { + max-height: 220px; + overflow-y: auto; + font-size: 0.85rem; +} + +.popup-scrollable .badge { + font-size: 0.75rem; +} diff --git a/loko/interventions/static/interventions/interventions_index.js b/loko/interventions/static/interventions/interventions_index.js index befb50b..e60d231 100644 --- a/loko/interventions/static/interventions/interventions_index.js +++ b/loko/interventions/static/interventions/interventions_index.js @@ -1,8 +1,15 @@ var interventions_geojson = {}; +let allInterventions = []; +let filteredInterventions = []; let visibleRows = []; +let currentMapFilteredData = []; -let allInterventions = []; // full dataset -let currentMapFilteredData = []; // result of map-based filtering +let currentPage = 1; +let pageSize = 50; +let sortField = 'code'; +let sortDir = 'asc'; +let hiddenColumns = new Set(); +const INTERVENTIONS_COLUMNS_KEY = 'interventions_columns_hidden'; let selectedLocations = []; // items: { ct, obj, type, type_label, thematic, name, code, label, lon, lat } @@ -156,6 +163,10 @@ map.on('click', 'interventions', function(e) { .setLngLat(e.lngLat) .setHTML(popupContent) .addTo(map); + + if (features[0] && features[0].properties && features[0].properties.id) { + highlightInterventionRow(features[0].properties.id); + } }); map.on('mouseenter', 'interventions', () => { @@ -165,26 +176,30 @@ map.on('mouseleave', 'interventions', () => { map.getCanvas().style.cursor = ''; }); +function highlightInterventionRow(id) { + const numId = parseInt(id, 10); + const index = filteredInterventions.findIndex(r => r.id === numId); + if (index === -1) return; + const targetPage = Math.floor(index / pageSize) + 1; + if (targetPage !== currentPage) { + currentPage = targetPage; + renderTable(); + } + const rowEl = document.querySelector(`#interventions-table tbody tr[data-id="${numId}"]`); + if (rowEl) { + document.querySelectorAll('#interventions-table tbody tr.rowhighlight').forEach(el => el.classList.remove('rowhighlight')); + rowEl.classList.add('rowhighlight'); + rowEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + } +} + function persistInterventionsListContext() { - const $table = $('#interventions-table'); - if (!$table.length || typeof $table.bootstrapTable !== 'function') { - return; - } - - let data; - try { - data = $table.bootstrapTable('getData'); - } catch (error) { - console.warn('Unable to retrieve interventions data for navigation context.', error); - return; - } - - if (!Array.isArray(data) || data.length === 0) { + if (!Array.isArray(filteredInterventions) || filteredInterventions.length === 0) { sessionStorage.removeItem(INTERVENTION_LIST_CONTEXT_KEY); return; } - const items = data + const items = filteredInterventions .map(row => { if (!row || row.id === undefined || row.id === null) { return null; @@ -203,12 +218,12 @@ function persistInterventionsListContext() { return; } - const options = $table.bootstrapTable('getOptions') || {}; + const searchInput = document.getElementById('interventions-search-input'); const context = { items, - sortName: options.sortName || null, - sortOrder: options.sortOrder || null, - searchText: options.searchText || '', + sortName: sortField || null, + sortOrder: sortDir || null, + searchText: searchInput ? searchInput.value.trim() : '', listUrl: window.location.pathname + (window.location.search || ''), timestamp: Date.now() }; @@ -227,7 +242,12 @@ interventionsgeojsonloader.loadGeojson(interventionsgeojsonUrl+`?${params}`); // Reset table if error occurs when loading GeoJSON interventionsgeojsonloader.addEventListener("geojsonError", event => { - loadDataIntoInterventionsTable([]); + document.getElementById('interventions-spinner')?.classList.add('d-none'); + document.getElementById('interventions-table-container')?.classList.remove('d-none'); + allInterventions = []; + filteredInterventions = []; + renderTable(); + updateResultCount(0); }); // Constante de limite @@ -235,19 +255,17 @@ const MAX_ITEMS = 5000; // Load data into table when GeoJSON is loaded interventionsgeojsonloader.addEventListener("geojsonLoaded", event => { - var geojson_data = event.detail; - loadDataIntoInterventionsTable(geojson_data.features); + var geojson_data = event.detail; interventions_geojson = geojson_data; - allInterventions = geojson_data.features; // Store the full dataset for later use + const features = geojson_data.features || []; + loadDataIntoInterventionsTable(features); currentMapFilteredData = [...allInterventions]; // Afficher/masquer l'alerte "limite atteinte" const alertEl = document.getElementById('filter-limit-alert'); if (alertEl) { - const reached = (allInterventions?.length || 0) >= MAX_ITEMS; + const reached = (features.length || 0) >= MAX_ITEMS; alertEl.classList.toggle('d-none', !reached); - // (Optionnel) infobulle avec le nombre exact reçu - // if (reached) alertEl.title = `Réception: ${allInterventions.length} éléments (seuls 5 000 sont affichés)`; } }); @@ -279,23 +297,15 @@ document.getElementById('apply-filters').addEventListener('click', async functio } } - // Save preferences before redirecting - await savePreferencesFromForm(params); - if (selectedLocations.length) { const payload = selectedLocations.map(x => ({ ct: x.ct, obj: x.obj })); params.set('locations', JSON.stringify(payload)); - } - else { + } else { params.delete('locations'); } - // Update URL - const newUrl = `${window.location.pathname}?${params.toString()}`; - window.history.pushState({}, '', newUrl); - - // Update filter badge after URL change - updateFilterBadge(); + // Save preferences before redirecting + await savePreferencesFromForm(params); // Preserve active search if any const _activeQ = $('#interventions-search-input').val().trim(); @@ -305,6 +315,13 @@ document.getElementById('apply-filters').addEventListener('click', async functio params.set('search_mode', _activeMode); } + // Update URL + const newUrl = `${window.location.pathname}?${params.toString()}`; + window.history.pushState({}, '', newUrl); + + // Update filter badge and tab badges after URL change + updateFilterTabBadges(); + // Load filtered data handleGeojsonMapLoading(init=false, interventionsgeojsonloader, 'interventions'); interventionsgeojsonloader.loadGeojson(interventionsgeojsonUrl+`?${params}`); @@ -331,6 +348,9 @@ document.getElementById('reset-filters').addEventListener('click', async functio checkbox.checked = false; }); + selectedLocations = []; + renderLocationChips(); + togglePauseReasonFilter(); filterCategoriesByThematics(); filterProvidersAndContracts(); @@ -352,7 +372,7 @@ document.getElementById('reset-filters').addEventListener('click', async functio 'Content-Type': 'application/json', 'X-CSRFToken': getCookie('csrftoken') }, - body: JSON.stringify({ filters: {}, sort: {} }) + body: JSON.stringify({ filters: {}, sort: {}, columns: [...hiddenColumns] }) }); } catch (err) { // Silently fail @@ -362,8 +382,8 @@ document.getElementById('reset-filters').addEventListener('click', async functio // Remove URL parameters window.history.pushState({}, '', window.location.pathname); - // Update filter badge after URL change - updateFilterBadge(); + // Update filter badge & tab badges after URL change + updateFilterTabBadges(); if (typeof checkActiveFavoriteMatch === 'function') { checkActiveFavoriteMatch(); @@ -393,71 +413,88 @@ $("#filterzone").on( "click", function(e) { // Loads data into table function loadDataIntoInterventionsTable(features) { - const tableData = features.map((feature) => { + const tableData = (features || []).map((feature) => { + const props = feature.properties || {}; + const coords = feature.geometry && feature.geometry.coordinates ? feature.geometry.coordinates : []; return { - actions: '', // Column for action icons, manipulated by the formatter - id: feature.id, - code: feature.properties.code, - status: feature.properties.status, - prep_state: feature.properties.prep_state, - maintain_type: feature.properties.maintain_type, - title: feature.properties.title, - location_code: feature.properties.location_code || '', - begin_time: feature.properties.begin_time, - assigned_provider: feature.properties.assigned_provider || '', - contract_number: feature.properties.contract_number || '', - contract_provider: feature.properties.contract_provider || '', - origin_type: feature.properties.origin_type || '', - source_category: feature.properties.source_category || '', - is_assigned: Boolean(feature.properties.is_assigned), - team: feature.properties.team || '', - member: feature.properties.member || '', - lon: feature.geometry && feature.geometry.coordinates ? feature.geometry.coordinates[0] : null, // Longitude - lat: feature.geometry && feature.geometry.coordinates ? feature.geometry.coordinates[1] : null // Latitude + id: feature.id || props.id, + code: props.code || '', + status: props.status || '', + prep_state: props.prep_state || '', + maintain_type: props.maintain_type || '', + title: props.title || '', + location_code: props.location_code || '', + begin_time: props.begin_time || '', + assigned_provider: props.assigned_provider || '', + contract_number: props.contract_number || '', + contract_provider: props.contract_provider || '', + origin_type: props.origin_type || '', + source_category: props.source_category || '', + is_assigned: Boolean(props.is_assigned), + team: props.team || '', + member: props.member || '', + is_blocked_due_to_contract: Boolean(props.is_blocked_due_to_contract), + contract: props.contract || null, + lon: coords.length ? coords[0] : null, + lat: coords.length ? coords[1] : null }; }); - $('#interventions-spinner').hide(); - $('#interventions-table-container').show(); - $('#interventions-table').bootstrapTable('load', tableData); + allInterventions = tableData; + filteredInterventions = [...allInterventions]; + sortData(filteredInterventions, sortField, sortDir); + + currentPage = 1; + renderTable(); + + document.getElementById('interventions-spinner')?.classList.add('d-none'); + document.getElementById('interventions-table-container')?.classList.remove('d-none'); + + visibleRows = filteredInterventions; + syncMapWithTable(); + + updateResultCount(filteredInterventions.length); + updateFilterTabBadges(); + persistInterventionsListContext(); } // Refresh table view when switching tabs $('#interventions-table-tab').on('click', function () { - $('#interventions-table').bootstrapTable('resetView'); + renderTable(); }); -// Add action buttons for each row of the table -function actionsFormatter(value, row, index) { - resp = ''; - // resp = ``; +function escapeHtml(str) { + if (str === null || str === undefined) return ''; + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} +function maintainTypeFormatter(type) { + const types = { + 'corrective': { label: gettext('Corrective'), badge: 'danger' }, + 'preventive': { label: gettext('Préventive'), badge: 'warning' }, + 'ameliorative': { label: gettext('Améliorative'), badge: 'success' }, + }; + const entry = types[type]; + if (!entry) return type ? `${escapeHtml(type)}` : '-'; + return `${escapeHtml(entry.label)}`; +} - if (selected_interventions_set.has(row.id)) { - icon_select = 'bi-check-square-fill'; - } else { - icon_select = 'bi-square'; - } - resp = resp + - ``; - - if (row.lon) { - resp = resp + - ` - `; - } - - - - return resp; +function prepStateFormatter(state) { + const states = { + 'processed': { label: gettext('Prêt'), badge: 'success', icon: 'bi-box-seam' }, + 'not_processed': { label: gettext('En attente'), badge: 'danger', icon: 'bi-box' }, + 'none': { label: '-', badge: 'secondary', icon: '' } + }; + const entry = states[state]; + if (!entry || state === 'none') return '-'; + return `${escapeHtml(entry.label)}`; } function codeFormatter(value, row) { @@ -624,125 +661,705 @@ map.on('mouseleave', 'interventions-clusters', () => { -// Initialization after page load -$( document ).ready(function() { - // 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 || 'asc'; +// ─── Custom Table Core ─────────────────────────────────────────────── + +const AVAILABLE_COLUMNS = [ + { key: 'code', label: gettext('Code') }, + { key: 'status', label: gettext('Statut') }, + { key: 'title', label: gettext('Titre') }, + { key: 'location_code', label: gettext('Localisation') }, + { key: 'begin_time', label: gettext('Début') }, + { key: 'maintain_type', label: gettext('Maintenance') }, + { key: 'prep_state', label: gettext('Stock') }, + { key: 'contract_number', label: gettext('Contrat') }, + { key: 'assigned_provider', label: gettext('Prestataire') }, + { key: 'team', label: gettext('Équipe') }, + { key: 'member', label: gettext('Membre') }, + { key: 'origin_type', label: gettext("Type d'origine") }, + { key: 'source_category', label: gettext("Source") } +]; + +function getHiddenColumns() { + try { + if (typeof SAVED_COLUMNS !== 'undefined' && Array.isArray(SAVED_COLUMNS) && SAVED_COLUMNS.length > 0) { + return new Set(SAVED_COLUMNS); + } + const raw = localStorage.getItem(INTERVENTIONS_COLUMNS_KEY); + if (raw) { + const parsed = JSON.parse(raw); + if (Array.isArray(parsed)) return new Set(parsed); + } + return new Set(['contract_number', 'origin_type', 'source_category']); + } catch (e) { + return new Set(['contract_number', 'origin_type', 'source_category']); } - - $('#interventions-table').bootstrapTable({ - columns: [ - { field: 'actions', title: '', formatter:'actionsFormatter', class: 'text-nowrap', searchable: false }, - { field: 'code', title: gettext('Code'), formatter: 'codeFormatter', class: 'text-nowrap', sortable: true }, - { field: 'status', title: gettext('Statut'), formatter: 'statusFormatter', class: 'text-nowrap', sortable: true }, - { field: 'title', title: gettext('Titre'), formatter: 'titleFormatter', sortable: true }, - { field: 'location_code', title: gettext('Localisation'), class: 'text-nowrap', sortable: true }, - { field: 'begin_time', title: gettext('Début'), formatter: 'dateFormatter', class: 'text-nowrap', sortable: true }, - { field: 'contract_number', title: gettext('Contrat'), formatter: 'contractFormatter', class: 'text-nowrap', visible: false, sortable: true }, - { field: 'origin_type', title: gettext("Type d'origine"), class: 'text-nowrap', visible: false, sortable: true }, - { field: 'source_category', title: gettext("Source"), class: 'text-nowrap', visible: false, sortable: true }, - ], - height: calculateTableHeight(), // Apply the calculated height - ...initialSort // Spread saved sort if available +} + +function saveHiddenColumns(set) { + try { + localStorage.setItem(INTERVENTIONS_COLUMNS_KEY, JSON.stringify([...set])); + } catch (e) { + // ignore + } + saveColumnPreferences(); +} + +function saveColumnPreferences() { + if (typeof savePreferencesUrl === 'undefined') return; + const filters = getCurrentFilters(); + const sort = { sortName: sortField, sortOrder: sortDir }; + fetch(savePreferencesUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCookie('csrftoken') + }, + body: JSON.stringify({ filters: filters, sort: sort, columns: [...hiddenColumns] }) + }).catch(err => console.warn('Could not save column preferences:', err)); +} + +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); }); - // Restore saved column visibility - _restoreColumnVisibility($('#interventions-table')); - - $('#interventions-table').on('post-body.bs.table', function() { - // Persist context and mark truncated titles - persistInterventionsListContext(); - detectTruncatedTitles(); - // Recalculate widths/height without triggering another post-body loop - const $table = $('#interventions-table'); - if ($table.data('bootstrap.table')) { - $table.bootstrapTable('resetView', { height: calculateTableHeight() }); + 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(); + }); + + document.querySelector('.columns-dropdown-menu')?.addEventListener('click', (e) => { + e.stopPropagation(); + }); + + 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(); + }); + + document.getElementById('cols-reset-default')?.addEventListener('click', (e) => { + e.preventDefault(); + hiddenColumns = new Set(['contract_number', 'origin_type', 'source_category']); + saveHiddenColumns(hiddenColumns); + container.querySelectorAll('.col-toggle-check').forEach(cb => { + cb.checked = !hiddenColumns.has(cb.dataset.column); + }); + 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); + }); + }); +} + +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 === 'begin_time') { + 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 = ['begin_time'].includes(field) ? 'desc' : 'asc'; + } + + sortData(filteredInterventions, sortField, sortDir); + currentPage = 1; + renderTable(); + savePreferencesFromUrl(); +} + +function updateHeaderSortIcons() { + document.querySelectorAll('#interventions-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('interventions-table-tbody'); + if (!tbody) return; + + const totalItems = filteredInterventions.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 = filteredInterventions.slice(startIndex, endIndex); + + if (totalItems === 0) { + tbody.innerHTML = ` +