feat: enhance panel type filter with group selection, checkboxes, and bulk management controls.
This commit is contained in:
parent
1b38a8059e
commit
82e20f648e
3 changed files with 444 additions and 88 deletions
|
|
@ -1657,6 +1657,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
Array.from(panelTypeSelect.options).forEach(opt => opt.selected = false);
|
||||
Array.from(organizationSelect.options).forEach(opt => opt.selected = false);
|
||||
|
||||
panelTypeSelect.dispatchEvent(new Event('change'));
|
||||
|
||||
document.getElementById('installation_year').value = '';
|
||||
|
||||
currentFilters = {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,35 @@
|
|||
box-shadow: 0 4px 12px rgba(47, 62, 139, 0.06) !important;
|
||||
}
|
||||
|
||||
.accordion-header {
|
||||
transition: background-color 0.2s ease;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.accordion-header:hover {
|
||||
background-color: #f8f9fa !important;
|
||||
}
|
||||
|
||||
.accordion-header:hover .accordion-button {
|
||||
background-color: transparent !important;
|
||||
color: #0d6efd !important;
|
||||
}
|
||||
|
||||
.accordion-header.is-open,
|
||||
.accordion-header:has(.accordion-button:not(.collapsed)) {
|
||||
background-color: rgba(13, 110, 253, 0.05) !important;
|
||||
border-bottom: 1px solid rgba(13, 110, 253, 0.1) !important;
|
||||
}
|
||||
|
||||
.accordion-header.is-open .accordion-button,
|
||||
.accordion-header:has(.accordion-button:not(.collapsed)) .accordion-button {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.accordion-header .accordion-button {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.accordion-button {
|
||||
padding: 10px 16px !important;
|
||||
font-size: 0.9rem !important;
|
||||
|
|
@ -65,6 +94,23 @@
|
|||
background-size: 1rem !important;
|
||||
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1) !important;
|
||||
}
|
||||
|
||||
.group-select-all {
|
||||
cursor: pointer;
|
||||
width: 1.15rem;
|
||||
height: 1.15rem;
|
||||
transition: all 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
.group-select-all:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.group-select-all:indeterminate {
|
||||
background-color: #0d6efd;
|
||||
border-color: #0d6efd;
|
||||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e");
|
||||
}
|
||||
|
||||
.accordion-body {
|
||||
background-color: #fbfbfc !important;
|
||||
|
|
@ -215,20 +261,42 @@
|
|||
<!-- Accordion for panel categories -->
|
||||
<div class="accordion mb-3 shadow-sm border rounded" id="panel-type-accordion" style="max-height: 500px; overflow-y: auto;">
|
||||
{% for group_key, group_label in panel_types_groups %}
|
||||
<div class="accordion-item border-0 border-bottom">
|
||||
<h2 class="accordion-header" id="heading-{{ group_key }}">
|
||||
<button class="accordion-button collapsed py-2 px-3 fw-bold bg-white text-dark shadow-none"
|
||||
<div class="accordion-item border-0 border-bottom" data-group-key="{{ group_key }}">
|
||||
<div class="accordion-header d-flex align-items-center bg-white" id="heading-{{ group_key }}">
|
||||
<div class="ps-2 pe-1 py-1 d-flex align-items-center" title="{% translate 'Sélectionner / désélectionner tout le groupe' %}">
|
||||
<input class="form-check-input group-select-all m-0"
|
||||
type="checkbox"
|
||||
id="check-group-{{ group_key }}"
|
||||
data-group="{{ group_key }}"
|
||||
aria-label="{% translate 'Sélectionner tous les' %} {{ group_label }}">
|
||||
</div>
|
||||
<button class="accordion-button collapsed py-2 ps-2 pe-3 fw-bold text-dark shadow-none flex-grow-1"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#collapse-{{ group_key }}"
|
||||
aria-expanded="false"
|
||||
aria-controls="collapse-{{ group_key }}"
|
||||
data-group="{{ group_key }}">
|
||||
{{ group_label }}
|
||||
<span class="group-title me-auto">{{ group_label }}</span>
|
||||
<span class="badge rounded-pill bg-light text-muted border group-count-badge me-2" id="badge-{{ group_key }}" style="font-size: 0.72rem; font-weight: 500;">0</span>
|
||||
</button>
|
||||
</h2>
|
||||
</div>
|
||||
<div id="collapse-{{ group_key }}" class="accordion-collapse collapse" aria-labelledby="heading-{{ group_key }}" data-bs-parent="#panel-type-accordion">
|
||||
<div class="accordion-body p-2 bg-light border-top">
|
||||
<!-- Category Action Toolbar -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 pb-1 border-bottom">
|
||||
<span class="small text-muted group-status-text" id="status-{{ group_key }}" style="font-size: 0.75rem;">
|
||||
0 sélectionné(s)
|
||||
</span>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary py-0 px-2 select-all-btn" data-group="{{ group_key }}" style="font-size: 0.72rem;">
|
||||
<i class="bi bi-check-all me-1"></i>{% translate "Tout cocher" %}
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary py-0 px-2 deselect-all-btn" data-group="{{ group_key }}" style="font-size: 0.72rem;">
|
||||
<i class="bi bi-x me-1"></i>{% translate "Tout décocher" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-2 justify-content-start">
|
||||
{% for group, panels in panel_types_grouped.items %}
|
||||
{% if group == group_key %}
|
||||
|
|
@ -236,6 +304,7 @@
|
|||
<div class="panel-item text-center p-1 border rounded bg-white shadow-sm hover-grow"
|
||||
data-id="{{ panel.id }}"
|
||||
data-code="{{ panel.code }}"
|
||||
data-group="{{ group_key }}"
|
||||
data-image-url="{{ panel.image_url|default:'' }}"
|
||||
style="cursor: pointer; width: 68px; transition: transform 0.15s ease;"
|
||||
title="{{ panel.code }} - {{ panel.name }}">
|
||||
|
|
@ -255,7 +324,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Selected panels list -->
|
||||
<div id="selected-panels-list" class="border rounded p-2 bg-light" style="max-height: 150px; overflow-y: auto;">
|
||||
<div id="selected-panels-list" class="border rounded p-2 bg-light" style="max-height: 180px; overflow-y: auto;">
|
||||
<div class="text-muted small fst-italic empty-message text-center">{% translate "Aucun type sélectionné" %}</div>
|
||||
<!-- Selected items will be added here via JS -->
|
||||
</div>
|
||||
|
|
@ -1933,109 +2002,323 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||
const panelTypeSelect = document.getElementById('panel_type');
|
||||
if (!panelTypeSelect) return;
|
||||
|
||||
const accordionButtons = document.querySelectorAll('#panel-type-accordion .accordion-button');
|
||||
const accordion = document.getElementById('panel-type-accordion');
|
||||
const selectedList = document.getElementById('selected-panels-list');
|
||||
const emptyMessage = selectedList.querySelector('.empty-message');
|
||||
const emptyMessage = selectedList ? selectedList.querySelector('.empty-message') : null;
|
||||
if (!selectedList) return;
|
||||
|
||||
// Helper to update selected list UI
|
||||
// Helper to load images for a category
|
||||
function loadGroupImages(group) {
|
||||
const collapseDiv = document.querySelector(`#collapse-${group}`);
|
||||
if (!collapseDiv) return;
|
||||
collapseDiv.querySelectorAll('.panel-item').forEach(item => {
|
||||
const placeholder = item.querySelector('.panel-img-placeholder');
|
||||
if (placeholder && placeholder.children.length === 0) {
|
||||
const imageUrl = item.dataset.imageUrl;
|
||||
const code = item.dataset.code;
|
||||
if (imageUrl) {
|
||||
const img = document.createElement('img');
|
||||
img.src = imageUrl;
|
||||
img.style.maxHeight = '100%';
|
||||
img.style.maxWidth = '100%';
|
||||
placeholder.appendChild(img);
|
||||
} else if (typeof buildPanelIconElement === 'function') {
|
||||
const img = buildPanelIconElement(code);
|
||||
img.style.height = '40px';
|
||||
img.style.width = '40px';
|
||||
placeholder.appendChild(img);
|
||||
} else {
|
||||
placeholder.textContent = code;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Map option values to option elements for fast lookup
|
||||
function getOptionMap() {
|
||||
const map = new Map();
|
||||
Array.from(panelTypeSelect.options).forEach(opt => {
|
||||
map.set(String(opt.value), opt);
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
// Helper to update selected list UI & Group states
|
||||
function updateSelectedList() {
|
||||
const selectedOptions = Array.from(panelTypeSelect.options).filter(opt => opt.selected);
|
||||
|
||||
// Clear list (except empty message)
|
||||
const optionMap = getOptionMap();
|
||||
const selectedIds = new Set(
|
||||
Array.from(panelTypeSelect.options)
|
||||
.filter(opt => opt.selected)
|
||||
.map(opt => String(opt.value))
|
||||
);
|
||||
|
||||
// 1. Update active states on grid items
|
||||
document.querySelectorAll('.panel-item').forEach(item => {
|
||||
if (selectedIds.has(String(item.dataset.id))) {
|
||||
item.classList.add('selected');
|
||||
} else {
|
||||
item.classList.remove('selected');
|
||||
}
|
||||
});
|
||||
|
||||
// 2. Clear selected list container (except empty message)
|
||||
Array.from(selectedList.children).forEach(child => {
|
||||
if (!child.classList.contains('empty-message')) {
|
||||
child.remove();
|
||||
}
|
||||
});
|
||||
|
||||
if (selectedOptions.length === 0) {
|
||||
emptyMessage.style.display = 'block';
|
||||
// 3. Process groups to calculate counts, update checkboxes, and build selection items
|
||||
const groupItems = document.querySelectorAll('#panel-type-accordion .accordion-item');
|
||||
const fullySelectedGroups = new Set();
|
||||
const groupSelectedPanelsMap = new Map();
|
||||
|
||||
groupItems.forEach(groupEl => {
|
||||
const groupKey = groupEl.dataset.groupKey;
|
||||
if (!groupKey) return;
|
||||
|
||||
const panelsInGroup = Array.from(groupEl.querySelectorAll('.panel-item'));
|
||||
const totalInGroup = panelsInGroup.length;
|
||||
const selectedPanels = panelsInGroup.filter(p => selectedIds.has(String(p.dataset.id)));
|
||||
const count = selectedPanels.length;
|
||||
|
||||
groupSelectedPanelsMap.set(groupKey, selectedPanels);
|
||||
|
||||
// Update group checkbox state (checked / indeterminate / unchecked)
|
||||
const checkbox = document.getElementById(`check-group-${groupKey}`);
|
||||
if (checkbox) {
|
||||
if (count === 0) {
|
||||
checkbox.checked = false;
|
||||
checkbox.indeterminate = false;
|
||||
} else if (count === totalInGroup && totalInGroup > 0) {
|
||||
checkbox.checked = true;
|
||||
checkbox.indeterminate = false;
|
||||
fullySelectedGroups.add(groupKey);
|
||||
} else {
|
||||
checkbox.checked = false;
|
||||
checkbox.indeterminate = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Update header badge
|
||||
const badge = document.getElementById(`badge-${groupKey}`);
|
||||
if (badge) {
|
||||
if (count > 0) {
|
||||
badge.textContent = `${count}/${totalInGroup}`;
|
||||
badge.className = 'badge rounded-pill bg-primary text-white border-0 group-count-badge me-2';
|
||||
} else {
|
||||
badge.textContent = '0';
|
||||
badge.className = 'badge rounded-pill bg-light text-muted border group-count-badge me-2';
|
||||
}
|
||||
}
|
||||
|
||||
// Update status text inside accordion body
|
||||
const statusText = document.getElementById(`status-${groupKey}`);
|
||||
if (statusText) {
|
||||
if (count === totalInGroup && totalInGroup > 0) {
|
||||
statusText.innerHTML = `<span class="text-primary fw-bold"><i class="bi bi-check-circle-fill me-1"></i>Tous sélectionnés (${totalInGroup})</span>`;
|
||||
} else if (count > 0) {
|
||||
statusText.textContent = `${count} / ${totalInGroup} sélectionné(s)`;
|
||||
} else {
|
||||
statusText.textContent = `0 / ${totalInGroup} sélectionné(s)`;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 4. Render selected list
|
||||
if (selectedIds.size === 0) {
|
||||
if (emptyMessage) emptyMessage.style.display = 'block';
|
||||
} else {
|
||||
emptyMessage.style.display = 'none';
|
||||
selectedOptions.forEach(opt => {
|
||||
const id = opt.value;
|
||||
const code = opt.text.trim();
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'd-flex justify-content-between align-items-center bg-white border rounded p-1 mb-1 shadow-sm';
|
||||
item.innerHTML = `
|
||||
<span class="small fw-bold ms-1 text-primary">${code}</span>
|
||||
<button type="button" class="btn btn-sm btn-link text-danger p-0 me-1 remove-panel-type" data-id="${id}">
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
`;
|
||||
selectedList.appendChild(item);
|
||||
if (emptyMessage) emptyMessage.style.display = 'none';
|
||||
|
||||
// Selection header with total count and "Tout effacer" button
|
||||
const headerRow = document.createElement('div');
|
||||
headerRow.className = 'd-flex justify-content-between align-items-center mb-2 pb-1 border-bottom';
|
||||
headerRow.innerHTML = `
|
||||
<span class="small fw-bold text-dark">${selectedIds.size} sélectionné(s)</span>
|
||||
<button type="button" class="btn btn-link btn-sm text-danger p-0 text-decoration-none" id="clear-all-panel-types" style="font-size: 0.75rem;">
|
||||
<i class="bi bi-trash3 me-1"></i>Tout effacer
|
||||
</button>
|
||||
`;
|
||||
selectedList.appendChild(headerRow);
|
||||
|
||||
const handledPanelIds = new Set();
|
||||
|
||||
// Display consolidated cards for fully selected groups first
|
||||
groupItems.forEach(groupEl => {
|
||||
const groupKey = groupEl.dataset.groupKey;
|
||||
if (!groupKey) return;
|
||||
const groupLabel = groupEl.querySelector('.group-title')?.textContent?.trim() || groupKey;
|
||||
const selectedPanels = groupSelectedPanelsMap.get(groupKey) || [];
|
||||
|
||||
if (fullySelectedGroups.has(groupKey)) {
|
||||
// Entire group selected
|
||||
const groupItem = document.createElement('div');
|
||||
groupItem.className = 'd-flex justify-content-between align-items-center bg-primary-subtle border border-primary-subtle rounded p-1 mb-1 shadow-sm';
|
||||
groupItem.innerHTML = `
|
||||
<div class="d-flex align-items-center text-truncate me-2">
|
||||
<i class="bi bi-folder-check text-primary me-1 ms-1"></i>
|
||||
<span class="small fw-bold text-primary text-truncate">${groupLabel}</span>
|
||||
<span class="badge bg-primary text-white ms-2" style="font-size: 0.65rem;">Tous (${selectedPanels.length})</span>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-link text-danger p-0 me-1 remove-group-type" data-group="${groupKey}" title="Désélectionner tout le groupe">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
`;
|
||||
selectedList.appendChild(groupItem);
|
||||
selectedPanels.forEach(p => handledPanelIds.add(String(p.dataset.id)));
|
||||
} else if (selectedPanels.length > 0) {
|
||||
// Partially selected group: list individual items
|
||||
selectedPanels.forEach(p => {
|
||||
const id = String(p.dataset.id);
|
||||
const code = p.dataset.code || optionMap.get(id)?.text?.trim() || id;
|
||||
handledPanelIds.add(id);
|
||||
|
||||
const item = document.createElement('div');
|
||||
item.className = 'd-flex justify-content-between align-items-center bg-white border rounded p-1 mb-1 shadow-sm';
|
||||
item.innerHTML = `
|
||||
<span class="small fw-bold ms-1 text-primary">${code}</span>
|
||||
<button type="button" class="btn btn-sm btn-link text-danger p-0 me-1 remove-panel-type" data-id="${id}" title="Retirer">
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
`;
|
||||
selectedList.appendChild(item);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Any remaining panels not in groups (fallback)
|
||||
selectedIds.forEach(id => {
|
||||
if (!handledPanelIds.has(id)) {
|
||||
const opt = optionMap.get(id);
|
||||
const code = opt ? opt.text.trim() : id;
|
||||
const item = document.createElement('div');
|
||||
item.className = 'd-flex justify-content-between align-items-center bg-white border rounded p-1 mb-1 shadow-sm';
|
||||
item.innerHTML = `
|
||||
<span class="small fw-bold ms-1 text-primary">${code}</span>
|
||||
<button type="button" class="btn btn-sm btn-link text-danger p-0 me-1 remove-panel-type" data-id="${id}" title="Retirer">
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
`;
|
||||
selectedList.appendChild(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Update active state in grid
|
||||
document.querySelectorAll('.panel-item').forEach(item => {
|
||||
const id = item.dataset.id;
|
||||
if (Array.from(panelTypeSelect.options).find(o => o.value === id && o.selected)) {
|
||||
item.classList.add('selected');
|
||||
} else {
|
||||
item.classList.remove('selected');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle accordion toggle to load images
|
||||
accordionButtons.forEach(btn => {
|
||||
// Set group selection helper
|
||||
function setGroupSelection(groupKey, isSelected) {
|
||||
const collapseDiv = document.querySelector(`#collapse-${groupKey}`);
|
||||
if (!collapseDiv) return;
|
||||
const optionMap = getOptionMap();
|
||||
|
||||
collapseDiv.querySelectorAll('.panel-item').forEach(item => {
|
||||
const opt = optionMap.get(String(item.dataset.id));
|
||||
if (opt) {
|
||||
opt.selected = isSelected;
|
||||
}
|
||||
});
|
||||
|
||||
if (isSelected) {
|
||||
loadGroupImages(groupKey);
|
||||
}
|
||||
updateSelectedList();
|
||||
}
|
||||
|
||||
// Group checkbox change handler
|
||||
document.addEventListener('change', function(e) {
|
||||
if (e.target && e.target.classList.contains('group-select-all')) {
|
||||
const groupKey = e.target.dataset.group;
|
||||
setGroupSelection(groupKey, e.target.checked);
|
||||
}
|
||||
});
|
||||
|
||||
// Toolbar and action button handlers via event delegation
|
||||
document.addEventListener('click', function(e) {
|
||||
const selectAllBtn = e.target.closest('.select-all-btn');
|
||||
if (selectAllBtn) {
|
||||
const groupKey = selectAllBtn.dataset.group;
|
||||
setGroupSelection(groupKey, true);
|
||||
return;
|
||||
}
|
||||
|
||||
const deselectAllBtn = e.target.closest('.deselect-all-btn');
|
||||
if (deselectAllBtn) {
|
||||
const groupKey = deselectAllBtn.dataset.group;
|
||||
setGroupSelection(groupKey, false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove entire group from selected list
|
||||
const removeGroupBtn = e.target.closest('.remove-group-type');
|
||||
if (removeGroupBtn) {
|
||||
const groupKey = removeGroupBtn.dataset.group;
|
||||
setGroupSelection(groupKey, false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove single panel from selected list
|
||||
const removeBtn = e.target.closest('.remove-panel-type');
|
||||
if (removeBtn) {
|
||||
const id = String(removeBtn.dataset.id);
|
||||
const optionMap = getOptionMap();
|
||||
const opt = optionMap.get(id);
|
||||
if (opt) {
|
||||
opt.selected = false;
|
||||
updateSelectedList();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear all panel types
|
||||
const clearAllBtn = e.target.closest('#clear-all-panel-types');
|
||||
if (clearAllBtn) {
|
||||
Array.from(panelTypeSelect.options).forEach(opt => opt.selected = false);
|
||||
updateSelectedList();
|
||||
return;
|
||||
}
|
||||
|
||||
// Panel item click in grid
|
||||
const item = e.target.closest('.panel-item');
|
||||
if (item) {
|
||||
const id = String(item.dataset.id);
|
||||
const optionMap = getOptionMap();
|
||||
const opt = optionMap.get(id);
|
||||
if (opt) {
|
||||
opt.selected = !opt.selected;
|
||||
updateSelectedList();
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Handle accordion toggle to load images & sync visual open class
|
||||
document.querySelectorAll('#panel-type-accordion .accordion-button').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const group = btn.dataset.group;
|
||||
const collapseDiv = document.querySelector(`#collapse-${group}`);
|
||||
if (collapseDiv) {
|
||||
collapseDiv.querySelectorAll('.panel-item').forEach(item => {
|
||||
const placeholder = item.querySelector('.panel-img-placeholder');
|
||||
if (placeholder && placeholder.children.length === 0) {
|
||||
const imageUrl = item.dataset.imageUrl;
|
||||
const code = item.dataset.code;
|
||||
|
||||
if (imageUrl) {
|
||||
const img = document.createElement('img');
|
||||
img.src = imageUrl;
|
||||
img.style.maxHeight = '100%';
|
||||
img.style.maxWidth = '100%';
|
||||
placeholder.appendChild(img);
|
||||
} else if (typeof buildPanelIconElement === 'function') {
|
||||
const img = buildPanelIconElement(code);
|
||||
img.style.height = '40px';
|
||||
img.style.width = '40px';
|
||||
placeholder.appendChild(img);
|
||||
} else {
|
||||
placeholder.textContent = code;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
loadGroupImages(group);
|
||||
});
|
||||
});
|
||||
|
||||
// Handle panel item click
|
||||
document.addEventListener('click', function(e) {
|
||||
const item = e.target.closest('.panel-item');
|
||||
if (item) {
|
||||
const id = item.dataset.id;
|
||||
const option = Array.from(panelTypeSelect.options).find(o => o.value === id);
|
||||
if (option) {
|
||||
option.selected = !option.selected;
|
||||
updateSelectedList();
|
||||
}
|
||||
}
|
||||
document.querySelectorAll('#panel-type-accordion .accordion-collapse').forEach(collapse => {
|
||||
collapse.addEventListener('show.bs.collapse', () => {
|
||||
const header = collapse.previousElementSibling;
|
||||
if (header) header.classList.add('is-open');
|
||||
});
|
||||
collapse.addEventListener('hide.bs.collapse', () => {
|
||||
const header = collapse.previousElementSibling;
|
||||
if (header) header.classList.remove('is-open');
|
||||
});
|
||||
});
|
||||
|
||||
// Handle remove click
|
||||
document.addEventListener('click', function(e) {
|
||||
const btn = e.target.closest('.remove-panel-type');
|
||||
if (btn) {
|
||||
const id = btn.dataset.id;
|
||||
const option = Array.from(panelTypeSelect.options).find(o => o.value === id);
|
||||
if (option) {
|
||||
option.selected = false;
|
||||
updateSelectedList();
|
||||
}
|
||||
}
|
||||
});
|
||||
// Listen to panelTypeSelect changes and reset button
|
||||
panelTypeSelect.addEventListener('change', updateSelectedList);
|
||||
const resetFiltersBtn = document.getElementById('reset-filters');
|
||||
if (resetFiltersBtn) {
|
||||
resetFiltersBtn.addEventListener('click', function() {
|
||||
setTimeout(updateSelectedList, 0);
|
||||
});
|
||||
}
|
||||
|
||||
// Initial update
|
||||
updateSelectedList();
|
||||
|
|
|
|||
|
|
@ -1202,3 +1202,74 @@ class SupportRepresentationAndModelTests(TestCase):
|
|||
self.assertTrue(tl_support['properties'].get('is_standalone_support'))
|
||||
|
||||
|
||||
class SignPanelCategoryFilterTest(TestCase):
|
||||
"""Tests for panel type category grouping and filter controls."""
|
||||
|
||||
def setUp(self):
|
||||
from django.contrib.auth.models import User
|
||||
from common.models import UserConfig, AppView
|
||||
from sign.models import SignPanelType
|
||||
|
||||
self.user = User.objects.create_superuser(username="filter_test_user", password="password123", email="filter@test.com")
|
||||
self.sign_view, _ = AppView.objects.get_or_create(
|
||||
code='sign',
|
||||
defaults={
|
||||
'name_fr': 'Signalisation',
|
||||
'name_nl': 'Signalisatie',
|
||||
'url_name': 'sign:index',
|
||||
}
|
||||
)
|
||||
self.user_config = UserConfig.objects.create(user=self.user)
|
||||
self.user_config.accessible_views.add(self.sign_view)
|
||||
|
||||
# Create panel types across different categories
|
||||
self.type_danger_1 = SignPanelType.objects.create(code="A1a", name_fr="Virage à droite", name_nl="Bocht rechts")
|
||||
self.type_danger_2 = SignPanelType.objects.create(code="A1b", name_fr="Virage à gauche", name_nl="Bocht links")
|
||||
self.type_priority = SignPanelType.objects.create(code="B1", name_fr="Céder le passage", name_nl="Voorrang verlenen")
|
||||
self.type_prohibition = SignPanelType.objects.create(code="C1", name_fr="Sens interdit", name_nl="Verboden richting")
|
||||
|
||||
def test_get_grouped_panel_types(self):
|
||||
"""Verify get_grouped_panel_types categorizes panel types according to Belgian traffic code."""
|
||||
from sign.views import get_grouped_panel_types
|
||||
|
||||
grouped, groups = get_grouped_panel_types()
|
||||
|
||||
# Check group labels exist
|
||||
group_keys = [g[0] for g in groups]
|
||||
self.assertIn("danger", group_keys)
|
||||
self.assertIn("priority", group_keys)
|
||||
self.assertIn("prohibition", group_keys)
|
||||
|
||||
# Check danger group contains A1a and A1b
|
||||
danger_codes = [p['code'] for p in grouped['danger']]
|
||||
self.assertIn("A1a", danger_codes)
|
||||
self.assertIn("A1b", danger_codes)
|
||||
|
||||
# Check priority contains B1
|
||||
priority_codes = [p['code'] for p in grouped['priority']]
|
||||
self.assertIn("B1", priority_codes)
|
||||
|
||||
def test_index_view_renders_category_filter_controls(self):
|
||||
"""Verify the sign index view renders the category select-all checkboxes and quick-action toolbar."""
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(reverse('sign:index'))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
content = response.content.decode('utf-8')
|
||||
|
||||
# Verify accordion item with group key
|
||||
self.assertIn('data-group-key="danger"', content)
|
||||
# Verify group select-all checkbox
|
||||
self.assertIn('class="form-check-input group-select-all m-0"', content)
|
||||
self.assertIn('id="check-group-danger"', content)
|
||||
self.assertIn('data-group="danger"', content)
|
||||
# Verify count badge
|
||||
self.assertIn('id="badge-danger"', content)
|
||||
# Verify toolbar buttons: Tout cocher / Tout décocher
|
||||
self.assertIn('select-all-btn', content)
|
||||
self.assertIn('deselect-all-btn', content)
|
||||
# Verify data-group attribute on panel item
|
||||
self.assertIn('data-group="danger"', content)
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue