{% macro icon_for_type(type) %}
{% set t = (type|default('')|upper)|replace({'-':'_',' ':'_'}) %}
{% if t starts with 'GARAGE' %}
fa-solid fa-car
{% elseif t starts with 'DISTR' %}
fa-solid fa-store
{% elseif t starts with 'GROUPE_DISTRIBUTEUR' or t starts with 'GROUPE' or t starts with 'GROUP' %}
fa-solid fa-object-group
{% elseif t starts with 'DPAN' %}
fa-solid fa-building-columns
{% else %}
fa-regular fa-circle-question
{% endif %}
{% endmacro %}
{% from _self import icon_for_type %}
<header class="header z-index-50">
<nav class="navbar py-3 px-0 shadow-sm text-white position-relative">
<div class="container-fluid w-100">
<div class="navbar-holder d-flex align-items-center justify-content-between w-100">
<!-- === LOGO === -->
<div class="navbar-header">
<a class="menu-btn active" id="toggle-btn" href="#"><span></span><span></span><span></span></a>
<a href="{{ path('home.index') }}" class="brand-text d-none d-lg-inline-block px-3">
<img src="/images/identite/LOGO_LOGISTIC_FULL_SERVICE_DPAN001.png" class="img-fluid" width="150">
</a>
</div>
<!-- === MENU PRINCIPAL === -->
<ul class="nav-menu list-unstyled d-flex flex-md-row align-items-md-center gap-3 mb-0">
{% if app.user %}
{% set active = app.session.get('active_entity') %}
{% set userType = (app.user.userType|default('')|upper)|replace({'-':'_',' ':'_'}) %}
{% set fallbackType = null %}
{% set fallbackLabel = 'AUCUNE ENTITÉ' %}
{# Détermine le libellé et le type pour les non-mixed #}
{% if userType == 'MIXED' and not active %}
{% set fallbackLabel = 'CHOISIR UNE ENTITÉ' %}
{% elseif userType starts with 'GARAGE' and app.user.customerUser %}
{% set fallbackType = 'GARAGE' %}
{% set fallbackLabel = app.user.customerUser.raisonSociale|upper %}
{% elseif userType starts with 'DISTR' and app.user.distributeur %}
{% set fallbackType = 'DISTRIBUTEUR' %}
{% set fallbackLabel = app.user.distributeur.nomMagasin|upper %}
{% elseif userType starts with 'GROUPE' and app.user.groupDistributeurId %}
{% set fallbackType = 'GROUPE_DISTRIBUTEUR' %}
{% set fallbackLabel = app.user.groupDistributeurId.nomGroupeDistributeur|upper %}
{% elseif userType starts with 'DPAN' or is_granted('ROLE_DPAN') %}
{% set fallbackType = 'DPAN' %}
{% set fallbackLabel = 'DPAN' %}
{% endif %}
{% if userType starts with 'DPAN' or is_granted('ROLE_DPAN') %}
<!-- === BARRE DE RECHERCHE GLOBALE === -->
<li class="nav-item position-relative flex-grow-1 mx-2">
<div class="global-search w-100">
<div class="search-chip d-flex align-items-center gap-2 w-100">
<i class="fa-solid fa-magnifying-glass text-white-50"></i>
<input
id="globalSearchInput"
type="text"
class="search-input flex-grow-1"
placeholder="Rechercher une demande, un garage, un distributeur, une plaque…"
autocomplete="off"
>
</div>
</div>
<!-- === Conteneur dropdown dynamique === -->
<ul id="globalSearchResults" class="dropdown-menu shadow fade search-results"></ul>
</li>
{% endif %}
{# === BADGE ENTITÉ / DROPDOWN === #}
{% if active %}
{% set typeKey = active.type|lower|replace({' ':'-','_':'-'}) %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle d-flex align-items-center gap-2 entity-chip entity-chip--type-{{ typeKey }}"
id="entityMenu" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<i class="{{ icon_for_type(active.type) }} fa-fw chip-icon"></i>
<span class="entity-label">{{ active.label|upper }}</span>
</a>
<ul class="dropdown-menu dropdown-menu-end shadow fade" aria-labelledby="entityMenu">
<li>
<a class="dropdown-item d-flex align-items-center gap-2"
href="#" data-bs-toggle="modal" data-bs-target="#entityModalQuick">
<i class="fa-solid fa-shuffle text-primary"></i>
<span>Changer d’entité active</span>
</a>
</li>
<li><hr class="dropdown-divider my-1"></li>
<li>
<a class="dropdown-item d-flex align-items-center gap-2"
href="{{ path('profile.edit', { open: 'working-entity' }) }}">
<i class="fa-solid fa-briefcase text-secondary"></i>
<span>Modifier l’entité de travail</span>
</a>
</li>
</ul>
</li>
{% else %}
<li class="nav-item">
<span class="nav-link entity-chip d-inline-flex align-items-center gap-2 pe-none
{% if fallbackType %}entity-chip--type-{{ fallbackType|lower|replace({' ':'-','_':'-'}) }}{% endif %}">
<i class="{{ fallbackType ? icon_for_type(fallbackType) : 'fa-regular fa-circle-question' }} fa-fw chip-icon"></i>
<span class="entity-label">{{ fallbackLabel|upper }}</span>
</span>
</li>
{% endif %}
{# === MENU UTILISATEUR === #}
<li class="nav-item dropdown">
<a class="nav-link text-white dropdown-toggle d-flex align-items-center gap-2"
id="user" href="#" data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa-regular fa-user"></i>
<span class="user-label">{{ app.user.fullName }}</span>
</a>
<ul class="dropdown-menu dropdown-menu-end shadow fade" aria-labelledby="user">
{% if app.token is defined and app.token.originalToken is defined %}
<li>
<a class="dropdown-item d-flex align-items-center gap-2 text-danger"
href="{{ path('admin') ~ '?_switch_user=_exit' }}">
<i class="fa-solid fa-person-walking-dashed-line-arrow-right"></i>
<span>Quitter l’impersonation</span>
</a>
</li>
<li><hr class="dropdown-divider my-1"></li>
{% endif %}
<li>
<a class="dropdown-item d-flex align-items-center gap-2"
href="{{ path('profile.edit') }}">
<i class="fa-solid fa-user-gear text-primary"></i>
<span>Modifier les informations</span>
</a>
</li>
<li><hr class="dropdown-divider my-1"></li>
<li>
<a class="dropdown-item d-flex align-items-center gap-2"
href="{{ path('security.logout', { id: app.user.id }) }}">
<i class="fa-solid fa-right-from-bracket text-muted"></i>
<span>Déconnexion</span>
</a>
</li>
</ul>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link text-white d-flex align-items-center gap-2" href="{{ path('security.login') }}">
<i class="fa-solid fa-right-to-bracket"></i>
<span>Connexion</span>
</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
</header>
<script>
document.addEventListener('DOMContentLoaded', () => {
const input = document.getElementById('globalSearchInput');
const resultsBox = document.getElementById('globalSearchResults');
let debounceTimer = null;
// === Gestion de l'historique local ===
const HISTORY_KEY = 'fullservice_search_history';
let history = JSON.parse(localStorage.getItem(HISTORY_KEY) || '[]');
const saveToHistory = (term) => {
if (!term) return;
history = [term, ...history.filter(t => t !== term)].slice(0, 5);
localStorage.setItem(HISTORY_KEY, JSON.stringify(history));
};
// Affiche l'historique au focus (si champ vide)
input.addEventListener('focus', () => {
if (history.length && input.value.trim() === '') {
renderResults({ history });
}
});
input.addEventListener('input', function() {
const query = this.value.trim();
clearTimeout(debounceTimer);
if (query.length < 2) {
resultsBox.classList.remove('show');
return;
}
debounceTimer = setTimeout(() => {
fetch(`/search/live?q=${encodeURIComponent(query)}`, {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(res => res.json())
.then(data => {
saveToHistory(query);
renderResults(data, query);
})
.catch(err => console.error('Erreur recherche :', err));
}, 250);
});
// === Rendu du dropdown ===
function renderResults(data, query = '') {
resultsBox.innerHTML = '';
const regex = query ? new RegExp('(' + query + ')', 'ig') : null;
// --- Historique ---
if (data.history) {
resultsBox.innerHTML = '<div class="section-title">Dernières recherches</div>';
data.history.forEach(term => {
const li = document.createElement('li');
li.innerHTML = `
<button type="button" class="dropdown-item text-muted text-start search-history-item">
<i class="fa-regular fa-clock me-1"></i> ${term}
</button>`;
li.querySelector('.search-history-item').addEventListener('click', () => {
input.value = term;
resultsBox.classList.remove('show');
input.dispatchEvent(new Event('input'));
});
resultsBox.appendChild(li);
});
resultsBox.classList.add('show');
return;
}
// --- Regroupement par section ---
const sections = ['demandes', 'garages', 'distributeurs'];
let totalResults = 0;
sections.forEach(section => {
const results = data[section] || [];
if (!results.length) return;
totalResults += results.length;
const title = {
demandes: 'Demandes',
garages: 'Garages',
distributeurs: 'Distributeurs'
}[section];
// Titre de section
resultsBox.innerHTML += `<div class="section-title">${title}</div>`;
// Résultats
results.forEach(item => {
const li = document.createElement('li');
const text = regex ? item.title.replace(regex, '<span class="highlight">$1</span>') : item.title;
// Choix de l’icône selon le type
const icon = {
demandes: 'fa-solid fa-file-lines text-primary',
garages: 'fa-solid fa-car text-info',
distributeurs: 'fa-solid fa-store text-warning'
}[section];
// Badge d’état (si fourni)
const stateBadge = item.state
? `<span class="badge bg-${item.state.color} ms-auto">${item.state.label}</span>`
: '';
li.innerHTML = `
<a href="${item.url}" class="dropdown-item d-flex justify-content-between align-items-center gap-2">
<div class="d-flex align-items-center gap-2">
<i class="${icon} icon-type"></i>
<span class="result-text">${text}</span>
</div>
${stateBadge}
</a>`;
resultsBox.appendChild(li);
});
});
// Aucun résultat ?
if (totalResults === 0) {
resultsBox.innerHTML = `
<div class="section-title">Aucun résultat</div>
<li class="dropdown-item text-muted small">Aucun élément ne correspond à votre recherche.</li>`;
}
resultsBox.classList.add('show');
}
// Fermer au clic extérieur
document.addEventListener('click', e => {
if (!resultsBox.contains(e.target) && e.target !== input) {
resultsBox.classList.remove('show');
}
});
});
</script>
<style>
/* === DROPDOWNS === */
.dropdown-menu {
font-size: 0.9rem;
border-radius: .5rem;
min-width: 14rem;
margin-top: 0;
top: 100%;
transition: opacity .15s ease-in-out, transform .15s ease-in-out;
}
.dropdown-menu.show {
opacity: 1;
transform: translateY(2px); /* léger mouvement fluide */
}
.dropdown-item i {
width: 1.2rem;
text-align: center;
}
.dropdown-item:hover {
background-color: #f1f3f5;
}
/* === BADGE ENTITÉ === */
.entity-chip {
color: #e9ecef;
background: rgba(255,255,255,.06);
border: 1px solid rgba(255,255,255,.22);
border-radius: .5rem;
height: 36px;
padding: 0 .9rem;
font-size: .9rem;
font-weight: 500;
letter-spacing: .3px;
text-transform: uppercase;
transition: all .2s ease;
}
.entity-chip:hover {
background: rgba(255,255,255,.12);
border-color: rgba(255,255,255,.35);
text-decoration: none;
}
.entity-chip .chip-icon {
opacity: .9;
transform: translateY(1px);
font-size: 1rem;
}
/* === ALIGNEMENT / HARMONISATION DU TEXTE === */
.entity-label, .user-label {
font-size: .9rem;
font-weight: 500;
color: #f8f9fa;
text-transform: none;
line-height: 1;
display: inline-block;
}
/* === COULEURS CONTEXTUELLES === */
.entity-chip--type-garage .chip-icon { color: #4dabf7; }
.entity-chip--type-distributor .chip-icon { color: #ffd43b; }
.entity-chip--type-group .chip-icon { color: #63e6be; }
.entity-chip--type-dpan .chip-icon { color: #4CAF50; }
/* === RÉPONSE RESPONSIVE === */
@media (max-width: 991.98px) {
.entity-label, .user-label { display: none; }
.entity-chip { padding: 0 .5rem; height: 34px; }
}
/* === BARRE DE RECHERCHE === */
.global-search {
position: relative;
min-width: 560px; /* élargie */
}
.search-chip {
background: rgba(255,255,255,.06);
border: 1px solid rgba(255,255,255,.22);
border-radius: .5rem;
padding: 0 .9rem;
height: 38px;
transition: all .2s ease;
}
.search-chip:hover,
.search-chip:focus-within {
background: rgba(255,255,255,.12);
border-color: rgba(255,255,255,.35);
}
.search-input {
border: none;
background: transparent;
color: #f8f9fa;
font-size: .9rem;
width: 100%;
outline: none;
}
.search-input::placeholder {
color: rgba(255,255,255,.4);
}
/* === MENU DES RÉSULTATS === */
.search-results {
position: absolute;
top: 42px;
left: 0;
width: 100%;
background: #fff;
border-radius: .5rem;
overflow: hidden;
z-index: 2000;
max-height: 350px;
overflow-y: auto;
display: none;
font-size: .9rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.search-results.show {
display: block;
}
.search-results .section-title {
font-size: .75rem;
text-transform: uppercase;
color: #868e96;
padding: .5rem .75rem .25rem;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
font-weight: 600;
letter-spacing: .3px;
}
/* === ÉLÉMENTS DES RÉSULTATS === */
.search-results .dropdown-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: .6rem;
padding: .4rem .75rem;
white-space: normal;
transition: background-color .15s ease;
}
.search-results .dropdown-item:hover {
background-color: #f8f9fa;
}
.search-results .icon-type {
flex-shrink: 0;
width: 1.2rem;
text-align: center;
opacity: .7;
}
.search-results .result-text {
flex: 1 1 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.search-results .highlight {
background-color: #fff3bf;
padding: 0 2px;
}
/* === BADGE D'ÉTAT === */
.search-results .badge {
flex-shrink: 0;
margin-left: auto;
font-size: .7rem;
font-weight: 500;
border-radius: .4rem;
padding: .3em .6em;
line-height: 1.1;
text-transform: none;
}
/* Réactive les couleurs Bootstrap */
.search-results .badge.bg-success { background-color: #198754 !important; color: #fff !important; }
.search-results .badge.bg-danger { background-color: #dc3545 !important; color: #fff !important; }
.search-results .badge.bg-secondary{ background-color: #6c757d !important; color: #fff !important; }
.search-results .badge.bg-dark { background-color: #212529 !important; color: #fff !important; }
.search-results .badge.bg-teal { background-color: #20c997 !important; color: #fff !important; }
/* === HISTORIQUE DE RECHERCHE === */
.search-results .search-history-item {
display: flex;
align-items: center;
gap: .5rem;
justify-content: flex-start;
font-size: .9rem;
padding: .4rem .75rem;
color: #6c757d;
background: none;
border: none;
width: 100%;
text-align: left;
}
.search-results .search-history-item i {
opacity: .6;
width: 1rem;
text-align: center;
}
.search-results .search-history-item:hover {
background-color: #f8f9fa;
color: #212529;
}
</style>