É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\Requests\Admin\StoreSectionRequest;
|
||||
use App\Http\Requests\Admin\UpdateSectionRequest;
|
||||
use App\Models\Lieu;
|
||||
use App\Models\Section;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
@@ -22,8 +21,7 @@ class SectionController extends Controller
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
$lieux = Lieu::orderBy('nom_long')->get(['id', 'nom_long', 'nom']);
|
||||
return view('admin.sections.create', compact('lieux'));
|
||||
return view('admin.sections.create');
|
||||
}
|
||||
|
||||
public function store(StoreSectionRequest $request): RedirectResponse
|
||||
@@ -42,8 +40,8 @@ class SectionController extends Controller
|
||||
|
||||
public function edit(Section $section): View
|
||||
{
|
||||
$lieux = Lieu::orderBy('nom_long')->get(['id', 'nom_long', 'nom']);
|
||||
return view('admin.sections.edit', compact('section', 'lieux'));
|
||||
$section->load('lieu');
|
||||
return view('admin.sections.edit', compact('section'));
|
||||
}
|
||||
|
||||
public function update(UpdateSectionRequest $request, Section $section): RedirectResponse
|
||||
|
||||
@@ -4,5 +4,5 @@ namespace App\Http\Controllers;
|
||||
|
||||
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\UpdateLieuRequest;
|
||||
use App\Models\Lieu;
|
||||
use App\Models\LieuType;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\View\View;
|
||||
|
||||
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);
|
||||
|
||||
// Arbre complet trié par nom_long pour l'affichage
|
||||
$lieux = Lieu::with('parent')
|
||||
->orderBy('nom_long')
|
||||
->paginate(50);
|
||||
$query = Lieu::with(['parent', 'lieuType'])->orderBy('nom_long');
|
||||
|
||||
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
|
||||
{
|
||||
$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
|
||||
@@ -43,7 +90,7 @@ class LieuController extends Controller
|
||||
{
|
||||
$this->authorize('view', $lieu);
|
||||
|
||||
$lieu->load('parent', 'enfants');
|
||||
$lieu->load('parent', 'enfants', 'lieuType');
|
||||
|
||||
return view('lieux.show', compact('lieu'));
|
||||
}
|
||||
@@ -52,20 +99,15 @@ class LieuController extends Controller
|
||||
{
|
||||
$this->authorize('update', $lieu);
|
||||
|
||||
// Exclure le lieu lui-même et ses descendants pour éviter les cycles
|
||||
$descendants = $this->getDescendantIds($lieu);
|
||||
$parents = Lieu::whereNotIn('id', [...$descendants, $lieu->id])
|
||||
->orderBy('nom_long')
|
||||
->get(['id', 'nom_long']);
|
||||
$lieu->load('parent', 'lieuType');
|
||||
$lieuTypes = LieuType::orderBy('ordre')->get(['id', 'nom']);
|
||||
|
||||
return view('lieux.edit', compact('lieu', 'parents'));
|
||||
return view('lieux.edit', compact('lieu', 'lieuTypes'));
|
||||
}
|
||||
|
||||
public function update(UpdateLieuRequest $request, Lieu $lieu): RedirectResponse
|
||||
{
|
||||
$lieu->update($request->validated());
|
||||
|
||||
// Recalculer nom_long des enfants en cascade
|
||||
$this->recalculerEnfants($lieu);
|
||||
|
||||
return redirect()->route('lieux.show', $lieu)
|
||||
@@ -86,12 +128,27 @@ class LieuController extends Controller
|
||||
->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
|
||||
{
|
||||
$ids = [];
|
||||
foreach ($lieu->enfants as $enfant) {
|
||||
$ids[] = $enfant->id;
|
||||
$ids = array_merge($ids, $this->getDescendantIds($enfant));
|
||||
$ids = array_merge($ids, $this->getDescendantIds($enfant));
|
||||
}
|
||||
return $ids;
|
||||
}
|
||||
@@ -100,7 +157,7 @@ class LieuController extends Controller
|
||||
{
|
||||
$lieu->load('enfants');
|
||||
foreach ($lieu->enfants as $enfant) {
|
||||
$enfant->update([]); // déclenche le booted() hook qui recalcule nom_long
|
||||
$enfant->update([]);
|
||||
$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\UpdateSourceRequest;
|
||||
use App\Models\Depot;
|
||||
use App\Models\Lieu;
|
||||
use App\Models\Source;
|
||||
use App\Models\SourceType;
|
||||
use App\Models\User;
|
||||
use App\Notifications\SourceAValiderNotification;
|
||||
use App\Notifications\SourceRejeteeNotification;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class SourceController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorize('viewAny', Source::class);
|
||||
|
||||
$user = auth()->user();
|
||||
|
||||
$query = Source::with(['sourceType', 'depot'])
|
||||
$query = Source::with(['sourceType', 'depot', 'lieu'])
|
||||
->withCount('releves');
|
||||
|
||||
if (! $user->isSectionManager()) {
|
||||
// Membre : sources terminées + sources assignées
|
||||
$assignedIds = $user->sourcesAssignees()->pluck('sources.id');
|
||||
$query->where(function ($q) use ($assignedIds) {
|
||||
$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
|
||||
@@ -71,6 +121,7 @@ class SourceController extends Controller
|
||||
{
|
||||
$this->authorize('update', $source);
|
||||
|
||||
$source->loadMissing('lieu');
|
||||
$sourceTypes = SourceType::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.');
|
||||
}
|
||||
|
||||
$previousStatus = $source->status;
|
||||
$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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user