From 5e2fb023c9f4f590b01b6aa9a9208d211e7705a6 Mon Sep 17 00:00:00 2001 From: kdeterme Date: Mon, 31 Aug 2026 11:55:08 +0200 Subject: [PATCH] feat: implement dynamic location chips count and interactive date filter timeline preview --- .../interventions/interventions_index.js | 370 +++++++++++++++++- .../interventions/interventions_index.html | 320 +++++++++++++-- 2 files changed, 638 insertions(+), 52 deletions(-) diff --git a/loko/interventions/static/interventions/interventions_index.js b/loko/interventions/static/interventions/interventions_index.js index 1206d2b..8188e81 100644 --- a/loko/interventions/static/interventions/interventions_index.js +++ b/loko/interventions/static/interventions/interventions_index.js @@ -336,6 +336,7 @@ document.getElementById('reset-filters').addEventListener('click', async functio document.getElementById('begin_date_max').value = ''; document.getElementById('end_date_min').value = ''; document.getElementById('end_date_max').value = ''; + updateDateFilterTimelinePreview(); // Save empty preferences if (typeof savePreferencesUrl !== 'undefined') { @@ -1543,13 +1544,34 @@ function hasLocation(ct, obj) { } function renderLocationChips() { 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 = ''; // État vide → message léger if (!selectedLocations.length) { const empty = document.createElement('div'); - empty.className = 'text-muted fst-italic'; - empty.textContent = gettext('Aucune localisation sélectionnée'); + empty.className = 'text-muted small fst-italic py-1'; + empty.innerHTML = `${gettext('Aucune localisation sélectionnée (toutes les localisations sont incluses)')}`; wrap.appendChild(empty); return; } @@ -1561,43 +1583,35 @@ function renderLocationChips() { selectedLocations.forEach(item => { 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'); - info.className = 'd-flex flex-column flex-grow-1 me-3'; - const title = document.createElement('span'); - title.className = 'fw-medium text-truncate'; - title.title = item.label || ''; - title.textContent = item.label || ''; - info.append(title); + info.className = 'd-flex align-items-center flex-grow-1 me-2 text-truncate'; + info.innerHTML = `${item.label || ''}`; // Badge thématique (à droite du label) 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 || ''; // Bouton supprimer const btn = document.createElement('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('data-action', 'remove-location'); btn.setAttribute('data-ct', item.ct); btn.setAttribute('data-obj', item.obj); - btn.innerHTML = ''; + btn.innerHTML = ''; li.append(info, badge, btn); ul.appendChild(li); }); 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) => { const btn = e.target.closest('[data-action="remove-location"]'); @@ -2519,9 +2533,10 @@ async function savePreferencesFromForm(urlParams) { // ==================== Initialization ==================== -// Initialize filter badge and sort on page load +// Initialize filter badge, sort, and date timeline preview on page load document.addEventListener('DOMContentLoaded', function() { updateFilterBadge(); + initDateFilterPresets(); // Save preferences when table sort changes (user action only) const $table = $('#interventions-table'); @@ -2659,3 +2674,318 @@ function updateInterventionManagers() { 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 = `${gettext("Attention : la date minimale (Du) ne peut pas être postérieure à la date maximale (Au).")}`; + 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")} ${_formatDateFr(bMin)} ${gettext("et le")} ${_formatDateFr(bMax)}` + : (bMin ? `${gettext("à partir du")} ${_formatDateFr(bMin)}` : `${gettext("jusqu'au")} ${_formatDateFr(bMax)}`); + + const endDesc = eMin && eMax + ? `${gettext("entre le")} ${_formatDateFr(eMin)} ${gettext("et le")} ${_formatDateFr(eMax)}` + : (eMin ? `${gettext("à partir du")} ${_formatDateFr(eMin)}` : `${gettext("jusqu'au")} ${_formatDateFr(eMax)}`); + + summaryText.innerHTML = `${gettext("Sélectionne les interventions dont le")} ${gettext("début")} ${gettext("est")} ${beginDesc}, ${gettext("et dont la")} ${gettext("fin (statut Terminé)")} ${gettext("est")} ${endDesc}.`; + } else if (hasBegin) { + const beginDesc = bMin && bMax + ? `${gettext("entre le")} ${_formatDateFr(bMin)} ${gettext("et le")} ${_formatDateFr(bMax)}` + : (bMin ? `${gettext("à partir du")} ${_formatDateFr(bMin)}` : `${gettext("jusqu'au")} ${_formatDateFr(bMax)}`); + + summaryText.innerHTML = `${gettext("Sélectionne les interventions dont le")} ${gettext("début")} ${gettext("est")} ${beginDesc} (${gettext("aucune restriction sur la date de fin")}).`; + } else if (hasEnd) { + const endDesc = eMin && eMax + ? `${gettext("entre le")} ${_formatDateFr(eMin)} ${gettext("et le")} ${_formatDateFr(eMax)}` + : (eMin ? `${gettext("à partir du")} ${_formatDateFr(eMin)}` : `${gettext("jusqu'au")} ${_formatDateFr(eMax)}`); + + summaryText.innerHTML = `${gettext("Sélectionne les interventions dont la")} ${gettext("fin (passage à Terminé)")} ${gettext("est")} ${endDesc} (${gettext("aucune restriction sur la date de début")}).`; + } +} + +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(); +} diff --git a/loko/interventions/templates/interventions/interventions_index.html b/loko/interventions/templates/interventions/interventions_index.html index a361261..c236982 100644 --- a/loko/interventions/templates/interventions/interventions_index.html +++ b/loko/interventions/templates/interventions/interventions_index.html @@ -93,7 +93,20 @@ } .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 */ @@ -405,6 +418,87 @@ border-color: #6c757d !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; + } {% endblock head %} @@ -732,39 +826,201 @@ -
- -
- +
+
+ +
+ +
-
- +
+
+ + +
+
+
-
-
-
- -
- - → - -
-
-
- -
- - → - +
+
+
+ +
+
-
+ +
+
+ + + {% translate "Démarrage / Planification" %} + +
+
+
+
+ {% translate "Du" %} + +
+
+
+ +
+
+
+ {% translate "Au" %} + +
+
+
+
+ + +
+
+ + + {% translate "Fin travaux (statut Terminé)" %} + +
+
+
+
+ {% translate "Du" %} + +
+
+
+ +
+
+
+ {% translate "Au" %} + +
+
+
+
+ + +
+ {% translate "Périodes rapides (fin)" %} : + + + + +
+
+ + +
+
+
+
+
+ {% translate "Ligne du temps & Intervalles" %} +
+ {% translate "Aperçu interactif" %} +
+ + +
+ +
+
+
+ {% translate "Passé" %} + {% translate "Aujourd'hui" %} + {% translate "Futur" %} +
+
+ + +
+
+ + {% translate "Intervalle Date de début" %} + + + {% translate "Toutes dates" %} + +
+
+
+
+
+
+ + +
+
+ + {% translate "Intervalle Date de fin (Terminé)" %} + + + {% translate "Toutes dates" %} + +
+
+
+
+
+
+
+ + +
+
+ +
+ {% translate "Aucune restriction de date : toutes les interventions sont incluses." %} +
+
+
+
+ + +
+
+ {% translate "Comment fonctionnent ces deux intervalles ?" %} +
+
+
+ + {% translate "Date de début" %} [Min → Max] : {% translate "sélectionne les interventions dont le démarrage a lieu dans cette période." %} +
+
+ + {% translate "Date de fin" %} [Min → Max] : {% translate "sélectionne les interventions achevées (statut Terminé) dans cette période." %} +
+
+
+
+
+
+ + +
+
+
-
+
+
+
- -
{% if to_be_planned_soon %}