É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:
2026-06-04 17:17:53 +02:00
parent 7609d35287
commit d064f8d28e
54 changed files with 2861 additions and 116 deletions
@@ -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>
+8 -12
View File
@@ -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>
+96 -25
View File
@@ -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>
+31 -15
View File
@@ -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">
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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"
+66 -8
View File
@@ -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>
+4
View File
@@ -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>
+203
View File
@@ -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>
+100
View File
@@ -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>
+15
View File
@@ -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>
+28
View File
@@ -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>
+27
View File
@@ -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>
+120
View File
@@ -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>
+74
View File
@@ -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>
+42
View File
@@ -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>
+112 -8
View File
@@ -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>
+11 -2
View File
@@ -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>