feat(projects): upgrade filter UI to modern modal with badge counter and contextual status styles
This commit is contained in:
parent
244b16b3d3
commit
aa75bcbfeb
9 changed files with 457 additions and 141 deletions
Binary file not shown.
|
|
@ -863,3 +863,28 @@ msgstr "Invalid date for “%(name)s”. "
|
||||||
#: projects/views.py:1104
|
#: projects/views.py:1104
|
||||||
msgid "Valeurs sauvegardées avec succès."
|
msgid "Valeurs sauvegardées avec succès."
|
||||||
msgstr "Values saved successfully."
|
msgstr "Values saved successfully."
|
||||||
|
|
||||||
|
#: projects/templates/projects/projects_index.html
|
||||||
|
msgid "Filtrer les projets"
|
||||||
|
msgstr "Filter projects"
|
||||||
|
|
||||||
|
#: projects/templates/projects/projects_index.html
|
||||||
|
msgid "Sélectionnez les critères à afficher sur la carte et dans le tableau"
|
||||||
|
msgstr "Select the criteria to display on the map and in the table"
|
||||||
|
|
||||||
|
#: projects/templates/projects/projects_index.html
|
||||||
|
msgid "Filtres"
|
||||||
|
msgstr "Filters"
|
||||||
|
|
||||||
|
#: projects/templates/projects/projects_index.html
|
||||||
|
msgid "Période"
|
||||||
|
msgstr "Period"
|
||||||
|
|
||||||
|
#: projects/templates/projects/projects_index.html
|
||||||
|
msgid "Effacer dates"
|
||||||
|
msgstr "Clear dates"
|
||||||
|
|
||||||
|
#: projects/templates/projects/projects_index.html
|
||||||
|
msgid "Tout effacer"
|
||||||
|
msgstr "Clear all"
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -850,3 +850,28 @@ msgstr "Date invalide pour « %(name)s ». Format attendu : AAAA-MM-JJ."
|
||||||
#: projects/views.py:1104
|
#: projects/views.py:1104
|
||||||
msgid "Valeurs sauvegardées avec succès."
|
msgid "Valeurs sauvegardées avec succès."
|
||||||
msgstr "Valeurs sauvegardées avec succès."
|
msgstr "Valeurs sauvegardées avec succès."
|
||||||
|
|
||||||
|
#: projects/templates/projects/projects_index.html
|
||||||
|
msgid "Filtrer les projets"
|
||||||
|
msgstr "Filtrer les projets"
|
||||||
|
|
||||||
|
#: projects/templates/projects/projects_index.html
|
||||||
|
msgid "Sélectionnez les critères à afficher sur la carte et dans le tableau"
|
||||||
|
msgstr "Sélectionnez les critères à afficher sur la carte et dans le tableau"
|
||||||
|
|
||||||
|
#: projects/templates/projects/projects_index.html
|
||||||
|
msgid "Filtres"
|
||||||
|
msgstr "Filtres"
|
||||||
|
|
||||||
|
#: projects/templates/projects/projects_index.html
|
||||||
|
msgid "Période"
|
||||||
|
msgstr "Période"
|
||||||
|
|
||||||
|
#: projects/templates/projects/projects_index.html
|
||||||
|
msgid "Effacer dates"
|
||||||
|
msgstr "Effacer dates"
|
||||||
|
|
||||||
|
#: projects/templates/projects/projects_index.html
|
||||||
|
msgid "Tout effacer"
|
||||||
|
msgstr "Tout effacer"
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -850,3 +850,28 @@ msgstr "Ongeldige datum voor “%(name)s”. Verwacht formaat: JJJJ-MM-DD."
|
||||||
#: projects/views.py:1104
|
#: projects/views.py:1104
|
||||||
msgid "Valeurs sauvegardées avec succès."
|
msgid "Valeurs sauvegardées avec succès."
|
||||||
msgstr "Waarden succesvol opgeslagen."
|
msgstr "Waarden succesvol opgeslagen."
|
||||||
|
|
||||||
|
#: projects/templates/projects/projects_index.html
|
||||||
|
msgid "Filtrer les projets"
|
||||||
|
msgstr "Projecten filteren"
|
||||||
|
|
||||||
|
#: projects/templates/projects/projects_index.html
|
||||||
|
msgid "Sélectionnez les critères à afficher sur la carte et dans le tableau"
|
||||||
|
msgstr "Selecteer de criteria die op de kaart en in de tabel moeten worden weergegeven"
|
||||||
|
|
||||||
|
#: projects/templates/projects/projects_index.html
|
||||||
|
msgid "Filtres"
|
||||||
|
msgstr "Filters"
|
||||||
|
|
||||||
|
#: projects/templates/projects/projects_index.html
|
||||||
|
msgid "Période"
|
||||||
|
msgstr "Periode"
|
||||||
|
|
||||||
|
#: projects/templates/projects/projects_index.html
|
||||||
|
msgid "Effacer dates"
|
||||||
|
msgstr "Datums wissen"
|
||||||
|
|
||||||
|
#: projects/templates/projects/projects_index.html
|
||||||
|
msgid "Tout effacer"
|
||||||
|
msgstr "Alles wissen"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,11 +79,49 @@ projectsgeojsonloader.addEventListener("geojsonLoaded", event => {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Apply the correct width to the dropdown-menu
|
// Filter badge & active filter count
|
||||||
|
function countActiveFilters() {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
let count = 0;
|
||||||
|
['status', 'thematic', 'tag'].forEach(param => {
|
||||||
|
const values = urlParams.getAll(param);
|
||||||
|
if (values.length > 0 && values[0] !== '') {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (urlParams.get('begin_date') || urlParams.get('end_date')) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFilterBadge() {
|
||||||
|
const count = countActiveFilters();
|
||||||
|
const badge = document.getElementById('filter-badge');
|
||||||
|
const countSpan = document.getElementById('filter-count');
|
||||||
|
if (badge && countSpan) {
|
||||||
|
if (count > 0) {
|
||||||
|
countSpan.textContent = count;
|
||||||
|
badge.classList.remove('d-none');
|
||||||
|
} else {
|
||||||
|
badge.classList.add('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
const parent_col = $('#col-left')[0];
|
updateFilterBadge();
|
||||||
const parentWidth = Math.max(550, parent_col.offsetWidth - 40);
|
|
||||||
$('.filter-dropdown').width(`${parentWidth}px`);
|
const clearDatesBtn = document.getElementById('clear-dates-btn');
|
||||||
|
if (clearDatesBtn) {
|
||||||
|
clearDatesBtn.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const beginInput = document.getElementById('begin_date');
|
||||||
|
const endInput = document.getElementById('end_date');
|
||||||
|
if (beginInput) beginInput.value = '';
|
||||||
|
if (endInput) endInput.value = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Apply filters
|
// Apply filters
|
||||||
|
|
@ -109,6 +147,9 @@ document.getElementById('apply-filters').addEventListener('click', function(e) {
|
||||||
const newUrl = `${window.location.pathname}?${params.toString()}`;
|
const newUrl = `${window.location.pathname}?${params.toString()}`;
|
||||||
window.history.pushState({}, '', newUrl);
|
window.history.pushState({}, '', newUrl);
|
||||||
|
|
||||||
|
// Update filter badge
|
||||||
|
updateFilterBadge();
|
||||||
|
|
||||||
// Load filtered data
|
// Load filtered data
|
||||||
handleGeojsonMapLoading(init=false, projectsgeojsonloader, 'projects');
|
handleGeojsonMapLoading(init=false, projectsgeojsonloader, 'projects');
|
||||||
projectsgeojsonloader.loadGeojson(projectsgeojsonUrl+`?${params}`);
|
projectsgeojsonloader.loadGeojson(projectsgeojsonUrl+`?${params}`);
|
||||||
|
|
@ -116,11 +157,10 @@ document.getElementById('apply-filters').addEventListener('click', function(e) {
|
||||||
// Save preferences
|
// Save preferences
|
||||||
savePreferencesFromForm(params);
|
savePreferencesFromForm(params);
|
||||||
|
|
||||||
// Trouver le dropdown parent et le refermer
|
// Fermer la modale
|
||||||
const dropdownEl = this.closest('.dropdown');
|
const filterModal = bootstrap.Modal.getInstance(document.getElementById('filterModal'));
|
||||||
const dropdown = bootstrap.Dropdown.getInstance(dropdownEl.querySelector('[data-bs-toggle="dropdown"]'));
|
if (filterModal) {
|
||||||
if (dropdown) {
|
filterModal.hide();
|
||||||
dropdown.hide(); // ✅ ferme proprement le dropdown
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -131,9 +171,7 @@ document.getElementById('reset-filters').addEventListener('click', function(e) {
|
||||||
form.reset(); // Reset form fields
|
form.reset(); // Reset form fields
|
||||||
|
|
||||||
// Uncheck all filter checkboxes
|
// Uncheck all filter checkboxes
|
||||||
form.querySelectorAll('input[type="checkbox"][name="status"]').forEach(cb => { cb.checked = false; });
|
form.querySelectorAll('input[type="checkbox"]').forEach(cb => { cb.checked = false; });
|
||||||
form.querySelectorAll('input[type="checkbox"][name="thematic"]').forEach(cb => { cb.checked = false; });
|
|
||||||
form.querySelectorAll('input[type="checkbox"][name="tag"]').forEach(cb => { cb.checked = false; });
|
|
||||||
|
|
||||||
// Explicitly clear date fields
|
// Explicitly clear date fields
|
||||||
document.getElementById('begin_date').value = '';
|
document.getElementById('begin_date').value = '';
|
||||||
|
|
@ -142,6 +180,9 @@ document.getElementById('reset-filters').addEventListener('click', function(e) {
|
||||||
// Remove URL parameters
|
// Remove URL parameters
|
||||||
window.history.pushState({}, '', window.location.pathname);
|
window.history.pushState({}, '', window.location.pathname);
|
||||||
|
|
||||||
|
// Update filter badge
|
||||||
|
updateFilterBadge();
|
||||||
|
|
||||||
// Reload all data without filters
|
// Reload all data without filters
|
||||||
handleGeojsonMapLoading(init=false, projectsgeojsonloader, 'projects');
|
handleGeojsonMapLoading(init=false, projectsgeojsonloader, 'projects');
|
||||||
projectsgeojsonloader.loadGeojson(projectsgeojsonUrl);
|
projectsgeojsonloader.loadGeojson(projectsgeojsonUrl);
|
||||||
|
|
@ -155,11 +196,10 @@ document.getElementById('reset-filters').addEventListener('click', function(e) {
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trouver le dropdown parent et le refermer
|
// Fermer la modale
|
||||||
const dropdownEl = this.closest('.dropdown');
|
const filterModal = bootstrap.Modal.getInstance(document.getElementById('filterModal'));
|
||||||
const dropdown = bootstrap.Dropdown.getInstance(dropdownEl.querySelector('[data-bs-toggle="dropdown"]'));
|
if (filterModal) {
|
||||||
if (dropdown) {
|
filterModal.hide();
|
||||||
dropdown.hide(); // ✅ ferme proprement le dropdown
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,19 +17,132 @@
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Styles pour afficher les options de filtre sous forme de badges checkables */
|
/* ─── Modern Airbnb-style Filter Modal ──────────────────── */
|
||||||
|
#filterModal .modal-dialog {
|
||||||
|
max-width: 820px;
|
||||||
|
margin: 1.5rem auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#filterModal .modal-content {
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 18px;
|
||||||
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-height: 88vh;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#filterModal .modal-header {
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
border-bottom: 1px solid #f1f5f9;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#filterModal .modal-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#filterModal .btn-close {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
opacity: 0.5;
|
||||||
|
transition: opacity 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#filterModal .btn-close:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#filterModal .modal-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
background-color: #f8fafc;
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#filterModal .modal-footer {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border-top: 1px solid #f1f5f9;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filter Section Cards */
|
||||||
|
.filter-section {
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 1.15rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
|
||||||
|
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section:hover {
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.85rem;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section-header label,
|
||||||
|
.filter-section-header .section-title {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
margin: 0;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.35rem;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section-buttons button {
|
||||||
|
padding: 0.25rem 0.65rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #64748b;
|
||||||
|
transition: all 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-section-buttons button:hover {
|
||||||
|
background-color: #f1f5f9;
|
||||||
|
color: #0f172a;
|
||||||
|
border-color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Filter Options Badges & Chips */
|
||||||
.filter-options {
|
.filter-options {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 0.5rem;
|
gap: 0.45rem;
|
||||||
padding: 0.5rem;
|
background-color: transparent !important;
|
||||||
background-color: #f8f9fa;
|
border: none !important;
|
||||||
border-radius: 0.375rem;
|
padding: 0 !important;
|
||||||
border: 1px solid #dee2e6;
|
min-height: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-option {
|
.filter-option {
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -38,55 +151,204 @@
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
cursor: pointer;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-option label.badge-label {
|
.filter-option label {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: 0.35rem;
|
||||||
margin: 0;
|
padding: 0.4rem 0.85rem;
|
||||||
|
font-size: 0.82rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
color: #334155;
|
||||||
|
background-color: #ffffff;
|
||||||
|
border: 1.5px solid #e2e8f0;
|
||||||
|
border-radius: 9999px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: #e7f1ff;
|
transition: all 0.15s ease;
|
||||||
color: #0c5cc9;
|
user-select: none;
|
||||||
border-radius: 1rem;
|
|
||||||
padding: 0.4rem 0.6rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
border: 1px solid #b8d4f1;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
text-align: center;
|
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Default badges - checked state */
|
.filter-option label:hover {
|
||||||
.filter-option input[type="checkbox"]:checked + label.badge-label {
|
background-color: #f8fafc;
|
||||||
background-color: #0c5cc9;
|
border-color: #94a3b8;
|
||||||
color: white;
|
color: #0f172a;
|
||||||
border-color: #0c5cc9;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Default badges - hover */
|
/* Default badges (Thematics, Tags) - checked state */
|
||||||
.filter-option input[type="checkbox"]:hover + label.badge-label {
|
.filter-option input[type="checkbox"]:checked + label:not(.context-badge) {
|
||||||
background-color: #cfe2f3;
|
background-color: #0f172a !important;
|
||||||
|
border-color: #0f172a !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 2px 6px rgba(15, 23, 42, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Contextual color badges - inactive state (outline style) */
|
||||||
|
.filter-option label.context-badge {
|
||||||
|
border-color: currentColor;
|
||||||
|
color: inherit;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-option label.btn-outline-primary { border-color: #0c5cc9; color: #0c5cc9; }
|
||||||
|
.filter-option label.btn-outline-danger { border-color: #dc3545; color: #dc3545; }
|
||||||
|
.filter-option label.btn-outline-warning { border-color: #ffc107; color: #b45309; }
|
||||||
|
.filter-option label.btn-outline-success { border-color: #198754; color: #198754; }
|
||||||
|
.filter-option label.btn-outline-info { border-color: #0dcaf0; color: #087990; }
|
||||||
|
.filter-option label.btn-outline-dark { border-color: #343a40; color: #343a40; }
|
||||||
|
.filter-option label.btn-outline-secondary { border-color: #6c757d; color: #6c757d; }
|
||||||
|
|
||||||
|
/* Contextual status badges - checked state (filled solid) */
|
||||||
|
.filter-option input[type="checkbox"]:checked + label.context-badge {
|
||||||
|
color: #ffffff !important;
|
||||||
|
font-weight: 600;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-option input[type="checkbox"]:checked + label.btn-outline-primary { background-color: #0c5cc9 !important; border-color: #0c5cc9 !important; }
|
||||||
|
.filter-option input[type="checkbox"]:checked + label.btn-outline-danger { background-color: #dc3545 !important; border-color: #dc3545 !important; }
|
||||||
|
.filter-option input[type="checkbox"]:checked + label.btn-outline-warning { background-color: #ffc107 !important; border-color: #ffc107 !important; color: #212529 !important; }
|
||||||
|
.filter-option input[type="checkbox"]:checked + label.btn-outline-success { background-color: #198754 !important; border-color: #198754 !important; }
|
||||||
|
.filter-option input[type="checkbox"]:checked + label.btn-outline-info { background-color: #0dcaf0 !important; border-color: #0dcaf0 !important; color: #212529 !important; }
|
||||||
|
.filter-option input[type="checkbox"]:checked + label.btn-outline-dark { background-color: #343a40 !important; border-color: #343a40 !important; }
|
||||||
|
.filter-option input[type="checkbox"]:checked + label.btn-outline-secondary { background-color: #6c757d !important; border-color: #6c757d !important; }
|
||||||
|
|
||||||
|
/* Filter trigger badge */
|
||||||
|
#filter-badge {
|
||||||
|
background-color: #0f172a !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock head %}
|
{% endblock head %}
|
||||||
|
|
||||||
#interventions-table-container {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
#filter-dropdown-wrapper {
|
|
||||||
position: absolute;
|
|
||||||
top: 0.5rem;
|
|
||||||
left: 0.5rem;
|
|
||||||
z-index: 10;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<!-- Modal de filtre des projets -->
|
||||||
|
<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-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div>
|
||||||
|
<h5 class="modal-title" id="filterModalLabel">{% translate "Filtrer les projets" %}</h5>
|
||||||
|
<div class="text-muted small" style="font-size: 0.8rem;">{% translate "Sélectionnez les critères à afficher sur la carte et dans le tableau" %}</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{% translate 'Fermer' %}"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form method="get" id="filter-form">
|
||||||
|
<!-- Statut -->
|
||||||
|
<div class="filter-section">
|
||||||
|
<div class="filter-section-header">
|
||||||
|
<label>{% translate "Statut" %}</label>
|
||||||
|
<div class="filter-section-buttons">
|
||||||
|
<button type="button" class="btn select-all-btn" data-field="status">{% translate "Tous" %}</button>
|
||||||
|
<button type="button" class="btn deselect-all-btn" data-field="status">{% translate "Aucun" %}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="filter-options" data-field="status">
|
||||||
|
{% for key, value, css_class in status_choices %}
|
||||||
|
<div class="filter-option">
|
||||||
|
<input type="checkbox" name="status" id="status_{{ key }}" value="{{ key }}" {% if key in selected_statuses %}checked{% endif %}>
|
||||||
|
<label for="status_{{ key }}" class="context-badge btn-outline-{{ css_class }}">{{ value }}</label>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Thématiques -->
|
||||||
|
<div class="filter-section">
|
||||||
|
<div class="filter-section-header">
|
||||||
|
<label>{% translate "Thématiques" %}</label>
|
||||||
|
<div class="filter-section-buttons">
|
||||||
|
<button type="button" class="btn select-all-btn" data-field="thematic">{% translate "Tous" %}</button>
|
||||||
|
<button type="button" class="btn deselect-all-btn" data-field="thematic">{% translate "Aucun" %}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="filter-options" data-field="thematic">
|
||||||
|
{% for thematic in thematic_choices %}
|
||||||
|
<div class="filter-option">
|
||||||
|
<input type="checkbox" name="thematic" id="thematic_{{ thematic.code }}" value="{{ thematic.code }}" {% if thematic.code in selected_thematics %}checked{% endif %}>
|
||||||
|
<label for="thematic_{{ thematic.code }}">
|
||||||
|
{% if thematic.icon %}<i class="bi {{ thematic.icon }} me-1"></i>{% endif %}{{ thematic.name }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tags -->
|
||||||
|
<div class="filter-section">
|
||||||
|
<div class="filter-section-header">
|
||||||
|
<label>{% translate "Tags" %}</label>
|
||||||
|
<div class="filter-section-buttons">
|
||||||
|
<button type="button" class="btn select-all-btn" data-field="tag">{% translate "Tous" %}</button>
|
||||||
|
<button type="button" class="btn deselect-all-btn" data-field="tag">{% translate "Aucun" %}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="filter-options" data-field="tag">
|
||||||
|
{% for tag in tags %}
|
||||||
|
<div class="filter-option">
|
||||||
|
<input type="checkbox" name="tag" id="tag_{{ tag.slug }}" value="{{ tag.slug }}" {% if tag.slug in selected_tags %}checked{% endif %}>
|
||||||
|
<label for="tag_{{ tag.slug }}">
|
||||||
|
<i class="bi bi-tag me-1"></i>{{ tag.name }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Période / Dates -->
|
||||||
|
<div class="filter-section">
|
||||||
|
<div class="filter-section-header">
|
||||||
|
<label class="d-flex align-items-center">
|
||||||
|
<i class="bi bi-calendar3 me-2 text-primary"></i>{% translate "Période" %}
|
||||||
|
</label>
|
||||||
|
<div class="filter-section-buttons">
|
||||||
|
<button type="button" class="btn" id="clear-dates-btn">
|
||||||
|
<i class="bi bi-x-circle me-1"></i>{% translate "Effacer dates" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row g-2 align-items-center">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<span class="input-group-text bg-white text-muted">
|
||||||
|
<i class="bi bi-play-circle-fill text-success me-1"></i>{% translate "Date de début" %}
|
||||||
|
</span>
|
||||||
|
<input type="date" name="begin_date" id="begin_date" class="form-control form-control-sm" value="{{ begin_date }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<span class="input-group-text bg-white text-muted">
|
||||||
|
<i class="bi bi-check-circle-fill text-primary me-1"></i>{% translate "Date de fin" %}
|
||||||
|
</span>
|
||||||
|
<input type="date" name="end_date" id="end_date" class="form-control form-control-sm" value="{{ end_date }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" id="reset-filters" class="btn btn-link text-muted p-0 text-decoration-none d-flex align-items-center gap-1" style="font-size: 0.85rem; font-weight: 500;">
|
||||||
|
<i class="bi bi-arrow-counterclockwise"></i> {% translate "Tout effacer" %}
|
||||||
|
</button>
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary px-3 py-2" data-bs-dismiss="modal" style="border-radius: 8px; font-weight: 500;">{% translate "Annuler" %}</button>
|
||||||
|
<button type="button" id="apply-filters" class="btn btn-sm btn-primary px-4 py-2" style="border-radius: 8px; font-weight: 600;">
|
||||||
|
<i class="bi bi-check2 me-1"></i>{% translate "Filtrer" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row mt-2" style="height: calc(100vh - 200px);">
|
<div class="row mt-2" style="height: calc(100vh - 200px);">
|
||||||
<div id="col-left" class="col-md-6" style="max-height: calc(100vh - 200px); overflow: hidden;">
|
<div id="col-left" class="col-md-6" style="max-height: calc(100vh - 200px); overflow: hidden;">
|
||||||
<ul class="nav nav-tabs" id="tablesTab" role="tablist">
|
<ul class="nav nav-tabs" id="tablesTab" role="tablist">
|
||||||
|
|
@ -111,8 +373,6 @@
|
||||||
<div class="tab-content my-2" id="tablesTabContent">
|
<div class="tab-content my-2" id="tablesTabContent">
|
||||||
<div class="tab-pane fade show active" id="projects-table-content" role="tabpanel" aria-labelledby="projects-table-tab">
|
<div class="tab-pane fade show active" id="projects-table-content" role="tabpanel" aria-labelledby="projects-table-tab">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Spinner -->
|
<!-- Spinner -->
|
||||||
<div id="projects-spinner">
|
<div id="projects-spinner">
|
||||||
<div class="spinner-border text-primary" role="status">
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
|
@ -121,87 +381,14 @@
|
||||||
</div>
|
</div>
|
||||||
<div id="projects-table-container">
|
<div id="projects-table-container">
|
||||||
|
|
||||||
<!-- Filter form -->
|
<!-- Filter button -->
|
||||||
<div id="filter-dropdown-wrapper" class="float-left">
|
<div id="filter-dropdown-wrapper" class="float-left mb-2">
|
||||||
<div class="dropdown me-2">
|
<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 dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false" data-bs-auto-close="outside">
|
<i class="bi bi-funnel"></i> {% translate "Filtre" %}
|
||||||
{% translate "Filtre" %}
|
<span id="filter-badge" class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-black d-none">
|
||||||
|
<span id="filter-count">0</span>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown-menu p-2 bg-light filter-dropdown" id="filter-dropdown">
|
|
||||||
<form method="get" id="filter-form" class="row g-3 mb-4">
|
|
||||||
<!-- Statut -->
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="filter-section-header d-flex justify-content-between align-items-center mb-2">
|
|
||||||
<label class="form-label mb-0">{% translate "Statut" %}</label>
|
|
||||||
<div class="filter-section-buttons">
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary select-all-btn py-0 px-2 small" data-field="status" style="font-size: 0.75rem;">{% translate "Tous" %}</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary deselect-all-btn py-0 px-2 small" data-field="status" style="font-size: 0.75rem;">{% translate "Aucun" %}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="filter-options" data-field="status">
|
|
||||||
{% for key, value in status_choices %}
|
|
||||||
<div class="filter-option">
|
|
||||||
<input type="checkbox" name="status" id="status_{{ key }}" value="{{ key }}" {% if key in selected_statuses %}checked{% endif %}>
|
|
||||||
<label for="status_{{ key }}" class="badge-label">{{ value }}</label>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Thématiques -->
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="filter-section-header d-flex justify-content-between align-items-center mb-2">
|
|
||||||
<label class="form-label mb-0">{% translate "Thématiques" %}</label>
|
|
||||||
<div class="filter-section-buttons">
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary select-all-btn py-0 px-2 small" data-field="thematic" style="font-size: 0.75rem;">{% translate "Tous" %}</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary deselect-all-btn py-0 px-2 small" data-field="thematic" style="font-size: 0.75rem;">{% translate "Aucun" %}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="filter-options" data-field="thematic">
|
|
||||||
{% for thematic in thematic_choices %}
|
|
||||||
<div class="filter-option">
|
|
||||||
<input type="checkbox" name="thematic" id="thematic_{{ thematic.code }}" value="{{ thematic.code }}" {% if thematic.code in selected_thematics %}checked{% endif %}>
|
|
||||||
<label for="thematic_{{ thematic.code }}" class="badge-label">
|
|
||||||
{% if thematic.icon %}<i class="bi {{ thematic.icon }} me-1"></i>{% endif %}{{ thematic.name }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tags -->
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="filter-section-header d-flex justify-content-between align-items-center mb-2">
|
|
||||||
<label class="form-label mb-0">{% translate "Tags" %}</label>
|
|
||||||
<div class="filter-section-buttons">
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary select-all-btn py-0 px-2 small" data-field="tag" style="font-size: 0.75rem;">{% translate "Tous" %}</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary deselect-all-btn py-0 px-2 small" data-field="tag" style="font-size: 0.75rem;">{% translate "Aucun" %}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="filter-options" data-field="tag">
|
|
||||||
{% for tag in tags %}
|
|
||||||
<div class="filter-option">
|
|
||||||
<input type="checkbox" name="tag" id="tag_{{ tag.slug }}" value="{{ tag.slug }}" {% if tag.slug in selected_tags %}checked{% endif %}>
|
|
||||||
<label for="tag_{{ tag.slug }}" class="badge-label">{{ tag.name }}</label>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label for="begin_date" class="form-label">{% translate "Date de Début" %}</label>
|
|
||||||
<input type="date" name="begin_date" id="begin_date" class="form-control" value="{{ begin_date }}">
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<label for="end_date" class="form-label">{% translate "Date de Fin" %}</label>
|
|
||||||
<input type="date" name="end_date" id="end_date" class="form-control" value="{{ end_date }}">
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<button type="button" id="apply-filters" class="btn btn-primary">{% translate "Filtrer" %}</button>
|
|
||||||
<button type="button" id="reset-filters" class="btn btn-secondary">{% translate "Réinitialiser" %}</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<table id="projects-table" class="table"
|
<table id="projects-table" class="table"
|
||||||
|
|
|
||||||
|
|
@ -100,8 +100,22 @@ def projects_index(request):
|
||||||
selected_tags = request.GET.getlist('tag')
|
selected_tags = request.GET.getlist('tag')
|
||||||
selected_thematics = request.GET.getlist('thematic')
|
selected_thematics = request.GET.getlist('thematic')
|
||||||
|
|
||||||
|
status_classes = {
|
||||||
|
'in_preparation': 'secondary',
|
||||||
|
'planned': 'warning',
|
||||||
|
'in_progress': 'primary',
|
||||||
|
'finished': 'info',
|
||||||
|
'invoiced': 'dark',
|
||||||
|
'closed': 'dark',
|
||||||
|
'canceled': 'secondary',
|
||||||
|
}
|
||||||
|
status_choices_with_classes = [
|
||||||
|
(key, value, status_classes.get(key, 'secondary'))
|
||||||
|
for key, value in Project._meta.get_field('status').choices
|
||||||
|
]
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'status_choices': Project._meta.get_field('status').choices,
|
'status_choices': status_choices_with_classes,
|
||||||
# 'maintain_type_choices': Project._meta.get_field('maintain_type').choices,
|
# 'maintain_type_choices': Project._meta.get_field('maintain_type').choices,
|
||||||
'selected_statuses': statuses,
|
'selected_statuses': statuses,
|
||||||
# 'selected_maintain_types': maintain_types,
|
# 'selected_maintain_types': maintain_types,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue