diff --git a/app/Http/Controllers/ImportController.php b/app/Http/Controllers/ImportController.php new file mode 100644 index 0000000..9fc3fcb --- /dev/null +++ b/app/Http/Controllers/ImportController.php @@ -0,0 +1,131 @@ +authorize('create', [Releve::class, $source]); + + $source->load('sourceType.fields'); + + return view('sources.import', compact('source')); + } + + public function store(Request $request, Source $source): RedirectResponse + { + $this->authorize('create', [Releve::class, $source]); + + $request->validate([ + 'fichier' => ['required', 'file', 'mimes:csv,txt', 'max:10240'], + ]); + + $source->load('sourceType.fields'); + $fieldsByLabel = $source->sourceType->fields->keyBy('label'); + + $path = $request->file('fichier')->getRealPath(); + $handle = fopen($path, 'r'); + + // Suppression du BOM UTF-8 éventuel + $bom = fread($handle, 3); + if ($bom !== "\xEF\xBB\xBF") { + rewind($handle); + } + + // Détection automatique du séparateur (; ou ,) + $firstLine = fgets($handle); + rewind($handle); + if ($bom === "\xEF\xBB\xBF") { + fread($handle, 3); + } + $sep = substr_count($firstLine, ';') >= substr_count($firstLine, ',') ? ';' : ','; + + $header = fgetcsv($handle, 0, $sep); + if (! $header) { + fclose($handle); + return back()->with('error', 'Fichier CSV vide ou invalide.'); + } + $header = array_map('trim', $header); + + $imported = 0; + $userId = auth()->id(); + + while (($line = fgetcsv($handle, 0, $sep)) !== false) { + if (count(array_filter($line, fn ($v) => $v !== '')) === 0) { + continue; + } + + $data = []; + foreach ($header as $i => $label) { + $field = $fieldsByLabel->get($label); + if (! $field) { + continue; + } + $data[$field->name] = $this->parseValue(trim($line[$i] ?? ''), $field->type); + } + + $source->releves()->create([ + 'data' => $data, + 'created_by' => $userId, + 'updated_by' => $userId, + ]); + $imported++; + } + + fclose($handle); + + if ($imported === 0) { + return back()->with('error', 'Aucun relevé importé — vérifiez que les en-têtes correspondent aux libellés des champs.'); + } + + return redirect()->route('sources.releves.index', $source) + ->with('success', "{$imported} relevé(s) importé(s) avec succès."); + } + + private function parseValue(string $raw, FieldType $type): mixed + { + return match ($type) { + FieldType::Boolean => in_array(mb_strtolower($raw), ['oui', 'yes', '1', 'true'], true), + FieldType::Number => $raw !== '' ? (float) str_replace(',', '.', $raw) : null, + FieldType::Date => $this->parseDate($raw), + FieldType::Place => $this->parsePlace($raw), + default => $raw !== '' ? $raw : null, + }; + } + + private function parseDate(string $raw): array + { + if ($raw === '') { + return ['valeur' => null, 'calendrier' => 'gregorien']; + } + // Format export : "YYYY-MM-DD" ou "YYYY-MM-DD (calendrier)" + if (preg_match('/^(.+?)\s*\((\w+)\)\s*$/', $raw, $m)) { + return ['valeur' => trim($m[1]), 'calendrier' => trim($m[2])]; + } + return ['valeur' => $raw, 'calendrier' => 'gregorien']; + } + + private function parsePlace(string $raw): ?array + { + if ($raw === '') { + return null; + } + $like = DbCompat::like(); + $lieu = Lieu::where('nom_long', $raw)->first(['id', 'nom_long']) + ?? Lieu::where('nom_long', $like, $raw . '%')->first(['id', 'nom_long']); + + return $lieu + ? ['id' => $lieu->id, 'nom_long' => $lieu->nom_long] + : ['id' => null, 'nom_long' => $raw]; + } +} diff --git a/resources/views/sources/import.blade.php b/resources/views/sources/import.blade.php new file mode 100644 index 0000000..66d485e --- /dev/null +++ b/resources/views/sources/import.blade.php @@ -0,0 +1,84 @@ + + +
+

Importer des relevés

+

+ Source : {{ $source->nom }} +

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

{{ $message }}

+ @enderror +
+ +
+ +
+
+
+ + {{-- Format attendu --}} +
+

Format attendu

+
    +
  • Encodage UTF-8, séparateur point-virgule (;) ou virgule (,)
  • +
  • Première ligne = en-têtes correspondant aux libellés des champs
  • +
  • Dates au format AAAA-MM-JJ, ou AAAA-MM-JJ (calendrier) pour julien/républicain
  • +
  • Booléens : Oui / Non
  • +
  • Lieux : nom long du lieu (ex : Bordeaux, Gironde, France)
  • +
+ @if($source->releves->isNotEmpty() ?? $source->releves()->exists()) +

+ Astuce : utilisez + l'export CSV + d'une source existante comme modèle. +

+ @endif + + @if($source->sourceType->fields->isNotEmpty()) +
+

Colonnes attendues pour ce type de source

+
+ @foreach($source->sourceType->fields->sortBy('order') as $field) + + {{ $field->label }} + @if($field->required) + * + @endif + ({{ $field->type->value }}) + + @endforeach +
+
+ @endif +
+ + ← Retour à la source +
+
diff --git a/resources/views/sources/show.blade.php b/resources/views/sources/show.blade.php index b50dbeb..80b59d3 100644 --- a/resources/views/sources/show.blade.php +++ b/resources/views/sources/show.blade.php @@ -142,6 +142,10 @@ @endif @can('create', [App\Models\Releve::class, $source]) + + ↑ Importer CSV + + Nouveau relevé diff --git a/routes/web.php b/routes/web.php index 4d93762..a6d580d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -3,6 +3,7 @@ use App\Http\Controllers\CarteController; use App\Http\Controllers\DashboardController; use App\Http\Controllers\ExportController; +use App\Http\Controllers\ImportController; use App\Http\Controllers\LieuController; use App\Http\Controllers\NotificationController; use App\Http\Controllers\ProfileController; @@ -61,6 +62,8 @@ Route::middleware('auth')->group(function () { Route::get('carte', [CarteController::class, 'index'])->name('carte'); Route::get('carte/data', [CarteController::class, 'data'])->name('carte.data'); Route::get('export/source/{source}/csv', [ExportController::class, 'sourceCsv'])->name('export.source.csv'); + Route::get('sources/{source}/import', [ImportController::class, 'create'])->name('sources.import.create'); + Route::post('sources/{source}/import', [ImportController::class, 'store'])->name('sources.import.store'); Route::get('notifications', [NotificationController::class, 'index'])->name('notifications.index'); Route::post('notifications/{id}/read', [NotificationController::class, 'markAsRead'])->name('notifications.read');