Étapes 6-9 + types de lieux + picker + filtres

- Étape 6 : formulaire de saisie dynamique des relevés (piloté par source_type_fields, calendriers grégorien/julien/républicain)
- Étape 7 : workflow de statut des sources + notifications mail+DB (SourceAValider, SourceRejetee)
- Étape 8 : recherche fulltext PostgreSQL avec filtres type/lieu/années et CTE récursive pour les subdivisions de lieux
- Étape 9 : export GEDCOM 5.5.1 (GedcomExportService + DateConversionService)
- Types de lieux : CRUD admin (LieuTypeController) avec champ ordre
- Composant lieu-picker : modale Alpine.js avec recherche AJAX + debounce
- Filtres sources : statut, type, lieu (CTE récursive), période annee_debut/annee_fin
- Filtres lieux : type, texte, lieu parent avec descendants (CTE récursive)
- Migration : lieu_id + annee_debut + annee_fin sur sources

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 17:17:53 +02:00
parent 7609d35287
commit d064f8d28e
54 changed files with 2861 additions and 116 deletions
+115
View File
@@ -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;
}
}