feat: implement dynamic location chips count and interactive date filter timeline preview

This commit is contained in:
kdeterme 2026-08-31 11:55:08 +02:00
parent 5c042531e5
commit 5e2fb023c9
2 changed files with 638 additions and 52 deletions

View file

@ -336,6 +336,7 @@ document.getElementById('reset-filters').addEventListener('click', async functio
document.getElementById('begin_date_max').value = ''; document.getElementById('begin_date_max').value = '';
document.getElementById('end_date_min').value = ''; document.getElementById('end_date_min').value = '';
document.getElementById('end_date_max').value = ''; document.getElementById('end_date_max').value = '';
updateDateFilterTimelinePreview();
// Save empty preferences // Save empty preferences
if (typeof savePreferencesUrl !== 'undefined') { if (typeof savePreferencesUrl !== 'undefined') {
@ -1543,13 +1544,34 @@ function hasLocation(ct, obj) {
} }
function renderLocationChips() { function renderLocationChips() {
const wrap = document.getElementById('selected-locations-chips'); const wrap = document.getElementById('selected-locations-chips');
const countBadge = document.getElementById('selected-locations-badge');
const clearBtn = document.getElementById('clear-locations');
if (countBadge) {
if (selectedLocations.length > 0) {
countBadge.textContent = `${selectedLocations.length} ${selectedLocations.length > 1 ? gettext('sélectionnées') : gettext('sélectionnée')}`;
countBadge.classList.remove('d-none');
} else {
countBadge.classList.add('d-none');
}
}
if (clearBtn) {
if (selectedLocations.length > 0) {
clearBtn.classList.remove('d-none');
} else {
clearBtn.classList.add('d-none');
}
}
if (!wrap) return;
wrap.innerHTML = ''; wrap.innerHTML = '';
// État vide → message léger // État vide → message léger
if (!selectedLocations.length) { if (!selectedLocations.length) {
const empty = document.createElement('div'); const empty = document.createElement('div');
empty.className = 'text-muted fst-italic'; empty.className = 'text-muted small fst-italic py-1';
empty.textContent = gettext('Aucune localisation sélectionnée'); empty.innerHTML = `<i class="bi bi-info-circle me-1"></i>${gettext('Aucune localisation sélectionnée (toutes les localisations sont incluses)')}`;
wrap.appendChild(empty); wrap.appendChild(empty);
return; return;
} }
@ -1561,43 +1583,35 @@ function renderLocationChips() {
selectedLocations.forEach(item => { selectedLocations.forEach(item => {
const li = document.createElement('li'); const li = document.createElement('li');
li.className = 'list-group-item d-flex justify-content-between align-items-center location-item'; li.className = 'list-group-item d-flex justify-content-between align-items-center location-item py-1 px-2 mb-1 border rounded shadow-sm';
// Bloc texte (label + sous-ligne) // Bloc texte (icône + label)
const info = document.createElement('div'); const info = document.createElement('div');
info.className = 'd-flex flex-column flex-grow-1 me-3'; info.className = 'd-flex align-items-center flex-grow-1 me-2 text-truncate';
const title = document.createElement('span'); info.innerHTML = `<i class="bi bi-geo-alt text-primary me-2 flex-shrink-0"></i><span class="fw-medium text-truncate small" title="${item.label || ''}">${item.label || ''}</span>`;
title.className = 'fw-medium text-truncate';
title.title = item.label || '';
title.textContent = item.label || '';
info.append(title);
// Badge thématique (à droite du label) // Badge thématique (à droite du label)
const badge = document.createElement('span'); const badge = document.createElement('span');
badge.className = 'badge text-bg-secondary me-2 align-self-center'; badge.className = 'badge bg-secondary bg-opacity-10 text-secondary border border-secondary border-opacity-25 me-2 align-self-center';
badge.style.fontSize = '0.7rem';
badge.textContent = item.type_label || ''; badge.textContent = item.type_label || '';
// Bouton supprimer // Bouton supprimer
const btn = document.createElement('button'); const btn = document.createElement('button');
btn.type = 'button'; btn.type = 'button';
btn.className = 'btn btn-sm btn-outline-secondary'; btn.className = 'btn btn-xs btn-outline-danger p-0 px-1 border-0';
btn.style.lineHeight = '1';
btn.setAttribute('aria-label', gettext('Retirer cette localisation')); btn.setAttribute('aria-label', gettext('Retirer cette localisation'));
btn.setAttribute('data-action', 'remove-location'); btn.setAttribute('data-action', 'remove-location');
btn.setAttribute('data-ct', item.ct); btn.setAttribute('data-ct', item.ct);
btn.setAttribute('data-obj', item.obj); btn.setAttribute('data-obj', item.obj);
btn.innerHTML = '<i class="bi bi-x"></i>'; btn.innerHTML = '<i class="bi bi-x-circle-fill fs-6"></i>';
li.append(info, badge, btn); li.append(info, badge, btn);
ul.appendChild(li); ul.appendChild(li);
}); });
wrap.appendChild(ul); wrap.appendChild(ul);
// Compteur (optionnel)
// const count = document.createElement('div');
// count.className = 'form-text mt-1';
// count.textContent = `${selectedLocations.length} localisation(s) sélectionnée(s)`;
// wrap.appendChild(count);
} }
document.getElementById('selected-locations-chips')?.addEventListener('click', (e) => { document.getElementById('selected-locations-chips')?.addEventListener('click', (e) => {
const btn = e.target.closest('[data-action="remove-location"]'); const btn = e.target.closest('[data-action="remove-location"]');
@ -2519,9 +2533,10 @@ async function savePreferencesFromForm(urlParams) {
// ==================== Initialization ==================== // ==================== Initialization ====================
// Initialize filter badge and sort on page load // Initialize filter badge, sort, and date timeline preview on page load
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
updateFilterBadge(); updateFilterBadge();
initDateFilterPresets();
// Save preferences when table sort changes (user action only) // Save preferences when table sort changes (user action only)
const $table = $('#interventions-table'); const $table = $('#interventions-table');
@ -2659,3 +2674,318 @@ function updateInterventionManagers() {
row.classList.add('d-none'); row.classList.add('d-none');
} }
} }
// ==================== Date Filter Timeline & Interval Preview ====================
function _formatDateFr(dateStr) {
if (!dateStr) return '';
const parts = dateStr.split('-');
if (parts.length === 3) {
return `${parts[2]}/${parts[1]}/${parts[0]}`;
}
return dateStr;
}
function _formatIsoDate(d) {
const yyyy = d.getFullYear();
const mm = String(d.getMonth() + 1).padStart(2, '0');
const dd = String(d.getDate()).padStart(2, '0');
return `${yyyy}-${mm}-${dd}`;
}
function updateDateFilterTimelinePreview() {
const beginMinInput = document.getElementById('begin_date_min');
const beginMaxInput = document.getElementById('begin_date_max');
const endMinInput = document.getElementById('end_date_min');
const endMaxInput = document.getElementById('end_date_max');
if (!beginMinInput || !beginMaxInput || !endMinInput || !endMaxInput) return;
const bMin = beginMinInput.value;
const bMax = beginMaxInput.value;
const eMin = endMinInput.value;
const eMax = endMaxInput.value;
const beginBar = document.getElementById('timeline-bar-begin');
const endBar = document.getElementById('timeline-bar-end');
const beginText = document.getElementById('timeline-begin-text');
const endText = document.getElementById('timeline-end-text');
const summaryBox = document.getElementById('timeline-summary-box');
const summaryText = document.getElementById('timeline-summary-text');
const summaryIcon = document.getElementById('timeline-summary-icon');
const axisMinEl = document.getElementById('timeline-axis-min');
const axisMaxEl = document.getElementById('timeline-axis-max');
const rangeBadge = document.getElementById('timeline-range-badge');
if (!beginBar || !endBar || !beginText || !endText || !summaryText) return;
const hasBegin = !!(bMin || bMax);
const hasEnd = !!(eMin || eMax);
// Check consistency
const bInvalid = bMin && bMax && new Date(bMin) > new Date(bMax);
const eInvalid = eMin && eMax && new Date(eMin) > new Date(eMax);
if (bInvalid || eInvalid) {
if (summaryBox) {
summaryBox.className = 'timeline-summary-box p-2 rounded border small mb-2 has-warning';
}
if (summaryIcon) {
summaryIcon.className = 'bi bi-exclamation-triangle-fill text-danger mt-1 flex-shrink-0';
}
summaryText.innerHTML = `<span class="text-danger fw-semibold">${gettext("Attention : la date minimale (Du) ne peut pas être postérieure à la date maximale (Au).")}</span>`;
if (rangeBadge) {
rangeBadge.className = 'badge bg-danger text-white border small';
rangeBadge.textContent = gettext("Incohérence");
}
return;
}
if (summaryIcon) {
summaryIcon.className = 'bi bi-info-circle text-primary mt-1 flex-shrink-0';
}
// Determine timeline scale
const dates = [];
if (bMin) dates.push(new Date(bMin));
if (bMax) dates.push(new Date(bMax));
if (eMin) dates.push(new Date(eMin));
if (eMax) dates.push(new Date(eMax));
const today = new Date();
today.setHours(0, 0, 0, 0);
let axisMinDate, axisMaxDate;
if (dates.length === 0) {
// Default scale: current quarter (from 1 month ago to 2 months ahead)
axisMinDate = new Date(today.getFullYear(), today.getMonth() - 1, 1);
axisMaxDate = new Date(today.getFullYear(), today.getMonth() + 2, 0);
} else {
const minTimestamp = Math.min(...dates.map(d => d.getTime()));
const maxTimestamp = Math.max(...dates.map(d => d.getTime()));
const dayMs = 24 * 60 * 60 * 1000;
const spanDays = Math.max(14, (maxTimestamp - minTimestamp) / dayMs);
const padDays = Math.max(5, Math.round(spanDays * 0.15));
axisMinDate = new Date(minTimestamp - padDays * dayMs);
axisMaxDate = new Date(maxTimestamp + padDays * dayMs);
}
const axisMinTime = axisMinDate.getTime();
const axisMaxTime = axisMaxDate.getTime();
const totalDuration = Math.max(1, axisMaxTime - axisMinTime);
if (axisMinEl) {
const dStr = dates.length === 0 ? gettext("Passé") : axisMinDate.toLocaleDateString(undefined, { day: '2-digit', month: '2-digit', year: '2-digit' });
axisMinEl.textContent = dStr;
}
if (axisMaxEl) {
const dStr = dates.length === 0 ? gettext("Futur") : axisMaxDate.toLocaleDateString(undefined, { day: '2-digit', month: '2-digit', year: '2-digit' });
axisMaxEl.textContent = dStr;
}
// Helper to calculate percentage position [0..100]
function toPercent(dateStr) {
if (!dateStr) return null;
const t = new Date(dateStr).getTime();
const pct = ((t - axisMinTime) / totalDuration) * 100;
return Math.max(0, Math.min(100, pct));
}
// --- Begin track ---
if (!hasBegin) {
beginBar.className = 'timeline-bar timeline-bar-begin is-all';
beginBar.style.left = '0%';
beginBar.style.width = '100%';
beginText.textContent = gettext("Toutes dates");
beginText.className = 'badge bg-secondary bg-opacity-10 text-secondary border border-secondary border-opacity-25';
} else {
beginBar.className = 'timeline-bar timeline-bar-begin';
beginText.className = 'badge bg-success bg-opacity-10 text-success border border-success border-opacity-25';
if (bMin && bMax) {
const pMin = toPercent(bMin);
const pMax = toPercent(bMax);
const w = Math.max(2, pMax - pMin);
beginBar.style.left = `${pMin}%`;
beginBar.style.width = `${w}%`;
beginText.textContent = `${gettext("Du")} ${_formatDateFr(bMin)} ${gettext("au")} ${_formatDateFr(bMax)}`;
} else if (bMin) {
const pMin = toPercent(bMin);
beginBar.classList.add('is-open-right');
beginBar.style.left = `${pMin}%`;
beginBar.style.width = `${100 - pMin}%`;
beginText.textContent = `${gettext("À partir du")} ${_formatDateFr(bMin)}`;
} else if (bMax) {
const pMax = toPercent(bMax);
beginBar.classList.add('is-open-left');
beginBar.style.left = '0%';
beginBar.style.width = `${pMax}%`;
beginText.textContent = `${gettext("Jusqu'au")} ${_formatDateFr(bMax)}`;
}
}
// --- End track ---
if (!hasEnd) {
endBar.className = 'timeline-bar timeline-bar-end is-all';
endBar.style.left = '0%';
endBar.style.width = '100%';
endText.textContent = gettext("Toutes dates");
endText.className = 'badge bg-secondary bg-opacity-10 text-secondary border border-secondary border-opacity-25';
} else {
endBar.className = 'timeline-bar timeline-bar-end';
endText.className = 'badge bg-primary bg-opacity-10 text-primary border border-primary border-opacity-25';
if (eMin && eMax) {
const pMin = toPercent(eMin);
const pMax = toPercent(eMax);
const w = Math.max(2, pMax - pMin);
endBar.style.left = `${pMin}%`;
endBar.style.width = `${w}%`;
endText.textContent = `${gettext("Du")} ${_formatDateFr(eMin)} ${gettext("au")} ${_formatDateFr(eMax)}`;
} else if (eMin) {
const pMin = toPercent(eMin);
endBar.classList.add('is-open-right');
endBar.style.left = `${pMin}%`;
endBar.style.width = `${100 - pMin}%`;
endText.textContent = `${gettext("À partir du")} ${_formatDateFr(eMin)}`;
} else if (eMax) {
const pMax = toPercent(eMax);
endBar.classList.add('is-open-left');
endBar.style.left = '0%';
endBar.style.width = `${pMax}%`;
endText.textContent = `${gettext("Jusqu'au")} ${_formatDateFr(eMax)}`;
}
}
// --- Summary & Badges ---
if (summaryBox) {
if (hasBegin || hasEnd) {
summaryBox.className = 'timeline-summary-box p-2 rounded border small mb-2 has-filter';
} else {
summaryBox.className = 'timeline-summary-box p-2 rounded border small mb-2';
}
}
if (rangeBadge) {
if (hasBegin && hasEnd) {
rangeBadge.className = 'badge bg-primary text-white border small';
rangeBadge.textContent = gettext("Début & Fin filtrés");
} else if (hasBegin) {
rangeBadge.className = 'badge bg-success text-white border small';
rangeBadge.textContent = gettext("Début filtré");
} else if (hasEnd) {
rangeBadge.className = 'badge bg-primary text-white border small';
rangeBadge.textContent = gettext("Fin filtrée");
} else {
rangeBadge.className = 'badge bg-white text-secondary border small';
rangeBadge.textContent = gettext("Aperçu interactif");
}
}
// Natural Language Summary formulation
if (!hasBegin && !hasEnd) {
summaryText.innerHTML = gettext("Aucune restriction de date : toutes les interventions sont incluses.");
} else if (hasBegin && hasEnd) {
const beginDesc = bMin && bMax
? `${gettext("entre le")} <strong>${_formatDateFr(bMin)}</strong> ${gettext("et le")} <strong>${_formatDateFr(bMax)}</strong>`
: (bMin ? `${gettext("à partir du")} <strong>${_formatDateFr(bMin)}</strong>` : `${gettext("jusqu'au")} <strong>${_formatDateFr(bMax)}</strong>`);
const endDesc = eMin && eMax
? `${gettext("entre le")} <strong>${_formatDateFr(eMin)}</strong> ${gettext("et le")} <strong>${_formatDateFr(eMax)}</strong>`
: (eMin ? `${gettext("à partir du")} <strong>${_formatDateFr(eMin)}</strong>` : `${gettext("jusqu'au")} <strong>${_formatDateFr(eMax)}</strong>`);
summaryText.innerHTML = `${gettext("Sélectionne les interventions dont le")} <strong class="text-success">${gettext("début")}</strong> ${gettext("est")} ${beginDesc}, ${gettext("et dont la")} <strong class="text-primary">${gettext("fin (statut Terminé)")}</strong> ${gettext("est")} ${endDesc}.`;
} else if (hasBegin) {
const beginDesc = bMin && bMax
? `${gettext("entre le")} <strong>${_formatDateFr(bMin)}</strong> ${gettext("et le")} <strong>${_formatDateFr(bMax)}</strong>`
: (bMin ? `${gettext("à partir du")} <strong>${_formatDateFr(bMin)}</strong>` : `${gettext("jusqu'au")} <strong>${_formatDateFr(bMax)}</strong>`);
summaryText.innerHTML = `${gettext("Sélectionne les interventions dont le")} <strong class="text-success">${gettext("début")}</strong> ${gettext("est")} ${beginDesc} <em>(${gettext("aucune restriction sur la date de fin")})</em>.`;
} else if (hasEnd) {
const endDesc = eMin && eMax
? `${gettext("entre le")} <strong>${_formatDateFr(eMin)}</strong> ${gettext("et le")} <strong>${_formatDateFr(eMax)}</strong>`
: (eMin ? `${gettext("à partir du")} <strong>${_formatDateFr(eMin)}</strong>` : `${gettext("jusqu'au")} <strong>${_formatDateFr(eMax)}</strong>`);
summaryText.innerHTML = `${gettext("Sélectionne les interventions dont la")} <strong class="text-primary">${gettext("fin (passage à Terminé)")}</strong> ${gettext("est")} ${endDesc} <em>(${gettext("aucune restriction sur la date de début")})</em>.`;
}
}
function initDateFilterPresets() {
document.querySelectorAll('.date-preset-btn').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
const preset = this.getAttribute('data-preset');
const target = this.getAttribute('data-target');
const today = new Date();
let minVal = '', maxVal = '';
if (preset === 'clear') {
document.getElementById('begin_date_min').value = '';
document.getElementById('begin_date_max').value = '';
document.getElementById('end_date_min').value = '';
document.getElementById('end_date_max').value = '';
updateDateFilterTimelinePreview();
return;
} else if (preset === 'today') {
const s = _formatIsoDate(today);
minVal = s;
maxVal = s;
} else if (preset === 'this_month') {
const firstDay = new Date(today.getFullYear(), today.getMonth(), 1);
const lastDay = new Date(today.getFullYear(), today.getMonth() + 1, 0);
minVal = _formatIsoDate(firstDay);
maxVal = _formatIsoDate(lastDay);
} else if (preset === 'last_month') {
const firstDay = new Date(today.getFullYear(), today.getMonth() - 1, 1);
const lastDay = new Date(today.getFullYear(), today.getMonth(), 0);
minVal = _formatIsoDate(firstDay);
maxVal = _formatIsoDate(lastDay);
} else if (preset === 'this_year') {
const firstDay = new Date(today.getFullYear(), 0, 1);
const lastDay = new Date(today.getFullYear(), 11, 31);
minVal = _formatIsoDate(firstDay);
maxVal = _formatIsoDate(lastDay);
}
if (target === 'begin') {
document.getElementById('begin_date_min').value = minVal;
document.getElementById('begin_date_max').value = maxVal;
} else if (target === 'both') {
document.getElementById('begin_date_min').value = minVal;
document.getElementById('begin_date_max').value = '';
document.getElementById('end_date_min').value = '';
document.getElementById('end_date_max').value = maxVal;
} else {
// Apply presets on end dates (completion period)
document.getElementById('begin_date_min').value = '';
document.getElementById('begin_date_max').value = '';
document.getElementById('end_date_min').value = minVal;
document.getElementById('end_date_max').value = maxVal;
}
updateDateFilterTimelinePreview();
});
});
['begin_date_min', 'begin_date_max', 'end_date_min', 'end_date_max'].forEach(id => {
const el = document.getElementById(id);
if (el) {
el.addEventListener('input', updateDateFilterTimelinePreview);
el.addEventListener('change', updateDateFilterTimelinePreview);
}
});
const filterModal = document.getElementById('filterModal');
if (filterModal) {
filterModal.addEventListener('shown.bs.modal', function() {
updateDateFilterTimelinePreview();
});
}
updateDateFilterTimelinePreview();
}

View file

@ -93,7 +93,20 @@
} }
.location-item { .location-item {
background-color: #e1f6ff; background-color: #f8fafc;
border-color: #e2e8f0;
border-left: 3px solid #0d6efd !important;
transition: all 0.15s ease-in-out;
}
.location-item:hover {
background-color: #f1f5f9;
border-color: #cbd5e1;
}
#selected-locations-chips {
max-height: 150px;
overflow-y: auto;
} }
/* Styles pour la modale de filtre */ /* Styles pour la modale de filtre */
@ -405,6 +418,87 @@
border-color: #6c757d !important; border-color: #6c757d !important;
color: white !important; color: white !important;
} }
/* Timeline preview in filter modal */
.date-timeline-card {
background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%);
border-color: #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;
}
.timeline-bar.is-open-left {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
.timeline-bar.is-open-right {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.timeline-summary-box {
transition: all 0.2s ease;
}
.timeline-summary-box.has-filter {
background-color: #f0fdf4 !important;
border-color: #86efac !important;
}
.timeline-summary-box.has-warning {
background-color: #fef2f2 !important;
border-color: #fca5a5 !important;
}
</style> </style>
{% endblock head %} {% endblock head %}
@ -732,39 +826,201 @@
</div> </div>
<!-- Localisations (full width) --> <!-- Localisations (full width) -->
<div class="mt-3 mb-3"> <div class="filter-section mt-3 pt-3 border-top">
<label class="form-label">{% translate "Localisations" %}</label> <div class="filter-section-header">
<div id="location-autocomplete-container" class="position-relative"> <label class="d-flex align-items-center">
<input id="location-autocomplete" type="text" class="form-control" <i class="bi bi-geo-alt-fill me-2 text-danger"></i>{% translate "Localisations" %}
placeholder="{% translate 'Chercher une localisation (min. 2 caractères)…' %}"> <span class="badge bg-danger bg-opacity-10 text-danger border border-danger border-opacity-25 small ms-2 d-none" id="selected-locations-badge"></span>
</label>
<div class="filter-section-buttons">
<button id="clear-locations" type="button" class="btn btn-sm btn-outline-secondary d-none">
<i class="bi bi-x-circle me-1"></i>{% translate "Effacer localisations" %}
</button>
</div>
</div> </div>
<div id="selected-locations-chips" class="mt-2 d-flex flex-wrap gap-2"></div> <div id="location-autocomplete-container" class="position-relative mb-2">
<button id="clear-locations" type="button" class="btn btn-sm btn-outline-secondary mt-2"> <div class="input-group input-group-sm">
{% translate "Effacer les localisations" %} <span class="input-group-text bg-light text-muted"><i class="bi bi-search"></i></span>
</button> <input id="location-autocomplete" type="text" class="form-control"
placeholder="{% translate 'Chercher une rue, un carrefour, un équipement (min. 2 caractères)…' %}">
</div>
</div>
<div id="selected-locations-chips" class="mt-2"></div>
</div> </div>
<!-- Dates --> <!-- Dates -->
<div class="row mt-3"> <div class="row mt-3 pt-3 border-top">
<div class="col-6"> <div class="col-lg-6">
<div> <div class="filter-section-header">
<label class="form-label">{% translate "Date de début" %}</label> <label class="d-flex align-items-center">
<div class="d-flex align-items-center gap-2"> <i class="bi bi-calendar3 me-2 text-primary"></i>{% translate "Filtres par date" %}
<input type="date" name="begin_date_min" id="begin_date_min" class="form-control" placeholder="Min" value="{{ begin_date_min }}"> </label>
<span class="mx-1">→</span> <div class="filter-section-buttons">
<input type="date" name="begin_date_max" id="begin_date_max" class="form-control" placeholder="Max" value="{{ begin_date_max }}"> <button type="button" class="btn btn-sm btn-outline-secondary date-preset-btn" data-preset="clear">
</div> <i class="bi bi-x-circle me-1"></i>{% translate "Effacer dates" %}
</div> </button>
<div class="mt-3">
<label class="form-label">{% translate "Date de fin" %}</label>
<div class="d-flex align-items-center gap-2">
<input type="date" name="end_date_min" id="end_date_min" class="form-control" placeholder="Min" value="{{ end_date_min }}">
<span class="mx-1">→</span>
<input type="date" name="end_date_max" id="end_date_max" class="form-control" placeholder="Max" value="{{ end_date_max }}">
</div> </div>
</div> </div>
<div class="form-check mt-3"> <!-- Date de début -->
<div class="date-filter-group mb-3 p-2 rounded bg-light bg-opacity-50 border">
<div class="d-flex justify-content-between align-items-center mb-1">
<label class="form-label mb-0 fw-semibold text-success small">
<i class="bi bi-play-circle-fill me-1"></i>{% translate "Date de début" %}
</label>
<span class="badge bg-success bg-opacity-10 text-success border border-success border-opacity-25" style="font-size: 0.7rem;">
{% translate "Démarrage / Planification" %}
</span>
</div>
<div class="row g-2 align-items-center">
<div class="col-5">
<div class="input-group input-group-sm">
<span class="input-group-text py-0 px-2 text-muted small">{% translate "Du" %}</span>
<input type="date" name="begin_date_min" id="begin_date_min" class="form-control form-control-sm" placeholder="Min" value="{{ begin_date_min }}">
</div>
</div>
<div class="col-2 text-center text-muted">
<i class="bi bi-arrow-right"></i>
</div>
<div class="col-5">
<div class="input-group input-group-sm">
<span class="input-group-text py-0 px-2 text-muted small">{% translate "Au" %}</span>
<input type="date" name="begin_date_max" id="begin_date_max" class="form-control form-control-sm" placeholder="Max" value="{{ begin_date_max }}">
</div>
</div>
</div>
</div>
<!-- Date de fin -->
<div class="date-filter-group mb-3 p-2 rounded bg-light bg-opacity-50 border">
<div class="d-flex justify-content-between align-items-center mb-1">
<label class="form-label mb-0 fw-semibold text-primary small">
<i class="bi bi-check-circle-fill me-1"></i>{% translate "Date de fin" %}
</label>
<span class="badge bg-primary bg-opacity-10 text-primary border border-primary border-opacity-25" style="font-size: 0.7rem;">
{% translate "Fin travaux (statut Terminé)" %}
</span>
</div>
<div class="row g-2 align-items-center">
<div class="col-5">
<div class="input-group input-group-sm">
<span class="input-group-text py-0 px-2 text-muted small">{% translate "Du" %}</span>
<input type="date" name="end_date_min" id="end_date_min" class="form-control form-control-sm" placeholder="Min" value="{{ end_date_min }}">
</div>
</div>
<div class="col-2 text-center text-muted">
<i class="bi bi-arrow-right"></i>
</div>
<div class="col-5">
<div class="input-group input-group-sm">
<span class="input-group-text py-0 px-2 text-muted small">{% translate "Au" %}</span>
<input type="date" name="end_date_max" id="end_date_max" class="form-control form-control-sm" placeholder="Max" value="{{ end_date_max }}">
</div>
</div>
</div>
</div>
<!-- Raccourcis rapides / Presets -->
<div class="d-flex flex-wrap align-items-center gap-1 mb-2">
<span class="text-muted small me-1" style="font-size: 0.75rem;"><i class="bi bi-lightning-charge me-1"></i>{% translate "Périodes rapides (fin)" %} :</span>
<button type="button" class="btn btn-xs btn-outline-secondary date-preset-btn py-0 px-2" style="font-size: 0.72rem;" data-preset="today">{% translate "Aujourd'hui" %}</button>
<button type="button" class="btn btn-xs btn-outline-secondary date-preset-btn py-0 px-2" style="font-size: 0.72rem;" data-preset="this_month">{% translate "Ce mois" %}</button>
<button type="button" class="btn btn-xs btn-outline-secondary date-preset-btn py-0 px-2" style="font-size: 0.72rem;" data-preset="last_month">{% translate "Mois dernier" %}</button>
<button type="button" class="btn btn-xs btn-outline-secondary date-preset-btn py-0 px-2" style="font-size: 0.72rem;" data-preset="this_year">{% translate "Cette année" %}</button>
</div>
</div>
<!-- Panneau interactif de ligne du temps & explication d'intervalle -->
<div class="col-lg-6">
<div class="date-timeline-card h-100 p-3 rounded-3 border bg-light shadow-sm d-flex flex-column justify-content-between">
<div>
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="fw-semibold text-dark small d-flex align-items-center">
<i class="bi bi-clock-history text-primary me-2"></i>{% translate "Ligne du temps & Intervalles" %}
</div>
<span class="badge bg-white text-secondary border small" id="timeline-range-badge">{% translate "Aperçu interactif" %}</span>
</div>
<!-- Timeline Visual Track -->
<div class="timeline-visual-container position-relative my-3 p-3 bg-white rounded border shadow-sm">
<!-- Time Axis Line -->
<div class="timeline-axis position-relative mb-3">
<div class="timeline-axis-line"></div>
<div class="d-flex justify-content-between text-muted text-uppercase" style="font-size: 0.68rem; margin-top: 3px;">
<span id="timeline-axis-min">{% translate "Passé" %}</span>
<span class="badge bg-light text-secondary border px-1" id="timeline-today-marker"><i class="bi bi-calendar-event me-1"></i>{% translate "Aujourd'hui" %}</span>
<span id="timeline-axis-max">{% translate "Futur" %}</span>
</div>
</div>
<!-- Start Interval Track (Green) -->
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-1" style="font-size: 0.75rem;">
<span class="text-success fw-semibold">
<i class="bi bi-play-circle-fill me-1"></i>{% translate "Intervalle Date de début" %}
</span>
<span class="badge bg-success bg-opacity-10 text-success border border-success border-opacity-25" id="timeline-begin-text">
{% translate "Toutes dates" %}
</span>
</div>
<div class="timeline-bar-wrapper">
<div class="timeline-track-bg"></div>
<div class="timeline-bar timeline-bar-begin is-all" id="timeline-bar-begin"></div>
</div>
</div>
<!-- End Interval Track (Blue) -->
<div>
<div class="d-flex justify-content-between align-items-center mb-1" style="font-size: 0.75rem;">
<span class="text-primary fw-semibold">
<i class="bi bi-check-circle-fill me-1"></i>{% translate "Intervalle Date de fin (Terminé)" %}
</span>
<span class="badge bg-primary bg-opacity-10 text-primary border border-primary border-opacity-25" id="timeline-end-text">
{% translate "Toutes dates" %}
</span>
</div>
<div class="timeline-bar-wrapper">
<div class="timeline-track-bg"></div>
<div class="timeline-bar timeline-bar-end is-all" id="timeline-bar-end"></div>
</div>
</div>
</div>
<!-- Natural Language Summary Box -->
<div class="timeline-summary-box p-2 rounded border bg-white small mb-2" id="timeline-summary-box">
<div class="d-flex align-items-start gap-2">
<i class="bi bi-info-circle text-primary mt-1 flex-shrink-0" id="timeline-summary-icon"></i>
<div id="timeline-summary-text" class="text-muted" style="font-size: 0.8rem; line-height: 1.35;">
{% translate "Aucune restriction de date : toutes les interventions sont incluses." %}
</div>
</div>
</div>
</div>
<!-- Pedagogy diagram / Lifecycle explainer -->
<div class="p-2 rounded bg-white bg-opacity-75 border border-dashed mt-2" style="font-size: 0.72rem;">
<div class="text-muted fw-semibold mb-1">
<i class="bi bi-lightbulb text-warning me-1"></i>{% translate "Comment fonctionnent ces deux intervalles ?" %}
</div>
<div class="text-muted" style="line-height: 1.3;">
<div class="d-flex align-items-center gap-1 mb-1">
<span class="badge bg-success p-1 rounded-circle"></span>
<span><strong>{% translate "Date de début" %} [Min → Max]</strong> : {% translate "sélectionne les interventions dont le démarrage a lieu dans cette période." %}</span>
</div>
<div class="d-flex align-items-center gap-1">
<span class="badge bg-primary p-1 rounded-circle"></span>
<span><strong>{% translate "Date de fin" %} [Min → Max]</strong> : {% translate "sélectionne les interventions achevées (statut Terminé) dans cette période." %}</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Options supplémentaires (En retard / Suivies) -->
<div class="row mt-3 pt-3 border-top">
<div class="col-md-6">
<div class="form-check">
<input <input
class="form-check-input" class="form-check-input"
type="checkbox" type="checkbox"
@ -774,10 +1030,12 @@
{% if overdue == '1' %}checked{% endif %} {% if overdue == '1' %}checked{% endif %}
> >
<label class="form-check-label" for="overdue"> <label class="form-check-label" for="overdue">
{% translate "Interventions non traitées (en retard)" %} <i class="bi bi-clock-history text-danger me-1"></i>{% translate "Interventions non traitées (en retard)" %}
</label> </label>
</div> </div>
<div class="form-check mt-2"> </div>
<div class="col-md-6">
<div class="form-check">
<input <input
class="form-check-input" class="form-check-input"
type="checkbox" type="checkbox"
@ -787,12 +1045,10 @@
{% if followed_only == '1' %}checked{% endif %} {% if followed_only == '1' %}checked{% endif %}
> >
<label class="form-check-label" for="followed_only"> <label class="form-check-label" for="followed_only">
{% translate "Interventions suivies uniquement" %} <i class="bi bi-star-fill text-warning me-1"></i>{% translate "Interventions suivies uniquement" %}
</label> </label>
</div> </div>
</div> </div>
<div class="col-6"></div>
</div> </div>
{% if to_be_planned_soon %} {% if to_be_planned_soon %}