a1860e9462
- CarteController : index() + data() (JSON) — requête lieux géolocalisés
ayant des sources avec relevés, agrégats par lieu
- Lieu model : relations sources() et releves() (hasManyThrough)
- Vue carte/index.blade.php : carte Leaflet pleine hauteur, marqueurs
colorés par nombre de sources (taille/couleur proportionnels),
popup par lieu avec liste des sources, statuts, années et lien recherche
- Tuiles OpenStreetMap inversées en mode sombre
- Route GET /carte + GET /carte/data
- Lien "Carte" dans la navigation desktop et mobile
- @stack('head') dans le layout pour injecter Leaflet uniquement sur la page carte
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
297 lines
19 KiB
PHP
297 lines
19 KiB
PHP
<nav x-data="{ open: false }" class="bg-white dark:bg-gray-800 border-b border-gray-100 dark:border-gray-700">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<div class="flex justify-between h-16">
|
|
<div class="flex">
|
|
<!-- Logo -->
|
|
<div class="shrink-0 flex items-center">
|
|
<a href="{{ route('dashboard') }}" class="flex items-center">
|
|
@if($siteLogoUrl)
|
|
<img src="{{ $siteLogoUrl }}" alt="{{ config('app.name') }}"
|
|
class="block w-auto object-contain"
|
|
style="max-height: 40px; max-width: 200px;">
|
|
@else
|
|
<span class="font-semibold text-gray-800 dark:text-gray-200 text-lg">{{ config('app.name') }}</span>
|
|
@endif
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Navigation Links -->
|
|
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
|
|
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
|
Tableau de bord
|
|
</x-nav-link>
|
|
|
|
<x-nav-link :href="route('sources.index')" :active="request()->routeIs('sources.*') && !request()->routeIs('sources.releves.*')">
|
|
Sources
|
|
</x-nav-link>
|
|
|
|
<x-nav-link :href="route('lieux.index')" :active="request()->routeIs('lieux.*')">
|
|
Lieux
|
|
</x-nav-link>
|
|
|
|
<x-nav-link :href="route('recherche')" :active="request()->routeIs('recherche')">
|
|
Recherche
|
|
</x-nav-link>
|
|
|
|
<x-nav-link :href="route('carte')" :active="request()->routeIs('carte*')">
|
|
Carte
|
|
</x-nav-link>
|
|
|
|
@if(auth()->user()->isSectionManager())
|
|
<div class="hidden sm:flex sm:items-center">
|
|
<x-dropdown align="left" width="w-56">
|
|
<x-slot name="trigger">
|
|
<button class="inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium leading-5 transition duration-150 ease-in-out
|
|
{{ request()->routeIs('admin.*') ? 'border-indigo-400 text-gray-900 dark:text-white' : 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:border-gray-300 dark:hover:border-gray-600' }}">
|
|
Administration
|
|
<svg class="ms-1 h-4 w-4 fill-current" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/>
|
|
</svg>
|
|
</button>
|
|
</x-slot>
|
|
|
|
<x-slot name="content">
|
|
@if(auth()->user()->isAdmin())
|
|
<x-dropdown-link :href="route('admin.dashboard')"
|
|
class="{{ request()->routeIs('admin.dashboard') ? 'font-semibold text-indigo-700 bg-indigo-50' : '' }}">
|
|
Tableau de bord admin
|
|
</x-dropdown-link>
|
|
<x-dropdown-link :href="route('admin.utilisateurs.index')">
|
|
Utilisateurs
|
|
</x-dropdown-link>
|
|
@endif
|
|
<x-dropdown-link :href="route('admin.sections.index')">
|
|
Sections
|
|
</x-dropdown-link>
|
|
@if(auth()->user()->isAdmin())
|
|
<x-dropdown-link :href="route('admin.depots.index')">
|
|
Dépôts d'archives
|
|
</x-dropdown-link>
|
|
<x-dropdown-link :href="route('admin.source-types.index')">
|
|
Types de sources
|
|
</x-dropdown-link>
|
|
<x-dropdown-link :href="route('admin.lieu-types.index')">
|
|
Types de lieux
|
|
</x-dropdown-link>
|
|
<div class="border-t border-gray-100 dark:border-gray-700 my-1"></div>
|
|
<x-dropdown-link :href="route('admin.parametres')">
|
|
Paramètres du site
|
|
</x-dropdown-link>
|
|
@endif
|
|
</x-slot>
|
|
</x-dropdown>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right side: theme toggle + notifications + user menu -->
|
|
<div class="hidden sm:flex sm:items-center sm:gap-1">
|
|
|
|
<!-- Sélecteur de thème -->
|
|
<div x-data="{
|
|
theme: localStorage.getItem('colorTheme') || 'auto',
|
|
apply(t) {
|
|
this.theme = t;
|
|
localStorage.setItem('colorTheme', t);
|
|
var dark = t === 'dark' || (t === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
document.documentElement.classList.toggle('dark', dark);
|
|
}
|
|
}" class="flex items-center">
|
|
<div class="flex items-center bg-gray-100 dark:bg-gray-700 rounded-full p-0.5 gap-0.5">
|
|
<!-- Clair -->
|
|
<button @click="apply('light')"
|
|
:class="theme === 'light' ? 'bg-white dark:bg-gray-600 shadow text-gray-800 dark:text-gray-100' : 'text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300'"
|
|
class="p-1.5 rounded-full transition-all" title="Mode clair">
|
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364-6.364l-.707.707M6.343 17.657l-.707.707M17.657 17.657l-.707-.707M6.343 6.343l-.707-.707M12 7a5 5 0 100 10A5 5 0 0012 7z"/>
|
|
</svg>
|
|
</button>
|
|
<!-- Automatique -->
|
|
<button @click="apply('auto')"
|
|
:class="theme === 'auto' ? 'bg-white dark:bg-gray-600 shadow text-gray-800 dark:text-gray-100' : 'text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300'"
|
|
class="p-1.5 rounded-full transition-all" title="Automatique (système)">
|
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
|
</svg>
|
|
</button>
|
|
<!-- Sombre -->
|
|
<button @click="apply('dark')"
|
|
:class="theme === 'dark' ? 'bg-white dark:bg-gray-600 shadow text-gray-800 dark:text-gray-100' : 'text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300'"
|
|
class="p-1.5 rounded-full transition-all" title="Mode sombre">
|
|
<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Notifications -->
|
|
@php $unreadCount = auth()->user()->unreadNotifications->count(); @endphp
|
|
<a href="{{ route('notifications.index') }}"
|
|
class="relative p-2 text-gray-500 dark:text-gray-400 hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
|
|
title="Notifications">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>
|
|
</svg>
|
|
@if($unreadCount > 0)
|
|
<span class="absolute top-1 right-1 inline-flex items-center justify-center w-4 h-4 text-xs font-bold text-white bg-red-500 rounded-full">
|
|
{{ $unreadCount > 9 ? '9+' : $unreadCount }}
|
|
</span>
|
|
@endif
|
|
</a>
|
|
|
|
<!-- Menu utilisateur -->
|
|
<x-dropdown align="right" width="48">
|
|
<x-slot name="trigger">
|
|
<button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-200 focus:outline-none transition ease-in-out duration-150">
|
|
<div>{{ Auth::user()->name }}</div>
|
|
<div class="ms-1">
|
|
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
|
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
|
</svg>
|
|
</div>
|
|
</button>
|
|
</x-slot>
|
|
|
|
<x-slot name="content">
|
|
<div class="px-4 py-2 text-xs text-gray-400 dark:text-gray-500 border-b border-gray-100 dark:border-gray-700">
|
|
{{ Auth::user()->role->label() }}
|
|
</div>
|
|
<x-dropdown-link :href="route('profile.edit')">
|
|
Mon profil
|
|
</x-dropdown-link>
|
|
<form method="POST" action="{{ route('logout') }}">
|
|
@csrf
|
|
<x-dropdown-link :href="route('logout')"
|
|
onclick="event.preventDefault(); this.closest('form').submit();">
|
|
Se déconnecter
|
|
</x-dropdown-link>
|
|
</form>
|
|
</x-slot>
|
|
</x-dropdown>
|
|
</div>
|
|
|
|
<!-- Hamburger (mobile) -->
|
|
<div class="-me-2 flex items-center sm:hidden">
|
|
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 dark:text-gray-500 hover:text-gray-500 dark:hover:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none transition duration-150 ease-in-out">
|
|
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
|
|
<path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
|
<path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Menu responsive (mobile) -->
|
|
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
|
|
<div class="pt-2 pb-3 space-y-1">
|
|
<x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
|
Tableau de bord
|
|
</x-responsive-nav-link>
|
|
<x-responsive-nav-link :href="route('sources.index')" :active="request()->routeIs('sources.*')">
|
|
Sources
|
|
</x-responsive-nav-link>
|
|
<x-responsive-nav-link :href="route('lieux.index')" :active="request()->routeIs('lieux.*')">
|
|
Lieux
|
|
</x-responsive-nav-link>
|
|
<x-responsive-nav-link :href="route('recherche')" :active="request()->routeIs('recherche')">
|
|
Recherche
|
|
</x-responsive-nav-link>
|
|
<x-responsive-nav-link :href="route('carte')" :active="request()->routeIs('carte*')">
|
|
Carte
|
|
</x-responsive-nav-link>
|
|
@if(auth()->user()->isSectionManager())
|
|
@if(auth()->user()->isAdmin())
|
|
<x-responsive-nav-link :href="route('admin.dashboard')" :active="request()->routeIs('admin.dashboard')">
|
|
Tableau de bord admin
|
|
</x-responsive-nav-link>
|
|
<x-responsive-nav-link :href="route('admin.utilisateurs.index')" :active="request()->routeIs('admin.utilisateurs.*')">
|
|
Utilisateurs
|
|
</x-responsive-nav-link>
|
|
@endif
|
|
<x-responsive-nav-link :href="route('admin.sections.index')" :active="request()->routeIs('admin.sections.*')">
|
|
Sections
|
|
</x-responsive-nav-link>
|
|
@if(auth()->user()->isAdmin())
|
|
<x-responsive-nav-link :href="route('admin.depots.index')" :active="request()->routeIs('admin.depots.*')">
|
|
Dépôts d'archives
|
|
</x-responsive-nav-link>
|
|
<x-responsive-nav-link :href="route('admin.source-types.index')" :active="request()->routeIs('admin.source-types.*')">
|
|
Types de sources
|
|
</x-responsive-nav-link>
|
|
<x-responsive-nav-link :href="route('admin.lieu-types.index')" :active="request()->routeIs('admin.lieu-types.*')">
|
|
Types de lieux
|
|
</x-responsive-nav-link>
|
|
<x-responsive-nav-link :href="route('admin.parametres')">
|
|
Paramètres du site
|
|
</x-responsive-nav-link>
|
|
@endif
|
|
@endif
|
|
</div>
|
|
|
|
<!-- Options utilisateur (mobile) -->
|
|
<div class="pt-4 pb-1 border-t border-gray-200 dark:border-gray-700">
|
|
<div class="px-4">
|
|
<div class="font-medium text-base text-gray-800 dark:text-gray-200">{{ Auth::user()->name }}</div>
|
|
<div class="font-medium text-sm text-gray-500 dark:text-gray-400">{{ Auth::user()->email }}</div>
|
|
<div class="text-xs text-gray-400 dark:text-gray-500">{{ Auth::user()->role->label() }}</div>
|
|
</div>
|
|
|
|
<!-- Sélecteur de thème mobile -->
|
|
<div x-data="{
|
|
theme: localStorage.getItem('colorTheme') || 'auto',
|
|
apply(t) {
|
|
this.theme = t;
|
|
localStorage.setItem('colorTheme', t);
|
|
var dark = t === 'dark' || (t === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
document.documentElement.classList.toggle('dark', dark);
|
|
}
|
|
}" class="px-4 pt-3 pb-1">
|
|
<p class="text-xs text-gray-400 dark:text-gray-500 mb-2">Apparence</p>
|
|
<div class="flex gap-2">
|
|
<button @click="apply('light')"
|
|
:class="theme === 'light' ? 'bg-indigo-50 dark:bg-indigo-900/50 text-indigo-700 dark:text-indigo-300 border-indigo-300 dark:border-indigo-600' : 'border-gray-200 dark:border-gray-600 text-gray-600 dark:text-gray-400'"
|
|
class="flex-1 flex items-center justify-center gap-1.5 py-1.5 text-xs border rounded-md transition-colors">
|
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364-6.364l-.707.707M6.343 17.657l-.707.707M17.657 17.657l-.707-.707M6.343 6.343l-.707-.707M12 7a5 5 0 100 10A5 5 0 0012 7z"/>
|
|
</svg>
|
|
Clair
|
|
</button>
|
|
<button @click="apply('auto')"
|
|
:class="theme === 'auto' ? 'bg-indigo-50 dark:bg-indigo-900/50 text-indigo-700 dark:text-indigo-300 border-indigo-300 dark:border-indigo-600' : 'border-gray-200 dark:border-gray-600 text-gray-600 dark:text-gray-400'"
|
|
class="flex-1 flex items-center justify-center gap-1.5 py-1.5 text-xs border rounded-md transition-colors">
|
|
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
|
</svg>
|
|
Auto
|
|
</button>
|
|
<button @click="apply('dark')"
|
|
:class="theme === 'dark' ? 'bg-indigo-50 dark:bg-indigo-900/50 text-indigo-700 dark:text-indigo-300 border-indigo-300 dark:border-indigo-600' : 'border-gray-200 dark:border-gray-600 text-gray-600 dark:text-gray-400'"
|
|
class="flex-1 flex items-center justify-center gap-1.5 py-1.5 text-xs border rounded-md transition-colors">
|
|
<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M21 12.79A9 9 0 1111.21 3 7 7 0 0021 12.79z"/>
|
|
</svg>
|
|
Sombre
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-3 space-y-1">
|
|
<x-responsive-nav-link :href="route('profile.edit')">Mon profil</x-responsive-nav-link>
|
|
<form method="POST" action="{{ route('logout') }}">
|
|
@csrf
|
|
<x-responsive-nav-link :href="route('logout')"
|
|
onclick="event.preventDefault(); this.closest('form').submit();">
|
|
Se déconnecter
|
|
</x-responsive-nav-link>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</nav>
|