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:
parent
ad1a339dbe
commit
8da1542ebb
3 changed files with 103 additions and 52 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue