fix(interventions): polish filter modal UX and add global select-all checkbox

- Remove active filter bar above toolbar
- Fix 'All' / 'None' buttons for thematic tiles in filter modal
- Stabilize modal height and top alignment across all 3 tabs
- Replace toolbar select buttons with synchronized global checkboxes
This commit is contained in:
kdeterme 2026-10-12 20:04:54 +02:00
parent ad1a339dbe
commit 8da1542ebb
3 changed files with 103 additions and 52 deletions

View file

@ -39,7 +39,7 @@
/* ─── Airbnb-style Filter Modal ────────────────────────────────────── */ /* ─── Airbnb-style Filter Modal ────────────────────────────────────── */
#filterModal .modal-dialog { #filterModal .modal-dialog {
max-width: 880px; max-width: 880px;
margin: 1.5rem auto; margin: 1.75rem auto;
} }
#filterModal .modal-content { #filterModal .modal-content {
@ -48,7 +48,8 @@
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
max-height: 88vh; height: 84vh;
max-height: 84vh;
overflow: hidden; overflow: hidden;
background-color: #ffffff; background-color: #ffffff;
} }
@ -119,6 +120,8 @@
#filterModal .modal-body { #filterModal .modal-body {
padding: 1.25rem 1.5rem; padding: 1.25rem 1.5rem;
overflow-y: auto; overflow-y: auto;
flex: 1 1 auto;
min-height: 0;
background-color: #f8fafc; background-color: #f8fafc;
} }
@ -772,6 +775,29 @@
user-select: none; user-select: none;
} }
.interventions-table th.col-actions {
text-align: center !important;
width: 52px;
min-width: 52px;
padding: 0.55rem 0.25rem !important;
}
#th-select-all-checkbox,
#select-all-checkbox {
width: 16px;
height: 16px;
cursor: pointer;
border-color: #cbd5e1;
}
#th-select-all-checkbox:checked,
#select-all-checkbox:checked,
#th-select-all-checkbox:indeterminate,
#select-all-checkbox:indeterminate {
background-color: #2563eb;
border-color: #2563eb;
}
.interventions-table thead th.sortable { .interventions-table thead th.sortable {
cursor: pointer; cursor: pointer;
transition: background-color 0.12s ease; transition: background-color 0.12s ease;

View file

@ -869,10 +869,11 @@ function renderTable() {
applyColumnVisibility(); applyColumnVisibility();
detectTruncatedTitles(); detectTruncatedTitles();
persistInterventionsListContext(); persistInterventionsListContext();
updateInputAndStatus();
} }
function renderRowHtml(row) { function renderRowHtml(row) {
const isSelected = selected_interventions_set.has(row.id); const isSelected = selected_interventions_set.has(parseInt(row.id, 10));
const codeLabel = escapeHtml(row.code || `INT#${row.id}`); const codeLabel = escapeHtml(row.code || `INT#${row.id}`);
const detailUrl = `${interventionsurl}${row.id}/`; const detailUrl = `${interventionsurl}${row.id}/`;
@ -1449,31 +1450,64 @@ function toggleSelectedIntervention(interventionId, $button) {
function updateInputAndStatus() { function updateInputAndStatus() {
const statusBar = document.getElementById("selection-status"); const statusBar = document.getElementById("selection-status");
const ids = Array.from(selected_interventions_set); const count = selected_interventions_set.size;
const count = ids.length; if (statusBar) {
const msg = ngettext( if (count > 0) {
"%(count)s intervention sélectionnée", const msg = ngettext(
"%(count)s interventions sélectionnées", "%(count)s sélectionnée",
count "%(count)s sélectionnées",
); count
statusBar.innerText = interpolate(msg, { count }, true); );
statusBar.innerText = `(${interpolate(msg, { count }, true)})`;
} else {
statusBar.innerText = "(0)";
}
}
const rows = filteredInterventions && filteredInterventions.length ? filteredInterventions : allInterventions;
const totalRows = rows ? rows.length : 0;
const cbToolbar = document.getElementById("select-all-checkbox");
const cbTh = document.getElementById("th-select-all-checkbox");
[cbToolbar, cbTh].forEach(cb => {
if (!cb) return;
if (totalRows === 0 || count === 0) {
cb.checked = false;
cb.indeterminate = false;
} else if (count >= totalRows) {
cb.checked = true;
cb.indeterminate = false;
} else {
cb.checked = false;
cb.indeterminate = true;
}
});
}
function handleGlobalSelectAll(checked) {
const rows = filteredInterventions && filteredInterventions.length ? filteredInterventions : allInterventions;
if (checked) {
rows.forEach(row => {
selected_interventions_set.add(parseInt(row.id, 10));
});
} else {
selected_interventions_set.clear();
}
renderTable();
updateInputAndStatus();
} }
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
const cbToolbar = document.getElementById("select-all-checkbox");
const cbTh = document.getElementById("th-select-all-checkbox");
document.getElementById("select-all")?.addEventListener("click", () => { cbToolbar?.addEventListener("change", function () {
const rows = filteredInterventions && filteredInterventions.length ? filteredInterventions : allInterventions; handleGlobalSelectAll(this.checked);
rows.forEach(row => {
selected_interventions_set.add(parseInt(row.id));
});
renderTable();
updateInputAndStatus();
}); });
document.getElementById("deselect-all")?.addEventListener("click", () => { cbTh?.addEventListener("change", function () {
selected_interventions_set.clear(); handleGlobalSelectAll(this.checked);
renderTable();
updateInputAndStatus();
}); });
// Initial state // Initial state
@ -2485,12 +2519,13 @@ function togglePauseReasonFilter() {
btn.addEventListener('click', function(e) { btn.addEventListener('click', function(e) {
e.preventDefault(); e.preventDefault();
const field = this.getAttribute('data-field'); const field = this.getAttribute('data-field');
const container = document.querySelector(`.filter-options[data-field="${field}"]`); if (!field) return;
const container = document.querySelector(`.filter-options[data-field="${field}"], .thematic-grid[data-field="${field}"]`);
if (container) { if (container) {
const checkboxes = container.querySelectorAll('input[type="checkbox"]:not(:disabled)'); const checkboxes = container.querySelectorAll('input[type="checkbox"]:not(:disabled)');
checkboxes.forEach(checkbox => { checkboxes.forEach(checkbox => {
// Only check visible checkboxes (for categories filtered by thematics) // Only check visible checkboxes (for categories filtered by thematics, or thematic tiles)
const parent = checkbox.closest('.filter-option'); const parent = checkbox.closest('.filter-option, .thematic-tile');
if (!parent || parent.style.display !== 'none') { if (!parent || parent.style.display !== 'none') {
checkbox.checked = true; checkbox.checked = true;
} }
@ -2504,15 +2539,10 @@ function togglePauseReasonFilter() {
filterCategoriesByThematics(); filterCategoriesByThematics();
filterProvidersAndContracts(); filterProvidersAndContracts();
} }
if (field === 'provider') { if (field === 'provider' || field === 'contract' || field === 'assigned_team') {
filterProvidersAndContracts();
}
if (field === 'contract') {
filterProvidersAndContracts();
}
if (field === 'assigned_team') {
filterProvidersAndContracts(); filterProvidersAndContracts();
} }
updateFilterTabBadges();
} }
}); });
}); });
@ -2521,7 +2551,8 @@ function togglePauseReasonFilter() {
btn.addEventListener('click', function(e) { btn.addEventListener('click', function(e) {
e.preventDefault(); e.preventDefault();
const field = this.getAttribute('data-field'); const field = this.getAttribute('data-field');
const container = document.querySelector(`.filter-options[data-field="${field}"]`); if (!field) return;
const container = document.querySelector(`.filter-options[data-field="${field}"], .thematic-grid[data-field="${field}"]`);
if (container) { if (container) {
const checkboxes = container.querySelectorAll('input[type="checkbox"]:not(:disabled)'); const checkboxes = container.querySelectorAll('input[type="checkbox"]:not(:disabled)');
checkboxes.forEach(checkbox => { checkboxes.forEach(checkbox => {
@ -2536,15 +2567,10 @@ function togglePauseReasonFilter() {
filterCategoriesByThematics(); filterCategoriesByThematics();
filterProvidersAndContracts(); filterProvidersAndContracts();
} }
if (field === 'provider') { if (field === 'provider' || field === 'contract' || field === 'assigned_team') {
filterProvidersAndContracts();
}
if (field === 'contract') {
filterProvidersAndContracts();
}
if (field === 'assigned_team') {
filterProvidersAndContracts(); filterProvidersAndContracts();
} }
updateFilterTabBadges();
} }
}); });
}); });

View file

@ -50,7 +50,7 @@
<!-- ─── Modale Filtre des interventions (3 Onglets) ─────────────── --> <!-- ─── Modale Filtre des interventions (3 Onglets) ─────────────── -->
<div class="modal fade" id="filterModal" tabindex="-1" aria-labelledby="filterModalLabel" aria-hidden="true"> <div class="modal fade" id="filterModal" tabindex="-1" aria-labelledby="filterModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable"> <div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<div class="w-100"> <div class="w-100">
@ -106,7 +106,7 @@
<button type="button" class="btn deselect-all-btn" data-field="thematic">{% translate "Aucun" %}</button> <button type="button" class="btn deselect-all-btn" data-field="thematic">{% translate "Aucun" %}</button>
</div> </div>
</div> </div>
<div class="thematic-grid" data-field="thematic"> <div class="thematic-grid filter-options" data-field="thematic">
{% for key, value, icon in thematic_choices %} {% for key, value, icon in thematic_choices %}
<div class="thematic-tile"> <div class="thematic-tile">
<input type="checkbox" name="thematic" id="thematic_{{ key }}" value="{{ key }}" {% if key in selected_thematics %}checked{% endif %}> <input type="checkbox" name="thematic" id="thematic_{{ key }}" value="{{ key }}" {% if key in selected_thematics %}checked{% endif %}>
@ -768,11 +768,6 @@
</div> </div>
</div> </div>
<!-- Puces des filtres actifs -->
<div class="active-filters-bar d-none" id="active-filters-bar">
<!-- Injecté dynamiquement par JS -->
</div>
<!-- Barre d'outils du tableau --> <!-- Barre d'outils du tableau -->
<div class="interventions-toolbar d-flex justify-content-between align-items-center flex-wrap gap-2 mb-2"> <div class="interventions-toolbar d-flex justify-content-between align-items-center flex-wrap gap-2 mb-2">
<div class="d-flex align-items-center flex-wrap gap-2"> <div class="d-flex align-items-center flex-wrap gap-2">
@ -866,10 +861,12 @@
<!-- Outils de sélection et sélecteur de page --> <!-- Outils de sélection et sélecteur de page -->
<div class="d-flex align-items-center flex-wrap gap-2"> <div class="d-flex align-items-center flex-wrap gap-2">
<div class="d-flex gap-1 align-items-center"> <div class="d-flex align-items-center gap-2">
<button type="button" class="btn btn-xs btn-outline-secondary py-1 px-2" style="font-size: 0.76rem;" id="select-all">{% translate "Tout sélectionner" %}</button> <div class="form-check d-flex align-items-center mb-0 ps-0">
<button type="button" class="btn btn-xs btn-outline-secondary py-1 px-2" style="font-size: 0.76rem;" id="deselect-all">{% translate "Tout désélectionner" %}</button> <input type="checkbox" class="form-check-input me-1 mt-0" id="select-all-checkbox" style="cursor: pointer;">
<span id="selection-status" class="text-muted small ms-1" style="font-size: 0.78rem;">0 intervention sélectionnée</span> <label for="select-all-checkbox" class="form-check-label small text-secondary cursor-pointer user-select-none" style="font-size: 0.8rem;">{% translate "Tout sélectionner" %}</label>
</div>
<span id="selection-status" class="text-muted small" style="font-size: 0.78rem;">(0)</span>
</div> </div>
<span id="filter-limit-alert" class="alert alert-info py-1 px-2 mb-0 d-none small"> <span id="filter-limit-alert" class="alert alert-info py-1 px-2 mb-0 d-none small">
@ -902,7 +899,9 @@
<table id="interventions-table" class="interventions-table"> <table id="interventions-table" class="interventions-table">
<thead> <thead>
<tr> <tr>
<th class="col-actions" style="width: 52px; text-align: center;"></th> <th class="col-actions" style="width: 52px; text-align: center;">
<input type="checkbox" class="form-check-input mt-0" id="th-select-all-checkbox" style="cursor: pointer;" title="{% translate 'Tout sélectionner' %}">
</th>
<th class="sortable col-code" data-field="code"> <th class="sortable col-code" data-field="code">
<div class="sort-header"> <div class="sort-header">
<span>{% translate "Code" %}</span> <span>{% translate "Code" %}</span>