diff --git a/loko/controls/static/controls/controls_index.css b/loko/controls/static/controls/controls_index.css index 6c7c634..ae412af 100644 --- a/loko/controls/static/controls/controls_index.css +++ b/loko/controls/static/controls/controls_index.css @@ -3,7 +3,8 @@ ========================================================================== */ /* Supprime le contour bleu Bootstrap au focus sur l'input de recherche */ -#controls-search-input:focus { +#controls-search-input:focus, +#project-autocomplete-input:focus { outline: none; box-shadow: none; border-color: #dee2e6; @@ -149,6 +150,83 @@ cursor: pointer; } +/* Project Autocomplete & Chips */ +.project-chip { + font-size: 0.8rem; + font-weight: 500; + border-radius: 1rem !important; + transition: background-color 0.15s ease; + border-color: #ced4da !important; +} + +.project-chip:hover { + background-color: #f1f3f5 !important; +} + +.project-chip .remove-project-btn { + opacity: 0.6; + transition: opacity 0.15s ease; +} + +.project-chip .remove-project-btn:hover { + opacity: 1; +} + +#project-autocomplete-dropdown .list-group-item { + cursor: pointer; + font-size: 0.85rem; + padding: 0.45rem 0.75rem; + border-left: none; + border-right: none; +} + +#project-autocomplete-dropdown .list-group-item:hover, +#project-autocomplete-dropdown .list-group-item.active-item { + background-color: #e9ecef; +} + +#project-autocomplete-dropdown .list-group-item.selected-project { + background-color: #e7f1ff; + color: #0c5cc9; +} + +/* Limites de largeur pour les colonnes Code et Thématiques */ +#controls-table th[data-field="control_code"], +#controls-table td.col-code { + max-width: 140px; + width: 140px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +#controls-table td.col-code a { + display: inline-block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: middle; +} + +#controls-table th[data-field="thematic_names"], +#controls-table td.col-thematics { + max-width: 180px; + width: 180px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +#controls-table td.col-thematics .thematic-content { + display: inline-block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: middle; +} + /* Layout helpers */ .controls-layout #controls-table-container { position: relative; diff --git a/loko/controls/static/controls/controls_index.js b/loko/controls/static/controls/controls_index.js index 128847c..befb256 100644 --- a/loko/controls/static/controls/controls_index.js +++ b/loko/controls/static/controls/controls_index.js @@ -232,8 +232,8 @@ function initControlsTable() { $table.bootstrapTable({ columns: [ { field: 'actions', title: '', formatter: 'actionsFormatter', class: 'text-nowrap' }, - { field: 'control_code', title: gettext('Code'), formatter: 'codeFormatter', class: 'text-nowrap', sortable: true }, - { field: 'thematic_names', title: gettext('Thématiques'), formatter: 'thematicFormatter', class: 'text-nowrap', sortable: true }, + { field: 'control_code', title: gettext('Code'), formatter: 'codeFormatter', class: 'text-nowrap col-code', sortable: true }, + { field: 'thematic_names', title: gettext('Thématiques'), formatter: 'thematicFormatter', class: 'col-thematics', sortable: true }, { field: 'category_names', title: gettext('Catégories'), sortable: true }, { field: 'control_type_label', title: gettext('Type'), formatter: 'controlTypeFormatter', class: 'text-nowrap', sortable: true }, { field: 'status_label', title: gettext('Statut'), formatter: 'statusFormatter', class: 'text-nowrap', sortable: true }, @@ -403,6 +403,26 @@ function setupFilterForm() { const mineCheck = form.querySelector('input[name="mine"]'); if (mineCheck) mineCheck.checked = false; + // Clear project chips + const projContainer = document.getElementById('selected-projects-container'); + if (projContainer) { + projContainer.querySelectorAll('.project-chip').forEach(chip => chip.remove()); + let noProjMsg = document.getElementById('no-project-selected-msg'); + if (!noProjMsg) { + noProjMsg = document.createElement('div'); + noProjMsg.id = 'no-project-selected-msg'; + noProjMsg.className = 'text-muted small align-self-center ps-1'; + noProjMsg.textContent = gettext('Aucun projet sélectionné (tous les projets sont inclus)'); + projContainer.appendChild(noProjMsg); + } else { + noProjMsg.style.display = ''; + } + } + const projInput = document.getElementById('project-autocomplete-input'); + if (projInput) projInput.value = ''; + const projClear = document.getElementById('project-search-clear-btn'); + if (projClear) projClear.style.display = 'none'; + // Reset category view document.querySelectorAll('.filter-options[data-field="category"] .filter-option[data-parent-id]:not([data-parent-id=""])').forEach(ch => { ch.style.display = 'none'; @@ -582,6 +602,180 @@ function setupFilterForm() { } filterCategoriesByThematics(); + setupProjectAutocomplete(); +} + +function setupProjectAutocomplete() { + const input = document.getElementById('project-autocomplete-input'); + const dropdown = document.getElementById('project-autocomplete-dropdown'); + const container = document.getElementById('selected-projects-container'); + const clearBtn = document.getElementById('project-search-clear-btn'); + const clearAllBtn = document.getElementById('clear-all-projects-btn'); + + if (!input || !dropdown || !container) return; + + let debounceTimer = null; + let activeIndex = -1; + + function closeDropdown() { + dropdown.classList.add('d-none'); + dropdown.innerHTML = ''; + activeIndex = -1; + } + + function getSelectedProjectIds() { + return Array.from(container.querySelectorAll('input[name="project"]')).map(inp => String(inp.value)); + } + + function updateEmptyMessage() { + const hasChips = container.querySelectorAll('.project-chip').length > 0; + let msg = document.getElementById('no-project-selected-msg'); + if (!hasChips) { + if (!msg) { + msg = document.createElement('div'); + msg.id = 'no-project-selected-msg'; + msg.className = 'text-muted small align-self-center ps-1'; + msg.textContent = gettext('Aucun projet sélectionné (tous les projets sont inclus)'); + container.appendChild(msg); + } else { + msg.style.display = ''; + } + } else if (msg) { + msg.style.display = 'none'; + } + } + + function addProjectChip(id, name) { + const currentIds = getSelectedProjectIds(); + if (currentIds.includes(String(id))) { + return; // already added + } + + const chip = document.createElement('div'); + chip.className = 'project-chip badge bg-white text-dark border d-inline-flex align-items-center gap-1 py-1 px-2'; + chip.setAttribute('data-project-id', id); + chip.innerHTML = ` + + ${name} + + + `; + + container.appendChild(chip); + updateEmptyMessage(); + } + + // Remove project chip handler (delegated) + container.addEventListener('click', (e) => { + const removeBtn = e.target.closest('.remove-project-btn'); + if (removeBtn) { + const chip = removeBtn.closest('.project-chip'); + if (chip) { + chip.remove(); + updateEmptyMessage(); + } + } + }); + + // Clear all projects button + clearAllBtn?.addEventListener('click', (e) => { + e.preventDefault(); + container.querySelectorAll('.project-chip').forEach(chip => chip.remove()); + updateEmptyMessage(); + }); + + // Autocomplete search + input.addEventListener('input', () => { + const q = input.value.trim(); + if (clearBtn) clearBtn.style.display = q ? '' : 'none'; + + clearTimeout(debounceTimer); + if (q.length < 1) { + closeDropdown(); + return; + } + + debounceTimer = setTimeout(() => { + if (typeof projectsAutocompleteUrl === 'undefined') return; + + fetch(`${projectsAutocompleteUrl}?q=${encodeURIComponent(q)}`) + .then(res => res.json()) + .then(data => { + if (!data || !data.length) { + dropdown.innerHTML = `