Skip to content
Merged

Dev #3273

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 65 additions & 9 deletions app/Http/Controllers/BulkEventUploadController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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', [
Expand Down Expand Up @@ -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,
]);
}

/**
Expand Down
12 changes: 11 additions & 1 deletion app/Imports/GenericEventsImport.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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'])
Expand Down
8 changes: 8 additions & 0 deletions app/Services/BulkEventImportResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,19 @@ class BulkEventImportResult
/** @var array<int, array{id: int, title: string, url: string}> Created events for report links */
public array $created = [];

/** @var array<int, true> 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[] = [
Expand Down
4 changes: 2 additions & 2 deletions resources/views/admin/bulk-upload/index.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class="w-full max-w-md px-3 py-2 border rounded"
<div class="flex flex-wrap items-center gap-2 mb-1">
<label for="bulk-upload-file" class="font-medium">Excel / CSV file <span class="text-red-600">*</span></label>
<input type="file" name="file" id="bulk-upload-file" accept=".csv,.xlsx,.xls" required
class="text-sm file:mr-2 file:py-2 file:px-4 file:rounded-full file:border-0 file:font-semibold file:bg-gray-200 file:text-gray-700 hover:file:bg-gray-300 file:cursor-pointer cursor-pointer" aria-required="true">
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">
<span id="bulk-upload-file-name" class="text-sm text-gray-600 italic">No file chosen</span>
<span id="bulk-upload-file-attached" class="hidden text-sm font-medium text-green-700 bg-green-100 px-2 py-0.5 rounded">Attached</span>
</div>
Expand Down Expand Up @@ -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…';
});
})();
</script>
Expand Down
73 changes: 73 additions & 0 deletions resources/views/admin/bulk-upload/preview.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
@extends('layout.base')

@section('content')
<section id="codeweek-bulk-upload-preview-page" class="codeweek-page">
<section class="codeweek-content-header" style="display: flex; justify-content: space-between; align-items: center;">
<h1>Bulk Event Upload – validation preview</h1>
<a href="{{ route('admin.bulk-upload.index') }}" class="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300">Upload another file</a>
</section>

<section class="codeweek-content-wrapper">
<p class="mb-4">Rows below show validation results: <span class="bg-trans-success text-green-800 px-1 rounded">green</span> = valid, <span class="bg-trans-danger text-red-800 px-1 rounded">red</span> = problem. You can still run the import; invalid rows will be skipped. Click <strong>Import</strong> to run the import.</p>

@if ($errors->any())
<div class="mb-4 p-4 rounded bg-red-50 border border-red-200">
<ul class="list-disc list-inside text-red-700">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif

<div class="overflow-x-auto mb-6">
<table class="w-full border-collapse border border-gray-300">
<thead>
<tr class="bg-gray-100">
<th class="border border-gray-300 px-3 py-2 text-left">Row</th>
<th class="border border-gray-300 px-3 py-2 text-left">Status</th>
<th class="border border-gray-300 px-3 py-2 text-left">Details</th>
</tr>
</thead>
<tbody>
@forelse ($row_statuses as $item)
<tr class="{{ $item['valid'] ? 'bg-trans-success' : 'bg-trans-danger' }}">
<td class="border border-gray-300 px-3 py-2 font-medium">{{ $item['row'] }}</td>
<td class="border border-gray-300 px-3 py-2">
@if ($item['valid'])
<span class="text-green-800 font-medium">Valid</span>
@else
<span class="text-red-800 font-medium">Problem</span>
@endif
</td>
<td class="border border-gray-300 px-3 py-2 {{ $item['valid'] ? 'text-green-700' : 'text-red-700' }}">
@if ($item['valid'])
@else
{{ $item['message'] ?? 'Validation failed' }}
@endif
</td>
</tr>
@empty
<tr>
<td colspan="3" class="border border-gray-300 px-3 py-4 text-gray-500 text-center">No data rows to show.</td>
</tr>
@endforelse
</tbody>
</table>
</div>

<form method="POST" action="{{ route('admin.bulk-upload.import') }}" class="inline" id="bulk-upload-import-form">
@csrf
<input type="hidden" name="import_payload" value="{{ $import_payload ?? '' }}">
<button type="submit" id="bulk-upload-import-btn" class="bg-primary cursor-pointer px-6 py-3 rounded-full font-semibold text-white hover:opacity-90 duration-300">Import</button>
</form>
<script>
document.getElementById('bulk-upload-import-form').addEventListener('submit', function () {
var btn = document.getElementById('bulk-upload-import-btn');
if (btn) { btn.disabled = true; btn.textContent = 'Importing…'; }
});
</script>
</section>
</section>
@endsection
1 change: 1 addition & 0 deletions routes/web.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Loading