diff --git a/app/Http/Controllers/LieuController.php b/app/Http/Controllers/LieuController.php index d2aa5ee..e087e73 100644 --- a/app/Http/Controllers/LieuController.php +++ b/app/Http/Controllers/LieuController.php @@ -10,6 +10,7 @@ use App\Support\DbCompat; use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; +use Illuminate\Http\Response; use Illuminate\Support\Facades\DB; use Illuminate\View\View; @@ -129,6 +130,112 @@ class LieuController extends Controller ->with('success', 'Lieu supprimé.'); } + public function exportCsv(): Response + { + $this->authorize('viewAny', Lieu::class); + + $lieux = Lieu::with(['lieuType', 'parent'])->orderBy('nom_long')->get(); + + $handle = fopen('php://temp', 'r+'); + fwrite($handle, "\xEF\xBB\xBF"); + + fputcsv($handle, ['nom', 'code', 'type', 'lieu_parent', 'latitude', 'longitude', 'note'], ';'); + + foreach ($lieux as $lieu) { + fputcsv($handle, [ + $lieu->nom, + $lieu->code ?? '', + $lieu->lieuType?->nom ?? '', + $lieu->parent?->nom_long ?? '', + $lieu->latitude ?? '', + $lieu->longitude ?? '', + $lieu->note ?? '', + ], ';'); + } + + rewind($handle); + $csv = stream_get_contents($handle); + fclose($handle); + + return response($csv, 200, [ + 'Content-Type' => 'text/csv; charset=UTF-8', + 'Content-Disposition' => 'attachment; filename="lieux.csv"', + ]); + } + + public function importCreate(): View + { + $this->authorize('create', Lieu::class); + + return view('lieux.import'); + } + + public function importStore(Request $request): RedirectResponse + { + $this->authorize('create', Lieu::class); + + $request->validate([ + 'fichier' => ['required', 'file', 'mimes:csv,txt', 'max:10240'], + ]); + + $handle = fopen($request->file('fichier')->getRealPath(), 'r'); + + $bom = fread($handle, 3); + if ($bom !== "\xEF\xBB\xBF") { + rewind($handle); + } + + $firstLine = fgets($handle); + rewind($handle); + if ($bom === "\xEF\xBB\xBF") { + fread($handle, 3); + } + $sep = substr_count($firstLine, ';') >= substr_count($firstLine, ',') ? ';' : ','; + + $header = array_map('trim', fgetcsv($handle, 0, $sep) ?: []); + $colIdx = array_flip($header); + + $lieuTypesCache = LieuType::all()->keyBy('nom'); + $imported = 0; + + while (($line = fgetcsv($handle, 0, $sep)) !== false) { + $nom = trim($line[$colIdx['nom'] ?? -1] ?? ''); + if ($nom === '') { + continue; + } + + $parentNomLong = trim($line[$colIdx['lieu_parent'] ?? -1] ?? ''); + $typeName = trim($line[$colIdx['type'] ?? -1] ?? ''); + + $parentId = $parentNomLong + ? Lieu::where('nom_long', $parentNomLong)->value('id') + : null; + $lieuTypeId = $typeName + ? $lieuTypesCache->get($typeName)?->id + : null; + + Lieu::create([ + 'nom' => $nom, + 'code' => trim($line[$colIdx['code'] ?? -1] ?? '') ?: null, + 'lieu_type_id' => $lieuTypeId, + 'lieu_parent_id'=> $parentId, + 'latitude' => ($v = trim($line[$colIdx['latitude'] ?? -1] ?? '')) !== '' ? (float) str_replace(',', '.', $v) : null, + 'longitude' => ($v = trim($line[$colIdx['longitude'] ?? -1] ?? '')) !== '' ? (float) str_replace(',', '.', $v) : null, + 'note' => trim($line[$colIdx['note'] ?? -1] ?? '') ?: null, + ]); + $imported++; + } + + fclose($handle); + + if ($imported === 0) { + return back()->with('error', 'Aucun lieu importé — vérifiez que la colonne « nom » est présente.'); + } + + return redirect()->route('lieux.index') + ->with('success', "{$imported} lieu(x) importé(s) avec succès."); + } + private function getDescendantAndSelfIds(int $lieuId): array { $rows = DB::select(" diff --git a/resources/views/lieux/import.blade.php b/resources/views/lieux/import.blade.php new file mode 100644 index 0000000..61d7b84 --- /dev/null +++ b/resources/views/lieux/import.blade.php @@ -0,0 +1,80 @@ + + +

Importer des lieux

+
+ +
+ + @if(session('error')) +
+ {{ session('error') }} +
+ @endif + +
+
+ @csrf +
+ + + @error('fichier') +

{{ $message }}

+ @enderror +
+
+ +
+
+
+ +
+

Format attendu

+
    +
  • Encodage UTF-8, séparateur point-virgule (;) ou virgule (,)
  • +
  • Première ligne = en-têtes (noms exacts ci-dessous)
  • +
  • Le parent est identifié par son nom long complet (ex : Gironde, France)
  • +
  • Le type est identifié par son nom exact tel que défini dans les types de lieux
  • +
+ +
+ + + + @foreach(['nom *', 'code', 'type', 'lieu_parent', 'latitude', 'longitude', 'note'] as $col) + + @endforeach + + + + + + + + + + + + + +
{{ $col }}
Bordeaux33063CommuneGironde, France44.8378-0.5792
+
+ +

+ Astuce : utilisez + l'export CSV + de la liste des lieux comme modèle. +

+
+ + ← Retour aux lieux +
+
diff --git a/resources/views/lieux/index.blade.php b/resources/views/lieux/index.blade.php index c031dc5..4ac2c1c 100644 --- a/resources/views/lieux/index.blade.php +++ b/resources/views/lieux/index.blade.php @@ -2,12 +2,22 @@

Lieux

- @can('create', App\Models\Lieu::class) - - + Nouveau lieu +
+ + ↓ CSV - @endcan + @can('create', App\Models\Lieu::class) + + ↑ Importer CSV + + + + Nouveau lieu + + @endcan +
diff --git a/routes/web.php b/routes/web.php index a6d580d..e4996ae 100644 --- a/routes/web.php +++ b/routes/web.php @@ -46,7 +46,10 @@ Route::middleware('auth')->group(function () { Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); - Route::get('lieux/search', [LieuController::class, 'search'])->name('lieux.search'); + Route::get('lieux/search', [LieuController::class, 'search'])->name('lieux.search'); + Route::get('lieux/export/csv', [LieuController::class, 'exportCsv'])->name('lieux.export.csv'); + Route::get('lieux/import', [LieuController::class, 'importCreate'])->name('lieux.import.create'); + Route::post('lieux/import', [LieuController::class, 'importStore'])->name('lieux.import.store'); Route::resource('lieux', LieuController::class)->parameters(['lieux' => 'lieu']); Route::resource('sources', SourceController::class);