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 = ` + + +
+
+
${gettext('Aucune intervention trouvée')}
+
${gettext('Aucune intervention ne correspond à vos critères de recherche actuels.')}
+ +
+ + + `; + updatePaginationBar(0, 0, 0, 1); + updateHeaderSortIcons(); + applyColumnVisibility(); + return; + } + + let rowsHtml = ''; + for (let i = 0; i < pageItems.length; i++) { + rowsHtml += renderRowHtml(pageItems[i]); + } + tbody.innerHTML = rowsHtml; + + updatePaginationBar(startIndex + 1, endIndex, totalItems, totalPages); + updateHeaderSortIcons(); + applyColumnVisibility(); + detectTruncatedTitles(); + persistInterventionsListContext(); +} + +function renderRowHtml(row) { + const isSelected = selected_interventions_set.has(row.id); + const codeLabel = escapeHtml(row.code || `INT#${row.id}`); + const detailUrl = `${interventionsurl}${row.id}/`; + + const maintenanceTypeColor = { + 'corrective': 'danger', + 'preventive': 'warning', + 'ameliorative': 'success', + }; + const maintenanceColorClass = maintenanceTypeColor[row.maintain_type] || 'secondary'; + const gearIcon = ``; + + const selectIcon = isSelected ? 'bi-check-square-fill text-primary' : 'bi-square text-muted'; + const selectBtn = ``; + + const zoomBtn = (row.lon && row.lat) + ? `` + : ``; + + const statusHtml = statusFormatter(row.status, row); + const titleHtml = titleFormatter(row.title, row); + const beginTimeHtml = dateFormatter(row.begin_time); + const maintainTypeHtml = maintainTypeFormatter(row.maintain_type); + const prepStateHtml = prepStateFormatter(row.prep_state); + const contractFormatted = contractFormatter(row.contract_number, row); + const contractHtml = contractFormatted ? escapeHtml(contractFormatted) : '-'; + const locationHtml = escapeHtml(row.location_code || '-'); + const providerHtml = escapeHtml(row.assigned_provider || '-'); + const teamHtml = escapeHtml(row.team || '-'); + const memberHtml = escapeHtml(row.member || '-'); + const originHtml = escapeHtml(row.origin_type || '-'); + const sourceHtml = escapeHtml(row.source_category || '-'); + + return ` + + ${selectBtn}${zoomBtn} + + ${gearIcon}${codeLabel} + + ${statusHtml} + ${titleHtml} + ${locationHtml} + ${beginTimeHtml} + ${maintainTypeHtml} + ${prepStateHtml} + ${contractHtml} + ${providerHtml} + ${teamHtml} + ${memberHtml} + ${originHtml} + ${sourceHtml} + + `; +} + +function updatePaginationBar(start, end, total, totalPages) { + const infoEl = document.getElementById('interventions-pagination-info'); + const listEl = document.getElementById('interventions-pagination-list'); + if (!infoEl || !listEl) return; + + if (total === 0) { + infoEl.textContent = gettext('Aucune intervention chargée'); + listEl.innerHTML = ''; + return; + } + + infoEl.innerHTML = interpolate( + gettext('Affichage de %(start)s à %(end)s sur %(total)s intervention(s)'), + { start, end, total }, + true + ); + + if (totalPages <= 1) { + listEl.innerHTML = ''; + return; + } + + let pagesHtml = ''; + + // First & Previous + pagesHtml += ` +
  • + +
  • +
  • + +
  • + `; + + // Numerical pages (window of 5 around current) + const windowSize = 5; + let startPage = Math.max(1, currentPage - Math.floor(windowSize / 2)); + let endPage = Math.min(totalPages, startPage + windowSize - 1); + if (endPage - startPage + 1 < windowSize) { + startPage = Math.max(1, endPage - windowSize + 1); + } + + if (startPage > 1) { + pagesHtml += `
  • …
  • `; + } + + for (let p = startPage; p <= endPage; p++) { + pagesHtml += ` +
  • + ${p} +
  • + `; + } + + if (endPage < totalPages) { + pagesHtml += `
  • …
  • `; + } + + // Next & Last + pagesHtml += ` +
  • + +
  • +
  • + +
  • + `; + + listEl.innerHTML = pagesHtml; +} + +function updateResultCount(count) { + const badge = document.getElementById('interventions-counter-badge'); + if (badge) { + badge.textContent = count; + } +} + +function updateFilterTabBadges() { + const form = document.getElementById('filter-form'); + if (!form) return; + + // Tab 1: Classification & Statut + let count1 = 0; + const tab1Fields = ['thematic', 'status', 'pause_reason', 'category', 'maintain_type', 'type', 'priority']; + tab1Fields.forEach(f => { + const checked = form.querySelectorAll(`input[name="${f}"]:checked`); + if (checked.length > 0) count1++; + }); + if (form.querySelector('input[name="is_blocked_due_to_contract"]:checked')) count1++; + if (form.querySelector('input[name="followed_only"]:checked')) count1++; + + const badge1 = document.getElementById('tab-badge-classification'); + if (badge1) { + if (count1 > 0) { + badge1.textContent = count1; + badge1.classList.remove('d-none'); + } else { + badge1.classList.add('d-none'); + } + } + + // Tab 2: Acteurs & Marchés + let count2 = 0; + const tab2Fields = ['provider', 'contract', 'assigned_team', 'assigned_member', 'prep_state']; + tab2Fields.forEach(f => { + const checked = form.querySelectorAll(`input[name="${f}"]:checked`); + if (checked.length > 0) count2++; + }); + const personVal = document.getElementById('filter-person-select')?.value; + if (personVal) count2++; + + const badge2 = document.getElementById('tab-badge-actors'); + if (badge2) { + if (count2 > 0) { + badge2.textContent = count2; + badge2.classList.remove('d-none'); + } else { + badge2.classList.add('d-none'); + } + } + + // Tab 3: Dates & Lieux + let count3 = 0; + if (selectedLocations && selectedLocations.length > 0) count3++; + const bMin = document.getElementById('begin_date_min')?.value; + const bMax = document.getElementById('begin_date_max')?.value; + if (bMin || bMax) count3++; + const eMin = document.getElementById('end_date_min')?.value; + const eMax = document.getElementById('end_date_max')?.value; + if (eMin || eMax) count3++; + if (form.querySelector('input[name="overdue"]:checked')) count3++; + if (form.querySelector('input[name="to_be_planned_soon"]')?.value) count3++; + + const badge3 = document.getElementById('tab-badge-dates'); + if (badge3) { + if (count3 > 0) { + badge3.textContent = count3; + badge3.classList.remove('d-none'); + } else { + badge3.classList.add('d-none'); + } + } + + // Overall filter count badge + const totalCount = count1 + count2 + count3; + const filterBadge = document.getElementById('filter-badge'); + const filterCountSpan = document.getElementById('filter-count'); + if (filterBadge && filterCountSpan) { + if (totalCount > 0) { + filterCountSpan.textContent = totalCount; + filterBadge.classList.remove('d-none'); + } else { + filterBadge.classList.add('d-none'); + } + } + + renderActiveFilterChips(); +} + +function updateFilterBadge() { + updateFilterTabBadges(); +} + +function renderActiveFilterChips() { + const bar = document.getElementById('active-filters-bar'); + if (!bar) return; + + const chips = []; + const params = new URLSearchParams(window.location.search); + + params.getAll('thematic').forEach(val => { + const labelEl = document.querySelector(`label[for="thematic_${val}"] .thematic-label`); + chips.push({ param: 'thematic', val, label: labelEl ? labelEl.textContent.trim() : val }); + }); + + params.getAll('status').forEach(val => { + const labelEl = document.querySelector(`label[for="status_${val}"]`); + chips.push({ param: 'status', val, label: labelEl ? labelEl.textContent.trim() : val }); + }); + + params.getAll('priority').forEach(val => { + chips.push({ param: 'priority', val, label: `Priorité ${val}` }); + }); + + params.getAll('maintain_type').forEach(val => { + const labelEl = document.querySelector(`label[for="maintain_type_${val}"]`); + chips.push({ param: 'maintain_type', val, label: labelEl ? labelEl.textContent.trim() : val }); + }); + + params.getAll('provider').forEach(val => { + const labelEl = document.querySelector(`label[for="provider_${val}"]`); + chips.push({ param: 'provider', val, label: labelEl ? labelEl.textContent.trim() : val }); + }); + + params.getAll('contract').forEach(val => { + const labelEl = document.querySelector(`label[for="contract_${val}"]`); + chips.push({ param: 'contract', val, label: labelEl ? labelEl.textContent.trim() : val }); + }); + + params.getAll('assigned_team').forEach(val => { + const labelEl = document.querySelector(`label[for="assigned_team_${val}"]`); + chips.push({ param: 'assigned_team', val, label: labelEl ? labelEl.textContent.trim() : val }); + }); + + params.getAll('assigned_member').forEach(val => { + const labelEl = document.querySelector(`label[for="assigned_member_${val}"]`); + chips.push({ param: 'assigned_member', val, label: labelEl ? labelEl.textContent.trim() : val }); + }); + + params.getAll('category').forEach(val => { + const labelEl = document.querySelector(`label[for="category_${val}"]`); + chips.push({ param: 'category', val, label: labelEl ? labelEl.textContent.trim() : val }); + }); + + params.getAll('prep_state').forEach(val => { + const labelEl = document.querySelector(`label[for="prep_state_${val}"]`); + chips.push({ param: 'prep_state', val, label: labelEl ? labelEl.textContent.trim() : val }); + }); + + if (params.get('overdue') === 'true' || params.get('overdue') === '1') { + chips.push({ param: 'overdue', val: 'true', label: gettext('En retard') }); + } + + if (params.get('is_blocked_due_to_contract') === 'true' || params.get('is_blocked_due_to_contract') === '1') { + chips.push({ param: 'is_blocked_due_to_contract', val: 'true', label: gettext('Marché bloqué') }); + } + + if (params.get('followed_only') === 'true' || params.get('followed_only') === '1') { + chips.push({ param: 'followed_only', val: 'true', label: gettext('Suivies uniquement') }); + } + + const bMin = params.get('begin_date_min'); + const bMax = params.get('begin_date_max'); + if (bMin || bMax) { + const label = bMin && bMax ? `${bMin} → ${bMax}` : (bMin ? `≥ ${bMin}` : `≤ ${bMax}`); + chips.push({ param: 'begin_date', val: 'begin_date', label: `${gettext('Début')} : ${label}` }); + } + + const eMin = params.get('end_date_min'); + const eMax = params.get('end_date_max'); + if (eMin || eMax) { + const label = eMin && eMax ? `${eMin} → ${eMax}` : (eMin ? `≥ ${eMin}` : `≤ ${eMax}`); + chips.push({ param: 'end_date', val: 'end_date', label: `${gettext('Fin')} : ${label}` }); + } + + if (selectedLocations && selectedLocations.length > 0) { + selectedLocations.forEach(loc => { + chips.push({ param: 'locations', val: `${loc.ct}_${loc.obj}`, label: loc.label || loc.name || loc.code }); + }); + } + + const q = $('#interventions-search-input').val().trim() || params.get('q'); + if (q) { + chips.push({ param: 'q', val: q, label: `"${escapeHtml(q)}"` }); + } + + if (chips.length === 0) { + bar.classList.add('d-none'); + bar.innerHTML = ''; + return; + } + + bar.classList.remove('d-none'); + let html = `${gettext('Filtres actifs')} :`; + chips.forEach(chip => { + html += ` + + ${escapeHtml(chip.label)} + + + `; + }); + html += ` + + `; + bar.innerHTML = html; + + bar.querySelectorAll('.remove-filter-btn').forEach(btn => { + btn.addEventListener('click', function(e) { + e.preventDefault(); + removeSingleFilter(this.dataset.param, this.dataset.val); + }); + }); + + document.getElementById('btn-clear-all-chips')?.addEventListener('click', function(e) { + e.preventDefault(); + document.getElementById('reset-filters')?.click(); + }); +} + +function removeSingleFilter(param, val) { + if (param === 'q') { + $('#interventions-search-input').val(''); + $('#interventions-search-clear').css('visibility', 'hidden'); + runInterventionSearch(); + return; + } + if (param === 'begin_date') { + document.getElementById('begin_date_min').value = ''; + document.getElementById('begin_date_max').value = ''; + updateDateFilterTimelinePreview(); + } else if (param === 'end_date') { + document.getElementById('end_date_min').value = ''; + document.getElementById('end_date_max').value = ''; + updateDateFilterTimelinePreview(); + } else if (param === 'locations') { + const parts = val.split('_'); + selectedLocations = selectedLocations.filter(loc => !(String(loc.ct) === String(parts[0]) && String(loc.obj) === String(parts[1]))); + renderLocationChips(); + } else { + const checkbox = document.querySelector(`input[name="${param}"][value="${val}"]`); + if (checkbox) { + checkbox.checked = false; + } + } + document.getElementById('apply-filters')?.click(); +} + +function initInterventionsTable() { + hiddenColumns = getHiddenColumns(); + + if (typeof SAVED_SORT !== 'undefined' && SAVED_SORT && SAVED_SORT.sortName) { + sortField = SAVED_SORT.sortName; + sortDir = SAVED_SORT.sortOrder || 'asc'; + } else { + sortField = 'code'; + sortDir = 'asc'; + } + + 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(); + }); + } + + initColumnChooser(); + + document.querySelectorAll('#interventions-table thead th.sortable').forEach(th => { + th.addEventListener('click', function () { + const field = this.dataset.field; + if (!field) return; + handleHeaderSort(field); + }); + }); + + document.getElementById('interventions-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('interventions-table-scrollable'); + if (scrollable) scrollable.scrollTo({ top: 0, behavior: 'smooth' }); } }); - // Register actions to the table - registerDetailAction('#interventions-table', '.detail-btn', interventionsurl); - registerZoomAction('#interventions-table', '.zoom-btn', map, 17); + const tbody = document.getElementById('interventions-table-tbody'); + if (tbody) { + tbody.addEventListener('click', (e) => { + const zoomBtn = e.target.closest('.zoom-btn'); + if (zoomBtn && map) { + const lon = parseFloat(zoomBtn.dataset.lon); + const lat = parseFloat(zoomBtn.dataset.lat); + if (!isNaN(lon) && !isNaN(lat)) { + map.flyTo({ + center: [lon, lat], + zoom: 17, + essential: true + }); + } + return; + } - // Register intervention selection action to table interventions - registerSelectInterventionAction('#interventions-table', '.intervention-btn-toggle'); + const toggleBtn = e.target.closest('.intervention-btn-toggle'); + if (toggleBtn) { + const id = parseInt(toggleBtn.dataset.id, 10); + if (id) { + toggleSelectedIntervention(id, $(toggleBtn)); + const tr = toggleBtn.closest('tr'); + if (tr) { + tr.classList.toggle('selected', selected_interventions_set.has(id)); + } + } + return; + } + }); + } - // Réappliquer la hauteur quand une colonne est (dé)masquée, et mémoriser la visibilité - $('#interventions-table').on('column-switch.bs.table', function () { - _saveColumnVisibility($('#interventions-table')); - // After toggling columns, recalc widths for proper alignment - setTimeout(function(){ - $('#interventions-table').bootstrapTable('resetView'); - }, 0); - }); + // Modal input changes update filter tab badges dynamically + document.getElementById('filter-form')?.addEventListener('change', updateFilterTabBadges); +} +// Initialization after page load +$( document ).ready(function() { + initInterventionsTable(); }); - - - - function syncMapWithTable() { if (!map.getLayer('interventions')) { return; } - // Si le tableau montre toutes les lignes => reset du filtre if (visibleRows.length === allInterventions.length) { map.setFilter('interventions', null); return; } - // Extrait les IDs à afficher const idsToShow = visibleRows.map(row => row.id); - // Applique un filtre sur la couche MapLibre if (idsToShow.length > 0) { map.setFilter('interventions', ['in', ['get', 'id'], ['literal', idsToShow]]); } else { - // Si aucune ligne visible, masquer tous les points map.setFilter('interventions', ['==', ['get', 'id'], '__none__']); } } - -function getSearchableVisibleFields() { - const columns = $('#interventions-table').bootstrapTable('getVisibleColumns'); - return columns - .filter(col => col.searchable !== false) - .map(col => col.field) - .filter(Boolean); -} - -function customSearch(data, text) { - const searchableFields = getSearchableVisibleFields(); - const lowerText = text.toLowerCase(); - - const filtered = data.filter(row => - searchableFields.some(field => { - const value = row[field]; - return value != null && value.toString().toLowerCase().includes(lowerText); - }) - ); - - visibleRows = filtered; - syncMapWithTable(); - - return filtered; -} - - - map.on('draw.create', e => updateSelection(e.features[0])); map.on('draw.update', e => updateSelection(e.features[0])); map.on('draw.delete', e => { resetMapFilter(); }); - function updateSelection(geometry) { const source = map.getSource('interventions'); if (!source) return; @@ -752,31 +1369,25 @@ function updateSelection(geometry) { const selectedIds = data.features .filter(f => f && f.geometry && f.geometry.type === "Point" && turf.booleanPointInPolygon(f, geometry)) - .map(f => f.properties.id); + .map(f => f.properties.id || f.id); - currentMapFilteredData = allInterventions.filter(row => selectedIds.includes(row.id)); + filteredInterventions = allInterventions.filter(row => selectedIds.includes(row.id)); + currentPage = 1; + renderTable(); - // Recharge la table — recherche utilisateur sera réappliquée juste après - loadDataIntoInterventionsTable(currentMapFilteredData); - - // Simule une recherche utilisateur en réinjectant le texte existant - const currentSearchText = $('#interventions-table').bootstrapTable('getOptions').searchText || ''; - $('#interventions-table').bootstrapTable('refreshOptions', { - searchText: currentSearchText - }); + visibleRows = filteredInterventions; + syncMapWithTable(); + updateResultCount(filteredInterventions.length); } - -// Réinitialiser le filtre carte function resetMapFilter() { - // Recharge les données - loadDataIntoInterventionsTable(allInterventions); + filteredInterventions = [...allInterventions]; + currentPage = 1; + renderTable(); - // Simule une recherche utilisateur en réinjectant le texte existant - const currentSearchText = $('#interventions-table').bootstrapTable('getOptions').searchText || ''; - $('#interventions-table').bootstrapTable('refreshOptions', { - searchText: currentSearchText - }); + visibleRows = filteredInterventions; + syncMapWithTable(); + updateResultCount(filteredInterventions.length); } @@ -850,27 +1461,18 @@ function updateInputAndStatus() { document.addEventListener("DOMContentLoaded", function () { - document.getElementById("select-all").addEventListener("click", () => { - const allRows = $('#interventions-table').bootstrapTable('getData'); - allRows.forEach(row => { + document.getElementById("select-all")?.addEventListener("click", () => { + const rows = filteredInterventions && filteredInterventions.length ? filteredInterventions : allInterventions; + rows.forEach(row => { selected_interventions_set.add(parseInt(row.id)); - const btn = document.querySelector(`.intervention-btn-toggle[data-id="${row.id}"]`); - if (btn) { - $(btn).find('i').removeClass('bi-square').addClass('bi-check-square-fill'); - } }); + renderTable(); updateInputAndStatus(); }); - document.getElementById("deselect-all").addEventListener("click", () => { - selected_interventions_set.clear(); - const allRows = $('#interventions-table').bootstrapTable('getData'); - allRows.forEach(row => { - const btn = document.querySelector(`.intervention-btn-toggle[data-id="${row.id}"]`); - if (btn) { - $(btn).find('i').removeClass('bi-check-square-fill').addClass('bi-square'); - } - }); + document.getElementById("deselect-all")?.addEventListener("click", () => { + selected_interventions_set.clear(); + renderTable(); updateInputAndStatus(); }); @@ -1300,14 +1902,18 @@ document.addEventListener("DOMContentLoaded", function () { showToast(interpolate(msg, { count }, true), "success"); // Mise à jour directe des lignes dans le tableau + const statusLabel = statusSelect?.options[statusSelect.selectedIndex]?.text || newStatus; validIds.forEach(id => { - $('#interventions-table').bootstrapTable('updateByUniqueId', { - id: id, - row: { - status: newStatus + const numId = parseInt(id); + [allInterventions, filteredInterventions].forEach(arr => { + const target = arr.find(row => parseInt(row.id) === numId); + if (target) { + target.status = newStatus; + target.status_label = statusLabel; } }); }); + renderTable(); } // Check if any emails failed @@ -1321,8 +1927,9 @@ document.addEventListener("DOMContentLoaded", function () { showToast(gettext("Une erreur est survenue."), "danger"); }); - // Optionnel : recharger le tableau - $('#interventions-table').bootstrapTable('refresh'); + // Recharger les données GeoJSON (met à jour la carte et le tableau) + const currentParams = window.location.search ? window.location.search.substring(1) : ''; + interventionsgeojsonloader.loadGeojson(interventionsgeojsonUrl + (currentParams ? '?' + currentParams : '')); statusModal.hide(); statusErrorMsg.classList.add('d-none'); @@ -1378,8 +1985,9 @@ document.addEventListener("DOMContentLoaded", function () { // success showToast(data.message || gettext("Interventions associées."), 'success'); associateOrderModal?.hide(); - // Optionnel: rafraîchir table pour refléter éventuelles colonnes liées - $('#interventions-table').bootstrapTable('refresh'); + // Rafraîchir les données GeoJSON + const currentParams = window.location.search ? window.location.search.substring(1) : ''; + interventionsgeojsonloader.loadGeojson(interventionsgeojsonUrl + (currentParams ? '?' + currentParams : '')); } } catch (e) { showToast(gettext("Une erreur est survenue."), 'danger'); @@ -1451,12 +2059,9 @@ function calculateTableHeight() { // Helper to consistently apply the computed table height function refreshTableHeight() { - const $table = $('#interventions-table'); - // Guard: avoid calling before bootstrap-table is initialized - if (!$table.data('bootstrap.table')) return; - const tableHeight = calculateTableHeight(); - $table.bootstrapTable('refreshOptions', { height: tableHeight }); - $table.bootstrapTable('resetView'); + if (typeof detectTruncatedTitles === 'function') { + detectTruncatedTitles(); + } } // Function to save layout preference to backend @@ -2218,52 +2823,16 @@ function getCookie(name) { // ==================== Column Visibility Persistence ==================== -function _getHiddenColumns($table) { - try { - const allCols = $table.bootstrapTable('getOptions').columns[0] || []; - const visibleFields = $table.bootstrapTable('getVisibleColumns').map((c) => c.field); - return allCols - .filter((c) => c.field && c.field !== 'actions' && !visibleFields.includes(c.field)) - .map((c) => c.field); - } catch (e) { - return []; - } +function _getHiddenColumns() { + return Array.from(hiddenColumns); } -function _saveColumnVisibility($table) { - if (typeof savePreferencesUrl === 'undefined') return; - const hiddenFields = _getHiddenColumns($table); - const filters = getCurrentFilters(); - const sort = getCurrentSort(); - fetch(savePreferencesUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-CSRFToken': getCookie('csrftoken') - }, - body: JSON.stringify({ filters: filters, sort: sort, columns: hiddenFields }) - }).catch(err => console.warn('Could not save column preferences:', err)); +function _saveColumnVisibility() { + saveColumnPreferences(); } -function _restoreColumnVisibility($table) { - try { - // null = jamais sauvegardé, on laisse les défauts de la définition des colonnes - if (typeof SAVED_COLUMNS === 'undefined' || SAVED_COLUMNS === null) return; - if (!Array.isArray(SAVED_COLUMNS)) return; - - // On récupère tous les champs définis (hors 'actions') - const allCols = $table.bootstrapTable('getOptions').columns[0] || []; - allCols.forEach((col) => { - if (!col.field || col.field === 'actions') return; - if (SAVED_COLUMNS.includes(col.field)) { - $table.bootstrapTable('hideColumn', col.field); - } else { - $table.bootstrapTable('showColumn', col.field); - } - }); - } catch (e) { - // ignore - } +function _restoreColumnVisibility() { + applyColumnVisibility(); } // ==================== Filter and Sort Preferences Management ==================== @@ -2368,21 +2937,10 @@ function getCurrentFilters() { * Get current sort settings from the table */ function getCurrentSort() { - const $table = $('#interventions-table'); - if (!$table.length || typeof $table.bootstrapTable !== 'function') { - return {}; - } - - try { - const options = $table.bootstrapTable('getOptions') || {}; - return { - sortName: options.sortName || null, - sortOrder: options.sortOrder || null - }; - } catch (error) { - console.warn('Unable to get current sort:', error); - return {}; - } + return { + sortName: sortField || null, + sortOrder: sortDir || null + }; } /** @@ -2393,11 +2951,9 @@ function savePreferences() { const filters = getCurrentFilters(); const sort = getCurrentSort(); - const $table = $('#interventions-table'); - const columns = $table.length && $table.data('bootstrap.table') ? _getHiddenColumns($table) : undefined; + const columns = Array.from(hiddenColumns); - const payload = { filters: filters, sort: sort }; - if (columns !== undefined) payload.columns = columns; + const payload = { filters: filters, sort: sort, columns: columns }; fetch(savePreferencesUrl, { method: 'POST', @@ -2409,48 +2965,6 @@ function savePreferences() { }).catch(err => console.warn('Could not save preferences:', err)); } -// ==================== Filter and Sort Preferences Management ==================== - -// Count active filters from URL parameters -function countActiveFilters() { - const urlParams = new URLSearchParams(window.location.search); - let count = 0; - - // Define filter parameters used in the global filter modal - const filterParams = [ - 'thematic', 'status', 'priority', 'maintain_type', 'category', 'type', - 'provider', 'contract', 'assigned_team', 'assigned_member', - 'assigned_controller', 'intervention_manager', 'inspector', 'pause_reason', - 'begin_date_min', 'begin_date_max', 'end_date_min', 'end_date_max', 'locations', - 'overdue', 'to_be_planned_soon', 'followed_only', 'is_blocked_due_to_contract' - ]; - - filterParams.forEach(param => { - const values = urlParams.getAll(param); - if (values.length > 0 && values[0] !== '') { - count++; // Each filter type counts as 1, regardless of number of selected values - } - }); - - return count; -} - -// Update filter badge display -function updateFilterBadge() { - const count = countActiveFilters(); - const badge = document.getElementById('filter-badge'); - const countSpan = document.getElementById('filter-count'); - - if (badge && countSpan) { - if (count > 0) { - countSpan.textContent = count; - badge.classList.remove('d-none'); - } else { - badge.classList.add('d-none'); - } - } -} - // Save preferences from URL parameters (used when filters are already applied) function savePreferencesFromUrl() { if (typeof savePreferencesUrl === 'undefined') { @@ -2480,23 +2994,10 @@ function savePreferencesFromUrl() { }); // Get current sort from table - const $table = $('#interventions-table'); - let sort = {}; + const sort = getCurrentSort(); + const columns = Array.from(hiddenColumns); + const payload = { filters, sort, columns }; - if ($table.length && $table.data('bootstrap.table')) { - const options = $table.bootstrapTable('getOptions'); - if (options.sortName) { - sort = { - sortName: options.sortName, - sortOrder: options.sortOrder || 'asc' - }; - } - } - - // Send to server - const columns = $table.length && $table.data('bootstrap.table') ? _getHiddenColumns($table) : undefined; - const payload = { filters, sort }; - if (columns !== undefined) payload.columns = columns; fetch(savePreferencesUrl, { method: 'POST', headers: { @@ -2533,25 +3034,9 @@ async function savePreferencesFromForm(urlParams) { } }); - // Get current sort from table (if available) - const $table = $('#interventions-table'); - let sort = {}; - - if ($table.length && $table.data('bootstrap.table')) { - const options = $table.bootstrapTable('getOptions'); - if (options.sortName) { - sort = { - sortName: options.sortName, - sortOrder: options.sortOrder || 'asc' - }; - } - } - - // Send to server with keepalive to ensure request completes even after redirect - const $tableForm = $('#interventions-table'); - const columnsForm = $tableForm.length && $tableForm.data('bootstrap.table') ? _getHiddenColumns($tableForm) : undefined; - const payloadForm = { filters, sort }; - if (columnsForm !== undefined) payloadForm.columns = columnsForm; + const sort = getCurrentSort(); + const columns = Array.from(hiddenColumns); + const payloadForm = { filters, sort, columns }; try { await fetch(savePreferencesUrl, { method: 'POST', @@ -2575,14 +3060,6 @@ document.addEventListener('DOMContentLoaded', function() { initDateFilterPresets(); initRolePersonFilter(); initFavoriteFilters(); - - // Save preferences when table sort changes (user action only) - const $table = $('#interventions-table'); - if ($table.length) { - $table.on('sort.bs.table', function() { - setTimeout(savePreferencesFromUrl, 500); // Debounce and save after sort is applied - }); - } }); // Update badge after page fully loads (including after filter redirect) diff --git a/loko/interventions/templates/interventions/interventions_index.html b/loko/interventions/templates/interventions/interventions_index.html index dacbd64..1693230 100644 --- a/loko/interventions/templates/interventions/interventions_index.html +++ b/loko/interventions/templates/interventions/interventions_index.html @@ -3,1112 +3,638 @@ {% load i18n %} {% block head %} - - - - + + + + {% endblock head %} - {% block content %} - +