Étapes 6-9 + types de lieux + picker + filtres
- Étape 6 : formulaire de saisie dynamique des relevés (piloté par source_type_fields, calendriers grégorien/julien/républicain) - Étape 7 : workflow de statut des sources + notifications mail+DB (SourceAValider, SourceRejetee) - Étape 8 : recherche fulltext PostgreSQL avec filtres type/lieu/années et CTE récursive pour les subdivisions de lieux - Étape 9 : export GEDCOM 5.5.1 (GedcomExportService + DateConversionService) - Types de lieux : CRUD admin (LieuTypeController) avec champ ordre - Composant lieu-picker : modale Alpine.js avec recherche AJAX + debounce - Filtres sources : statut, type, lieu (CTE récursive), période annee_debut/annee_fin - Filtres lieux : type, texte, lieu parent avec descendants (CTE récursive) - Migration : lieu_id + annee_debut + annee_fin sur sources Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
<div class="space-y-5">
|
||||
<div>
|
||||
<label for="nom" class="block text-sm font-medium text-gray-700">Nom <span class="text-red-500">*</span></label>
|
||||
<input type="text" id="nom" name="nom" value="{{ old('nom', $lieuType?->nom) }}" required
|
||||
placeholder="ex : Pays, Région, Département, Ville…"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('nom') border-red-500 @enderror">
|
||||
@error('nom') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="ordre" class="block text-sm font-medium text-gray-700">Ordre d'affichage <span class="text-red-500">*</span></label>
|
||||
<input type="number" id="ordre" name="ordre" value="{{ old('ordre', $lieuType?->ordre ?? 0) }}"
|
||||
min="0" max="999" required
|
||||
class="mt-1 block w-32 rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
|
||||
<p class="mt-1 text-xs text-gray-400">Les valeurs les plus basses apparaissent en premier (ex : Pays=0, Région=10, Département=20, Ville=30…)</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,15 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header"><h2 class="text-xl font-semibold text-gray-800">Nouveau type de lieu</h2></x-slot>
|
||||
<div class="py-8 max-w-lg mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<form method="POST" action="{{ route('admin.lieu-types.store') }}">
|
||||
@csrf
|
||||
@include('admin.lieu-types._form', ['lieuType' => null])
|
||||
<div class="mt-6 flex gap-4">
|
||||
<button type="submit" class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">Créer</button>
|
||||
<a href="{{ route('admin.lieu-types.index') }}" class="text-sm text-gray-500 self-center hover:text-gray-700">Annuler</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,15 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header"><h2 class="text-xl font-semibold text-gray-800">Modifier : {{ $lieuType->nom }}</h2></x-slot>
|
||||
<div class="py-8 max-w-lg mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<form method="POST" action="{{ route('admin.lieu-types.update', $lieuType) }}">
|
||||
@csrf @method('PUT')
|
||||
@include('admin.lieu-types._form', ['lieuType' => $lieuType])
|
||||
<div class="mt-6 flex gap-4">
|
||||
<button type="submit" class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">Enregistrer</button>
|
||||
<a href="{{ route('admin.lieu-types.index') }}" class="text-sm text-gray-500 self-center hover:text-gray-700">Annuler</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,56 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-semibold text-gray-800">Types de lieux</h2>
|
||||
<a href="{{ route('admin.lieu-types.create') }}"
|
||||
class="px-4 py-2 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700">+ Nouveau type</a>
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-8 max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
@foreach(['success','error'] as $flash)
|
||||
@if(session($flash))
|
||||
<div class="mb-4 p-4 rounded-md {{ $flash === 'success' ? 'bg-green-50 border border-green-200 text-green-800' : 'bg-red-50 border border-red-200 text-red-800' }}">
|
||||
{{ session($flash) }}
|
||||
</div>
|
||||
@endif
|
||||
@endforeach
|
||||
|
||||
<div class="bg-white shadow rounded-lg overflow-hidden">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Ordre</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nom</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Lieux</th>
|
||||
<th class="px-6 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
@forelse($lieuTypes as $lt)
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-6 py-4 text-sm text-gray-400 w-16">{{ $lt->ordre }}</td>
|
||||
<td class="px-6 py-4 font-medium text-gray-900">{{ $lt->nom }}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500">{{ $lt->lieux_count }}</td>
|
||||
<td class="px-6 py-4 text-right text-sm space-x-3">
|
||||
<a href="{{ route('admin.lieu-types.edit', $lt) }}"
|
||||
class="text-gray-600 hover:text-indigo-600">Modifier</a>
|
||||
<form method="POST" action="{{ route('admin.lieu-types.destroy', $lt) }}" class="inline"
|
||||
x-data @submit.prevent="if(confirm('Supprimer ce type ?')) $el.submit()">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="text-red-500 hover:text-red-700">Supprimer</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="4" class="px-6 py-10 text-center text-gray-400">
|
||||
Aucun type de lieu défini.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -6,18 +6,14 @@
|
||||
@error('nom') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="lieu_id" class="block text-sm font-medium text-gray-700">Lieu de rattachement</label>
|
||||
<select id="lieu_id" name="lieu_id"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
|
||||
<option value="">— Aucun —</option>
|
||||
@foreach($lieux as $lieu)
|
||||
<option value="{{ $lieu->id }}" {{ old('lieu_id', $section?->lieu_id) == $lieu->id ? 'selected' : '' }}>
|
||||
{{ $lieu->nom_long ?? $lieu->nom }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<x-lieu-picker
|
||||
name="lieu_id"
|
||||
label="Lieu de rattachement"
|
||||
:value="old('lieu_id', $section?->lieu_id)"
|
||||
:display-value="old('lieu_id')
|
||||
? ''
|
||||
: ($section?->lieu?->nom_long ?? $section?->lieu?->nom ?? '')"
|
||||
/>
|
||||
|
||||
<div>
|
||||
<label for="adresse" class="block text-sm font-medium text-gray-700">Adresse</label>
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
{{--
|
||||
Composant de sélection d'un lieu par recherche contextuelle.
|
||||
|
||||
Paramètres :
|
||||
$name : nom du champ hidden (ex: "lieu_id")
|
||||
$label : libellé affiché au-dessus du champ
|
||||
$value : id du lieu sélectionné (null si aucun)
|
||||
$displayValue : texte affiché (nom_long du lieu sélectionné)
|
||||
$required : bool — rend le champ obligatoire
|
||||
$placeholder : texte quand rien n'est sélectionné
|
||||
--}}
|
||||
@props([
|
||||
'name',
|
||||
'label' => 'Lieu',
|
||||
'value' => null,
|
||||
'displayValue' => '',
|
||||
'required' => false,
|
||||
'placeholder' => 'Rechercher un lieu…',
|
||||
])
|
||||
|
||||
<div
|
||||
x-data="{
|
||||
open: false,
|
||||
search: '',
|
||||
results: [],
|
||||
loading: false,
|
||||
selected: {
|
||||
id: {{ $value ? (int)$value : 'null' }},
|
||||
name: {{ json_encode($displayValue ?: '') }}
|
||||
},
|
||||
debounceTimer: null,
|
||||
|
||||
openModal() {
|
||||
this.open = true;
|
||||
this.search = '';
|
||||
this.results = [];
|
||||
this.$nextTick(() => this.$refs.searchInput?.focus());
|
||||
},
|
||||
|
||||
onSearchInput() {
|
||||
clearTimeout(this.debounceTimer);
|
||||
this.debounceTimer = setTimeout(() => this.fetchResults(), 220);
|
||||
},
|
||||
|
||||
async fetchResults() {
|
||||
if (this.search.length < 1) { this.results = []; return; }
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await fetch(
|
||||
'{{ route('lieux.search') }}?q=' + encodeURIComponent(this.search),
|
||||
{ headers: { 'X-Requested-With': 'XMLHttpRequest' } }
|
||||
);
|
||||
this.results = await res.json();
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
select(lieu) {
|
||||
this.selected = { id: lieu.id, name: lieu.nom_long };
|
||||
this.open = false;
|
||||
this.search = '';
|
||||
this.results = [];
|
||||
},
|
||||
|
||||
clear() {
|
||||
this.selected = { id: null, name: '' };
|
||||
}
|
||||
}"
|
||||
@keydown.escape.window="open = false"
|
||||
>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
{{ $label }}
|
||||
@if($required) <span class="text-red-500">*</span> @endif
|
||||
</label>
|
||||
|
||||
{{-- Champ hidden pour la valeur soumise --}}
|
||||
<input type="hidden" name="{{ $name }}" :value="selected.id ?? ''">
|
||||
|
||||
{{-- Affichage du lieu sélectionné + boutons --}}
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
@click="openModal()"
|
||||
class="flex-1 text-left px-3 py-2 border border-gray-300 rounded-md bg-white text-sm shadow-sm
|
||||
hover:border-indigo-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 transition-colors
|
||||
@error($name) border-red-500 @enderror"
|
||||
>
|
||||
<span x-show="selected.id" class="text-gray-900" x-text="selected.name"></span>
|
||||
<span x-show="!selected.id" class="text-gray-400">{{ $placeholder }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
x-show="selected.id"
|
||||
@click="clear()"
|
||||
title="Effacer"
|
||||
class="px-2 py-2 text-gray-400 hover:text-red-500 transition-colors"
|
||||
>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@error($name)
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
|
||||
{{-- Modale de recherche --}}
|
||||
<div
|
||||
x-show="open"
|
||||
x-cloak
|
||||
class="fixed inset-0 z-50 flex items-start justify-center pt-24 px-4"
|
||||
@click.self="open = false"
|
||||
>
|
||||
{{-- Fond semi-transparent --}}
|
||||
<div class="absolute inset-0 bg-black/40" @click="open = false"></div>
|
||||
|
||||
{{-- Panneau --}}
|
||||
<div class="relative bg-white rounded-xl shadow-2xl w-full max-w-lg z-10 overflow-hidden">
|
||||
{{-- En-tête --}}
|
||||
<div class="px-4 py-3 border-b border-gray-100 flex items-center gap-3">
|
||||
<svg class="w-5 h-5 text-gray-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M21 21l-4.35-4.35M17 11A6 6 0 1 1 5 11a6 6 0 0 1 12 0z"/>
|
||||
</svg>
|
||||
<input
|
||||
x-ref="searchInput"
|
||||
type="text"
|
||||
x-model="search"
|
||||
@input="onSearchInput()"
|
||||
placeholder="Nom, code INSEE…"
|
||||
class="flex-1 text-sm outline-none placeholder-gray-400"
|
||||
>
|
||||
<button type="button" @click="open = false"
|
||||
class="text-gray-400 hover:text-gray-600">
|
||||
<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="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{-- Résultats --}}
|
||||
<div class="max-h-80 overflow-y-auto">
|
||||
{{-- Chargement --}}
|
||||
<div x-show="loading" class="px-4 py-6 text-center text-sm text-gray-400">
|
||||
Recherche…
|
||||
</div>
|
||||
|
||||
{{-- Aucun résultat --}}
|
||||
<div x-show="!loading && search.length > 0 && results.length === 0"
|
||||
class="px-4 py-6 text-center text-sm text-gray-400">
|
||||
Aucun lieu trouvé pour « <span x-text="search"></span> »
|
||||
</div>
|
||||
|
||||
{{-- Invite initiale --}}
|
||||
<div x-show="!loading && search.length === 0"
|
||||
class="px-4 py-6 text-center text-sm text-gray-400">
|
||||
Saisissez au moins une lettre pour rechercher
|
||||
</div>
|
||||
|
||||
{{-- Liste --}}
|
||||
<ul x-show="results.length > 0">
|
||||
<template x-for="lieu in results" :key="lieu.id">
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
@click="select(lieu)"
|
||||
class="w-full text-left px-4 py-3 hover:bg-indigo-50 flex items-center justify-between gap-3 transition-colors"
|
||||
>
|
||||
<div>
|
||||
<span class="text-sm font-medium text-gray-900" x-text="lieu.nom_long"></span>
|
||||
<span x-show="lieu.code"
|
||||
class="ml-2 text-xs text-gray-400"
|
||||
x-text="lieu.code"></span>
|
||||
</div>
|
||||
<span x-show="lieu.type"
|
||||
class="shrink-0 text-xs px-2 py-0.5 bg-gray-100 text-gray-500 rounded-full"
|
||||
x-text="lieu.type"></span>
|
||||
</button>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@can('create', App\Models\Lieu::class)
|
||||
{{-- Pied : créer un nouveau lieu --}}
|
||||
<div class="border-t border-gray-100 px-4 py-2.5">
|
||||
<a href="{{ route('lieux.create') }}" target="_blank"
|
||||
class="text-xs text-indigo-600 hover:underline">
|
||||
+ Créer un nouveau lieu
|
||||
</a>
|
||||
</div>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,30 +1,86 @@
|
||||
<nav x-data="{ open: false }" class="bg-white border-b border-gray-100">
|
||||
<!-- Primary Navigation Menu -->
|
||||
<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') }}">
|
||||
<x-application-logo class="block h-9 w-auto fill-current text-gray-800" />
|
||||
<a href="{{ route('dashboard') }}" class="font-semibold text-gray-800 text-lg">
|
||||
MesRelevés
|
||||
</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')">
|
||||
{{ __('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>
|
||||
|
||||
@if(auth()->user()->isSectionManager())
|
||||
<!-- Menu Administration -->
|
||||
<div class="hidden sm:flex sm:items-center" x-data="{ adminOpen: false }">
|
||||
<button @click="adminOpen = !adminOpen"
|
||||
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' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300' }}">
|
||||
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>
|
||||
|
||||
<div x-show="adminOpen" @click.outside="adminOpen = false" x-cloak
|
||||
class="absolute top-14 mt-1 w-48 bg-white rounded-md shadow-lg border border-gray-100 z-50">
|
||||
<a href="{{ route('admin.sections.index') }}"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">Sections</a>
|
||||
@if(auth()->user()->isAdmin())
|
||||
<a href="{{ route('admin.depots.index') }}"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">Dépôts d'archives</a>
|
||||
<a href="{{ route('admin.source-types.index') }}"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">Types de sources</a>
|
||||
<a href="{{ route('admin.lieu-types.index') }}"
|
||||
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">Types de lieux</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cloche notifications -->
|
||||
<div class="hidden sm:flex sm:items-center sm:ms-4">
|
||||
@php $unreadCount = auth()->user()->unreadNotifications->count(); @endphp
|
||||
<a href="{{ route('notifications.index') }}"
|
||||
class="relative p-2 text-gray-500 hover:text-indigo-600 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>
|
||||
</div>
|
||||
|
||||
<!-- Settings Dropdown -->
|
||||
<div class="hidden sm:flex sm:items-center sm:ms-6">
|
||||
<div class="hidden sm:flex sm:items-center sm:ms-2">
|
||||
<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 bg-white hover:text-gray-700 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" />
|
||||
@@ -34,18 +90,17 @@
|
||||
</x-slot>
|
||||
|
||||
<x-slot name="content">
|
||||
<div class="px-4 py-2 text-xs text-gray-400 border-b border-gray-100">
|
||||
{{ Auth::user()->role->label() }}
|
||||
</div>
|
||||
<x-dropdown-link :href="route('profile.edit')">
|
||||
{{ __('Profile') }}
|
||||
Mon profil
|
||||
</x-dropdown-link>
|
||||
|
||||
<!-- Authentication -->
|
||||
<form method="POST" action="{{ route('logout') }}">
|
||||
@csrf
|
||||
|
||||
<x-dropdown-link :href="route('logout')"
|
||||
onclick="event.preventDefault();
|
||||
this.closest('form').submit();">
|
||||
{{ __('Log Out') }}
|
||||
onclick="event.preventDefault(); this.closest('form').submit();">
|
||||
Se déconnecter
|
||||
</x-dropdown-link>
|
||||
</form>
|
||||
</x-slot>
|
||||
@@ -54,7 +109,7 @@
|
||||
|
||||
<!-- Hamburger -->
|
||||
<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 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out">
|
||||
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 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" />
|
||||
@@ -68,8 +123,30 @@
|
||||
<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')">
|
||||
{{ __('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>
|
||||
@if(auth()->user()->isSectionManager())
|
||||
<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>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Responsive Settings Options -->
|
||||
@@ -77,21 +154,15 @@
|
||||
<div class="px-4">
|
||||
<div class="font-medium text-base text-gray-800">{{ Auth::user()->name }}</div>
|
||||
<div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div>
|
||||
<div class="text-xs text-gray-400">{{ Auth::user()->role->label() }}</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 space-y-1">
|
||||
<x-responsive-nav-link :href="route('profile.edit')">
|
||||
{{ __('Profile') }}
|
||||
</x-responsive-nav-link>
|
||||
|
||||
<!-- Authentication -->
|
||||
<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();">
|
||||
{{ __('Log Out') }}
|
||||
onclick="event.preventDefault(); this.closest('form').submit();">
|
||||
Se déconnecter
|
||||
</x-responsive-nav-link>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -9,6 +9,27 @@
|
||||
@error('nom') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
{{-- Type de lieu --}}
|
||||
<div>
|
||||
<label for="lieu_type_id" class="block text-sm font-medium text-gray-700">Type <span class="text-red-500">*</span></label>
|
||||
<select id="lieu_type_id" name="lieu_type_id" required
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('lieu_type_id') border-red-500 @enderror">
|
||||
<option value="">— Choisir un type —</option>
|
||||
@foreach($lieuTypes as $lt)
|
||||
<option value="{{ $lt->id }}" {{ old('lieu_type_id', $lieu?->lieu_type_id) == $lt->id ? 'selected' : '' }}>
|
||||
{{ $lt->nom }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('lieu_type_id') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
|
||||
@if($lieuTypes->isEmpty())
|
||||
<p class="mt-1 text-sm text-amber-600">
|
||||
Aucun type défini.
|
||||
<a href="{{ route('admin.lieu-types.create') }}" class="underline">Créer des types →</a>
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Code --}}
|
||||
<div>
|
||||
<label for="code" class="block text-sm font-medium text-gray-700">Code (INSEE, postal…)</label>
|
||||
@@ -18,21 +39,16 @@
|
||||
maxlength="20">
|
||||
</div>
|
||||
|
||||
{{-- Parent --}}
|
||||
<div>
|
||||
<label for="lieu_parent_id" class="block text-sm font-medium text-gray-700">Lieu parent</label>
|
||||
<select id="lieu_parent_id" name="lieu_parent_id"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
|
||||
<option value="">— Aucun (lieu racine) —</option>
|
||||
@foreach($parents as $parent)
|
||||
<option value="{{ $parent->id }}"
|
||||
{{ old('lieu_parent_id', $lieu?->lieu_parent_id) == $parent->id ? 'selected' : '' }}>
|
||||
{{ $parent->nom_long ?? $parent->nom }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@error('lieu_parent_id') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
{{-- Lieu parent --}}
|
||||
<x-lieu-picker
|
||||
name="lieu_parent_id"
|
||||
label="Lieu parent"
|
||||
:value="old('lieu_parent_id', $lieu?->lieu_parent_id)"
|
||||
:display-value="old('lieu_parent_id')
|
||||
? ''
|
||||
: ($lieu?->parent?->nom_long ?? $lieu?->parent?->nom ?? '')"
|
||||
placeholder="— Aucun (lieu racine) —"
|
||||
/>
|
||||
|
||||
{{-- Coordonnées --}}
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<form method="POST" action="{{ route('lieux.store') }}">
|
||||
@csrf
|
||||
@include('lieux._form', ['lieu' => null, 'parents' => $parents])
|
||||
@include('lieux._form', ['lieu' => null])
|
||||
|
||||
<div class="mt-6 flex items-center gap-4">
|
||||
<button type="submit"
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<form method="POST" action="{{ route('lieux.update', $lieu) }}">
|
||||
@csrf @method('PUT')
|
||||
@include('lieux._form', ['lieu' => $lieu, 'parents' => $parents])
|
||||
@include('lieux._form', ['lieu' => $lieu])
|
||||
|
||||
<div class="mt-6 flex items-center gap-4">
|
||||
<button type="submit"
|
||||
|
||||
@@ -11,24 +11,78 @@
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="py-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6">
|
||||
|
||||
@if(session('success'))
|
||||
<div class="mb-4 p-4 bg-green-50 border border-green-200 text-green-800 rounded-md">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
<div class="p-4 bg-green-50 border border-green-200 text-green-800 rounded-md">{{ session('success') }}</div>
|
||||
@endif
|
||||
@if(session('error'))
|
||||
<div class="mb-4 p-4 bg-red-50 border border-red-200 text-red-800 rounded-md">
|
||||
{{ session('error') }}
|
||||
</div>
|
||||
<div class="p-4 bg-red-50 border border-red-200 text-red-800 rounded-md">{{ session('error') }}</div>
|
||||
@endif
|
||||
|
||||
{{-- Filtres --}}
|
||||
@php $hasFilters = request()->anyFilled(['lieu_type_id', 'q', 'lieu_id']); @endphp
|
||||
<div class="bg-white shadow rounded-lg p-5">
|
||||
<form method="GET" action="{{ route('lieux.index') }}">
|
||||
<div class="flex flex-wrap items-end gap-4">
|
||||
|
||||
<div class="flex-1 min-w-[200px]">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">Recherche</label>
|
||||
<input type="text" name="q" value="{{ request('q') }}"
|
||||
placeholder="Nom, code INSEE…"
|
||||
class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
|
||||
</div>
|
||||
|
||||
<div class="w-52">
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">Type de lieu</label>
|
||||
<select name="lieu_type_id"
|
||||
class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
|
||||
<option value="">— Tous les types —</option>
|
||||
@foreach($lieuTypes as $lt)
|
||||
<option value="{{ $lt->id }}" {{ request('lieu_type_id') == $lt->id ? 'selected' : '' }}>
|
||||
{{ $lt->nom }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 self-end">
|
||||
<button type="submit"
|
||||
class="px-5 py-2 bg-indigo-600 text-white text-sm font-medium rounded-md hover:bg-indigo-700">
|
||||
Filtrer
|
||||
</button>
|
||||
@if($hasFilters)
|
||||
<a href="{{ route('lieux.index') }}"
|
||||
class="px-4 py-2 border border-gray-300 text-gray-600 text-sm rounded-md hover:bg-gray-50">
|
||||
Effacer
|
||||
</a>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs bg-indigo-100 text-indigo-700">
|
||||
filtres actifs
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Filtre par lieu parent --}}
|
||||
<div class="mt-4 max-w-sm">
|
||||
<x-lieu-picker
|
||||
name="lieu_id"
|
||||
label="Lieu (et ses subdivisions)"
|
||||
:value="request('lieu_id')"
|
||||
:display-value="$lieuSelectionne?->nom_long ?? $lieuSelectionne?->nom ?? ''"
|
||||
placeholder="— Tous les lieux —"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- Tableau --}}
|
||||
<div class="bg-white shadow rounded-lg overflow-hidden">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Lieu</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Type</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Code</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Parent</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Coordonnées</th>
|
||||
@@ -43,6 +97,7 @@
|
||||
{{ $lieu->nom }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500">{{ $lieu->lieuType?->nom ?? '—' }}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500">{{ $lieu->code ?? '—' }}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500">
|
||||
@if($lieu->parent)
|
||||
@@ -76,7 +131,10 @@
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="5" class="px-6 py-10 text-center text-gray-400">Aucun lieu enregistré.</td>
|
||||
<td colspan="6" class="px-6 py-10 text-center text-gray-400">
|
||||
@if($hasFilters) Aucun lieu ne correspond aux filtres.
|
||||
@else Aucun lieu enregistré. @endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
|
||||
@@ -37,6 +37,10 @@
|
||||
<dt class="font-medium text-gray-500">Nom complet</dt>
|
||||
<dd class="col-span-2 text-gray-900">{{ $lieu->nom_long ?? $lieu->nom }}</dd>
|
||||
</div>
|
||||
<div class="px-6 py-4 grid grid-cols-3 gap-4 text-sm">
|
||||
<dt class="font-medium text-gray-500">Type</dt>
|
||||
<dd class="col-span-2 text-gray-900">{{ $lieu->lieuType?->nom ?? '—' }}</dd>
|
||||
</div>
|
||||
@if($lieu->code)
|
||||
<div class="px-6 py-4 grid grid-cols-3 gap-4 text-sm">
|
||||
<dt class="font-medium text-gray-500">Code</dt>
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-semibold text-gray-800">Notifications</h2>
|
||||
@if(auth()->user()->unreadNotifications->isNotEmpty())
|
||||
<form method="POST" action="{{ route('notifications.read-all') }}">
|
||||
@csrf
|
||||
<button type="submit"
|
||||
class="text-sm text-indigo-600 hover:underline">
|
||||
Tout marquer comme lu
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-8 max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
@if(session('success'))
|
||||
<div class="mb-4 p-4 bg-green-50 border border-green-200 text-green-800 rounded-md">
|
||||
{{ session('success') }}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="bg-white shadow rounded-lg divide-y divide-gray-100">
|
||||
@forelse($notifications as $notification)
|
||||
@php
|
||||
$data = $notification->data;
|
||||
$isRejet = ($data['type'] ?? '') === 'rejet';
|
||||
$isRead = $notification->read_at !== null;
|
||||
@endphp
|
||||
<div class="px-6 py-4 flex items-start gap-4 {{ $isRead ? 'opacity-60' : '' }}">
|
||||
{{-- Icône --}}
|
||||
<div class="shrink-0 mt-0.5">
|
||||
@if($isRejet)
|
||||
<span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-red-100 text-red-600">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</span>
|
||||
@else
|
||||
<span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-yellow-100 text-yellow-600">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Contenu --}}
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm text-gray-900">
|
||||
@if($isRejet)
|
||||
<strong>{{ $data['rejete_par'] }}</strong> a renvoyé la source
|
||||
<strong>{{ $data['source_nom'] }}</strong> en cours de saisie.
|
||||
@else
|
||||
<strong>{{ $data['soumis_par'] }}</strong> a soumis la source
|
||||
<strong>{{ $data['source_nom'] }}</strong> pour validation.
|
||||
@endif
|
||||
</p>
|
||||
<div class="mt-1 flex items-center gap-3 text-xs text-gray-400">
|
||||
<span>{{ $notification->created_at->diffForHumans() }}</span>
|
||||
@if(!$isRead)
|
||||
<span class="inline-block w-2 h-2 rounded-full bg-indigo-500"></span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Actions --}}
|
||||
<div class="shrink-0 flex items-center gap-3">
|
||||
<a href="{{ $data['url'] }}"
|
||||
class="text-sm text-indigo-600 hover:underline">
|
||||
Voir →
|
||||
</a>
|
||||
@if(!$isRead)
|
||||
<form method="POST" action="{{ route('notifications.read', $notification->id) }}">
|
||||
@csrf
|
||||
<button type="submit" class="text-xs text-gray-400 hover:text-gray-600" title="Marquer comme lu">✓</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@empty
|
||||
<div class="px-6 py-16 text-center text-gray-400">
|
||||
<svg class="mx-auto w-10 h-10 mb-3 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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>
|
||||
<p>Aucune notification</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
@if($notifications->hasPages())
|
||||
<div class="mt-4">{{ $notifications->links() }}</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,203 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<h2 class="text-xl font-semibold text-gray-800">Recherche dans les relevés</h2>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6">
|
||||
|
||||
{{-- Formulaire de recherche --}}
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<form method="GET" action="{{ route('recherche') }}" class="space-y-4">
|
||||
|
||||
{{-- Barre principale --}}
|
||||
<div class="flex gap-3">
|
||||
<div class="flex-1 relative">
|
||||
<div class="absolute inset-y-0 left-3 flex items-center pointer-events-none">
|
||||
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M21 21l-4.35-4.35M17 11A6 6 0 1 1 5 11a6 6 0 0 1 12 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<input type="text" name="q" value="{{ request('q') }}"
|
||||
placeholder="Nom, prénom, lieu, note…"
|
||||
autofocus
|
||||
class="block w-full pl-10 pr-4 py-2.5 border border-gray-300 rounded-md shadow-sm
|
||||
text-sm focus:border-indigo-500 focus:ring-indigo-500">
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="px-6 py-2.5 bg-indigo-600 text-white text-sm font-medium rounded-md hover:bg-indigo-700">
|
||||
Rechercher
|
||||
</button>
|
||||
@if(request()->anyFilled(['q', 'source_type_id', 'annee_debut', 'annee_fin']))
|
||||
<a href="{{ route('recherche') }}"
|
||||
class="px-4 py-2.5 border border-gray-300 text-gray-600 text-sm rounded-md hover:bg-gray-50">
|
||||
Effacer
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Filtres avancés --}}
|
||||
@php
|
||||
$hasAdvanced = request()->anyFilled(['source_type_id', 'lieu_id', 'annee_debut', 'annee_fin']);
|
||||
@endphp
|
||||
<div x-data="{ open: {{ $hasAdvanced ? 'true' : 'false' }} }">
|
||||
<button type="button" @click="open = !open"
|
||||
class="text-sm text-indigo-600 hover:underline flex items-center gap-1">
|
||||
<span x-text="open ? '▲ Masquer les filtres' : '▼ Filtres avancés'"></span>
|
||||
@if($hasAdvanced)
|
||||
<span class="ml-1 inline-flex items-center px-1.5 py-0.5 rounded-full text-xs bg-indigo-100 text-indigo-700">
|
||||
actifs
|
||||
</span>
|
||||
@endif
|
||||
</button>
|
||||
|
||||
<div x-show="open" x-cloak class="mt-4 space-y-4">
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">Type de source</label>
|
||||
<select name="source_type_id"
|
||||
class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
|
||||
<option value="">— Tous les types —</option>
|
||||
@foreach($sourceTypes as $st)
|
||||
<option value="{{ $st->id }}" {{ request('source_type_id') == $st->id ? 'selected' : '' }}>
|
||||
{{ $st->nom }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">Année de début</label>
|
||||
<input type="number" name="annee_debut" value="{{ request('annee_debut') }}"
|
||||
min="1000" max="2100" placeholder="ex : 1820"
|
||||
class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">Année de fin</label>
|
||||
<input type="number" name="annee_fin" value="{{ request('annee_fin') }}"
|
||||
min="1000" max="2100" placeholder="ex : 1830"
|
||||
class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Filtre par lieu --}}
|
||||
<div class="max-w-sm">
|
||||
<x-lieu-picker
|
||||
name="lieu_id"
|
||||
label="Lieu (et ses subdivisions)"
|
||||
:value="request('lieu_id')"
|
||||
:display-value="$lieuSelectionne?->nom_long ?? $lieuSelectionne?->nom ?? ''"
|
||||
placeholder="— Tous les lieux —"
|
||||
/>
|
||||
@if($lieuSelectionne)
|
||||
<p class="mt-1 text-xs text-gray-400">
|
||||
Inclut toutes les subdivisions de {{ $lieuSelectionne->nom_long ?? $lieuSelectionne->nom }}.
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- Résultats --}}
|
||||
@if($resultats !== null)
|
||||
<div>
|
||||
<p class="text-sm text-gray-500 mb-3">
|
||||
@if($total === 0)
|
||||
Aucun relevé trouvé.
|
||||
@else
|
||||
<strong>{{ number_format($total) }}</strong> relevé{{ $total > 1 ? 's' : '' }} trouvé{{ $total > 1 ? 's' : '' }}
|
||||
@if(request('q')) pour <em>« {{ request('q') }} »</em> @endif
|
||||
—
|
||||
<a href="{{ route('export.recherche', request()->query()) }}"
|
||||
class="text-indigo-600 hover:underline">
|
||||
↓ Exporter en GEDCOM
|
||||
</a>
|
||||
@endif
|
||||
</p>
|
||||
|
||||
@if($resultats->isNotEmpty())
|
||||
<div class="bg-white shadow rounded-lg overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 text-sm">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nom</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Prénom</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Date</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Source</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Type</th>
|
||||
<th class="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
@foreach($resultats as $releve)
|
||||
@php
|
||||
$data = $releve->data;
|
||||
$dateEvt = $data['date_evenement'] ?? null;
|
||||
$dateAffichee = is_array($dateEvt)
|
||||
? ($dateEvt['valeur'] ?? '—') . ($dateEvt['calendrier'] !== 'gregorien' ? ' (' . $dateEvt['calendrier'] . ')' : '')
|
||||
: ($releve->date_evenement ?? '—');
|
||||
@endphp
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-4 py-3 font-medium text-gray-900">
|
||||
@if(request('q') && $releve->nom)
|
||||
{!! preg_replace('/(' . preg_quote(request('q'), '/') . ')/i', '<mark class="bg-yellow-100 rounded px-0.5">$1</mark>', e($releve->nom)) !!}
|
||||
@else
|
||||
{{ $releve->nom ?? '—' }}
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3 text-gray-700">
|
||||
@if(request('q') && $releve->prenom)
|
||||
{!! preg_replace('/(' . preg_quote(request('q'), '/') . ')/i', '<mark class="bg-yellow-100 rounded px-0.5">$1</mark>', e($releve->prenom)) !!}
|
||||
@else
|
||||
{{ $releve->prenom ?? '—' }}
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3 text-gray-600 whitespace-nowrap">
|
||||
{{ $dateAffichee }}
|
||||
</td>
|
||||
<td class="px-4 py-3 text-gray-600">
|
||||
<a href="{{ route('sources.show', $releve->source) }}"
|
||||
class="hover:text-indigo-600 hover:underline">
|
||||
{{ $releve->source->nom }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-600">
|
||||
{{ $releve->source->sourceType->nom }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<a href="{{ route('releves.show', $releve) }}"
|
||||
class="text-indigo-600 hover:underline text-xs">
|
||||
Voir →
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if($resultats->hasPages())
|
||||
<div class="px-6 py-4 border-t border-gray-200">
|
||||
{{ $resultats->links() }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
{{-- État initial --}}
|
||||
<div class="text-center py-16 text-gray-400">
|
||||
<svg class="mx-auto w-12 h-12 mb-4 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
|
||||
d="M21 21l-4.35-4.35M17 11A6 6 0 1 1 5 11a6 6 0 0 1 12 0z"/>
|
||||
</svg>
|
||||
<p class="text-sm">Saisissez un nom, prénom, lieu ou tout autre terme pour rechercher dans les relevés.</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,100 @@
|
||||
{{--
|
||||
Rendu d'un champ dynamique selon son FieldType.
|
||||
Variables attendues : $field (SourceTypeField), $value (valeur courante ou null)
|
||||
--}}
|
||||
@php
|
||||
use App\Enums\FieldType;
|
||||
$name = "data[{$field->name}]";
|
||||
$inputId = "field_{$field->name}";
|
||||
$oldValue = old("data.{$field->name}", $value);
|
||||
@endphp
|
||||
|
||||
<div class="space-y-1">
|
||||
<label for="{{ $inputId }}" class="block text-sm font-medium text-gray-700">
|
||||
{{ $field->label }}
|
||||
@if($field->required) <span class="text-red-500">*</span> @endif
|
||||
</label>
|
||||
|
||||
@switch($field->type)
|
||||
|
||||
@case(FieldType::Text)
|
||||
<input type="text" id="{{ $inputId }}" name="{{ $name }}"
|
||||
value="{{ $oldValue }}"
|
||||
{{ $field->required ? 'required' : '' }}
|
||||
class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500 @error("data.{$field->name}") border-red-500 @enderror">
|
||||
@break
|
||||
|
||||
@case(FieldType::Textarea)
|
||||
<textarea id="{{ $inputId }}" name="{{ $name }}" rows="3"
|
||||
{{ $field->required ? 'required' : '' }}
|
||||
class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500 @error("data.{$field->name}") border-red-500 @enderror">{{ $oldValue }}</textarea>
|
||||
@break
|
||||
|
||||
@case(FieldType::Number)
|
||||
<input type="number" id="{{ $inputId }}" name="{{ $name }}"
|
||||
value="{{ $oldValue }}" step="any"
|
||||
{{ $field->required ? 'required' : '' }}
|
||||
class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500 @error("data.{$field->name}") border-red-500 @enderror">
|
||||
@break
|
||||
|
||||
@case(FieldType::Boolean)
|
||||
@php $checked = old("data.{$field->name}", $value) ? true : false; @endphp
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<input type="hidden" name="{{ $name }}" value="0">
|
||||
<input type="checkbox" id="{{ $inputId }}" name="{{ $name }}" value="1"
|
||||
{{ $checked ? 'checked' : '' }}
|
||||
class="rounded border-gray-300 text-indigo-600 focus:ring-indigo-500">
|
||||
<span class="text-sm text-gray-600">{{ $field->label }}</span>
|
||||
</div>
|
||||
@break
|
||||
|
||||
@case(FieldType::Select)
|
||||
<select id="{{ $inputId }}" name="{{ $name }}"
|
||||
{{ $field->required ? 'required' : '' }}
|
||||
class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500 @error("data.{$field->name}") border-red-500 @enderror">
|
||||
@if(!$field->required) <option value="">— Choisir —</option> @endif
|
||||
@foreach($field->options ?? [] as $opt)
|
||||
<option value="{{ $opt }}" {{ $oldValue === $opt ? 'selected' : '' }}>{{ $opt }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@break
|
||||
|
||||
@case(FieldType::Date)
|
||||
@php
|
||||
$dateVal = is_array($oldValue) ? ($oldValue['valeur'] ?? '') : '';
|
||||
$dateCal = is_array($oldValue) ? ($oldValue['calendrier'] ?? 'gregorien') : old("data.{$field->name}.calendrier", 'gregorien');
|
||||
@endphp
|
||||
<div x-data="{ cal: '{{ $dateCal }}' }" class="flex gap-2">
|
||||
{{-- Sélecteur de calendrier --}}
|
||||
<select name="{{ $name }}[calendrier]" x-model="cal"
|
||||
class="w-40 rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
|
||||
<option value="gregorien">Grégorien</option>
|
||||
<option value="julien">Julien</option>
|
||||
<option value="republicain">Républicain</option>
|
||||
</select>
|
||||
|
||||
{{-- Date grégorienne / julienne : input date HTML5 --}}
|
||||
<input x-show="cal !== 'republicain'"
|
||||
type="date" name="{{ $name }}[valeur]"
|
||||
value="{{ $dateCal !== 'republicain' ? $dateVal : '' }}"
|
||||
{{ $field->required ? 'required' : '' }}
|
||||
class="flex-1 rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
|
||||
|
||||
{{-- Date républicaine : saisie texte libre (ex: "15 Vendémiaire An III") --}}
|
||||
<input x-show="cal === 'republicain'" x-cloak
|
||||
type="text" name="{{ $name }}[valeur]"
|
||||
value="{{ $dateCal === 'republicain' ? $dateVal : '' }}"
|
||||
placeholder="ex : 15 Vendémiaire An III"
|
||||
class="flex-1 rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
|
||||
</div>
|
||||
@error("data.{$field->name}.valeur")
|
||||
<p class="text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
@break
|
||||
|
||||
@endswitch
|
||||
|
||||
@error("data.{$field->name}")
|
||||
<p class="text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
@@ -0,0 +1,15 @@
|
||||
{{-- $source->sourceType->fields doit être chargé --}}
|
||||
{{-- $releve : null pour création, instance pour édition --}}
|
||||
<div class="space-y-6">
|
||||
@forelse($source->sourceType->fields as $field)
|
||||
@php
|
||||
$rawValue = $releve?->data[$field->name] ?? null;
|
||||
@endphp
|
||||
@include('releves._field', ['field' => $field, 'value' => $rawValue])
|
||||
@empty
|
||||
<p class="text-sm text-gray-400 italic">
|
||||
Ce type de source n'a aucun champ défini.
|
||||
<a href="{{ route('admin.source-types.show', $source->sourceType) }}" class="text-indigo-600 hover:underline">Configurer les champs →</a>
|
||||
</p>
|
||||
@endforelse
|
||||
</div>
|
||||
@@ -0,0 +1,28 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-gray-800">Nouveau relevé</h2>
|
||||
<p class="text-sm text-gray-500 mt-0.5">
|
||||
Source : <a href="{{ route('sources.show', $source) }}" class="text-indigo-600 hover:underline">{{ $source->nom }}</a>
|
||||
· Type : {{ $source->sourceType->nom }}
|
||||
</p>
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-8 max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<form method="POST" action="{{ route('sources.releves.store', $source) }}">
|
||||
@csrf
|
||||
@include('releves._form', ['releve' => null])
|
||||
<div class="mt-8 pt-6 border-t border-gray-200 flex items-center gap-4">
|
||||
<button type="submit"
|
||||
class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">
|
||||
Enregistrer le relevé
|
||||
</button>
|
||||
<a href="{{ route('sources.releves.index', $source) }}"
|
||||
class="text-sm text-gray-500 hover:text-gray-700">Annuler</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,27 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-gray-800">Modifier le relevé #{{ $releve->id }}</h2>
|
||||
<p class="text-sm text-gray-500 mt-0.5">
|
||||
Source : <a href="{{ route('sources.show', $source) }}" class="text-indigo-600 hover:underline">{{ $source->nom }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-8 max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<form method="POST" action="{{ route('releves.update', $releve) }}">
|
||||
@csrf @method('PUT')
|
||||
@include('releves._form', ['releve' => $releve])
|
||||
<div class="mt-8 pt-6 border-t border-gray-200 flex items-center gap-4">
|
||||
<button type="submit"
|
||||
class="px-5 py-2 bg-indigo-600 text-white rounded-md hover:bg-indigo-700">
|
||||
Enregistrer
|
||||
</button>
|
||||
<a href="{{ route('releves.show', $releve) }}"
|
||||
class="text-sm text-gray-500 hover:text-gray-700">Annuler</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,120 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-gray-800">Relevés — {{ $source->nom }}</h2>
|
||||
<p class="text-sm text-gray-500 mt-0.5">
|
||||
Type : {{ $source->sourceType->nom }}
|
||||
@if($source->cote) · Cote : {{ $source->cote }} @endif
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<a href="{{ route('sources.show', $source) }}" class="text-sm text-indigo-600 hover:underline">← Source</a>
|
||||
<a href="{{ route('export.source', $source) }}"
|
||||
class="px-4 py-2 border border-gray-300 text-gray-700 text-sm rounded-md hover:bg-gray-50"
|
||||
title="Télécharger au format GEDCOM 5.5.1">
|
||||
↓ GEDCOM
|
||||
</a>
|
||||
@can('create', [App\Models\Releve::class, $source])
|
||||
<a href="{{ route('sources.releves.create', $source) }}"
|
||||
class="px-4 py-2 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700">
|
||||
+ Nouveau relevé
|
||||
</a>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
@if(session('success'))
|
||||
<div class="mb-4 p-4 bg-green-50 border border-green-200 text-green-800 rounded-md">{{ session('success') }}</div>
|
||||
@endif
|
||||
|
||||
@php
|
||||
// Colonnes à afficher : les 4 premiers champs du type de source
|
||||
$colonnes = $source->sourceType->fields->take(5);
|
||||
@endphp
|
||||
|
||||
<div class="bg-white shadow rounded-lg overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 text-sm">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
@foreach($colonnes as $col)
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase whitespace-nowrap">
|
||||
{{ $col->label }}
|
||||
</th>
|
||||
@endforeach
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Saisi par</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Date</th>
|
||||
<th class="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200">
|
||||
@forelse($releves as $releve)
|
||||
<tr class="hover:bg-gray-50">
|
||||
@foreach($colonnes as $col)
|
||||
<td class="px-4 py-3 text-gray-700">
|
||||
@php $val = $releve->data[$col->name] ?? null; @endphp
|
||||
@if(is_array($val))
|
||||
{{ $val['valeur'] ?? '' }}
|
||||
@if(!empty($val['calendrier']) && $val['calendrier'] !== 'gregorien')
|
||||
<span class="text-xs text-gray-400">({{ $val['calendrier'] }})</span>
|
||||
@endif
|
||||
@elseif(is_bool($val))
|
||||
{{ $val ? 'Oui' : 'Non' }}
|
||||
@else
|
||||
{{ $val ?? '—' }}
|
||||
@endif
|
||||
</td>
|
||||
@endforeach
|
||||
<td class="px-4 py-3 text-gray-500">{{ $releve->createur?->name ?? '—' }}</td>
|
||||
<td class="px-4 py-3 text-gray-500 whitespace-nowrap">{{ $releve->created_at->format('d/m/Y') }}</td>
|
||||
<td class="px-4 py-3 text-right whitespace-nowrap space-x-3">
|
||||
<a href="{{ route('releves.show', $releve) }}" class="text-indigo-600 hover:underline">Voir</a>
|
||||
@can('update', $releve)
|
||||
<a href="{{ route('releves.edit', $releve) }}" class="text-gray-600 hover:text-indigo-600">Modifier</a>
|
||||
@endcan
|
||||
@can('delete', $releve)
|
||||
<form method="POST" action="{{ route('releves.destroy', $releve) }}" class="inline"
|
||||
x-data @submit.prevent="if(confirm('Supprimer ce relevé ?')) $el.submit()">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit" class="text-red-500 hover:text-red-700">Supprimer</button>
|
||||
</form>
|
||||
@endcan
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="{{ $colonnes->count() + 3 }}"
|
||||
class="px-6 py-10 text-center text-gray-400">
|
||||
Aucun relevé pour cette source.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{{-- Navigation curseur (keyset pagination) --}}
|
||||
@if($releves->hasPages())
|
||||
<div class="px-6 py-4 border-t border-gray-200 flex items-center justify-between text-sm">
|
||||
<div>
|
||||
@if($releves->onFirstPage())
|
||||
<span class="text-gray-400">← Précédent</span>
|
||||
@else
|
||||
<a href="{{ $releves->previousPageUrl() }}" class="text-indigo-600 hover:underline">← Précédent</a>
|
||||
@endif
|
||||
</div>
|
||||
<div>
|
||||
@if($releves->hasMorePages())
|
||||
<a href="{{ $releves->nextPageUrl() }}" class="text-indigo-600 hover:underline">Suivant →</a>
|
||||
@else
|
||||
<span class="text-gray-400">Suivant →</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -0,0 +1,74 @@
|
||||
<x-app-layout>
|
||||
<x-slot name="header">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 class="text-xl font-semibold text-gray-800">Relevé #{{ $releve->id }}</h2>
|
||||
<p class="text-sm text-gray-500 mt-0.5">
|
||||
Source : <a href="{{ route('sources.show', $source) }}" class="text-indigo-600 hover:underline">{{ $source->nom }}</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
@can('update', $releve)
|
||||
<a href="{{ route('releves.edit', $releve) }}"
|
||||
class="px-4 py-2 bg-indigo-600 text-white text-sm rounded-md hover:bg-indigo-700">
|
||||
Modifier
|
||||
</a>
|
||||
@endcan
|
||||
@can('delete', $releve)
|
||||
<form method="POST" action="{{ route('releves.destroy', $releve) }}"
|
||||
x-data @submit.prevent="if(confirm('Supprimer ce relevé ?')) $el.submit()">
|
||||
@csrf @method('DELETE')
|
||||
<button type="submit"
|
||||
class="px-4 py-2 bg-red-600 text-white text-sm rounded-md hover:bg-red-700">
|
||||
Supprimer
|
||||
</button>
|
||||
</form>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-8 max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6">
|
||||
@if(session('success'))
|
||||
<div class="p-4 bg-green-50 border border-green-200 text-green-800 rounded-md">{{ session('success') }}</div>
|
||||
@endif
|
||||
|
||||
{{-- Champs du relevé --}}
|
||||
<div class="bg-white shadow rounded-lg divide-y divide-gray-100">
|
||||
@foreach($source->sourceType->fields as $field)
|
||||
@php $val = $releve->data[$field->name] ?? null; @endphp
|
||||
<div class="px-6 py-4 grid grid-cols-3 gap-4 text-sm">
|
||||
<dt class="font-medium text-gray-500">{{ $field->label }}</dt>
|
||||
<dd class="col-span-2 text-gray-900">
|
||||
@if($val === null || $val === '')
|
||||
<span class="text-gray-400">—</span>
|
||||
@elseif(is_array($val))
|
||||
{{ $val['valeur'] ?? '—' }}
|
||||
@if(!empty($val['calendrier']) && $val['calendrier'] !== 'gregorien')
|
||||
<span class="ml-1 text-xs text-gray-400 capitalize">({{ $val['calendrier'] }})</span>
|
||||
@endif
|
||||
@elseif(is_bool($val))
|
||||
<span class="{{ $val ? 'text-green-700' : 'text-gray-400' }}">
|
||||
{{ $val ? 'Oui' : 'Non' }}
|
||||
</span>
|
||||
@else
|
||||
{{ $val }}
|
||||
@endif
|
||||
</dd>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
{{-- Méta-données de saisie --}}
|
||||
<div class="bg-gray-50 rounded-lg px-6 py-4 text-xs text-gray-500 space-y-1">
|
||||
<p>Saisi par <strong>{{ $releve->createur?->name ?? '?' }}</strong> le {{ $releve->created_at->format('d/m/Y à H:i') }}</p>
|
||||
@if($releve->updated_at != $releve->created_at)
|
||||
<p>Modifié par <strong>{{ $releve->modificateur?->name ?? '?' }}</strong> le {{ $releve->updated_at->format('d/m/Y à H:i') }}</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 text-sm">
|
||||
<a href="{{ route('sources.releves.index', $source) }}" class="text-indigo-600 hover:underline">← Liste des relevés</a>
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
@@ -41,6 +41,48 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Lieu géographique couvert par la source --}}
|
||||
@php
|
||||
$lieuIdForm = old('lieu_id', $source?->lieu_id);
|
||||
$lieuDispForm = '';
|
||||
if ($lieuIdForm) {
|
||||
$lieuObj = ($source?->lieu_id == $lieuIdForm && $source?->lieu)
|
||||
? $source->lieu
|
||||
: \App\Models\Lieu::find($lieuIdForm, ['id', 'nom', 'nom_long']);
|
||||
$lieuDispForm = $lieuObj?->nom_long ?? $lieuObj?->nom ?? '';
|
||||
}
|
||||
@endphp
|
||||
<div>
|
||||
<x-lieu-picker
|
||||
name="lieu_id"
|
||||
label="Lieu couvert"
|
||||
:value="$lieuIdForm"
|
||||
:display-value="$lieuDispForm"
|
||||
placeholder="— Aucun lieu —"
|
||||
/>
|
||||
@error('lieu_id') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
|
||||
{{-- Période couverte --}}
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="annee_debut" class="block text-sm font-medium text-gray-700">Année de début</label>
|
||||
<input type="number" id="annee_debut" name="annee_debut"
|
||||
value="{{ old('annee_debut', $source?->annee_debut) }}"
|
||||
min="1000" max="2100" placeholder="ex : 1820"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('annee_debut') border-red-500 @enderror">
|
||||
@error('annee_debut') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
<div>
|
||||
<label for="annee_fin" class="block text-sm font-medium text-gray-700">Année de fin</label>
|
||||
<input type="number" id="annee_fin" name="annee_fin"
|
||||
value="{{ old('annee_fin', $source?->annee_fin) }}"
|
||||
min="1000" max="2100" placeholder="ex : 1870"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 @error('annee_fin') border-red-500 @enderror">
|
||||
@error('annee_fin') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="cote" class="block text-sm font-medium text-gray-700">Cote</label>
|
||||
|
||||
@@ -9,16 +9,103 @@
|
||||
</div>
|
||||
</x-slot>
|
||||
|
||||
<div class="py-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
@if(session('success')) <div class="mb-4 p-4 bg-green-50 border border-green-200 text-green-800 rounded-md">{{ session('success') }}</div> @endif
|
||||
<div class="py-8 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6">
|
||||
@if(session('success'))
|
||||
<div class="p-4 bg-green-50 border border-green-200 text-green-800 rounded-md">{{ session('success') }}</div>
|
||||
@endif
|
||||
|
||||
{{-- Filtres --}}
|
||||
@php
|
||||
$hasFilters = request()->anyFilled(['status', 'source_type_id', 'lieu_id', 'annee_debut', 'annee_fin']);
|
||||
@endphp
|
||||
<div class="bg-white shadow rounded-lg p-5">
|
||||
<form method="GET" action="{{ route('sources.index') }}">
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
|
||||
{{-- Statut --}}
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">Statut</label>
|
||||
<select name="status"
|
||||
class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
|
||||
<option value="">— Tous —</option>
|
||||
@foreach(\App\Enums\SourceStatus::cases() as $s)
|
||||
<option value="{{ $s->value }}" {{ request('status') === $s->value ? 'selected' : '' }}>
|
||||
{{ $s->label() }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{-- Type de source --}}
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">Type de source</label>
|
||||
<select name="source_type_id"
|
||||
class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
|
||||
<option value="">— Tous —</option>
|
||||
@foreach($sourceTypes as $st)
|
||||
<option value="{{ $st->id }}" {{ request('source_type_id') == $st->id ? 'selected' : '' }}>
|
||||
{{ $st->nom }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{{-- Année de début --}}
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">Période — de</label>
|
||||
<input type="number" name="annee_debut" value="{{ request('annee_debut') }}"
|
||||
min="1000" max="2100" placeholder="ex : 1820"
|
||||
class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
|
||||
</div>
|
||||
|
||||
{{-- Année de fin --}}
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">Période — à</label>
|
||||
<input type="number" name="annee_fin" value="{{ request('annee_fin') }}"
|
||||
min="1000" max="2100" placeholder="ex : 1870"
|
||||
class="block w-full rounded-md border-gray-300 shadow-sm text-sm focus:border-indigo-500 focus:ring-indigo-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Lieu --}}
|
||||
<div class="mt-4 max-w-sm">
|
||||
<x-lieu-picker
|
||||
name="lieu_id"
|
||||
label="Lieu (et ses subdivisions)"
|
||||
:value="request('lieu_id')"
|
||||
:display-value="$lieuSelectionne?->nom_long ?? $lieuSelectionne?->nom ?? ''"
|
||||
placeholder="— Tous les lieux —"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center gap-3">
|
||||
<button type="submit"
|
||||
class="px-5 py-2 bg-indigo-600 text-white text-sm font-medium rounded-md hover:bg-indigo-700">
|
||||
Filtrer
|
||||
</button>
|
||||
@if($hasFilters)
|
||||
<a href="{{ route('sources.index') }}"
|
||||
class="px-4 py-2 border border-gray-300 text-gray-600 text-sm rounded-md hover:bg-gray-50">
|
||||
Effacer les filtres
|
||||
</a>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs bg-indigo-100 text-indigo-700">
|
||||
filtres actifs
|
||||
</span>
|
||||
@endif
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- Tableau --}}
|
||||
<div class="bg-white shadow rounded-lg overflow-hidden">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<table class="min-w-full divide-y divide-gray-200 text-sm">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nom</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Type</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Statut</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Lieu</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Période</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Relevés</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Dépôt</th>
|
||||
<th class="px-6 py-3"></th>
|
||||
@@ -34,20 +121,30 @@
|
||||
'termine' => 'bg-green-100 text-green-700',
|
||||
];
|
||||
$color = $statusColors[$source->status->value] ?? 'bg-gray-100 text-gray-600';
|
||||
$periode = match(true) {
|
||||
$source->annee_debut && $source->annee_fin => $source->annee_debut . ' – ' . $source->annee_fin,
|
||||
(bool)$source->annee_debut => 'depuis ' . $source->annee_debut,
|
||||
(bool)$source->annee_fin => 'jusqu\'en ' . $source->annee_fin,
|
||||
default => '—',
|
||||
};
|
||||
@endphp
|
||||
<tr class="hover:bg-gray-50">
|
||||
<td class="px-6 py-4 font-medium">
|
||||
<a href="{{ route('sources.show', $source) }}" class="text-indigo-600 hover:underline">{{ $source->nom }}</a>
|
||||
@if($source->cote) <span class="ml-2 text-xs text-gray-400">{{ $source->cote }}</span> @endif
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500">{{ $source->sourceType->nom }}</td>
|
||||
<td class="px-6 py-4 text-gray-500">{{ $source->sourceType->nom }}</td>
|
||||
<td class="px-6 py-4">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ $color }}">
|
||||
{{ $source->status->label() }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500">{{ $source->releves_count }}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500">{{ $source->depot?->nom ?? '—' }}</td>
|
||||
<td class="px-6 py-4 text-gray-500 max-w-[180px] truncate" title="{{ $source->lieu?->nom_long ?? $source->lieu?->nom }}">
|
||||
{{ $source->lieu?->nom ?? '—' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-gray-500 whitespace-nowrap">{{ $periode }}</td>
|
||||
<td class="px-6 py-4 text-gray-500">{{ $source->releves_count }}</td>
|
||||
<td class="px-6 py-4 text-gray-500">{{ $source->depot?->nom ?? '—' }}</td>
|
||||
<td class="px-6 py-4 text-right text-sm space-x-3">
|
||||
@can('update', $source)
|
||||
<a href="{{ route('sources.edit', $source) }}" class="text-gray-600 hover:text-indigo-600">Modifier</a>
|
||||
@@ -55,11 +152,18 @@
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="6" class="px-6 py-10 text-center text-gray-400">Aucune source disponible.</td></tr>
|
||||
<tr>
|
||||
<td colspan="8" class="px-6 py-10 text-center text-gray-400">
|
||||
@if($hasFilters) Aucune source ne correspond aux filtres.
|
||||
@else Aucune source disponible. @endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
@if($sources->hasPages()) <div class="px-6 py-4 border-t">{{ $sources->links() }}</div> @endif
|
||||
@if($sources->hasPages())
|
||||
<div class="px-6 py-4 border-t">{{ $sources->links() }}</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</x-app-layout>
|
||||
|
||||
@@ -135,12 +135,21 @@
|
||||
<div class="bg-white shadow rounded-lg overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
||||
<h3 class="font-medium text-gray-900">Relevés ({{ $source->releves->count() }})</h3>
|
||||
{{-- Lien activé à l'étape 6 --}}
|
||||
@can('create', [App\Models\Releve::class, $source])
|
||||
<a href="{{ route('sources.releves.create', $source) }}"
|
||||
class="px-3 py-1.5 bg-indigo-600 text-white text-xs rounded-md hover:bg-indigo-700">
|
||||
+ Nouveau relevé
|
||||
</a>
|
||||
@endcan
|
||||
</div>
|
||||
@if($source->releves->isEmpty())
|
||||
<p class="px-6 py-8 text-center text-gray-400 text-sm">Aucun relevé pour cette source.</p>
|
||||
@else
|
||||
<p class="px-6 py-4 text-sm text-gray-500">{{ $source->releves->count() }} relevé(s) enregistré(s).</p>
|
||||
<p class="px-6 py-4 text-sm text-gray-500">
|
||||
<a href="{{ route('sources.releves.index', $source) }}" class="text-indigo-600 hover:underline">
|
||||
Voir les {{ $source->releves->count() }} relevé(s) →
|
||||
</a>
|
||||
</p>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user