diff --git a/app/Http/Controllers/BulkEventUploadController.php b/app/Http/Controllers/BulkEventUploadController.php index bd5a9fe45..b8625442a 100644 --- a/app/Http/Controllers/BulkEventUploadController.php +++ b/app/Http/Controllers/BulkEventUploadController.php @@ -30,13 +30,17 @@ public function index(Request $request): View $validationPassed = $request->session()->get(self::SESSION_VALIDATION_PASSED, false); $filePath = $request->session()->get(self::SESSION_FILE_PATH); $tempDisk = config('filesystems.bulk_upload_temp_disk', 'local'); - $importPayload = ''; - if ($validationPassed && $filePath) { - $importPayload = Crypt::encryptString(json_encode([ - 'path' => $filePath, - 'default_creator_email' => $request->session()->get(self::SESSION_DEFAULT_CREATOR), - 'disk' => $tempDisk, - ])); + $importPayload = $request->session()->get('bulk_upload_import_payload'); + if ($importPayload === null || $importPayload === '') { + if ($validationPassed && $filePath) { + $importPayload = Crypt::encryptString(json_encode([ + 'path' => $filePath, + 'default_creator_email' => $request->session()->get(self::SESSION_DEFAULT_CREATOR), + 'disk' => $tempDisk, + ])); + } else { + $importPayload = ''; + } } return view('admin.bulk-upload.index', [ @@ -125,8 +129,60 @@ function ($attribute, $value, $fail) { $request->session()->put(self::SESSION_VALIDATION_PASSED, true); $request->session()->forget(self::SESSION_VALIDATION_MISSING); - return redirect()->route('admin.bulk-upload.index') - ->with('success', 'File uploaded. All required columns are present. Click Import to run the import.'); + $defaultCreatorEmail = $validated['default_creator_email'] ?? null; + $importPayload = Crypt::encryptString(json_encode([ + 'path' => $path, + 'default_creator_email' => $defaultCreatorEmail, + 'disk' => $tempDisk, + ])); + + $result = new BulkEventImportResult; + $import = new GenericEventsImport($defaultCreatorEmail, $result, true); + Excel::import($import, $path, $tempDisk); + + $rowStatuses = []; + foreach ($result->valid as $row => $_) { + $rowStatuses[$row] = ['row' => $row, 'valid' => true]; + } + foreach ($result->failures as $row => $reason) { + $rowStatuses[$row] = ['row' => $row, 'valid' => false, 'message' => $reason]; + } + ksort($rowStatuses); + + return redirect()->route('admin.bulk-upload.preview') + ->with('bulk_upload_import_payload', $importPayload) + ->with('bulk_upload_row_statuses', array_values($rowStatuses)); + } + + /** + * Show validation preview: row-by-row green/red status. Payload from flash or session. + */ + public function preview(Request $request): View|RedirectResponse + { + $importPayload = $request->session()->get('bulk_upload_import_payload'); + $rowStatuses = $request->session()->get('bulk_upload_row_statuses', []); + + if ($importPayload === null || $importPayload === '') { + $filePath = $request->session()->get(self::SESSION_FILE_PATH); + $tempDisk = config('filesystems.bulk_upload_temp_disk', 'local'); + if ($filePath) { + $importPayload = Crypt::encryptString(json_encode([ + 'path' => $filePath, + 'default_creator_email' => $request->session()->get(self::SESSION_DEFAULT_CREATOR), + 'disk' => $tempDisk, + ])); + } + } + + if ($importPayload === null || $importPayload === '') { + return redirect()->route('admin.bulk-upload.index') + ->withErrors(['import' => 'No validated file found. Please upload and validate a file first.']); + } + + return view('admin.bulk-upload.preview', [ + 'import_payload' => $importPayload, + 'row_statuses' => $rowStatuses, + ]); } /** diff --git a/app/Imports/GenericEventsImport.php b/app/Imports/GenericEventsImport.php index f448adfa2..d81e1975c 100644 --- a/app/Imports/GenericEventsImport.php +++ b/app/Imports/GenericEventsImport.php @@ -22,13 +22,16 @@ class GenericEventsImport extends BaseEventsImport implements ToModel, WithCusto protected ?BulkEventImportResult $result = null; + protected bool $previewMode = false; + /** Current Excel row number (2 = first data row after header). */ protected int $currentRow = 2; - public function __construct(?string $defaultCreatorEmail = null, ?BulkEventImportResult $result = null) + public function __construct(?string $defaultCreatorEmail = null, ?BulkEventImportResult $result = null, bool $previewMode = false) { $this->defaultCreatorEmail = $defaultCreatorEmail ? trim($defaultCreatorEmail) : null; $this->result = $result; + $this->previewMode = $previewMode; } /** @@ -311,6 +314,13 @@ public function model(array $row): ?Model $attrs['contact_person'] = trim($row['contact_email']); } + // Preview mode: record row as valid and skip persistence + if ($this->previewMode && $this->result) { + $this->result->addValid($rowIndex); + + return null; + } + // 8) duplicate check: find existing by title + start_date + country_iso + organizer; if found, update instead of create $existing = Event::where('title', $attrs['title']) ->where('start_date', $attrs['start_date']) diff --git a/app/Services/BulkEventImportResult.php b/app/Services/BulkEventImportResult.php index 43efef826..3a1394c44 100644 --- a/app/Services/BulkEventImportResult.php +++ b/app/Services/BulkEventImportResult.php @@ -12,11 +12,19 @@ class BulkEventImportResult /** @var array Created events for report links */ public array $created = []; + /** @var array Row index (1-based) => valid (for preview) */ + public array $valid = []; + public function addFailure(int $rowIndex, string $reason): void { $this->failures[$rowIndex] = $reason; } + public function addValid(int $rowIndex): void + { + $this->valid[$rowIndex] = true; + } + public function addCreated(Event $event): void { $this->created[] = [ diff --git a/resources/views/admin/bulk-upload/index.blade.php b/resources/views/admin/bulk-upload/index.blade.php index 6c0f1bcd2..a0cf37757 100644 --- a/resources/views/admin/bulk-upload/index.blade.php +++ b/resources/views/admin/bulk-upload/index.blade.php @@ -52,7 +52,7 @@ class="w-full max-w-md px-3 py-2 border rounded"
+ class="text-sm file:mr-2 file:py-2 file:px-4 file:rounded-full file:border-0 file:font-semibold file:bg-primary file:text-white hover:file:opacity-90 file:cursor-pointer cursor-pointer" aria-required="true"> No file chosen
@@ -91,7 +91,7 @@ class="text-sm file:mr-2 file:py-2 file:px-4 file:rounded-full file:border-0 fil form.addEventListener('submit', function () { if (!fileInput.files || fileInput.files.length === 0) return; btn.disabled = true; - btn.textContent = 'Uploading…'; + btn.textContent = 'Validating…'; }); })(); diff --git a/resources/views/admin/bulk-upload/preview.blade.php b/resources/views/admin/bulk-upload/preview.blade.php new file mode 100644 index 000000000..190870635 --- /dev/null +++ b/resources/views/admin/bulk-upload/preview.blade.php @@ -0,0 +1,73 @@ +@extends('layout.base') + +@section('content') +
+
+

Bulk Event Upload – validation preview

+ Upload another file +
+ +
+

Rows below show validation results: green = valid, red = problem. You can still run the import; invalid rows will be skipped. Click Import to run the import.

+ + @if ($errors->any()) +
+
    + @foreach ($errors->all() as $error) +
  • {{ $error }}
  • + @endforeach +
+
+ @endif + +
+ + + + + + + + + + @forelse ($row_statuses as $item) + + + + + + @empty + + + + @endforelse + +
RowStatusDetails
{{ $item['row'] }} + @if ($item['valid']) + Valid + @else + Problem + @endif + + @if ($item['valid']) + — + @else + {{ $item['message'] ?? 'Validation failed' }} + @endif +
No data rows to show.
+
+ +
+ @csrf + + +
+ +
+
+@endsection diff --git a/routes/web.php b/routes/web.php index 53ae36e81..e33a54008 100644 --- a/routes/web.php +++ b/routes/web.php @@ -73,6 +73,7 @@ Route::middleware(['auth', 'role:super admin'])->group(function () { Route::get('/admin/bulk-upload', [BulkEventUploadController::class, 'index'])->name('admin.bulk-upload.index'); Route::post('/admin/bulk-upload/validate', [BulkEventUploadController::class, 'validateUpload'])->name('admin.bulk-upload.validate'); + Route::get('/admin/bulk-upload/preview', [BulkEventUploadController::class, 'preview'])->name('admin.bulk-upload.preview'); Route::post('/admin/bulk-upload/import', [BulkEventUploadController::class, 'import'])->name('admin.bulk-upload.import'); Route::get('/admin/bulk-upload/import', fn () => redirect()->route('admin.bulk-upload.index'))->name('admin.bulk-upload.import.get'); Route::get('/admin/bulk-upload/report', [BulkEventUploadController::class, 'report'])->name('admin.bulk-upload.report');