Import CSV des relevés d'une source
- ImportController : create() (formulaire) + store() (traitement) - Détection automatique du séparateur ; ou , - Suppression du BOM UTF-8 - Correspondance colonnes ↔ champs par libellé - Parsing des types : date (avec calendrier), booléen, nombre, lieu (recherche par nom_long), texte - Vue sources/import.blade.php : formulaire + liste des colonnes attendues - Routes sources.import.create / sources.import.store - Bouton "↑ Importer CSV" dans la fiche source (soumis aux droits create releve) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\FieldType;
|
||||
use App\Models\Lieu;
|
||||
use App\Models\Releve;
|
||||
use App\Models\Source;
|
||||
use App\Support\DbCompat;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ImportController extends Controller
|
||||
{
|
||||
public function create(Source $source): View
|
||||
{
|
||||
$this->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];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user