Export CSV des relevés d'une source
- ExportController::sourceCsv() : génère un CSV avec BOM UTF-8 (compatible Excel)
séparateur point-virgule, en-têtes = labels des champs, valeurs formatées
(dates avec calendrier si non grégorien, lieux = nom_long, booléens Oui/Non)
- Route GET export/source/{source}/csv → export.source.csv
- Boutons ↓ CSV et ↓ GEDCOM dans la section relevés de la fiche source
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Enums\FieldType;
|
||||||
use App\Enums\SourceStatus;
|
use App\Enums\SourceStatus;
|
||||||
use App\Models\Releve;
|
use App\Models\Releve;
|
||||||
use App\Models\Source;
|
use App\Models\Source;
|
||||||
@@ -17,6 +18,44 @@ class ExportController extends Controller
|
|||||||
private readonly GedcomExportService $gedcom,
|
private readonly GedcomExportService $gedcom,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/** Export CSV de tous les relevés d'une source */
|
||||||
|
public function sourceCsv(Source $source): Response
|
||||||
|
{
|
||||||
|
$this->authorize('view', $source);
|
||||||
|
|
||||||
|
$source->load(['sourceType.fields', 'releves']);
|
||||||
|
$fields = $source->sourceType->fields->sortBy('order');
|
||||||
|
|
||||||
|
$handle = fopen('php://temp', 'r+');
|
||||||
|
|
||||||
|
// BOM UTF-8 pour la compatibilité Excel
|
||||||
|
fwrite($handle, "\xEF\xBB\xBF");
|
||||||
|
|
||||||
|
// En-tête
|
||||||
|
fputcsv($handle, $fields->pluck('label')->toArray(), ';');
|
||||||
|
|
||||||
|
// Lignes
|
||||||
|
foreach ($source->releves as $releve) {
|
||||||
|
$row = [];
|
||||||
|
foreach ($fields as $field) {
|
||||||
|
$val = $releve->data[$field->name] ?? null;
|
||||||
|
$row[] = $this->formatCsvValue($val, $field->type);
|
||||||
|
}
|
||||||
|
fputcsv($handle, $row, ';');
|
||||||
|
}
|
||||||
|
|
||||||
|
rewind($handle);
|
||||||
|
$csv = stream_get_contents($handle);
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
$filename = $this->sanitizeFilename($source->nom) . '.csv';
|
||||||
|
|
||||||
|
return response($csv, 200, [
|
||||||
|
'Content-Type' => 'text/csv; charset=UTF-8',
|
||||||
|
'Content-Disposition' => "attachment; filename=\"{$filename}\"",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/** Export de tous les relevés d'une source */
|
/** Export de tous les relevés d'une source */
|
||||||
public function source(Source $source): Response
|
public function source(Source $source): Response
|
||||||
{
|
{
|
||||||
@@ -106,6 +145,26 @@ class ExportController extends Controller
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function formatCsvValue(mixed $val, FieldType $type): string
|
||||||
|
{
|
||||||
|
if ($val === null || $val === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return match ($type) {
|
||||||
|
FieldType::Boolean => $val ? 'Oui' : 'Non',
|
||||||
|
FieldType::Date => is_array($val)
|
||||||
|
? trim(($val['valeur'] ?? '') . (
|
||||||
|
! empty($val['calendrier']) && $val['calendrier'] !== 'gregorien'
|
||||||
|
? ' (' . $val['calendrier'] . ')'
|
||||||
|
: ''
|
||||||
|
))
|
||||||
|
: (string) $val,
|
||||||
|
FieldType::Place => is_array($val) ? ($val['nom_long'] ?? '') : (string) $val,
|
||||||
|
default => (string) $val,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private function sanitizeFilename(string $name): string
|
private function sanitizeFilename(string $name): string
|
||||||
{
|
{
|
||||||
$name = iconv('UTF-8', 'ASCII//TRANSLIT', $name) ?: $name;
|
$name = iconv('UTF-8', 'ASCII//TRANSLIT', $name) ?: $name;
|
||||||
|
|||||||
@@ -134,6 +134,17 @@
|
|||||||
<div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden">
|
<div class="bg-white dark:bg-gray-800 shadow rounded-lg overflow-hidden">
|
||||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
|
||||||
<h3 class="font-medium text-gray-900 dark:text-white">Relevés ({{ $source->releves->count() }})</h3>
|
<h3 class="font-medium text-gray-900 dark:text-white">Relevés ({{ $source->releves->count() }})</h3>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
@if($source->releves->isNotEmpty())
|
||||||
|
<a href="{{ route('export.source.csv', $source) }}"
|
||||||
|
class="px-3 py-1.5 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 text-xs rounded-md hover:bg-gray-50 dark:hover:bg-gray-600">
|
||||||
|
↓ CSV
|
||||||
|
</a>
|
||||||
|
<a href="{{ route('export.source', $source) }}"
|
||||||
|
class="px-3 py-1.5 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 text-xs rounded-md hover:bg-gray-50 dark:hover:bg-gray-600">
|
||||||
|
↓ GEDCOM
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
@can('create', [App\Models\Releve::class, $source])
|
@can('create', [App\Models\Releve::class, $source])
|
||||||
<a href="{{ route('sources.releves.create', $source) }}"
|
<a href="{{ route('sources.releves.create', $source) }}"
|
||||||
class="px-3 py-1.5 bg-indigo-600 text-white text-xs rounded-md hover:bg-indigo-700">
|
class="px-3 py-1.5 bg-indigo-600 text-white text-xs rounded-md hover:bg-indigo-700">
|
||||||
@@ -141,6 +152,7 @@
|
|||||||
</a>
|
</a>
|
||||||
@endcan
|
@endcan
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
@if($source->releves->isEmpty())
|
@if($source->releves->isEmpty())
|
||||||
<p class="px-6 py-8 text-center text-gray-400 dark:text-gray-500 text-sm">Aucun relevé pour cette source.</p>
|
<p class="px-6 py-8 text-center text-gray-400 dark:text-gray-500 text-sm">Aucun relevé pour cette source.</p>
|
||||||
@else
|
@else
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ Route::middleware('auth')->group(function () {
|
|||||||
Route::get('carte', [CarteController::class, 'index'])->name('carte');
|
Route::get('carte', [CarteController::class, 'index'])->name('carte');
|
||||||
Route::get('carte/data', [CarteController::class, 'data'])->name('carte.data');
|
Route::get('carte/data', [CarteController::class, 'data'])->name('carte.data');
|
||||||
Route::get('export/source/{source}', [ExportController::class, 'source'])->name('export.source');
|
Route::get('export/source/{source}', [ExportController::class, 'source'])->name('export.source');
|
||||||
|
Route::get('export/source/{source}/csv', [ExportController::class, 'sourceCsv'])->name('export.source.csv');
|
||||||
Route::get('export/recherche', [ExportController::class, 'recherche'])->name('export.recherche');
|
Route::get('export/recherche', [ExportController::class, 'recherche'])->name('export.recherche');
|
||||||
|
|
||||||
Route::get('notifications', [NotificationController::class, 'index'])->name('notifications.index');
|
Route::get('notifications', [NotificationController::class, 'index'])->name('notifications.index');
|
||||||
|
|||||||
Reference in New Issue
Block a user