É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,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Admin\StoreLieuTypeRequest;
|
||||||
|
use App\Models\LieuType;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class LieuTypeController extends Controller
|
||||||
|
{
|
||||||
|
public function index(): View
|
||||||
|
{
|
||||||
|
$lieuTypes = LieuType::withCount('lieux')->orderBy('ordre')->get();
|
||||||
|
return view('admin.lieu-types.index', compact('lieuTypes'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(): View
|
||||||
|
{
|
||||||
|
return view('admin.lieu-types.create');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(StoreLieuTypeRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
LieuType::create($request->validated());
|
||||||
|
return redirect()->route('admin.lieu-types.index')->with('success', 'Type de lieu créé.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit(LieuType $lieuType): View
|
||||||
|
{
|
||||||
|
return view('admin.lieu-types.edit', compact('lieuType'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(StoreLieuTypeRequest $request, LieuType $lieuType): RedirectResponse
|
||||||
|
{
|
||||||
|
$lieuType->update($request->validated());
|
||||||
|
return redirect()->route('admin.lieu-types.index')->with('success', 'Type de lieu mis à jour.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(LieuType $lieuType): RedirectResponse
|
||||||
|
{
|
||||||
|
if ($lieuType->lieux()->exists()) {
|
||||||
|
return back()->with('error', 'Impossible de supprimer un type utilisé par des lieux.');
|
||||||
|
}
|
||||||
|
$lieuType->delete();
|
||||||
|
return redirect()->route('admin.lieu-types.index')->with('success', 'Type de lieu supprimé.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ namespace App\Http\Controllers\Admin;
|
|||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\Admin\StoreSectionRequest;
|
use App\Http\Requests\Admin\StoreSectionRequest;
|
||||||
use App\Http\Requests\Admin\UpdateSectionRequest;
|
use App\Http\Requests\Admin\UpdateSectionRequest;
|
||||||
use App\Models\Lieu;
|
|
||||||
use App\Models\Section;
|
use App\Models\Section;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
@@ -22,8 +21,7 @@ class SectionController extends Controller
|
|||||||
|
|
||||||
public function create(): View
|
public function create(): View
|
||||||
{
|
{
|
||||||
$lieux = Lieu::orderBy('nom_long')->get(['id', 'nom_long', 'nom']);
|
return view('admin.sections.create');
|
||||||
return view('admin.sections.create', compact('lieux'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store(StoreSectionRequest $request): RedirectResponse
|
public function store(StoreSectionRequest $request): RedirectResponse
|
||||||
@@ -42,8 +40,8 @@ class SectionController extends Controller
|
|||||||
|
|
||||||
public function edit(Section $section): View
|
public function edit(Section $section): View
|
||||||
{
|
{
|
||||||
$lieux = Lieu::orderBy('nom_long')->get(['id', 'nom_long', 'nom']);
|
$section->load('lieu');
|
||||||
return view('admin.sections.edit', compact('section', 'lieux'));
|
return view('admin.sections.edit', compact('section'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(UpdateSectionRequest $request, Section $section): RedirectResponse
|
public function update(UpdateSectionRequest $request, Section $section): RedirectResponse
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ namespace App\Http\Controllers;
|
|||||||
|
|
||||||
abstract class Controller
|
abstract class Controller
|
||||||
{
|
{
|
||||||
//
|
use \Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Enums\SourceStatus;
|
||||||
|
use App\Models\Releve;
|
||||||
|
use App\Models\Source;
|
||||||
|
use App\Services\GedcomExportService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class ExportController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly GedcomExportService $gedcom,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/** Export de tous les relevés d'une source */
|
||||||
|
public function source(Source $source): Response
|
||||||
|
{
|
||||||
|
$this->authorize('view', $source);
|
||||||
|
|
||||||
|
$gedcomContent = $this->gedcom->exportSource($source);
|
||||||
|
$filename = $this->sanitizeFilename($source->nom) . '.ged';
|
||||||
|
|
||||||
|
return response($gedcomContent, 200, [
|
||||||
|
'Content-Type' => 'text/plain; charset=UTF-8',
|
||||||
|
'Content-Disposition' => "attachment; filename=\"{$filename}\"",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Export depuis les résultats de recherche (avec les mêmes filtres) */
|
||||||
|
public function recherche(Request $request): Response
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
$query = Releve::with(['source.sourceType', 'createur'])
|
||||||
|
->whereHas('source', function ($q) use ($user, $request) {
|
||||||
|
if (! $user->isSectionManager()) {
|
||||||
|
$assignedIds = $user->sourcesAssignees()->pluck('sources.id');
|
||||||
|
$q->where(function ($sq) use ($assignedIds) {
|
||||||
|
$sq->where('status', SourceStatus::Termine)
|
||||||
|
->orWhereIn('id', $assignedIds);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if ($request->filled('source_type_id')) {
|
||||||
|
$q->where('source_type_id', $request->integer('source_type_id'));
|
||||||
|
}
|
||||||
|
}); // $request déjà dans le use()
|
||||||
|
|
||||||
|
if ($request->filled('q')) {
|
||||||
|
$q = trim($request->get('q'));
|
||||||
|
$query->where(function ($wq) use ($q) {
|
||||||
|
$wq->where('nom', 'ilike', "%{$q}%")
|
||||||
|
->orWhere('prenom','ilike', "%{$q}%")
|
||||||
|
->orWhere('date_evenement', 'ilike', "%{$q}%")
|
||||||
|
->orWhereRaw(
|
||||||
|
"to_tsvector('french', data::text) @@ plainto_tsquery('french', ?)",
|
||||||
|
[$q]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('lieu_id')) {
|
||||||
|
$rows = DB::select("
|
||||||
|
WITH RECURSIVE descendants AS (
|
||||||
|
SELECT id, nom FROM lieux WHERE id = ?
|
||||||
|
UNION ALL
|
||||||
|
SELECT l.id, l.nom FROM lieux l
|
||||||
|
INNER JOIN descendants d ON l.lieu_parent_id = d.id
|
||||||
|
)
|
||||||
|
SELECT DISTINCT nom FROM descendants WHERE nom IS NOT NULL
|
||||||
|
", [$request->integer('lieu_id')]);
|
||||||
|
|
||||||
|
$noms = collect($rows)->pluck('nom')->filter();
|
||||||
|
if ($noms->isNotEmpty()) {
|
||||||
|
$pattern = $noms->map(fn ($n) => preg_quote($n, '/'))->join('|');
|
||||||
|
$query->whereRaw("data::text ~* ?", [$pattern]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('annee_debut')) {
|
||||||
|
$query->whereRaw('date_evenement >= ?', [$request->integer('annee_debut') . '-01-01']);
|
||||||
|
}
|
||||||
|
if ($request->filled('annee_fin')) {
|
||||||
|
$query->whereRaw('date_evenement <= ?', [$request->integer('annee_fin') . '-12-31']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$releves = $query->orderByRaw('nom ASC NULLS LAST')->get();
|
||||||
|
|
||||||
|
if ($releves->isEmpty()) {
|
||||||
|
return back()->with('error', 'Aucun relevé à exporter.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$titre = $request->filled('q') ? 'Recherche_' . $request->get('q') : 'Export';
|
||||||
|
$gedcomContent = $this->gedcom->exportReleves($releves, $titre);
|
||||||
|
$filename = $this->sanitizeFilename($titre) . '.ged';
|
||||||
|
|
||||||
|
return response($gedcomContent, 200, [
|
||||||
|
'Content-Type' => 'text/plain; charset=UTF-8',
|
||||||
|
'Content-Disposition' => "attachment; filename=\"{$filename}\"",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function sanitizeFilename(string $name): string
|
||||||
|
{
|
||||||
|
$name = iconv('UTF-8', 'ASCII//TRANSLIT', $name) ?: $name;
|
||||||
|
return preg_replace('/[^a-zA-Z0-9_-]/', '_', $name);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,30 +5,77 @@ namespace App\Http\Controllers;
|
|||||||
use App\Http\Requests\StoreLieuRequest;
|
use App\Http\Requests\StoreLieuRequest;
|
||||||
use App\Http\Requests\UpdateLieuRequest;
|
use App\Http\Requests\UpdateLieuRequest;
|
||||||
use App\Models\Lieu;
|
use App\Models\Lieu;
|
||||||
|
use App\Models\LieuType;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class LieuController extends Controller
|
class LieuController extends Controller
|
||||||
{
|
{
|
||||||
public function index(): View
|
public function search(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$q = trim($request->get('q', ''));
|
||||||
|
|
||||||
|
$lieux = Lieu::with('lieuType')
|
||||||
|
->where(function ($query) use ($q) {
|
||||||
|
$query->where('nom_long', 'ilike', "%{$q}%")
|
||||||
|
->orWhere('nom', 'ilike', "%{$q}%")
|
||||||
|
->orWhere('code', 'ilike', "%{$q}%");
|
||||||
|
})
|
||||||
|
->orderBy('nom_long')
|
||||||
|
->limit(25)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return response()->json($lieux->map(fn ($l) => [
|
||||||
|
'id' => $l->id,
|
||||||
|
'nom_long' => $l->nom_long ?? $l->nom,
|
||||||
|
'code' => $l->code,
|
||||||
|
'type' => $l->lieuType?->nom,
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Request $request): View
|
||||||
{
|
{
|
||||||
$this->authorize('viewAny', Lieu::class);
|
$this->authorize('viewAny', Lieu::class);
|
||||||
|
|
||||||
// Arbre complet trié par nom_long pour l'affichage
|
$query = Lieu::with(['parent', 'lieuType'])->orderBy('nom_long');
|
||||||
$lieux = Lieu::with('parent')
|
|
||||||
->orderBy('nom_long')
|
|
||||||
->paginate(50);
|
|
||||||
|
|
||||||
return view('lieux.index', compact('lieux'));
|
if ($request->filled('lieu_type_id')) {
|
||||||
|
$query->where('lieu_type_id', $request->integer('lieu_type_id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('q')) {
|
||||||
|
$q = trim($request->get('q'));
|
||||||
|
$query->where(function ($wq) use ($q) {
|
||||||
|
$wq->where('nom_long', 'ilike', "%{$q}%")
|
||||||
|
->orWhere('nom', 'ilike', "%{$q}%")
|
||||||
|
->orWhere('code', 'ilike', "%{$q}%");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('lieu_id')) {
|
||||||
|
$ids = $this->getDescendantAndSelfIds($request->integer('lieu_id'));
|
||||||
|
$query->whereIn('id', $ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
$lieuTypes = LieuType::orderBy('ordre')->get(['id', 'nom']);
|
||||||
|
$lieuSelectionne = $request->filled('lieu_id')
|
||||||
|
? Lieu::find($request->integer('lieu_id'), ['id', 'nom', 'nom_long'])
|
||||||
|
: null;
|
||||||
|
$lieux = $query->paginate(50)->withQueryString();
|
||||||
|
|
||||||
|
return view('lieux.index', compact('lieux', 'lieuTypes', 'lieuSelectionne'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create(): View
|
public function create(): View
|
||||||
{
|
{
|
||||||
$this->authorize('create', Lieu::class);
|
$this->authorize('create', Lieu::class);
|
||||||
|
|
||||||
$parents = Lieu::orderBy('nom_long')->get(['id', 'nom_long']);
|
$lieuTypes = LieuType::orderBy('ordre')->get(['id', 'nom']);
|
||||||
|
|
||||||
return view('lieux.create', compact('parents'));
|
return view('lieux.create', compact('lieuTypes'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store(StoreLieuRequest $request): RedirectResponse
|
public function store(StoreLieuRequest $request): RedirectResponse
|
||||||
@@ -43,7 +90,7 @@ class LieuController extends Controller
|
|||||||
{
|
{
|
||||||
$this->authorize('view', $lieu);
|
$this->authorize('view', $lieu);
|
||||||
|
|
||||||
$lieu->load('parent', 'enfants');
|
$lieu->load('parent', 'enfants', 'lieuType');
|
||||||
|
|
||||||
return view('lieux.show', compact('lieu'));
|
return view('lieux.show', compact('lieu'));
|
||||||
}
|
}
|
||||||
@@ -52,20 +99,15 @@ class LieuController extends Controller
|
|||||||
{
|
{
|
||||||
$this->authorize('update', $lieu);
|
$this->authorize('update', $lieu);
|
||||||
|
|
||||||
// Exclure le lieu lui-même et ses descendants pour éviter les cycles
|
$lieu->load('parent', 'lieuType');
|
||||||
$descendants = $this->getDescendantIds($lieu);
|
$lieuTypes = LieuType::orderBy('ordre')->get(['id', 'nom']);
|
||||||
$parents = Lieu::whereNotIn('id', [...$descendants, $lieu->id])
|
|
||||||
->orderBy('nom_long')
|
|
||||||
->get(['id', 'nom_long']);
|
|
||||||
|
|
||||||
return view('lieux.edit', compact('lieu', 'parents'));
|
return view('lieux.edit', compact('lieu', 'lieuTypes'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function update(UpdateLieuRequest $request, Lieu $lieu): RedirectResponse
|
public function update(UpdateLieuRequest $request, Lieu $lieu): RedirectResponse
|
||||||
{
|
{
|
||||||
$lieu->update($request->validated());
|
$lieu->update($request->validated());
|
||||||
|
|
||||||
// Recalculer nom_long des enfants en cascade
|
|
||||||
$this->recalculerEnfants($lieu);
|
$this->recalculerEnfants($lieu);
|
||||||
|
|
||||||
return redirect()->route('lieux.show', $lieu)
|
return redirect()->route('lieux.show', $lieu)
|
||||||
@@ -86,12 +128,27 @@ class LieuController extends Controller
|
|||||||
->with('success', 'Lieu supprimé.');
|
->with('success', 'Lieu supprimé.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function getDescendantAndSelfIds(int $lieuId): array
|
||||||
|
{
|
||||||
|
$rows = DB::select("
|
||||||
|
WITH RECURSIVE descendants AS (
|
||||||
|
SELECT id FROM lieux WHERE id = ?
|
||||||
|
UNION ALL
|
||||||
|
SELECT l.id FROM lieux l
|
||||||
|
INNER JOIN descendants d ON l.lieu_parent_id = d.id
|
||||||
|
)
|
||||||
|
SELECT id FROM descendants
|
||||||
|
", [$lieuId]);
|
||||||
|
|
||||||
|
return collect($rows)->pluck('id')->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
private function getDescendantIds(Lieu $lieu): array
|
private function getDescendantIds(Lieu $lieu): array
|
||||||
{
|
{
|
||||||
$ids = [];
|
$ids = [];
|
||||||
foreach ($lieu->enfants as $enfant) {
|
foreach ($lieu->enfants as $enfant) {
|
||||||
$ids[] = $enfant->id;
|
$ids[] = $enfant->id;
|
||||||
$ids = array_merge($ids, $this->getDescendantIds($enfant));
|
$ids = array_merge($ids, $this->getDescendantIds($enfant));
|
||||||
}
|
}
|
||||||
return $ids;
|
return $ids;
|
||||||
}
|
}
|
||||||
@@ -100,7 +157,7 @@ class LieuController extends Controller
|
|||||||
{
|
{
|
||||||
$lieu->load('enfants');
|
$lieu->load('enfants');
|
||||||
foreach ($lieu->enfants as $enfant) {
|
foreach ($lieu->enfants as $enfant) {
|
||||||
$enfant->update([]); // déclenche le booted() hook qui recalcule nom_long
|
$enfant->update([]);
|
||||||
$this->recalculerEnfants($enfant);
|
$this->recalculerEnfants($enfant);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class NotificationController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request): View
|
||||||
|
{
|
||||||
|
$notifications = $request->user()
|
||||||
|
->notifications()
|
||||||
|
->paginate(25);
|
||||||
|
|
||||||
|
return view('notifications.index', compact('notifications'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function markAsRead(Request $request, string $id): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->user()
|
||||||
|
->notifications()
|
||||||
|
->where('id', $id)
|
||||||
|
->first()
|
||||||
|
?->markAsRead();
|
||||||
|
|
||||||
|
return back();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function markAllAsRead(Request $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$request->user()->unreadNotifications->markAsRead();
|
||||||
|
|
||||||
|
return back()->with('success', 'Toutes les notifications ont été marquées comme lues.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Enums\SourceStatus;
|
||||||
|
use App\Models\Lieu;
|
||||||
|
use App\Models\Releve;
|
||||||
|
use App\Models\SourceType;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class RechercheController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request): View
|
||||||
|
{
|
||||||
|
$sourceTypes = SourceType::orderBy('nom')->get(['id', 'nom']);
|
||||||
|
$resultats = null;
|
||||||
|
$total = null;
|
||||||
|
|
||||||
|
// Charger le lieu sélectionné pour pré-remplir le picker
|
||||||
|
$lieuSelectionne = $request->filled('lieu_id')
|
||||||
|
? Lieu::find($request->integer('lieu_id'), ['id', 'nom', 'nom_long'])
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if ($request->anyFilled(['q', 'source_type_id', 'lieu_id', 'annee_debut', 'annee_fin'])) {
|
||||||
|
[$resultats, $total] = $this->search($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('recherche.index', compact('sourceTypes', 'resultats', 'total', 'lieuSelectionne'));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function search(Request $request): array
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
$query = Releve::with(['source.sourceType', 'source.depot', 'createur'])
|
||||||
|
->whereHas('source', function ($q) use ($user, $request) {
|
||||||
|
if (! $user->isSectionManager()) {
|
||||||
|
$assignedIds = $user->sourcesAssignees()->pluck('sources.id');
|
||||||
|
$q->where(function ($sq) use ($assignedIds) {
|
||||||
|
$sq->where('status', SourceStatus::Termine)
|
||||||
|
->orWhereIn('id', $assignedIds);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if ($request->filled('source_type_id')) {
|
||||||
|
$q->where('source_type_id', $request->integer('source_type_id'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Recherche textuelle ──────────────────────────────────────────────
|
||||||
|
if ($request->filled('q')) {
|
||||||
|
$q = trim($request->get('q'));
|
||||||
|
$query->where(function ($wq) use ($q) {
|
||||||
|
$wq->where('nom', 'ilike', "%{$q}%")
|
||||||
|
->orWhere('prenom','ilike', "%{$q}%")
|
||||||
|
->orWhere('date_evenement', 'ilike', "%{$q}%")
|
||||||
|
->orWhereRaw(
|
||||||
|
"to_tsvector('french', data::text) @@ plainto_tsquery('french', ?)",
|
||||||
|
[$q]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Filtre par lieu (+ descendants via CTE récursive) ────────────────
|
||||||
|
if ($request->filled('lieu_id')) {
|
||||||
|
$lieuNoms = $this->getLieuNoms($request->integer('lieu_id'));
|
||||||
|
if ($lieuNoms->isNotEmpty()) {
|
||||||
|
// Recherche regex case-insensitive dans le JSONB text
|
||||||
|
$pattern = $lieuNoms
|
||||||
|
->map(fn ($n) => preg_quote($n, '/'))
|
||||||
|
->join('|');
|
||||||
|
$query->whereRaw("data::text ~* ?", [$pattern]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Filtre par plage d'années ────────────────────────────────────────
|
||||||
|
if ($request->filled('annee_debut')) {
|
||||||
|
$query->whereRaw("date_evenement >= ?", [$request->integer('annee_debut') . '-01-01']);
|
||||||
|
}
|
||||||
|
if ($request->filled('annee_fin')) {
|
||||||
|
$query->whereRaw("date_evenement <= ?", [$request->integer('annee_fin') . '-12-31']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Tri + pagination ────────────────────────────────────────────────
|
||||||
|
$total = $query->count();
|
||||||
|
$resultats = $query
|
||||||
|
->orderByRaw('nom ASC NULLS LAST')
|
||||||
|
->orderByRaw('date_evenement ASC NULLS LAST')
|
||||||
|
->paginate(25)
|
||||||
|
->withQueryString();
|
||||||
|
|
||||||
|
return [$resultats, $total];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retourne les noms du lieu et de tous ses descendants via CTE récursive PostgreSQL.
|
||||||
|
*/
|
||||||
|
private function getLieuNoms(int $lieuId): \Illuminate\Support\Collection
|
||||||
|
{
|
||||||
|
$rows = DB::select("
|
||||||
|
WITH RECURSIVE descendants AS (
|
||||||
|
SELECT id, nom
|
||||||
|
FROM lieux
|
||||||
|
WHERE id = ?
|
||||||
|
UNION ALL
|
||||||
|
SELECT l.id, l.nom
|
||||||
|
FROM lieux l
|
||||||
|
INNER JOIN descendants d ON l.lieu_parent_id = d.id
|
||||||
|
)
|
||||||
|
SELECT DISTINCT nom FROM descendants WHERE nom IS NOT NULL
|
||||||
|
", [$lieuId]);
|
||||||
|
|
||||||
|
return collect($rows)->pluck('nom')->filter();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Enums\FieldType;
|
||||||
|
use App\Http\Requests\StoreReleveRequest;
|
||||||
|
use App\Http\Requests\UpdateReleveRequest;
|
||||||
|
use App\Models\Releve;
|
||||||
|
use App\Models\Source;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class ReleveController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Source $source): View
|
||||||
|
{
|
||||||
|
$this->authorize('viewAny', [Releve::class, $source]);
|
||||||
|
|
||||||
|
$source->load('sourceType.fields');
|
||||||
|
|
||||||
|
// Keyset pagination sur id (évite le COUNT sur des millions de lignes)
|
||||||
|
$releves = $source->releves()
|
||||||
|
->orderBy('id')
|
||||||
|
->cursorPaginate(25);
|
||||||
|
|
||||||
|
return view('releves.index', compact('source', 'releves'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(Source $source): View
|
||||||
|
{
|
||||||
|
$this->authorize('create', [Releve::class, $source]);
|
||||||
|
|
||||||
|
$source->load('sourceType.fields');
|
||||||
|
|
||||||
|
return view('releves.create', compact('source'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(StoreReleveRequest $request, Source $source): RedirectResponse
|
||||||
|
{
|
||||||
|
$data = $this->buildData($request->validated()['data'] ?? [], $source);
|
||||||
|
|
||||||
|
$source->releves()->create([
|
||||||
|
'data' => $data,
|
||||||
|
'created_by' => $request->user()->id,
|
||||||
|
'updated_by' => $request->user()->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('sources.releves.index', $source)
|
||||||
|
->with('success', 'Relevé ajouté.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(Source $source, Releve $releve): View
|
||||||
|
{
|
||||||
|
$this->authorize('view', $releve);
|
||||||
|
|
||||||
|
$source->load('sourceType.fields');
|
||||||
|
$releve->load('createur', 'modificateur');
|
||||||
|
|
||||||
|
return view('releves.show', compact('source', 'releve'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit(Source $source, Releve $releve): View
|
||||||
|
{
|
||||||
|
$this->authorize('update', $releve);
|
||||||
|
|
||||||
|
$source->load('sourceType.fields');
|
||||||
|
|
||||||
|
return view('releves.edit', compact('source', 'releve'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(UpdateReleveRequest $request, Source $source, Releve $releve): RedirectResponse
|
||||||
|
{
|
||||||
|
$data = $this->buildData($request->validated()['data'] ?? [], $source);
|
||||||
|
|
||||||
|
$releve->update([
|
||||||
|
'data' => $data,
|
||||||
|
'updated_by' => $request->user()->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return redirect()->route('sources.releves.show', [$source, $releve])
|
||||||
|
->with('success', 'Relevé mis à jour.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Source $source, Releve $releve): RedirectResponse
|
||||||
|
{
|
||||||
|
$this->authorize('delete', $releve);
|
||||||
|
|
||||||
|
$releve->delete();
|
||||||
|
|
||||||
|
return redirect()->route('sources.releves.index', $source)
|
||||||
|
->with('success', 'Relevé supprimé.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalise les données POST en structure JSONB propre
|
||||||
|
private function buildData(array $raw, Source $source): array
|
||||||
|
{
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
foreach ($source->sourceType->fields as $field) {
|
||||||
|
$value = $raw[$field->name] ?? null;
|
||||||
|
|
||||||
|
$data[$field->name] = match ($field->type) {
|
||||||
|
FieldType::Boolean => (bool) ($value ?? false),
|
||||||
|
FieldType::Number => $value !== null && $value !== '' ? (float) $value : null,
|
||||||
|
FieldType::Date => [
|
||||||
|
'valeur' => $value['valeur'] ?? null,
|
||||||
|
'calendrier' => $value['calendrier'] ?? 'gregorien',
|
||||||
|
],
|
||||||
|
default => $value === '' ? null : $value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,26 +6,29 @@ use App\Enums\SourceStatus;
|
|||||||
use App\Http\Requests\StoreSourceRequest;
|
use App\Http\Requests\StoreSourceRequest;
|
||||||
use App\Http\Requests\UpdateSourceRequest;
|
use App\Http\Requests\UpdateSourceRequest;
|
||||||
use App\Models\Depot;
|
use App\Models\Depot;
|
||||||
|
use App\Models\Lieu;
|
||||||
use App\Models\Source;
|
use App\Models\Source;
|
||||||
use App\Models\SourceType;
|
use App\Models\SourceType;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Notifications\SourceAValiderNotification;
|
||||||
|
use App\Notifications\SourceRejeteeNotification;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class SourceController extends Controller
|
class SourceController extends Controller
|
||||||
{
|
{
|
||||||
public function index(): View
|
public function index(Request $request): View
|
||||||
{
|
{
|
||||||
$this->authorize('viewAny', Source::class);
|
$this->authorize('viewAny', Source::class);
|
||||||
|
|
||||||
$user = auth()->user();
|
$user = auth()->user();
|
||||||
|
|
||||||
$query = Source::with(['sourceType', 'depot'])
|
$query = Source::with(['sourceType', 'depot', 'lieu'])
|
||||||
->withCount('releves');
|
->withCount('releves');
|
||||||
|
|
||||||
if (! $user->isSectionManager()) {
|
if (! $user->isSectionManager()) {
|
||||||
// Membre : sources terminées + sources assignées
|
|
||||||
$assignedIds = $user->sourcesAssignees()->pluck('sources.id');
|
$assignedIds = $user->sourcesAssignees()->pluck('sources.id');
|
||||||
$query->where(function ($q) use ($assignedIds) {
|
$query->where(function ($q) use ($assignedIds) {
|
||||||
$q->where('status', SourceStatus::Termine)
|
$q->where('status', SourceStatus::Termine)
|
||||||
@@ -33,9 +36,56 @@ class SourceController extends Controller
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$sources = $query->orderBy('nom')->paginate(25);
|
if ($request->filled('status')) {
|
||||||
|
$query->where('status', $request->input('status'));
|
||||||
|
}
|
||||||
|
|
||||||
return view('sources.index', compact('sources'));
|
if ($request->filled('source_type_id')) {
|
||||||
|
$query->where('source_type_id', $request->integer('source_type_id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('lieu_id')) {
|
||||||
|
$lieuIds = $this->getLieuDescendantIds($request->integer('lieu_id'));
|
||||||
|
$query->whereIn('lieu_id', $lieuIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('annee_debut')) {
|
||||||
|
$annee = $request->integer('annee_debut');
|
||||||
|
$query->where(function ($q) use ($annee) {
|
||||||
|
$q->whereNull('annee_fin')->orWhere('annee_fin', '>=', $annee);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('annee_fin')) {
|
||||||
|
$annee = $request->integer('annee_fin');
|
||||||
|
$query->where(function ($q) use ($annee) {
|
||||||
|
$q->whereNull('annee_debut')->orWhere('annee_debut', '<=', $annee);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$sourceTypes = SourceType::orderBy('nom')->get(['id', 'nom']);
|
||||||
|
$lieuSelectionne = $request->filled('lieu_id')
|
||||||
|
? Lieu::find($request->integer('lieu_id'), ['id', 'nom', 'nom_long'])
|
||||||
|
: null;
|
||||||
|
|
||||||
|
$sources = $query->orderBy('nom')->paginate(25)->withQueryString();
|
||||||
|
|
||||||
|
return view('sources.index', compact('sources', 'sourceTypes', 'lieuSelectionne'));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getLieuDescendantIds(int $lieuId): array
|
||||||
|
{
|
||||||
|
$rows = DB::select("
|
||||||
|
WITH RECURSIVE descendants AS (
|
||||||
|
SELECT id FROM lieux WHERE id = ?
|
||||||
|
UNION ALL
|
||||||
|
SELECT l.id FROM lieux l
|
||||||
|
INNER JOIN descendants d ON l.lieu_parent_id = d.id
|
||||||
|
)
|
||||||
|
SELECT id FROM descendants
|
||||||
|
", [$lieuId]);
|
||||||
|
|
||||||
|
return collect($rows)->pluck('id')->toArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function create(): View
|
public function create(): View
|
||||||
@@ -71,6 +121,7 @@ class SourceController extends Controller
|
|||||||
{
|
{
|
||||||
$this->authorize('update', $source);
|
$this->authorize('update', $source);
|
||||||
|
|
||||||
|
$source->loadMissing('lieu');
|
||||||
$sourceTypes = SourceType::orderBy('nom')->get(['id', 'nom']);
|
$sourceTypes = SourceType::orderBy('nom')->get(['id', 'nom']);
|
||||||
$depots = Depot::orderBy('nom')->get(['id', 'nom']);
|
$depots = Depot::orderBy('nom')->get(['id', 'nom']);
|
||||||
|
|
||||||
@@ -136,8 +187,28 @@ class SourceController extends Controller
|
|||||||
return back()->with('error', 'Transition non autorisée.');
|
return back()->with('error', 'Transition non autorisée.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$previousStatus = $source->status;
|
||||||
$source->update(['status' => $newStatus]);
|
$source->update(['status' => $newStatus]);
|
||||||
|
|
||||||
|
$user = auth()->user();
|
||||||
|
|
||||||
|
if ($newStatus === SourceStatus::AValider) {
|
||||||
|
// Notifier admins + responsables de section
|
||||||
|
$source->load('sourceType', 'depot');
|
||||||
|
User::whereIn('role', ['admin', 'section_manager'])
|
||||||
|
->where('id', '!=', $user->id)
|
||||||
|
->get()
|
||||||
|
->each(fn ($u) => $u->notify(new SourceAValiderNotification($source, $user)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($newStatus === SourceStatus::EnCours && $previousStatus === SourceStatus::AValider) {
|
||||||
|
// Rejet : notifier les membres assignés
|
||||||
|
$source->membres()
|
||||||
|
->where('users.id', '!=', $user->id)
|
||||||
|
->get()
|
||||||
|
->each(fn ($m) => $m->notify(new SourceRejeteeNotification($source, $user)));
|
||||||
|
}
|
||||||
|
|
||||||
return back()->with('success', 'Statut mis à jour : ' . $newStatus->label());
|
return back()->with('success', 'Statut mis à jour : ' . $newStatus->label());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Admin;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class StoreLieuTypeRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool { return $this->user()->isAdmin(); }
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
$ignore = $this->route('lieuType')?->id;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'nom' => ['required', 'string', 'max:100', Rule::unique('lieu_types', 'nom')->ignore($ignore)],
|
||||||
|
'ordre' => ['required', 'integer', 'min:0', 'max:999'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ class StoreLieuRequest extends FormRequest
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'nom' => ['required', 'string', 'max:255'],
|
'nom' => ['required', 'string', 'max:255'],
|
||||||
|
'lieu_type_id' => ['required', 'integer', 'exists:lieu_types,id'],
|
||||||
'code' => ['nullable', 'string', 'max:20'],
|
'code' => ['nullable', 'string', 'max:20'],
|
||||||
'lieu_parent_id'=> ['nullable', 'integer', 'exists:lieux,id'],
|
'lieu_parent_id'=> ['nullable', 'integer', 'exists:lieux,id'],
|
||||||
'latitude' => ['nullable', 'numeric', 'between:-90,90'],
|
'latitude' => ['nullable', 'numeric', 'between:-90,90'],
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use App\Enums\CalendarType;
|
||||||
|
use App\Enums\FieldType;
|
||||||
|
use App\Models\Source;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rules\Enum;
|
||||||
|
|
||||||
|
class StoreReleveRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
$source = $this->route('source');
|
||||||
|
return $this->user()->can('create', [app(\App\Models\Releve::class), $source]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
/** @var Source $source */
|
||||||
|
$source = $this->route('source');
|
||||||
|
$source->loadMissing('sourceType.fields');
|
||||||
|
|
||||||
|
$rules = [];
|
||||||
|
|
||||||
|
foreach ($source->sourceType->fields as $field) {
|
||||||
|
$base = "data.{$field->name}";
|
||||||
|
|
||||||
|
switch ($field->type) {
|
||||||
|
case FieldType::Date:
|
||||||
|
$rules["{$base}.valeur"] = [$field->required ? 'required' : 'nullable', 'string', 'max:50'];
|
||||||
|
$rules["{$base}.calendrier"] = ['required', new Enum(CalendarType::class)];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FieldType::Boolean:
|
||||||
|
$rules[$base] = ['nullable', 'boolean'];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FieldType::Number:
|
||||||
|
$rules[$base] = [$field->required ? 'required' : 'nullable', 'numeric'];
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FieldType::Select:
|
||||||
|
$options = $field->options ?? [];
|
||||||
|
$rules[$base] = [$field->required ? 'required' : 'nullable', 'string', 'in:' . implode(',', $options)];
|
||||||
|
break;
|
||||||
|
|
||||||
|
default: // text, textarea
|
||||||
|
$rules[$base] = [$field->required ? 'required' : 'nullable', 'string', 'max:2000'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rules;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,9 @@ class StoreSourceRequest extends FormRequest
|
|||||||
'description' => ['nullable', 'string'],
|
'description' => ['nullable', 'string'],
|
||||||
'source_type_id' => ['required', 'integer', 'exists:source_types,id'],
|
'source_type_id' => ['required', 'integer', 'exists:source_types,id'],
|
||||||
'depot_id' => ['nullable', 'integer', 'exists:depots,id'],
|
'depot_id' => ['nullable', 'integer', 'exists:depots,id'],
|
||||||
|
'lieu_id' => ['nullable', 'integer', 'exists:lieux,id'],
|
||||||
|
'annee_debut' => ['nullable', 'integer', 'min:1000', 'max:2100'],
|
||||||
|
'annee_fin' => ['nullable', 'integer', 'min:1000', 'max:2100', 'gte:annee_debut'],
|
||||||
'cote' => ['nullable', 'string', 'max:255'],
|
'cote' => ['nullable', 'string', 'max:255'],
|
||||||
'auteur' => ['nullable', 'string', 'max:255'],
|
'auteur' => ['nullable', 'string', 'max:255'],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class UpdateLieuRequest extends FormRequest
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
'nom' => ['required', 'string', 'max:255'],
|
'nom' => ['required', 'string', 'max:255'],
|
||||||
|
'lieu_type_id' => ['required', 'integer', 'exists:lieu_types,id'],
|
||||||
'code' => ['nullable', 'string', 'max:20'],
|
'code' => ['nullable', 'string', 'max:20'],
|
||||||
// Interdit de se choisir soi-même ou un descendant comme parent
|
// Interdit de se choisir soi-même ou un descendant comme parent
|
||||||
'lieu_parent_id'=> ['nullable', 'integer', 'exists:lieux,id', "not_in:{$lieuId}"],
|
'lieu_parent_id'=> ['nullable', 'integer', 'exists:lieux,id', "not_in:{$lieuId}"],
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use App\Enums\CalendarType;
|
||||||
|
use App\Enums\FieldType;
|
||||||
|
use App\Models\Source;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rules\Enum;
|
||||||
|
|
||||||
|
class UpdateReleveRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return $this->user()->can('update', $this->route('releve'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
/** @var \App\Models\Releve $releve */
|
||||||
|
$releve = $this->route('releve');
|
||||||
|
$releve->source->loadMissing('sourceType.fields');
|
||||||
|
|
||||||
|
$rules = [];
|
||||||
|
|
||||||
|
foreach ($releve->source->sourceType->fields as $field) {
|
||||||
|
$base = "data.{$field->name}";
|
||||||
|
|
||||||
|
switch ($field->type) {
|
||||||
|
case FieldType::Date:
|
||||||
|
$rules["{$base}.valeur"] = [$field->required ? 'required' : 'nullable', 'string', 'max:50'];
|
||||||
|
$rules["{$base}.calendrier"] = ['required', new Enum(CalendarType::class)];
|
||||||
|
break;
|
||||||
|
case FieldType::Boolean:
|
||||||
|
$rules[$base] = ['nullable', 'boolean'];
|
||||||
|
break;
|
||||||
|
case FieldType::Number:
|
||||||
|
$rules[$base] = [$field->required ? 'required' : 'nullable', 'numeric'];
|
||||||
|
break;
|
||||||
|
case FieldType::Select:
|
||||||
|
$options = $field->options ?? [];
|
||||||
|
$rules[$base] = [$field->required ? 'required' : 'nullable', 'string', 'in:' . implode(',', $options)];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$rules[$base] = [$field->required ? 'required' : 'nullable', 'string', 'max:2000'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $rules;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,9 @@ class UpdateSourceRequest extends FormRequest
|
|||||||
'description' => ['nullable', 'string'],
|
'description' => ['nullable', 'string'],
|
||||||
'source_type_id' => ['required', 'integer', 'exists:source_types,id'],
|
'source_type_id' => ['required', 'integer', 'exists:source_types,id'],
|
||||||
'depot_id' => ['nullable', 'integer', 'exists:depots,id'],
|
'depot_id' => ['nullable', 'integer', 'exists:depots,id'],
|
||||||
|
'lieu_id' => ['nullable', 'integer', 'exists:lieux,id'],
|
||||||
|
'annee_debut' => ['nullable', 'integer', 'min:1000', 'max:2100'],
|
||||||
|
'annee_fin' => ['nullable', 'integer', 'min:1000', 'max:2100', 'gte:annee_debut'],
|
||||||
'cote' => ['nullable', 'string', 'max:255'],
|
'cote' => ['nullable', 'string', 'max:255'],
|
||||||
'auteur' => ['nullable', 'string', 'max:255'],
|
'auteur' => ['nullable', 'string', 'max:255'],
|
||||||
];
|
];
|
||||||
|
|||||||
+6
-1
@@ -10,7 +10,12 @@ class Lieu extends Model
|
|||||||
{
|
{
|
||||||
protected $table = 'lieux';
|
protected $table = 'lieux';
|
||||||
|
|
||||||
protected $fillable = ['nom', 'code', 'lieu_parent_id', 'nom_long', 'latitude', 'longitude', 'note'];
|
protected $fillable = ['nom', 'code', 'lieu_type_id', 'lieu_parent_id', 'nom_long', 'latitude', 'longitude', 'note'];
|
||||||
|
|
||||||
|
public function lieuType(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(LieuType::class);
|
||||||
|
}
|
||||||
|
|
||||||
public function parent(): BelongsTo
|
public function parent(): BelongsTo
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class LieuType extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = ['nom', 'ordre'];
|
||||||
|
|
||||||
|
public function lieux(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Lieu::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
|||||||
|
|
||||||
class Source extends Model
|
class Source extends Model
|
||||||
{
|
{
|
||||||
protected $fillable = ['nom', 'description', 'source_type_id', 'depot_id', 'cote', 'auteur', 'status'];
|
protected $fillable = ['nom', 'description', 'source_type_id', 'depot_id', 'lieu_id', 'annee_debut', 'annee_fin', 'cote', 'auteur', 'status'];
|
||||||
|
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'status' => SourceStatus::class,
|
'status' => SourceStatus::class,
|
||||||
@@ -26,6 +26,11 @@ class Source extends Model
|
|||||||
return $this->belongsTo(Depot::class);
|
return $this->belongsTo(Depot::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function lieu(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Lieu::class);
|
||||||
|
}
|
||||||
|
|
||||||
public function membres(): BelongsToMany
|
public function membres(): BelongsToMany
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(User::class, 'source_user');
|
return $this->belongsToMany(User::class, 'source_user');
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Notifications;
|
||||||
|
|
||||||
|
use App\Models\Source;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
|
use Illuminate\Notifications\Notification;
|
||||||
|
|
||||||
|
class SourceAValiderNotification extends Notification
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public readonly Source $source,
|
||||||
|
public readonly User $soumispar,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function via(object $notifiable): array
|
||||||
|
{
|
||||||
|
return ['mail', 'database'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toMail(object $notifiable): MailMessage
|
||||||
|
{
|
||||||
|
return (new MailMessage)
|
||||||
|
->subject("Source à valider : {$this->source->nom}")
|
||||||
|
->greeting("Bonjour {$notifiable->name},")
|
||||||
|
->line("{$this->soumispar->name} a soumis la source **{$this->source->nom}** pour validation.")
|
||||||
|
->line("Type : {$this->source->sourceType->nom}")
|
||||||
|
->when($this->source->depot, fn ($m) => $m->line("Dépôt : {$this->source->depot->nom}"))
|
||||||
|
->action('Accéder à la source', route('sources.show', $this->source))
|
||||||
|
->line('Vous pouvez valider ou renvoyer cette source en cours de saisie.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toDatabase(object $notifiable): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'source_id' => $this->source->id,
|
||||||
|
'source_nom' => $this->source->nom,
|
||||||
|
'soumis_par' => $this->soumispar->name,
|
||||||
|
'url' => route('sources.show', $this->source),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toArray(object $notifiable): array
|
||||||
|
{
|
||||||
|
return $this->toDatabase($notifiable);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Notifications;
|
||||||
|
|
||||||
|
use App\Models\Source;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
|
use Illuminate\Notifications\Notification;
|
||||||
|
|
||||||
|
class SourceRejeteeNotification extends Notification
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public readonly Source $source,
|
||||||
|
public readonly User $rejetePar,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function via(object $notifiable): array
|
||||||
|
{
|
||||||
|
return ['mail', 'database'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toMail(object $notifiable): MailMessage
|
||||||
|
{
|
||||||
|
return (new MailMessage)
|
||||||
|
->subject("Relevés à corriger : {$this->source->nom}")
|
||||||
|
->greeting("Bonjour {$notifiable->name},")
|
||||||
|
->line("{$this->rejetePar->name} a renvoyé la source **{$this->source->nom}** en cours de saisie.")
|
||||||
|
->line("Des corrections sont nécessaires avant une nouvelle soumission.")
|
||||||
|
->action('Accéder à la source', route('sources.show', $this->source));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toDatabase(object $notifiable): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'source_id' => $this->source->id,
|
||||||
|
'source_nom' => $this->source->nom,
|
||||||
|
'rejete_par' => $this->rejetePar->name,
|
||||||
|
'url' => route('sources.show', $this->source),
|
||||||
|
'type' => 'rejet',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toArray(object $notifiable): array
|
||||||
|
{
|
||||||
|
return $this->toDatabase($notifiable);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Enums\SourceStatus;
|
||||||
|
use App\Models\Releve;
|
||||||
|
use App\Models\Source;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class RelevePolicy
|
||||||
|
{
|
||||||
|
public function viewAny(User $user, Source $source): bool
|
||||||
|
{
|
||||||
|
return $source->isVisibleBy($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function view(User $user, Releve $releve): bool
|
||||||
|
{
|
||||||
|
return $releve->source->isVisibleBy($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(User $user, Source $source): bool
|
||||||
|
{
|
||||||
|
if ($source->status === SourceStatus::Termine) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ($user->isAdmin() || $user->isSectionManager()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return $source->membres()->where('user_id', $user->id)->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(User $user, Releve $releve): bool
|
||||||
|
{
|
||||||
|
return $this->create($user, $releve->source);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete(User $user, Releve $releve): bool
|
||||||
|
{
|
||||||
|
return $user->isAdmin() || $user->isSectionManager();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use DateTime;
|
||||||
|
|
||||||
|
class DateConversionService
|
||||||
|
{
|
||||||
|
// Dates de début du 1er Vendémiaire pour chaque An républicain
|
||||||
|
private const YEAR_STARTS = [
|
||||||
|
1 => '1792-09-22',
|
||||||
|
2 => '1793-09-22',
|
||||||
|
3 => '1794-09-23',
|
||||||
|
4 => '1795-09-23',
|
||||||
|
5 => '1796-09-22',
|
||||||
|
6 => '1797-09-22',
|
||||||
|
7 => '1798-09-22',
|
||||||
|
8 => '1799-09-23',
|
||||||
|
9 => '1800-09-23',
|
||||||
|
10 => '1801-09-23',
|
||||||
|
11 => '1802-09-23',
|
||||||
|
12 => '1803-09-23',
|
||||||
|
13 => '1804-09-23',
|
||||||
|
14 => '1805-09-23',
|
||||||
|
];
|
||||||
|
|
||||||
|
private const MONTHS = [
|
||||||
|
'vendémiaire' => 1, 'vendemiaire' => 1,
|
||||||
|
'brumaire' => 2,
|
||||||
|
'frimaire' => 3,
|
||||||
|
'nivôse' => 4, 'nivose' => 4,
|
||||||
|
'pluviôse' => 5, 'pluviose' => 5,
|
||||||
|
'ventôse' => 6, 'ventose' => 6,
|
||||||
|
'germinal' => 7,
|
||||||
|
'floréal' => 8, 'floreal' => 8,
|
||||||
|
'prairial' => 9,
|
||||||
|
'messidor' => 10,
|
||||||
|
'thermidor' => 11,
|
||||||
|
'fructidor' => 12,
|
||||||
|
];
|
||||||
|
|
||||||
|
private const ROMAN = [
|
||||||
|
'XIV' => 14, 'XIII' => 13, 'XII' => 12, 'XI' => 11, 'X' => 10,
|
||||||
|
'IX' => 9, 'VIII' => 8, 'VII' => 7, 'VI' => 6, 'V' => 5,
|
||||||
|
'IV' => 4, 'III' => 3, 'II' => 2, 'I' => 1,
|
||||||
|
];
|
||||||
|
|
||||||
|
private const GEDCOM_MONTHS = [
|
||||||
|
1 => 'JAN', 2 => 'FEB', 3 => 'MAR', 4 => 'APR',
|
||||||
|
5 => 'MAY', 6 => 'JUN', 7 => 'JUL', 8 => 'AUG',
|
||||||
|
9 => 'SEP', 10 => 'OCT', 11 => 'NOV', 12 => 'DEC',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convertit un champ date JSONB { valeur, calendrier } en chaîne GEDCOM.
|
||||||
|
* Retourne null si la conversion échoue.
|
||||||
|
*/
|
||||||
|
public function toGedcomDate(?array $dateField): ?string
|
||||||
|
{
|
||||||
|
if (empty($dateField['valeur'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$valeur = trim($dateField['valeur']);
|
||||||
|
$calendrier = $dateField['calendrier'] ?? 'gregorien';
|
||||||
|
|
||||||
|
return match ($calendrier) {
|
||||||
|
'gregorien' => $this->gregorianToGedcom($valeur),
|
||||||
|
'julien' => $this->julianToGedcom($valeur),
|
||||||
|
'republicain' => $this->republicanToGedcom($valeur),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** YYYY-MM-DD → "D MON YYYY" */
|
||||||
|
public function gregorianToGedcom(string $date): ?string
|
||||||
|
{
|
||||||
|
$d = DateTime::createFromFormat('Y-m-d', $date);
|
||||||
|
if (! $d) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$day = (int) $d->format('j');
|
||||||
|
$month = self::GEDCOM_MONTHS[(int) $d->format('n')];
|
||||||
|
$year = $d->format('Y');
|
||||||
|
return "{$day} {$month} {$year}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Julien : même format YYYY-MM-DD, marqué @#DJULIAN@ */
|
||||||
|
private function julianToGedcom(string $date): ?string
|
||||||
|
{
|
||||||
|
$g = $this->gregorianToGedcom($date);
|
||||||
|
return $g ? "@#DJULIAN@ {$g}" : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "15 Vendémiaire An III" → date grégorienne GEDCOM.
|
||||||
|
* Retourne la date avec préfixe @#DFRENCH R@ (standard GEDCOM pour calendrier républicain).
|
||||||
|
*/
|
||||||
|
private function republicanToGedcom(string $date): ?string
|
||||||
|
{
|
||||||
|
$gregory = $this->republicanToGregorian($date);
|
||||||
|
if ($gregory) {
|
||||||
|
return $this->gregorianToGedcom($gregory);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback : on conserve la date telle quelle dans un format lisible
|
||||||
|
return "({$date})";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convertit "15 Vendémiaire An III" → "1794-10-06".
|
||||||
|
*/
|
||||||
|
public function republicanToGregorian(string $input): ?string
|
||||||
|
{
|
||||||
|
// Normaliser l'entrée
|
||||||
|
$input = trim($input);
|
||||||
|
|
||||||
|
// Pattern : "DD NomDuMois An N" ou "DD NomDuMois An XIV"
|
||||||
|
$pattern = '/^(\d{1,2})\s+([\wéèêôûî]+)\s+[Aa]n\s+([IVXLCDM\d]+)$/iu';
|
||||||
|
|
||||||
|
if (! preg_match($pattern, $input, $m)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$day = (int) $m[1];
|
||||||
|
$monthStr = mb_strtolower(trim($m[2]));
|
||||||
|
$yearStr = strtoupper(trim($m[3]));
|
||||||
|
|
||||||
|
// Résoudre le mois
|
||||||
|
$monthNum = self::MONTHS[$monthStr] ?? null;
|
||||||
|
if (! $monthNum || $day < 1 || $day > 30) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Résoudre l'année (chiffres arabes ou romains)
|
||||||
|
$yearNum = is_numeric($yearStr)
|
||||||
|
? (int) $yearStr
|
||||||
|
: (self::ROMAN[$yearStr] ?? null);
|
||||||
|
|
||||||
|
if (! $yearNum || ! isset(self::YEAR_STARTS[$yearNum])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculer le nombre de jours depuis le 1er Vendémiaire
|
||||||
|
$daysOffset = ($monthNum - 1) * 30 + ($day - 1);
|
||||||
|
|
||||||
|
$start = new DateTime(self::YEAR_STARTS[$yearNum]);
|
||||||
|
$start->modify("+{$daysOffset} days");
|
||||||
|
|
||||||
|
return $start->format('Y-m-d');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Releve;
|
||||||
|
use App\Models\Source;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
|
class GedcomExportService
|
||||||
|
{
|
||||||
|
// Mapping des noms de champs vers les tags GEDCOM
|
||||||
|
private const FIELD_EVENT_MAP = [
|
||||||
|
'naissance' => 'BIRT', 'birth' => 'BIRT',
|
||||||
|
'mariage' => 'MARR', 'marriage' => 'MARR',
|
||||||
|
'deces' => 'DEAT', 'décès' => 'DEAT', 'death' => 'DEAT',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly DateConversionService $dates,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function exportSource(Source $source): string
|
||||||
|
{
|
||||||
|
$source->load('sourceType.fields', 'releves.createur');
|
||||||
|
return $this->buildGedcom($source->releves, $source->nom, $source->sourceType->nom);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function exportReleves(Collection $releves, string $titre = 'Export'): string
|
||||||
|
{
|
||||||
|
return $this->buildGedcom($releves, $titre);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildGedcom(Collection $releves, string $sourceName, string $sourceTypeName = ''): string
|
||||||
|
{
|
||||||
|
$lines = [];
|
||||||
|
$lines[] = $this->header($sourceName);
|
||||||
|
|
||||||
|
$sourceTag = '@S1@';
|
||||||
|
$famId = 0;
|
||||||
|
$indiId = 0;
|
||||||
|
|
||||||
|
// Détecter le type d'événement depuis le nom du type de source
|
||||||
|
$eventType = $this->detectEventType($sourceTypeName ?: $sourceName);
|
||||||
|
|
||||||
|
$individuals = [];
|
||||||
|
$families = [];
|
||||||
|
|
||||||
|
foreach ($releves as $releve) {
|
||||||
|
$data = $releve->data ?? [];
|
||||||
|
|
||||||
|
$indiId++;
|
||||||
|
$indiTag = "@I{$indiId}@";
|
||||||
|
|
||||||
|
$nom = $data['nom'] ?? '';
|
||||||
|
$prenom = $data['prenom'] ?? $data['prenom_epoux'] ?? $data['prenom_epouse'] ?? '';
|
||||||
|
|
||||||
|
// ── Individu principal ──────────────────────────────────────────
|
||||||
|
$indi = [];
|
||||||
|
$indi[] = "0 {$indiTag} INDI";
|
||||||
|
if ($nom || $prenom) {
|
||||||
|
$indi[] = "1 NAME {$prenom} /{$nom}/";
|
||||||
|
if ($prenom) $indi[] = "1 GIVN {$prenom}";
|
||||||
|
if ($nom) $indi[] = "1 SURN {$nom}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Événement principal
|
||||||
|
$dateField = $data['date_evenement'] ?? $data['date_naissance'] ?? $data['date_mariage'] ?? null;
|
||||||
|
$gedDate = is_array($dateField) ? $this->dates->toGedcomDate($dateField) : null;
|
||||||
|
$lieu = $data['lieu_naissance'] ?? $data['lieu_evenement'] ?? $data['lieu_mariage'] ?? null;
|
||||||
|
|
||||||
|
$indi[] = "1 {$eventType}";
|
||||||
|
if ($gedDate) $indi[] = "2 DATE {$gedDate}";
|
||||||
|
if ($lieu) $indi[] = "2 PLAC {$lieu}";
|
||||||
|
|
||||||
|
// Référence à la source
|
||||||
|
$indi[] = "1 SOUR {$sourceTag}";
|
||||||
|
if (isset($data['numero_acte'])) {
|
||||||
|
$indi[] = "2 PAGE Acte n°{$data['numero_acte']}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notes (champs non mappés)
|
||||||
|
$noteFields = ['note', 'observation', 'remarque'];
|
||||||
|
foreach ($noteFields as $nf) {
|
||||||
|
if (!empty($data[$nf])) {
|
||||||
|
foreach ($this->wrapNote($data[$nf]) as $noteLine) {
|
||||||
|
$indi[] = $noteLine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$individuals[] = implode("\n", $indi);
|
||||||
|
|
||||||
|
// ── Famille (père/mère ou époux/épouse) ─────────────────────────
|
||||||
|
$hasFather = !empty($data['nom_pere']) || !empty($data['prenom_pere']);
|
||||||
|
$hasMother = !empty($data['nom_mere']) || !empty($data['prenom_mere']);
|
||||||
|
$isMarriage = $eventType === 'MARR';
|
||||||
|
|
||||||
|
if ($hasFather || $hasMother || $isMarriage) {
|
||||||
|
$famId++;
|
||||||
|
$famTag = "@F{$famId}@";
|
||||||
|
|
||||||
|
$fam = [];
|
||||||
|
$fam[] = "0 {$famTag} FAM";
|
||||||
|
|
||||||
|
if ($isMarriage) {
|
||||||
|
// Pour un mariage, créer époux et épouse séparément
|
||||||
|
if (!empty($data['nom_epoux']) || !empty($data['prenom_epoux'])) {
|
||||||
|
$indiId++;
|
||||||
|
$epouxTag = "@I{$indiId}@";
|
||||||
|
$individuals[] = implode("\n", [
|
||||||
|
"0 {$epouxTag} INDI",
|
||||||
|
"1 NAME {$data['prenom_epoux']} /{$data['nom_epoux']}/",
|
||||||
|
"1 FAMS {$famTag}",
|
||||||
|
"1 SOUR {$sourceTag}",
|
||||||
|
]);
|
||||||
|
$fam[] = "1 HUSB {$epouxTag}";
|
||||||
|
}
|
||||||
|
if (!empty($data['nom_epouse']) || !empty($data['prenom_epouse'])) {
|
||||||
|
$indiId++;
|
||||||
|
$epouseTag = "@I{$indiId}@";
|
||||||
|
$individuals[] = implode("\n", [
|
||||||
|
"0 {$epouseTag} INDI",
|
||||||
|
"1 NAME {$data['prenom_epouse']} /{$data['nom_epouse']}/",
|
||||||
|
"1 FAMS {$famTag}",
|
||||||
|
"1 SOUR {$sourceTag}",
|
||||||
|
]);
|
||||||
|
$fam[] = "1 WIFE {$epouseTag}";
|
||||||
|
}
|
||||||
|
$fam[] = "1 MARR";
|
||||||
|
if ($gedDate) $fam[] = "2 DATE {$gedDate}";
|
||||||
|
if ($lieu) $fam[] = "2 PLAC {$lieu}";
|
||||||
|
// Lier l'individu principal comme enfant si besoin
|
||||||
|
} else {
|
||||||
|
// Naissance/décès : père et mère
|
||||||
|
if ($hasFather) {
|
||||||
|
$indiId++;
|
||||||
|
$pereTag = "@I{$indiId}@";
|
||||||
|
$individuals[] = implode("\n", [
|
||||||
|
"0 {$pereTag} INDI",
|
||||||
|
"1 NAME " . ($data['prenom_pere'] ?? '') . " /" . ($data['nom_pere'] ?? '') . "/",
|
||||||
|
"1 FAMS {$famTag}",
|
||||||
|
"1 SOUR {$sourceTag}",
|
||||||
|
]);
|
||||||
|
$fam[] = "1 HUSB {$pereTag}";
|
||||||
|
}
|
||||||
|
if ($hasMother) {
|
||||||
|
$indiId++;
|
||||||
|
$mereTag = "@I{$indiId}@";
|
||||||
|
$individuals[] = implode("\n", [
|
||||||
|
"0 {$mereTag} INDI",
|
||||||
|
"1 NAME " . ($data['prenom_mere'] ?? '') . " /" . ($data['nom_mere'] ?? '') . "/",
|
||||||
|
"1 FAMS {$famTag}",
|
||||||
|
"1 SOUR {$sourceTag}",
|
||||||
|
]);
|
||||||
|
$fam[] = "1 WIFE {$mereTag}";
|
||||||
|
}
|
||||||
|
$fam[] = "1 CHIL {$indiTag}";
|
||||||
|
// Lier l'enfant à sa famille
|
||||||
|
$indi[] = "1 FAMC {$famTag}";
|
||||||
|
}
|
||||||
|
|
||||||
|
$families[] = implode("\n", $fam);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($individuals as $indi) {
|
||||||
|
$lines[] = $indi;
|
||||||
|
}
|
||||||
|
foreach ($families as $fam) {
|
||||||
|
$lines[] = $fam;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enregistrement SOURCE
|
||||||
|
$lines[] = implode("\n", [
|
||||||
|
"0 {$sourceTag} SOUR",
|
||||||
|
"1 TITL {$sourceName}",
|
||||||
|
"1 AUTH MesRelevés",
|
||||||
|
]);
|
||||||
|
|
||||||
|
$lines[] = "0 TRLR";
|
||||||
|
|
||||||
|
return implode("\n", $lines) . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
private function header(string $sourceName): string
|
||||||
|
{
|
||||||
|
$date = now()->format('d M Y');
|
||||||
|
$time = now()->format('H:i:s');
|
||||||
|
|
||||||
|
return implode("\n", [
|
||||||
|
'0 HEAD',
|
||||||
|
'1 SOUR MESRELEVES',
|
||||||
|
'2 NAME MesRelevés',
|
||||||
|
'2 VERS 1.0',
|
||||||
|
'1 DEST ANY',
|
||||||
|
"1 DATE {$date}",
|
||||||
|
"2 TIME {$time}",
|
||||||
|
'1 GEDC',
|
||||||
|
'2 VERS 5.5.1',
|
||||||
|
'2 FORM LINEAGE-LINKED',
|
||||||
|
'1 CHAR UTF-8',
|
||||||
|
"1 FILE {$sourceName}.ged",
|
||||||
|
'1 LANG French',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function detectEventType(string $name): string
|
||||||
|
{
|
||||||
|
$lower = mb_strtolower($name);
|
||||||
|
foreach (self::FIELD_EVENT_MAP as $keyword => $tag) {
|
||||||
|
if (str_contains($lower, $keyword)) {
|
||||||
|
return $tag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'EVEN';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Découpe une note longue en lignes GEDCOM (max 248 chars par ligne) */
|
||||||
|
private function wrapNote(string $text): array
|
||||||
|
{
|
||||||
|
$lines = [];
|
||||||
|
$chunks = mb_str_split($text, 248);
|
||||||
|
foreach ($chunks as $i => $chunk) {
|
||||||
|
$lines[] = ($i === 0 ? '1 NOTE ' : '2 CONT ') . $chunk;
|
||||||
|
}
|
||||||
|
return $lines;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('lieu_types', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('nom')->unique();
|
||||||
|
$table->unsignedSmallInteger('ordre')->default(0);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('lieu_types');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -10,10 +10,11 @@ return new class extends Migration
|
|||||||
{
|
{
|
||||||
Schema::create('lieux', function (Blueprint $table) {
|
Schema::create('lieux', function (Blueprint $table) {
|
||||||
$table->id();
|
$table->id();
|
||||||
|
$table->foreignId('lieu_type_id')->nullable()->constrained('lieu_types')->nullOnDelete();
|
||||||
$table->string('nom');
|
$table->string('nom');
|
||||||
$table->string('code')->nullable();
|
$table->string('code')->nullable();
|
||||||
$table->foreignId('lieu_parent_id')->nullable()->constrained('lieux')->nullOnDelete();
|
$table->foreignId('lieu_parent_id')->nullable()->constrained('lieux')->nullOnDelete();
|
||||||
$table->string('nom_long')->nullable(); // calculé : "Bordeaux, Gironde, France"
|
$table->string('nom_long')->nullable();
|
||||||
$table->decimal('latitude', 10, 7)->nullable();
|
$table->decimal('latitude', 10, 7)->nullable();
|
||||||
$table->decimal('longitude', 10, 7)->nullable();
|
$table->decimal('longitude', 10, 7)->nullable();
|
||||||
$table->text('note')->nullable();
|
$table->text('note')->nullable();
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('notifications', function (Blueprint $table) {
|
||||||
|
$table->uuid('id')->primary();
|
||||||
|
$table->string('type');
|
||||||
|
$table->morphs('notifiable');
|
||||||
|
$table->text('data');
|
||||||
|
$table->timestamp('read_at')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('notifications');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('sources', function (Blueprint $table) {
|
||||||
|
$table->foreignId('lieu_id')->nullable()->after('depot_id')->constrained('lieux')->nullOnDelete();
|
||||||
|
$table->unsignedSmallInteger('annee_debut')->nullable()->after('lieu_id');
|
||||||
|
$table->unsignedSmallInteger('annee_fin')->nullable()->after('annee_debut');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('sources', function (Blueprint $table) {
|
||||||
|
$table->dropForeign(['lieu_id']);
|
||||||
|
$table->dropColumn(['lieu_id', 'annee_debut', 'annee_fin']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -2,24 +2,164 @@
|
|||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Enums\FieldType;
|
||||||
|
use App\Enums\SourceStatus;
|
||||||
|
use App\Enums\UserRole;
|
||||||
|
use App\Models\Depot;
|
||||||
|
use App\Models\Lieu;
|
||||||
|
use App\Models\LieuType;
|
||||||
|
use App\Models\Releve;
|
||||||
|
use App\Models\Section;
|
||||||
|
use App\Models\Source;
|
||||||
|
use App\Models\SourceType;
|
||||||
|
use App\Models\SourceTypeField;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
||||||
class DatabaseSeeder extends Seeder
|
class DatabaseSeeder extends Seeder
|
||||||
{
|
{
|
||||||
use WithoutModelEvents;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Seed the application's database.
|
|
||||||
*/
|
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
// User::factory(10)->create();
|
// ── Utilisateurs ────────────────────────────────────────────────────
|
||||||
|
$admin = User::create([
|
||||||
|
'name' => 'Administrateur',
|
||||||
|
'email' => 'admin@mesreleves.local',
|
||||||
|
'password' => Hash::make('password'),
|
||||||
|
'role' => UserRole::Admin,
|
||||||
|
]);
|
||||||
|
|
||||||
User::factory()->create([
|
$responsable = User::create([
|
||||||
'name' => 'Test User',
|
'name' => 'Responsable Bordeaux',
|
||||||
'email' => 'test@example.com',
|
'email' => 'responsable@mesreleves.local',
|
||||||
|
'password' => Hash::make('password'),
|
||||||
|
'role' => UserRole::SectionManager,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$membre = User::create([
|
||||||
|
'name' => 'Marie Durand',
|
||||||
|
'email' => 'membre@mesreleves.local',
|
||||||
|
'password' => Hash::make('password'),
|
||||||
|
'role' => UserRole::Member,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ── Types de lieux ──────────────────────────────────────────────────
|
||||||
|
$typePays = LieuType::create(['nom' => 'Pays', 'ordre' => 0]);
|
||||||
|
$typeRegion = LieuType::create(['nom' => 'Région', 'ordre' => 10]);
|
||||||
|
$typeDept = LieuType::create(['nom' => 'Département', 'ordre' => 20]);
|
||||||
|
$typeVille = LieuType::create(['nom' => 'Ville', 'ordre' => 30]);
|
||||||
|
$typeCommune = LieuType::create(['nom' => 'Commune', 'ordre' => 35]);
|
||||||
|
$typeParoisse = LieuType::create(['nom' => 'Paroisse', 'ordre' => 40]);
|
||||||
|
$typeLieuDit = LieuType::create(['nom' => 'Lieu-dit', 'ordre' => 50]);
|
||||||
|
|
||||||
|
// ── Lieux ───────────────────────────────────────────────────────────
|
||||||
|
$france = Lieu::create(['nom' => 'France', 'lieu_type_id' => $typePays->id]);
|
||||||
|
$gironde = Lieu::create(['nom' => 'Gironde', 'code' => '33', 'lieu_type_id' => $typeDept->id, 'lieu_parent_id' => $france->id]);
|
||||||
|
$bordeaux = Lieu::create(['nom' => 'Bordeaux', 'code' => '33063', 'lieu_type_id' => $typeVille->id, 'lieu_parent_id' => $gironde->id,
|
||||||
|
'latitude' => 44.8378, 'longitude' => -0.5792]);
|
||||||
|
$begles = Lieu::create(['nom' => 'Bègles', 'code' => '33032', 'lieu_type_id' => $typeCommune->id, 'lieu_parent_id' => $gironde->id]);
|
||||||
|
|
||||||
|
// ── Section ─────────────────────────────────────────────────────────
|
||||||
|
$section = Section::create([
|
||||||
|
'nom' => 'Section Gironde',
|
||||||
|
'lieu_id' => $bordeaux->id,
|
||||||
|
'email_contact' => 'gironde@cgl.fr',
|
||||||
|
]);
|
||||||
|
$section->membres()->attach($responsable->id, ['role_in_section' => 'section_manager']);
|
||||||
|
$section->membres()->attach($membre->id, ['role_in_section' => 'member']);
|
||||||
|
|
||||||
|
// ── Dépôt ───────────────────────────────────────────────────────────
|
||||||
|
$depot = Depot::create([
|
||||||
|
'nom' => 'Archives Départementales de la Gironde',
|
||||||
|
'adresse_postale' => '72 cours Balguerie-Stuttenberg, 33000 Bordeaux',
|
||||||
|
'url' => 'https://archives.gironde.fr',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ── Type de source : État civil Naissance ────────────────────────────
|
||||||
|
$stNaissance = SourceType::create([
|
||||||
|
'nom' => 'État civil — Naissance',
|
||||||
|
'description' => 'Actes de naissance (format post-révolutionnaire)',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$champsNaissance = [
|
||||||
|
['name' => 'nom', 'label' => 'Nom', 'type' => FieldType::Text, 'required' => true, 'order' => 0],
|
||||||
|
['name' => 'prenom', 'label' => 'Prénom(s)', 'type' => FieldType::Text, 'required' => true, 'order' => 1],
|
||||||
|
['name' => 'date_evenement', 'label' => 'Date de naissance', 'type' => FieldType::Date, 'required' => true, 'order' => 2],
|
||||||
|
['name' => 'lieu_naissance', 'label' => 'Lieu de naissance', 'type' => FieldType::Text, 'required' => false, 'order' => 3],
|
||||||
|
['name' => 'numero_acte', 'label' => 'N° acte', 'type' => FieldType::Number, 'required' => false, 'order' => 4],
|
||||||
|
['name' => 'nom_pere', 'label' => 'Nom du père', 'type' => FieldType::Text, 'required' => false, 'order' => 5],
|
||||||
|
['name' => 'prenom_pere', 'label' => 'Prénom du père', 'type' => FieldType::Text, 'required' => false, 'order' => 6],
|
||||||
|
['name' => 'nom_mere', 'label' => 'Nom de la mère', 'type' => FieldType::Text, 'required' => false, 'order' => 7],
|
||||||
|
['name' => 'prenom_mere', 'label' => 'Prénom de la mère', 'type' => FieldType::Text, 'required' => false, 'order' => 8],
|
||||||
|
['name' => 'note', 'label' => 'Note', 'type' => FieldType::Textarea, 'required' => false, 'order' => 9],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($champsNaissance as $champ) {
|
||||||
|
SourceTypeField::create(['source_type_id' => $stNaissance->id, ...$champ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Type de source : Mariage ─────────────────────────────────────────
|
||||||
|
$stMariage = SourceType::create(['nom' => 'État civil — Mariage']);
|
||||||
|
|
||||||
|
$champsMariage = [
|
||||||
|
['name' => 'nom_epoux', 'label' => 'Nom de l\'époux', 'type' => FieldType::Text, 'required' => true, 'order' => 0],
|
||||||
|
['name' => 'prenom_epoux', 'label' => 'Prénom de l\'époux','type' => FieldType::Text, 'required' => true, 'order' => 1],
|
||||||
|
['name' => 'nom_epouse', 'label' => 'Nom de l\'épouse', 'type' => FieldType::Text, 'required' => true, 'order' => 2],
|
||||||
|
['name' => 'prenom_epouse', 'label' => 'Prénom de l\'épouse','type'=> FieldType::Text, 'required' => true, 'order' => 3],
|
||||||
|
['name' => 'date_mariage', 'label' => 'Date du mariage', 'type' => FieldType::Date, 'required' => true, 'order' => 4],
|
||||||
|
['name' => 'numero_acte', 'label' => 'N° acte', 'type' => FieldType::Number,'required'=> false, 'order' => 5],
|
||||||
|
['name' => 'note', 'label' => 'Note', 'type' => FieldType::Textarea,'required'=> false,'order'=> 6],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($champsMariage as $champ) {
|
||||||
|
SourceTypeField::create(['source_type_id' => $stMariage->id, ...$champ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Source : naissances Bordeaux (en cours, membre assigné) ──────────
|
||||||
|
$sourceNaissances = Source::create([
|
||||||
|
'nom' => 'Naissances Bordeaux 1820–1830',
|
||||||
|
'source_type_id' => $stNaissance->id,
|
||||||
|
'depot_id' => $depot->id,
|
||||||
|
'cote' => '4E 756',
|
||||||
|
'auteur' => 'Marie Durand',
|
||||||
|
'status' => SourceStatus::EnCours,
|
||||||
|
]);
|
||||||
|
$sourceNaissances->membres()->attach($membre->id);
|
||||||
|
|
||||||
|
// ── Relevés de la source naissances ──────────────────────────────────
|
||||||
|
$releves = [
|
||||||
|
['nom' => 'DUPONT', 'prenom' => 'Jean Marie', 'date_evenement' => ['valeur' => '1821-04-03', 'calendrier' => 'gregorien'], 'lieu_naissance' => 'Bordeaux', 'numero_acte' => 12, 'nom_pere' => 'DUPONT', 'prenom_pere' => 'Pierre', 'nom_mere' => 'MARTIN', 'prenom_mere' => 'Jeanne', 'note' => null],
|
||||||
|
['nom' => 'MARTIN', 'prenom' => 'Marie Anne', 'date_evenement' => ['valeur' => '1822-07-15', 'calendrier' => 'gregorien'], 'lieu_naissance' => 'Bordeaux', 'numero_acte' => 34, 'nom_pere' => 'MARTIN', 'prenom_pere' => 'Louis', 'nom_mere' => 'BERNARD', 'prenom_mere' => 'Claire', 'note' => null],
|
||||||
|
['nom' => 'BERNARD', 'prenom' => 'Pierre Louis', 'date_evenement' => ['valeur' => '1823-01-28', 'calendrier' => 'gregorien'], 'lieu_naissance' => 'Bègles', 'numero_acte' => 8, 'nom_pere' => 'BERNARD', 'prenom_pere' => 'Jacques', 'nom_mere' => 'LEBRUN', 'prenom_mere' => 'Marie', 'note' => 'Né prématurément'],
|
||||||
|
['nom' => 'LEBRUN', 'prenom' => 'Céleste', 'date_evenement' => ['valeur' => '6 Vendémiaire An XI', 'calendrier' => 'republicain'], 'lieu_naissance' => 'Bordeaux', 'numero_acte' => 51, 'nom_pere' => null, 'prenom_pere' => null, 'nom_mere' => 'LEBRUN', 'prenom_mere' => 'Angélique', 'note' => 'Père inconnu'],
|
||||||
|
['nom' => 'ROUX', 'prenom' => 'Henri Gustave', 'date_evenement' => ['valeur' => '1825-11-02', 'calendrier' => 'gregorien'], 'lieu_naissance' => 'Bordeaux', 'numero_acte' => 89, 'nom_pere' => 'ROUX', 'prenom_pere' => 'Étienne', 'nom_mere' => 'PETIT', 'prenom_mere' => 'Louise', 'note' => null],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($releves as $data) {
|
||||||
|
Releve::create([
|
||||||
|
'source_id' => $sourceNaissances->id,
|
||||||
|
'created_by' => $membre->id,
|
||||||
|
'updated_by' => $membre->id,
|
||||||
|
'data' => $data,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Source : mariages (à valider) ────────────────────────────────────
|
||||||
|
Source::create([
|
||||||
|
'nom' => 'Mariages Bordeaux 1815–1820',
|
||||||
|
'source_type_id' => $stMariage->id,
|
||||||
|
'depot_id' => $depot->id,
|
||||||
|
'cote' => '4E 750',
|
||||||
|
'status' => SourceStatus::AValider,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ── Source : à faire ─────────────────────────────────────────────────
|
||||||
|
Source::create([
|
||||||
|
'nom' => 'Naissances Bègles 1830–1840',
|
||||||
|
'source_type_id' => $stNaissance->id,
|
||||||
|
'depot_id' => $depot->id,
|
||||||
|
'cote' => '4E 820',
|
||||||
|
'status' => SourceStatus::AFaire,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
@error('nom') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<x-lieu-picker
|
||||||
<label for="lieu_id" class="block text-sm font-medium text-gray-700">Lieu de rattachement</label>
|
name="lieu_id"
|
||||||
<select id="lieu_id" name="lieu_id"
|
label="Lieu de rattachement"
|
||||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
|
:value="old('lieu_id', $section?->lieu_id)"
|
||||||
<option value="">— Aucun —</option>
|
:display-value="old('lieu_id')
|
||||||
@foreach($lieux as $lieu)
|
? ''
|
||||||
<option value="{{ $lieu->id }}" {{ old('lieu_id', $section?->lieu_id) == $lieu->id ? 'selected' : '' }}>
|
: ($section?->lieu?->nom_long ?? $section?->lieu?->nom ?? '')"
|
||||||
{{ $lieu->nom_long ?? $lieu->nom }}
|
/>
|
||||||
</option>
|
|
||||||
@endforeach
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label for="adresse" class="block text-sm font-medium text-gray-700">Adresse</label>
|
<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">
|
<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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div class="flex justify-between h-16">
|
<div class="flex justify-between h-16">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
<div class="shrink-0 flex items-center">
|
<div class="shrink-0 flex items-center">
|
||||||
<a href="{{ route('dashboard') }}">
|
<a href="{{ route('dashboard') }}" class="font-semibold text-gray-800 text-lg">
|
||||||
<x-application-logo class="block h-9 w-auto fill-current text-gray-800" />
|
MesRelevés
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Navigation Links -->
|
<!-- Navigation Links -->
|
||||||
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
|
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
|
||||||
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
||||||
{{ __('Dashboard') }}
|
Tableau de bord
|
||||||
</x-nav-link>
|
</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>
|
||||||
</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 -->
|
<!-- 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-dropdown align="right" width="48">
|
||||||
<x-slot name="trigger">
|
<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">
|
<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>{{ Auth::user()->name }}</div>
|
||||||
|
|
||||||
<div class="ms-1">
|
<div class="ms-1">
|
||||||
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
|
<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" />
|
<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>
|
||||||
|
|
||||||
<x-slot name="content">
|
<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')">
|
<x-dropdown-link :href="route('profile.edit')">
|
||||||
{{ __('Profile') }}
|
Mon profil
|
||||||
</x-dropdown-link>
|
</x-dropdown-link>
|
||||||
|
|
||||||
<!-- Authentication -->
|
|
||||||
<form method="POST" action="{{ route('logout') }}">
|
<form method="POST" action="{{ route('logout') }}">
|
||||||
@csrf
|
@csrf
|
||||||
|
|
||||||
<x-dropdown-link :href="route('logout')"
|
<x-dropdown-link :href="route('logout')"
|
||||||
onclick="event.preventDefault();
|
onclick="event.preventDefault(); this.closest('form').submit();">
|
||||||
this.closest('form').submit();">
|
Se déconnecter
|
||||||
{{ __('Log Out') }}
|
|
||||||
</x-dropdown-link>
|
</x-dropdown-link>
|
||||||
</form>
|
</form>
|
||||||
</x-slot>
|
</x-slot>
|
||||||
@@ -54,7 +109,7 @@
|
|||||||
|
|
||||||
<!-- Hamburger -->
|
<!-- Hamburger -->
|
||||||
<div class="-me-2 flex items-center sm:hidden">
|
<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">
|
<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="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" />
|
<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="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
|
||||||
<div class="pt-2 pb-3 space-y-1">
|
<div class="pt-2 pb-3 space-y-1">
|
||||||
<x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
<x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
|
||||||
{{ __('Dashboard') }}
|
Tableau de bord
|
||||||
</x-responsive-nav-link>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Responsive Settings Options -->
|
<!-- Responsive Settings Options -->
|
||||||
@@ -77,21 +154,15 @@
|
|||||||
<div class="px-4">
|
<div class="px-4">
|
||||||
<div class="font-medium text-base text-gray-800">{{ Auth::user()->name }}</div>
|
<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="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>
|
||||||
|
|
||||||
<div class="mt-3 space-y-1">
|
<div class="mt-3 space-y-1">
|
||||||
<x-responsive-nav-link :href="route('profile.edit')">
|
<x-responsive-nav-link :href="route('profile.edit')">Mon profil</x-responsive-nav-link>
|
||||||
{{ __('Profile') }}
|
|
||||||
</x-responsive-nav-link>
|
|
||||||
|
|
||||||
<!-- Authentication -->
|
|
||||||
<form method="POST" action="{{ route('logout') }}">
|
<form method="POST" action="{{ route('logout') }}">
|
||||||
@csrf
|
@csrf
|
||||||
|
|
||||||
<x-responsive-nav-link :href="route('logout')"
|
<x-responsive-nav-link :href="route('logout')"
|
||||||
onclick="event.preventDefault();
|
onclick="event.preventDefault(); this.closest('form').submit();">
|
||||||
this.closest('form').submit();">
|
Se déconnecter
|
||||||
{{ __('Log Out') }}
|
|
||||||
</x-responsive-nav-link>
|
</x-responsive-nav-link>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -9,6 +9,27 @@
|
|||||||
@error('nom') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
|
@error('nom') <p class="mt-1 text-sm text-red-600">{{ $message }}</p> @enderror
|
||||||
</div>
|
</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 --}}
|
{{-- Code --}}
|
||||||
<div>
|
<div>
|
||||||
<label for="code" class="block text-sm font-medium text-gray-700">Code (INSEE, postal…)</label>
|
<label for="code" class="block text-sm font-medium text-gray-700">Code (INSEE, postal…)</label>
|
||||||
@@ -18,21 +39,16 @@
|
|||||||
maxlength="20">
|
maxlength="20">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{-- Parent --}}
|
{{-- Lieu parent --}}
|
||||||
<div>
|
<x-lieu-picker
|
||||||
<label for="lieu_parent_id" class="block text-sm font-medium text-gray-700">Lieu parent</label>
|
name="lieu_parent_id"
|
||||||
<select id="lieu_parent_id" name="lieu_parent_id"
|
label="Lieu parent"
|
||||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
|
:value="old('lieu_parent_id', $lieu?->lieu_parent_id)"
|
||||||
<option value="">— Aucun (lieu racine) —</option>
|
:display-value="old('lieu_parent_id')
|
||||||
@foreach($parents as $parent)
|
? ''
|
||||||
<option value="{{ $parent->id }}"
|
: ($lieu?->parent?->nom_long ?? $lieu?->parent?->nom ?? '')"
|
||||||
{{ old('lieu_parent_id', $lieu?->lieu_parent_id) == $parent->id ? 'selected' : '' }}>
|
placeholder="— Aucun (lieu racine) —"
|
||||||
{{ $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>
|
|
||||||
|
|
||||||
{{-- Coordonnées --}}
|
{{-- Coordonnées --}}
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<div class="bg-white shadow rounded-lg p-6">
|
<div class="bg-white shadow rounded-lg p-6">
|
||||||
<form method="POST" action="{{ route('lieux.store') }}">
|
<form method="POST" action="{{ route('lieux.store') }}">
|
||||||
@csrf
|
@csrf
|
||||||
@include('lieux._form', ['lieu' => null, 'parents' => $parents])
|
@include('lieux._form', ['lieu' => null])
|
||||||
|
|
||||||
<div class="mt-6 flex items-center gap-4">
|
<div class="mt-6 flex items-center gap-4">
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<div class="bg-white shadow rounded-lg p-6">
|
<div class="bg-white shadow rounded-lg p-6">
|
||||||
<form method="POST" action="{{ route('lieux.update', $lieu) }}">
|
<form method="POST" action="{{ route('lieux.update', $lieu) }}">
|
||||||
@csrf @method('PUT')
|
@csrf @method('PUT')
|
||||||
@include('lieux._form', ['lieu' => $lieu, 'parents' => $parents])
|
@include('lieux._form', ['lieu' => $lieu])
|
||||||
|
|
||||||
<div class="mt-6 flex items-center gap-4">
|
<div class="mt-6 flex items-center gap-4">
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
|
|||||||
@@ -11,24 +11,78 @@
|
|||||||
</div>
|
</div>
|
||||||
</x-slot>
|
</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'))
|
@if(session('success'))
|
||||||
<div class="mb-4 p-4 bg-green-50 border border-green-200 text-green-800 rounded-md">
|
<div class="p-4 bg-green-50 border border-green-200 text-green-800 rounded-md">{{ session('success') }}</div>
|
||||||
{{ session('success') }}
|
|
||||||
</div>
|
|
||||||
@endif
|
@endif
|
||||||
@if(session('error'))
|
@if(session('error'))
|
||||||
<div class="mb-4 p-4 bg-red-50 border border-red-200 text-red-800 rounded-md">
|
<div class="p-4 bg-red-50 border border-red-200 text-red-800 rounded-md">{{ session('error') }}</div>
|
||||||
{{ session('error') }}
|
|
||||||
</div>
|
|
||||||
@endif
|
@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">
|
<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">
|
||||||
<thead class="bg-gray-50">
|
<thead class="bg-gray-50">
|
||||||
<tr>
|
<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">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">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">Parent</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Coordonnées</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 }}
|
{{ $lieu->nom }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</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">{{ $lieu->code ?? '—' }}</td>
|
||||||
<td class="px-6 py-4 text-sm text-gray-500">
|
<td class="px-6 py-4 text-sm text-gray-500">
|
||||||
@if($lieu->parent)
|
@if($lieu->parent)
|
||||||
@@ -76,7 +131,10 @@
|
|||||||
</tr>
|
</tr>
|
||||||
@empty
|
@empty
|
||||||
<tr>
|
<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>
|
</tr>
|
||||||
@endforelse
|
@endforelse
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -37,6 +37,10 @@
|
|||||||
<dt class="font-medium text-gray-500">Nom complet</dt>
|
<dt class="font-medium text-gray-500">Nom complet</dt>
|
||||||
<dd class="col-span-2 text-gray-900">{{ $lieu->nom_long ?? $lieu->nom }}</dd>
|
<dd class="col-span-2 text-gray-900">{{ $lieu->nom_long ?? $lieu->nom }}</dd>
|
||||||
</div>
|
</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)
|
@if($lieu->code)
|
||||||
<div class="px-6 py-4 grid grid-cols-3 gap-4 text-sm">
|
<div class="px-6 py-4 grid grid-cols-3 gap-4 text-sm">
|
||||||
<dt class="font-medium text-gray-500">Code</dt>
|
<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>
|
||||||
</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 class="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<div>
|
||||||
<label for="cote" class="block text-sm font-medium text-gray-700">Cote</label>
|
<label for="cote" class="block text-sm font-medium text-gray-700">Cote</label>
|
||||||
|
|||||||
@@ -9,16 +9,103 @@
|
|||||||
</div>
|
</div>
|
||||||
</x-slot>
|
</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> @endif
|
@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">
|
<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">
|
<thead class="bg-gray-50">
|
||||||
<tr>
|
<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">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">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">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">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 text-left text-xs font-medium text-gray-500 uppercase">Dépôt</th>
|
||||||
<th class="px-6 py-3"></th>
|
<th class="px-6 py-3"></th>
|
||||||
@@ -34,20 +121,30 @@
|
|||||||
'termine' => 'bg-green-100 text-green-700',
|
'termine' => 'bg-green-100 text-green-700',
|
||||||
];
|
];
|
||||||
$color = $statusColors[$source->status->value] ?? 'bg-gray-100 text-gray-600';
|
$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
|
@endphp
|
||||||
<tr class="hover:bg-gray-50">
|
<tr class="hover:bg-gray-50">
|
||||||
<td class="px-6 py-4 font-medium">
|
<td class="px-6 py-4 font-medium">
|
||||||
<a href="{{ route('sources.show', $source) }}" class="text-indigo-600 hover:underline">{{ $source->nom }}</a>
|
<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
|
@if($source->cote) <span class="ml-2 text-xs text-gray-400">{{ $source->cote }}</span> @endif
|
||||||
</td>
|
</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">
|
<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 }}">
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ $color }}">
|
||||||
{{ $source->status->label() }}
|
{{ $source->status->label() }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 text-sm text-gray-500">{{ $source->releves_count }}</td>
|
<td class="px-6 py-4 text-gray-500 max-w-[180px] truncate" title="{{ $source->lieu?->nom_long ?? $source->lieu?->nom }}">
|
||||||
<td class="px-6 py-4 text-sm text-gray-500">{{ $source->depot?->nom ?? '—' }}</td>
|
{{ $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">
|
<td class="px-6 py-4 text-right text-sm space-x-3">
|
||||||
@can('update', $source)
|
@can('update', $source)
|
||||||
<a href="{{ route('sources.edit', $source) }}" class="text-gray-600 hover:text-indigo-600">Modifier</a>
|
<a href="{{ route('sources.edit', $source) }}" class="text-gray-600 hover:text-indigo-600">Modifier</a>
|
||||||
@@ -55,11 +152,18 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@empty
|
@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
|
@endforelse
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</x-app-layout>
|
</x-app-layout>
|
||||||
|
|||||||
@@ -135,12 +135,21 @@
|
|||||||
<div class="bg-white shadow rounded-lg overflow-hidden">
|
<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">
|
<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>
|
<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>
|
</div>
|
||||||
@if($source->releves->isEmpty())
|
@if($source->releves->isEmpty())
|
||||||
<p class="px-6 py-8 text-center text-gray-400 text-sm">Aucun relevé pour cette source.</p>
|
<p class="px-6 py-8 text-center text-gray-400 text-sm">Aucun relevé pour cette source.</p>
|
||||||
@else
|
@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
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\Http\Controllers\Admin\DepotController;
|
use App\Http\Controllers\Admin\DepotController;
|
||||||
|
use App\Http\Controllers\Admin\LieuTypeController;
|
||||||
use App\Http\Controllers\Admin\SectionController;
|
use App\Http\Controllers\Admin\SectionController;
|
||||||
use App\Http\Controllers\Admin\SourceTypeController;
|
use App\Http\Controllers\Admin\SourceTypeController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::middleware(['auth', 'role:admin'])->prefix('admin')->name('admin.')->group(function () {
|
Route::middleware(['auth', 'role:admin'])->prefix('admin')->name('admin.')->group(function () {
|
||||||
|
Route::resource('lieu-types', LieuTypeController::class)
|
||||||
|
->parameters(['lieu-types' => 'lieuType'])
|
||||||
|
->except(['show']);
|
||||||
|
|
||||||
Route::resource('sections', SectionController::class);
|
Route::resource('sections', SectionController::class);
|
||||||
Route::post('sections/{section}/membres', [SectionController::class, 'addMembre'])->name('sections.membres.add');
|
Route::post('sections/{section}/membres', [SectionController::class, 'addMembre'])->name('sections.membres.add');
|
||||||
Route::delete('sections/{section}/membres/{user}', [SectionController::class, 'removeMembre'])->name('sections.membres.remove');
|
Route::delete('sections/{section}/membres/{user}', [SectionController::class, 'removeMembre'])->name('sections.membres.remove');
|
||||||
|
|||||||
+18
-1
@@ -1,7 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\ExportController;
|
||||||
use App\Http\Controllers\LieuController;
|
use App\Http\Controllers\LieuController;
|
||||||
|
use App\Http\Controllers\NotificationController;
|
||||||
use App\Http\Controllers\ProfileController;
|
use App\Http\Controllers\ProfileController;
|
||||||
|
use App\Http\Controllers\RechercheController;
|
||||||
|
use App\Http\Controllers\ReleveController;
|
||||||
use App\Http\Controllers\SourceController;
|
use App\Http\Controllers\SourceController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
@@ -18,12 +22,25 @@ Route::middleware('auth')->group(function () {
|
|||||||
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
|
Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
|
||||||
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
|
Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
|
||||||
|
|
||||||
Route::resource('lieux', LieuController::class);
|
Route::get('lieux/search', [LieuController::class, 'search'])->name('lieux.search');
|
||||||
|
Route::resource('lieux', LieuController::class)->parameters(['lieux' => 'lieu']);
|
||||||
|
|
||||||
Route::resource('sources', SourceController::class);
|
Route::resource('sources', SourceController::class);
|
||||||
Route::post('sources/{source}/membres', [SourceController::class, 'addMembre'])->name('sources.membres.add');
|
Route::post('sources/{source}/membres', [SourceController::class, 'addMembre'])->name('sources.membres.add');
|
||||||
Route::delete('sources/{source}/membres/{user}', [SourceController::class, 'removeMembre'])->name('sources.membres.remove');
|
Route::delete('sources/{source}/membres/{user}', [SourceController::class, 'removeMembre'])->name('sources.membres.remove');
|
||||||
Route::post('sources/{source}/transition', [SourceController::class, 'transition'])->name('sources.transition');
|
Route::post('sources/{source}/transition', [SourceController::class, 'transition'])->name('sources.transition');
|
||||||
|
|
||||||
|
Route::resource('sources.releves', ReleveController::class)
|
||||||
|
->shallow()
|
||||||
|
->parameters(['releves' => 'releve']);
|
||||||
|
|
||||||
|
Route::get('recherche', [RechercheController::class, 'index'])->name('recherche');
|
||||||
|
Route::get('export/source/{source}', [ExportController::class, 'source'])->name('export.source');
|
||||||
|
Route::get('export/recherche', [ExportController::class, 'recherche'])->name('export.recherche');
|
||||||
|
|
||||||
|
Route::get('notifications', [NotificationController::class, 'index'])->name('notifications.index');
|
||||||
|
Route::post('notifications/{id}/read', [NotificationController::class, 'markAsRead'])->name('notifications.read');
|
||||||
|
Route::post('notifications/read-all', [NotificationController::class, 'markAllAsRead'])->name('notifications.read-all');
|
||||||
});
|
});
|
||||||
|
|
||||||
require __DIR__.'/auth.php';
|
require __DIR__.'/auth.php';
|
||||||
|
|||||||
Reference in New Issue
Block a user