feat: implement unified asset index view with dynamic filtering modal and associated JavaScript logic

This commit is contained in:
Karl Determe 2026-08-25 12:07:16 +02:00
parent 2b8cdfabb7
commit d82dcde16d
2 changed files with 448 additions and 288 deletions

View file

@ -1171,14 +1171,14 @@ $(document).ready(function () {
document.getElementById('col-table').className = 'col-md-6';
const colMap = document.getElementById('col-map');
colMap.className = 'col-md-6';
colMap.style.cssText = 'position: relative; margin-top: 44px;';
colMap.style.cssText = 'position: relative; margin-top: 50px;';
setLayoutActive('both');
});
document.getElementById('layout-btn-map').addEventListener('click', function () {
document.getElementById('col-table').classList.add('d-none');
const colMap = document.getElementById('col-map');
colMap.className = 'col-12';
colMap.style.cssText = 'position: relative; margin-top: 44px;';
colMap.style.cssText = 'position: relative; margin-top: 50px;';
setLayoutActive('map');
});

View file

@ -3,74 +3,163 @@
{% load i18n %}
{% block head %}
<link href="{% static 'common/mapbox-gl-draw.css' %}" rel="stylesheet"/>
<link rel="stylesheet" href="{% static 'assets/assets_index.css' %}">
<link rel="stylesheet" href="{% static 'assets/info-panel.css' %}">
<style>
#assets-search-input:focus { outline: none; box-shadow: none; border-color: #dee2e6; }
#unified-empty-state { display: none; color: #6c757d; }
<link href="{% static 'common/mapbox-gl-draw.css' %}" rel="stylesheet" />
<link rel="stylesheet" href="{% static 'assets/assets_index.css' %}">
<link rel="stylesheet" href="{% static 'assets/info-panel.css' %}">
<style>
#assets-search-input:focus {
outline: none;
box-shadow: none;
border-color: #dee2e6;
}
#unified-empty-state {
display: none;
color: #6c757d;
}
/* ─── Filter modal ──────────────────────────────────── */
#filterModal .modal-dialog { max-width: 95%; }
#filterModal .modal-content { display: flex; flex-direction: column; max-height: 90vh; }
#filterModal .modal-body { flex: 1; overflow-y: auto; }
#filterModal .modal-footer { flex-shrink: 0; }
.filter-section { margin-bottom: 1rem; }
#filterModal .modal-dialog {
max-width: 95%;
}
#filterModal .modal-content {
display: flex;
flex-direction: column;
max-height: 90vh;
}
#filterModal .modal-body {
flex: 1;
overflow-y: auto;
}
#filterModal .modal-footer {
flex-shrink: 0;
}
.filter-section {
margin-bottom: 1rem;
}
.filter-section-header {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 0.75rem; gap: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
gap: 1rem;
}
.filter-section-header > label { font-weight: 600; color: #333; margin: 0; flex-shrink: 0; }
.filter-section-buttons { display: flex; gap: 0.5rem; margin-left: auto; flex-shrink: 0; }
.filter-section-buttons button { padding: 0.25rem 0.5rem; font-size: 0.75rem; white-space: nowrap; }
.filter-options {
display: flex; flex-wrap: wrap; gap: 0.5rem; min-height: 2.5rem;
padding: 0.5rem; background-color: #f8f9fa;
border-radius: 0.375rem; border: 1px solid #dee2e6;
.filter-section-header>label {
font-weight: 600;
color: #333;
margin: 0;
flex-shrink: 0;
}
.filter-option { display: flex; align-items: center; }
.filter-option input[type="checkbox"],
.filter-option input[type="radio"] {
position: absolute; opacity: 0; width: 0; height: 0; cursor: pointer;
.filter-section-buttons {
display: flex;
gap: 0.5rem;
margin-left: auto;
flex-shrink: 0;
}
.filter-option label {
display: inline-flex; align-items: center; justify-content: center;
margin: 0; font-weight: 500; cursor: pointer;
background-color: #e7f1ff; color: #0c5cc9;
border-radius: 1rem; padding: 0.35rem 0.75rem; font-size: 0.8rem;
border: 1px solid #b8d4f1; transition: all 0.15s ease;
.filter-section-buttons button {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
white-space: nowrap;
}
.filter-option input[type="checkbox"]:checked + label,
.filter-option input[type="radio"]:checked + label {
background-color: #0c5cc9; color: #fff; border-color: #0c5cc9;
.filter-options {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
min-height: 2.5rem;
padding: 0.5rem;
background-color: #f8f9fa;
border-radius: 0.375rem;
border: 1px solid #dee2e6;
}
.filter-option label:hover { background-color: #cfe2f3; }
.filter-option input:checked + label:hover { background-color: #0a4fb0; }
.filter-option input:disabled + label { opacity: 0.5; cursor: not-allowed; }
.filter-option {
display: flex;
align-items: center;
}
.filter-option input[type="checkbox"],
.filter-option input[type="radio"] {
position: absolute;
opacity: 0;
width: 0;
height: 0;
cursor: pointer;
}
.filter-option label {
display: inline-flex;
align-items: center;
justify-content: center;
margin: 0;
font-weight: 500;
cursor: pointer;
background-color: #e7f1ff;
color: #0c5cc9;
border-radius: 1rem;
padding: 0.35rem 0.75rem;
font-size: 0.8rem;
border: 1px solid #b8d4f1;
transition: all 0.15s ease;
white-space: nowrap;
}
.filter-option input[type="checkbox"]:checked+label,
.filter-option input[type="radio"]:checked+label {
background-color: #0c5cc9;
color: #fff;
border-color: #0c5cc9;
}
.filter-option label:hover {
background-color: #cfe2f3;
}
.filter-option input:checked+label:hover {
background-color: #0a4fb0;
}
.filter-option input:disabled+label {
opacity: 0.5;
cursor: not-allowed;
}
#map .maplibregl-popup-close-button {
font-size: 1.35rem;
line-height: 1;
padding: 0.2rem 0.45rem;
}
.unified-add-panel {
border: 1px solid #dee2e6;
border-radius: 0.75rem;
padding: 1rem;
background: #f8f9fa;
}
.unified-add-panel h6 {
font-size: 0.95rem;
margin-bottom: 0.75rem;
}
.unified-add-panel .form-select,
.unified-add-panel .form-control {
border-radius: 0.65rem;
}
#map .maplibregl-ctrl-top-right,
#map .mapboxgl-ctrl-top-right {
top: 10px !important;
}
</style>
</style>
{% endblock head %}
{% block content %}
@ -81,7 +170,8 @@
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="filterModalLabel">{% translate "Filtrer les assets" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% translate 'Fermer' %}"></button>
<button type="button" class="btn-close" data-bs-dismiss="modal"
aria-label="{% translate 'Fermer' %}"></button>
</div>
<div class="modal-body">
@ -90,19 +180,21 @@
<div class="filter-section-header">
<label>{% translate "Thématique" %}</label>
<div class="filter-section-buttons">
<button type="button" class="btn btn-sm btn-outline-secondary" id="thematic-select-all">{% translate "Tous" %}</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="thematic-deselect-all">{% translate "Aucun" %}</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="thematic-select-all">{%
translate "Tous" %}</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="thematic-deselect-all">{%
translate "Aucun" %}</button>
</div>
</div>
<div class="filter-options">
{% for thematic in accessible_thematics %}
<div class="filter-option">
<input type="checkbox" class="thematic-checkbox"
id="thematic-cb-{{ thematic.code }}"
value="{{ thematic.code }}"
{% if thematic.code in selected_thematics %}checked{% endif %}>
<input type="checkbox" class="thematic-checkbox" id="thematic-cb-{{ thematic.code }}"
value="{{ thematic.code }}" {% if thematic.code in selected_thematics %}checked{% endif
%}>
<label for="thematic-cb-{{ thematic.code }}">
{% if thematic.icon %}<i class="bi {{ thematic.icon }} me-1"></i>{% endif %}{{ thematic.name }}
{% if thematic.icon %}<i class="bi {{ thematic.icon }} me-1"></i>{% endif %}{{
thematic.name }}
</label>
</div>
{% endfor %}
@ -114,12 +206,15 @@
<div class="filter-section-header">
<label>{% translate "Catégorie" %}</label>
<div class="filter-section-buttons">
<button type="button" class="btn btn-sm btn-outline-secondary" id="category-select-all">{% translate "Toutes" %}</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="category-deselect-all">{% translate "Aucune" %}</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="category-select-all">{%
translate "Toutes" %}</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="category-deselect-all">{%
translate "Aucune" %}</button>
</div>
</div>
<div id="category-filter-options" class="filter-options">
<span class="text-muted small">{% translate "Sélectionnez d'abord une ou plusieurs thématiques." %}</span>
<span class="text-muted small">{% translate "Sélectionnez d'abord une ou plusieurs thématiques."
%}</span>
</div>
</div>
@ -128,13 +223,17 @@
<div class="filter-section-header">
<label>{% translate "Modèle" %}</label>
<div class="filter-section-buttons">
<button type="button" class="btn btn-sm btn-outline-secondary" id="model-select-all">{% translate "Tous" %}</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="model-deselect-all">{% translate "Aucun" %}</button>
<button type="button" class="btn btn-sm btn-outline-secondary d-none" id="model-filter-toggle"></button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="model-select-all">{%
translate "Tous" %}</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="model-deselect-all">{%
translate "Aucun" %}</button>
<button type="button" class="btn btn-sm btn-outline-secondary d-none"
id="model-filter-toggle"></button>
</div>
</div>
<div id="model-filter-options" class="filter-options">
<span class="text-muted small">{% translate "Sélectionnez d'abord une ou plusieurs catégories." %}</span>
<span class="text-muted small">{% translate "Sélectionnez d'abord une ou plusieurs catégories."
%}</span>
</div>
</div>
@ -144,7 +243,8 @@
<label>{% translate "Filtres thématiques" %}</label>
</div>
<div id="conditional-filter-options" class="filter-options">
<span class="text-muted small">{% translate "Sélectionnez d'abord une ou plusieurs thématiques." %}</span>
<span class="text-muted small">{% translate "Sélectionnez d'abord une ou plusieurs thématiques."
%}</span>
</div>
</div>
@ -155,21 +255,21 @@
</div>
<div class="filter-options">
<div class="filter-option">
<input type="radio" name="unified-type-filter" id="type-all" value="all"
{% if preventive_element_type != 'assets' and preventive_element_type != 'locations' %}checked{% endif %}
{% if preventive_element_type %}disabled{% endif %}>
<input type="radio" name="unified-type-filter" id="type-all" value="all" {% if
preventive_element_type !='assets' and preventive_element_type !='locations' %}checked{%
endif %} {% if preventive_element_type %}disabled{% endif %}>
<label for="type-all">{% translate "Assets et localisations" %}</label>
</div>
<div class="filter-option">
<input type="radio" name="unified-type-filter" id="type-locations" value="locations"
{% if preventive_element_type == 'locations' %}checked{% endif %}
{% if preventive_element_type %}disabled{% endif %}>
<input type="radio" name="unified-type-filter" id="type-locations" value="locations" {% if
preventive_element_type=='locations' %}checked{% endif %} {% if preventive_element_type
%}disabled{% endif %}>
<label for="type-locations">{% translate "Localisations uniquement" %}</label>
</div>
<div class="filter-option">
<input type="radio" name="unified-type-filter" id="type-assets" value="assets"
{% if preventive_element_type == 'assets' %}checked{% endif %}
{% if preventive_element_type %}disabled{% endif %}>
<input type="radio" name="unified-type-filter" id="type-assets" value="assets" {% if
preventive_element_type=='assets' %}checked{% endif %} {% if preventive_element_type
%}disabled{% endif %}>
<label for="type-assets">{% translate "Assets uniquement" %}</label>
</div>
</div>
@ -197,7 +297,8 @@
<div class="filter-options">
{% for status_value, status_label in asset_status_choices %}
<div class="filter-option">
<input type="checkbox" name="filter-status" id="filter-status-{{ status_value }}" value="{{ status_value }}">
<input type="checkbox" name="filter-status" id="filter-status-{{ status_value }}"
value="{{ status_value }}">
<label for="filter-status-{{ status_value }}">{{ status_label }}</label>
</div>
{% endfor %}
@ -212,10 +313,14 @@
<label>{% translate "Statut de validation" %}</label>
</div>
<div class="filter-options">
{% for validation_status_value, validation_status_label in asset_validation_status_choices %}
{% for validation_status_value, validation_status_label in asset_validation_status_choices
%}
<div class="filter-option">
<input type="checkbox" name="filter-validation-status" id="filter-validation-status-{{ validation_status_value }}" value="{{ validation_status_value }}">
<label for="filter-validation-status-{{ validation_status_value }}">{{ validation_status_label }}</label>
<input type="checkbox" name="filter-validation-status"
id="filter-validation-status-{{ validation_status_value }}"
value="{{ validation_status_value }}">
<label for="filter-validation-status-{{ validation_status_value }}">{{
validation_status_label }}</label>
</div>
{% endfor %}
</div>
@ -224,8 +329,10 @@
</div>
<div class="modal-footer">
<button type="button" id="reset-filters" class="btn btn-secondary">{% translate "Réinitialiser" %}</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% translate "Annuler" %}</button>
<button type="button" id="reset-filters" class="btn btn-secondary">{% translate "Réinitialiser"
%}</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% translate "Annuler"
%}</button>
<button type="button" id="apply-filters" class="btn btn-primary">{% translate "Filtrer" %}</button>
</div>
</div>
@ -233,12 +340,14 @@
</div>
{# ─── Add chooser modal ────────────────────────────────────────────────────── #}
<div class="modal fade" id="unifiedAddChooserModal" tabindex="-1" aria-labelledby="unifiedAddChooserModalLabel" aria-hidden="true">
<div class="modal fade" id="unifiedAddChooserModal" tabindex="-1" aria-labelledby="unifiedAddChooserModalLabel"
aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="unifiedAddChooserModalLabel">{% translate "Créer un élément" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% translate 'Fermer' %}"></button>
<button type="button" class="btn-close" data-bs-dismiss="modal"
aria-label="{% translate 'Fermer' %}"></button>
</div>
<div class="modal-body">
<div id="unified-add-chooser-alert" class="alert alert-info small mb-3">
@ -247,8 +356,10 @@
<div class="row g-3">
<div class="col-md-6">
<div class="unified-add-panel h-100">
<h6 class="d-flex align-items-center gap-2"><i class="bi bi-layers"></i>{% translate "Asset" %}</h6>
<label for="unified-add-asset-thematic" class="form-label small text-muted mb-1">{% translate "Thématique" %}</label>
<h6 class="d-flex align-items-center gap-2"><i class="bi bi-layers"></i>{% translate "Asset"
%}</h6>
<label for="unified-add-asset-thematic" class="form-label small text-muted mb-1">{%
translate "Thématique" %}</label>
<select id="unified-add-asset-thematic" class="form-select mb-3"></select>
<button type="button" id="unified-add-asset-open" class="btn btn-primary w-100">
<i class="bi bi-plus-circle me-1"></i>{% translate "Créer un asset" %}
@ -257,8 +368,10 @@
</div>
<div class="col-md-6">
<div class="unified-add-panel h-100">
<h6 class="d-flex align-items-center gap-2"><i class="bi bi-geo-alt"></i>{% translate "Localisation" %}</h6>
<label for="unified-add-location-thematic" class="form-label small text-muted mb-1">{% translate "Thématique" %}</label>
<h6 class="d-flex align-items-center gap-2"><i class="bi bi-geo-alt"></i>{% translate
"Localisation" %}</h6>
<label for="unified-add-location-thematic" class="form-label small text-muted mb-1">{%
translate "Thématique" %}</label>
<select id="unified-add-location-thematic" class="form-select mb-3"></select>
<button type="button" id="unified-add-location-open" class="btn btn-outline-primary w-100">
<i class="bi bi-plus-circle me-1"></i>{% translate "Créer une localisation" %}
@ -275,39 +388,46 @@
</div>
{# ─── Asset create modal ──────────────────────────────────────────────────── #}
<div class="modal fade" id="unifiedAddAssetModal" tabindex="-1" aria-labelledby="unifiedAddAssetModalLabel" aria-hidden="true">
<div class="modal fade" id="unifiedAddAssetModal" tabindex="-1" aria-labelledby="unifiedAddAssetModalLabel"
aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<form id="unifiedAddAssetForm" class="modal-content">
{% csrf_token %}
<input type="hidden" name="thematic_code" id="unified-add-asset-thematic-code">
<div class="modal-header">
<h5 class="modal-title" id="unifiedAddAssetModalLabel">{% translate "Créer un asset" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% translate 'Fermer' %}"></button>
<button type="button" class="btn-close" data-bs-dismiss="modal"
aria-label="{% translate 'Fermer' %}"></button>
</div>
<div class="modal-body">
<div id="unifiedAddAssetAlert" class="alert d-none" role="alert"></div>
<div class="mb-3">
<label for="unifiedAddAssetCategory" class="form-label">{% translate "Catégorie" %} <span class="text-danger">*</span></label>
<label for="unifiedAddAssetCategory" class="form-label">{% translate "Catégorie" %} <span
class="text-danger">*</span></label>
<select class="form-select" id="unifiedAddAssetCategory" name="category_id" required>
<option value="">{% translate "Sélectionner une catégorie..." %}</option>
</select>
</div>
<div class="mb-3">
<label for="unifiedAddAssetModel" class="form-label">{% translate "Modèle" %} <span class="text-danger">*</span></label>
<label for="unifiedAddAssetModel" class="form-label">{% translate "Modèle" %} <span
class="text-danger">*</span></label>
<select class="form-select" id="unifiedAddAssetModel" name="model_id" required disabled>
<option value="">{% translate "Sélectionner d'abord une catégorie..." %}</option>
</select>
</div>
<div class="mb-3 d-none" id="unifiedAddAssetLocationGroup">
<label for="unifiedAddAssetLocation" class="form-label">{% translate "Localisation" %} <span class="text-danger">*</span></label>
<label for="unifiedAddAssetLocation" class="form-label">{% translate "Localisation" %} <span
class="text-danger">*</span></label>
<select class="form-select" id="unifiedAddAssetLocation" name="location_id" disabled>
<option value="">{% translate "Chargement..." %}</option>
</select>
<div class="form-text text-muted" id="unifiedAddAssetLocationHelp"></div>
</div>
<div class="mb-3" id="unifiedAddAssetCodeGroup">
<label for="unifiedAddAssetCode" class="form-label">{% translate "Code" %} <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="unifiedAddAssetCode" name="code" maxlength="50" required>
<label for="unifiedAddAssetCode" class="form-label">{% translate "Code" %} <span
class="text-danger">*</span></label>
<input type="text" class="form-control" id="unifiedAddAssetCode" name="code" maxlength="50"
required>
<div class="form-text text-muted d-none" id="unifiedAddAssetCodeHelp"></div>
</div>
<div class="mb-3">
@ -319,11 +439,13 @@
<input type="text" class="form-control" id="unifiedAddAssetNameNl" name="name_nl" maxlength="200">
</div>
<div class="alert alert-info small mb-0">
<i class="bi bi-info-circle me-1"></i>{% translate "L'asset sera créé avec le statut 'À approuver'." %}
<i class="bi bi-info-circle me-1"></i>{% translate "L'asset sera créé avec le statut 'À approuver'."
%}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% translate "Annuler" %}</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% translate "Annuler"
%}</button>
<button type="submit" class="btn btn-primary" id="unifiedAddAssetSubmit">
<i class="bi bi-plus-circle me-1"></i>{% translate "Créer l'asset" %}
</button>
@ -333,7 +455,8 @@
</div>
{# ─── Location create modal ───────────────────────────────────────────────── #}
<div class="modal fade" id="unifiedAddLocationModal" tabindex="-1" aria-labelledby="unifiedAddLocationModalLabel" aria-hidden="true">
<div class="modal fade" id="unifiedAddLocationModal" tabindex="-1" aria-labelledby="unifiedAddLocationModalLabel"
aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<form id="unifiedAddLocationForm" class="modal-content">
{% csrf_token %}
@ -341,7 +464,8 @@
<input type="hidden" id="unified-add-location-type" name="location_type">
<div class="modal-header">
<h5 class="modal-title" id="unifiedAddLocationModalLabel">{% translate "Créer une localisation" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% translate 'Fermer' %}"></button>
<button type="button" class="btn-close" data-bs-dismiss="modal"
aria-label="{% translate 'Fermer' %}"></button>
</div>
<div class="modal-body">
<div id="unifiedAddLocationAlert" class="alert d-none" role="alert"></div>
@ -353,7 +477,8 @@
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% translate "Annuler" %}</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% translate "Annuler"
%}</button>
<button type="submit" class="btn btn-success" id="unifiedAddLocationSubmit">
<i class="bi bi-plus-circle me-1"></i>{% translate "Créer la localisation" %}
</button>
@ -383,12 +508,17 @@
{# Dropdown Tableaux spécifiques (générique selon les thématiques accessibles) #}
{% if specific_tables %}
<div class="dropdown">
<button class="btn btn-light btn-sm dropdown-toggle rounded-pill px-3 border shadow-sm d-inline-flex align-items-center"
<button
class="btn btn-light btn-sm dropdown-toggle rounded-pill px-3 border shadow-sm d-inline-flex align-items-center"
type="button" id="specificTablesDropdown" data-bs-toggle="dropdown" aria-expanded="false">
{% translate "Répertoires" %}
</button>
<ul class="dropdown-menu dropdown-menu-end shadow border-0 mt-1" aria-labelledby="specificTablesDropdown" style="min-width: 270px;">
<li><h6 class="dropdown-header text-uppercase small text-muted">{% translate "Répertoires spécifiques" %}</h6></li>
<ul class="dropdown-menu dropdown-menu-end shadow border-0 mt-1" aria-labelledby="specificTablesDropdown"
style="min-width: 270px;">
<li>
<h6 class="dropdown-header text-uppercase small text-muted">{% translate "Répertoires spécifiques"
%}</h6>
</li>
{% for table in specific_tables %}
<li>
<a class="dropdown-item d-flex align-items-center gap-3 py-2" href="{{ table.url }}">
@ -407,13 +537,16 @@
{% endif %}
<div class="btn-group" role="group" aria-label="{% translate 'Sélection de la vue' %}">
<button id="layout-btn-table" class="btn btn-light" type="button" title="{% translate 'Vue tableau uniquement' %}">
<button id="layout-btn-table" class="btn btn-light" type="button"
title="{% translate 'Vue tableau uniquement' %}">
<i class="bi bi-table"></i>
</button>
<button id="layout-btn-both" class="btn btn-light text-primary" type="button" title="{% translate 'Vue partagée' %}">
<button id="layout-btn-both" class="btn btn-light text-primary" type="button"
title="{% translate 'Vue partagée' %}">
<i class="bi bi-layout-split"></i>
</button>
<button id="layout-btn-map" class="btn btn-light" type="button" title="{% translate 'Vue carte uniquement' %}">
<button id="layout-btn-map" class="btn btn-light" type="button"
title="{% translate 'Vue carte uniquement' %}">
<i class="bi bi-globe-europe-africa"></i>
</button>
</div>
@ -427,24 +560,27 @@
<input type="text" id="assets-search-input" class="form-control form-control-sm"
style="width:100%; border-radius:20px 0 0 20px; border-color:#dee2e6; padding-right:1.6rem;"
placeholder="{% translate 'Code, nom, type…' %}">
<button type="button" id="assets-search-clear"
style="position:absolute; right:4px; top:50%; transform:translateY(-50%);
<button type="button" id="assets-search-clear" style="position:absolute; right:4px; top:50%; transform:translateY(-50%);
background:none; border:none; padding:0 2px; line-height:1;
color:#adb5bd; cursor:pointer; visibility:hidden;"
title="{% translate 'Effacer' %}">
color:#adb5bd; cursor:pointer; visibility:hidden;" title="{% translate 'Effacer' %}">
<i class="bi bi-x-lg" style="font-size:0.7rem;"></i>
</button>
</div>
<span class="input-group-text bg-white px-1" style="border-left:0; border-right:0; border-color:#dee2e6; flex-shrink:0;">
<span class="input-group-text bg-white px-1"
style="border-left:0; border-right:0; border-color:#dee2e6; flex-shrink:0;">
<div class="btn-group btn-group-sm" role="group">
<input type="radio" class="btn-check" name="assets-search-mode" id="assets-search-mode-and" value="and" checked>
<input type="radio" class="btn-check" name="assets-search-mode" id="assets-search-mode-and"
value="and" checked>
<label class="btn btn-outline-secondary py-0 px-2" for="assets-search-mode-and"
data-bs-toggle="tooltip" data-bs-placement="top"
title="{% translate 'Tous les mots recherchés doivent être présents' %}">{% translate "ET" %}</label>
<input type="radio" class="btn-check" name="assets-search-mode" id="assets-search-mode-or" value="or">
title="{% translate 'Tous les mots recherchés doivent être présents' %}">{% translate "ET"
%}</label>
<input type="radio" class="btn-check" name="assets-search-mode" id="assets-search-mode-or"
value="or">
<label class="btn btn-outline-secondary py-0 px-2" for="assets-search-mode-or"
data-bs-toggle="tooltip" data-bs-placement="top"
title="{% translate 'Au moins un des mots recherchés doit être présent' %}">{% translate "OU" %}</label>
title="{% translate 'Au moins un des mots recherchés doit être présent' %}">{% translate "OU"
%}</label>
</div>
</span>
<button class="btn btn-sm px-3" type="button" id="assets-search-btn"
@ -474,11 +610,11 @@
</button>
</div>
<div class="my-1 ms-1">
<button class="btn btn-light position-relative" type="button"
data-bs-toggle="modal" data-bs-target="#filterModal"
title="{% translate 'Filtres' %}">
<button class="btn btn-light position-relative" type="button" data-bs-toggle="modal"
data-bs-target="#filterModal" title="{% translate 'Filtres' %}">
<i class="bi bi-funnel"></i> {% translate "Filtres" %}
<span id="filter-badge" class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-primary d-none">
<span id="filter-badge"
class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-primary d-none">
<span id="filter-count">0</span>
</span>
</button>
@ -487,25 +623,31 @@
{# Tab content #}
<div class="tab-content d-flex flex-column" style="flex: 1; min-height: 0;">
<div class="tab-pane fade show active d-flex flex-column" style="flex: 1; min-height: 0; padding-top: 0.5rem;">
<div class="tab-pane fade show active d-flex flex-column"
style="flex: 1; min-height: 0; padding-top: 0.5rem;">
{# Selection toolbar #}
<div class="d-flex gap-2 flex-wrap align-items-center mb-1">
<button type="button" class="btn btn-sm btn-outline-secondary" id="unified-select-all">{% translate "Tout sélectionner" %}</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="unified-deselect-all">{% translate "Tout désélectionner" %}</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="unified-select-all">{%
translate "Tout sélectionner" %}</button>
<button type="button" class="btn btn-sm btn-outline-secondary" id="unified-deselect-all">{%
translate "Tout désélectionner" %}</button>
{% if preventive_draft_id %}
<button type="submit" class="btn btn-success btn-sm" form="preventive-select-form">{% translate "Valider la sélection" %}</button>
<button type="submit" class="btn btn-success btn-sm" form="preventive-select-form">{% translate
"Valider la sélection" %}</button>
{% endif %}
<div id="unified-selection-status" class="text-muted small mb-0">0 {% translate "sélectionné(s)" %}</div>
<div id="unified-selection-status" class="text-muted small mb-0">0 {% translate "sélectionné(s)"
%}</div>
{% if not preventive_draft_id %}
<div class="dropdown ms-auto" id="unified-bulk-action-dropdown">
<button class="btn btn-light btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">
<button class="btn btn-light btn-sm dropdown-toggle" type="button" data-bs-toggle="dropdown"
aria-expanded="false">
{% translate "Action" %}
</button>
<ul class="dropdown-menu dropdown-menu-end">
<li>
<a class="dropdown-item" href="#" id="unified-action-export"
data-bs-toggle="modal" data-bs-target="#unifiedExportModal">
<a class="dropdown-item" href="#" id="unified-action-export" data-bs-toggle="modal"
data-bs-target="#unifiedExportModal">
<i class="bi bi-download me-2"></i>{% translate "Exporter" %}
</a>
</li>
@ -513,7 +655,8 @@
<li>
<a class="dropdown-item" href="#" id="unified-action-bulk-status"
data-bs-toggle="modal" data-bs-target="#bulkStatusModal">
<i class="bi bi-pencil-square me-2"></i>{% translate "Modifier le statut en masse" %}
<i class="bi bi-pencil-square me-2"></i>{% translate "Modifier le statut en
masse" %}
</a>
</li>
{% endif %}
@ -521,7 +664,8 @@
<li>
<a class="dropdown-item" href="#" id="unified-action-bulk-validation-status"
data-bs-toggle="modal" data-bs-target="#bulkValidationStatusModal">
<i class="bi bi-shield-check me-2"></i>{% translate "Modifier le statut de validation en masse" %}
<i class="bi bi-shield-check me-2"></i>{% translate "Modifier le statut de
validation en masse" %}
</a>
</li>
{% endif %}
@ -534,7 +678,8 @@
<div style="position: relative;">
{# Spinner overlay — shown on top of existing table content during loading #}
<div id="unified-spinner" style="display:none; position:absolute; inset:0; z-index:10; background:rgba(255,255,255,0.75); justify-content:center; align-items:flex-start; padding-top:2rem;">
<div id="unified-spinner"
style="display:none; position:absolute; inset:0; z-index:10; background:rgba(255,255,255,0.75); justify-content:center; align-items:flex-start; padding-top:2rem;">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">{% translate "Loading..." %}</span>
</div>
@ -543,16 +688,13 @@
{# Empty state #}
<div id="unified-empty-state" class="text-center py-5">
<i class="bi bi-layers fs-1 text-muted"></i>
<p class="mt-2">{% translate "Sélectionnez une ou plusieurs thématiques dans les filtres, saisissez une recherche ou zoomez sur la carte." %}</p>
<p class="mt-2">{% translate "Sélectionnez une ou plusieurs thématiques dans les filtres,
saisissez une recherche ou zoomez sur la carte." %}</p>
</div>
{# Table #}
<table id="unified-table"
class="table table-hover table-sm"
data-pagination="true"
data-page-size="100"
data-click-to-select="true"
data-show-columns="true"
<table id="unified-table" class="table table-hover table-sm" data-pagination="true"
data-page-size="100" data-click-to-select="true" data-show-columns="true"
data-search="false">
</table>
@ -563,7 +705,7 @@
</div>
{# ─── Map column ──────────────────────────────────────────────────── #}
<div id="col-map" class="col-md-6" style="position: relative; margin-top: 44px;">
<div id="col-map" class="col-md-6" style="position: relative; margin-top: 50px;">
<div id="info-panel">
<div class="panel-header">
<button id="info-panel-back" class="btn btn-sm btn-outline-secondary me-2" style="display:none;">
@ -576,8 +718,11 @@
</div>
<div id="map" style="position: relative; height: calc(100vh - 200px);">
<div id="assets-hint" class="small fst-italic position-absolute start-50 translate-middle-x bg-white px-3 py-1 rounded-pill shadow-sm border" style="top: 10px; z-index: 5; visibility: hidden; color: #0d6efd; white-space: nowrap; pointer-events: none;">
<i class="bi bi-info-circle me-1"></i>{% translate "Choisissez une catégorie ou zoomez sur la carte pour voir les assets." %}
<div id="assets-hint"
class="small fst-italic position-absolute start-50 translate-middle-x bg-white px-3 py-1 rounded-pill shadow-sm border"
style="top: 10px; z-index: 5; visibility: hidden; color: #0d6efd; white-space: nowrap; pointer-events: none;">
<i class="bi bi-info-circle me-1"></i>{% translate "Choisissez une catégorie ou zoomez sur la carte
pour voir les assets." %}
</div>
<div id="totalsOverlay" class="totals-overlay">
<div class="totals-toolbar">
@ -590,35 +735,43 @@
</div>
</div>
<div>
<button id="totals_button" class="btn btn-light mt-2 mb-1" type="button">{% translate "Totaux" %}</button>
<button id="totals_button" class="btn btn-light mt-2 mb-1" type="button">{% translate "Totaux"
%}</button>
</div>
</div>
</div>
</div>
{% if not preventive_draft_id %}
<div class="modal fade" id="unifiedExportModal" tabindex="-1" aria-labelledby="unifiedExportModalLabel" aria-hidden="true">
<div class="modal fade" id="unifiedExportModal" tabindex="-1" aria-labelledby="unifiedExportModalLabel"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="unifiedExportModalLabel">{% translate "Exporter les assets sélectionnés" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% translate 'Fermer' %}"></button>
<h5 class="modal-title" id="unifiedExportModalLabel">{% translate "Exporter les assets sélectionnés" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"
aria-label="{% translate 'Fermer' %}"></button>
</div>
<div class="modal-body">
<p class="text-muted small">{% translate "Choisissez le format d'export :" %}</p>
<div class="form-check">
<input class="form-check-input" type="radio" name="unified-export-format" id="unified-export-format-csv" value="csv" checked>
<input class="form-check-input" type="radio" name="unified-export-format"
id="unified-export-format-csv" value="csv" checked>
<label class="form-check-label" for="unified-export-format-csv">CSV</label>
</div>
<div class="form-check">
<input class="form-check-input" type="radio" name="unified-export-format" id="unified-export-format-xlsx" value="xlsx">
<input class="form-check-input" type="radio" name="unified-export-format"
id="unified-export-format-xlsx" value="xlsx">
<label class="form-check-label" for="unified-export-format-xlsx">Excel (XLSX)</label>
</div>
<div id="unified-export-error" class="alert alert-danger d-none mt-3" role="alert"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% translate "Annuler" %}</button>
<button type="button" class="btn btn-primary" id="unified-confirm-export-btn">{% translate "Exporter" %}</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% translate "Annuler"
%}</button>
<button type="button" class="btn btn-primary" id="unified-confirm-export-btn">{% translate "Exporter"
%}</button>
</div>
</div>
</div>
@ -646,8 +799,10 @@
<div id="bulk-status-error" class="alert alert-danger d-none" role="alert"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% translate "Annuler" %}</button>
<button type="button" class="btn btn-primary" id="bulk-status-confirm-btn">{% translate "Enregistrer" %}</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% translate "Annuler"
%}</button>
<button type="button" class="btn btn-primary" id="bulk-status-confirm-btn">{% translate "Enregistrer"
%}</button>
</div>
</div>
</div>
@ -656,16 +811,19 @@
{# ─── Modal bulk validation status ──────────────────────────────────────── #}
{% if has_bulk_edit_validation_status_permission %}
<div class="modal fade" id="bulkValidationStatusModal" tabindex="-1" aria-labelledby="bulkValidationStatusModalLabel" aria-hidden="true">
<div class="modal fade" id="bulkValidationStatusModal" tabindex="-1" aria-labelledby="bulkValidationStatusModalLabel"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="bulkValidationStatusModalLabel">{% translate "Modifier le statut de validation en masse" %}</h5>
<h5 class="modal-title" id="bulkValidationStatusModalLabel">{% translate "Modifier le statut de
validation en masse" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="bulk-validation-status-select" class="form-label">{% translate "Nouveau statut de validation" %}</label>
<label for="bulk-validation-status-select" class="form-label">{% translate "Nouveau statut de
validation" %}</label>
<select class="form-select" id="bulk-validation-status-select">
<option value="">-</option>
{% for value, label in asset_validation_status_choices %}
@ -676,8 +834,10 @@
<div id="bulk-validation-status-error" class="alert alert-danger d-none" role="alert"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% translate "Annuler" %}</button>
<button type="button" class="btn btn-primary" id="bulk-validation-status-confirm-btn">{% translate "Enregistrer" %}</button>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% translate "Annuler"
%}</button>
<button type="button" class="btn btn-primary" id="bulk-validation-status-confirm-btn">{% translate
"Enregistrer" %}</button>
</div>
</div>
</div>
@ -699,32 +859,32 @@
{% endblock content %}
{% block script %}
<script src="{% static 'common/mapbox-gl-draw.js' %}"></script>
<script src="{% static 'common/layer-switcher-control.js' %}"></script>
<script>
<script src="{% static 'common/mapbox-gl-draw.js' %}"></script>
<script src="{% static 'common/layer-switcher-control.js' %}"></script>
<script>
const preventive_draft_id = "{{ preventive_draft_id|default:'' }}";
var selected_locations_set = new Set({{ selected_locations_ids|safe }});
const initial_selected_assets = {{ selected_assets_json|safe }};
var selected_locations_set = new Set({{ selected_locations_ids| safe }});
const initial_selected_assets = {{ selected_assets_json| safe }};
var selected_assets_set = new Set();
const preventive_element_type = "{{ preventive_element_type|default:'' }}";
const INITIAL_ASSET_LAYOUT = '{{ request.user.config.assets_list_layout|default:"both" }}';
const THEMATIC_CONFIG = {{ thematic_config_json|safe }};
const CONDITIONAL_FILTER_CONFIG = {{ conditional_filter_config_json|safe }};
const THEMATIC_CATEGORIES = {{ thematic_categories_json|safe }};
const ADD_MODAL_CONFIG = {{ add_modal_config_json|safe }};
const SELECTED_THEMATICS = {{ selected_thematics|safe }};
const HAS_URL_THEMATICS = {% if has_url_thematics %}true{% else %}false{% endif %};
const SELECTED_CATEGORIES = {{ selected_categories_json|safe }};
const THEMATIC_CONFIG = {{ thematic_config_json| safe }};
const CONDITIONAL_FILTER_CONFIG = {{ conditional_filter_config_json| safe }};
const THEMATIC_CATEGORIES = {{ thematic_categories_json| safe }};
const ADD_MODAL_CONFIG = {{ add_modal_config_json| safe }};
const SELECTED_THEMATICS = {{ selected_thematics| safe }};
const HAS_URL_THEMATICS = {% if has_url_thematics %}true{% else %} false{% endif %};
const SELECTED_CATEGORIES = {{ selected_categories_json| safe }};
const PREVENTIVE_DRAFT_ID = "{{ preventive_draft_id|default:'' }}";
const LOCKED_THEMATIC_CODE = "{{ locked_thematic_code|default:'' }}";
const PREVENTIVE_ELEMENT_TYPE = "{{ preventive_element_type|default:'' }}";
const SELECTED_ASSETS = {{ selected_assets_json|safe }};
const SELECTED_LOCATION_IDS = {{ selected_locations_ids|safe }};
const SELECTED_ASSETS = {{ selected_assets_json| safe }};
const SELECTED_LOCATION_IDS = {{ selected_locations_ids| safe }};
const LINK_ASSETS_URL = "{{ link_assets_url|default:'' }}";
const AUTOCOMPLETE_STREET_URL = "{% url 'common:autocomplete_street' %}";
const ASSET_MODELS_BY_CATEGORY_URL_TEMPLATE = "{% url 'assets:get_asset_models' 'THEMATIC_CODE' 0 %}";
const ASSETS_INDEX_PREFS = {{ assets_index_prefs_json|safe }};
const ASSETS_INDEX_PREFS = {{ assets_index_prefs_json| safe }};
const DEFAULT_THEMATIC_CODE = "{{ default_thematic_code|default:'' }}";
const SAVE_ASSETS_PREFS_URL = "{% url 'common:save_assets_index_preferences' %}";
const exportAssetsUrl = "{% url 'assets:export_assets' %}";
@ -746,11 +906,11 @@
artworkmonument: "{% static 'assets/artwork_statue_icon.svg' %}?v=20260521a",
// publiclighting (poteaux, luminaires) uses a dedicated lamp-post SVG
};
</script>
{% include "assets/includes/panel_rendering_functions.html" %}
<script src="{% static 'common/base_script.js' %}"></script>
<script src="{% static 'common/base_map.js' %}"></script>
<script src="{% static 'assets/sign_panels_shared.js' %}?v=20260703c"></script>
<script src="{% static 'assets/unified_map_layers.js' %}?v=20260726_pb"></script>
<script src="{% static 'assets/unified_index.js' %}?v=20260726_pb"></script>
</script>
{% include "assets/includes/panel_rendering_functions.html" %}
<script src="{% static 'common/base_script.js' %}"></script>
<script src="{% static 'common/base_map.js' %}"></script>
<script src="{% static 'assets/sign_panels_shared.js' %}?v=20260703c"></script>
<script src="{% static 'assets/unified_map_layers.js' %}?v=20260726_pb"></script>
<script src="{% static 'assets/unified_index.js' %}?v=20260726_pb"></script>
{% endblock script %}