Skip to content

Commit a64861a

Browse files
authored
Merge pull request #3273 from codeeu/dev
Dev
2 parents 9947023 + 25635ca commit a64861a

File tree

6 files changed

+160
-12
lines changed

6 files changed

+160
-12
lines changed

app/Http/Controllers/BulkEventUploadController.php

Lines changed: 65 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,17 @@ public function index(Request $request): View
3030
$validationPassed = $request->session()->get(self::SESSION_VALIDATION_PASSED, false);
3131
$filePath = $request->session()->get(self::SESSION_FILE_PATH);
3232
$tempDisk = config('filesystems.bulk_upload_temp_disk', 'local');
33-
$importPayload = '';
34-
if ($validationPassed && $filePath) {
35-
$importPayload = Crypt::encryptString(json_encode([
36-
'path' => $filePath,
37-
'default_creator_email' => $request->session()->get(self::SESSION_DEFAULT_CREATOR),
38-
'disk' => $tempDisk,
39-
]));
33+
$importPayload = $request->session()->get('bulk_upload_import_payload');
34+
if ($importPayload === null || $importPayload === '') {
35+
if ($validationPassed && $filePath) {
36+
$importPayload = Crypt::encryptString(json_encode([
37+
'path' => $filePath,
38+
'default_creator_email' => $request->session()->get(self::SESSION_DEFAULT_CREATOR),
39+
'disk' => $tempDisk,
40+
]));
41+
} else {
42+
$importPayload = '';
43+
}
4044
}
4145

4246
return view('admin.bulk-upload.index', [
@@ -125,8 +129,60 @@ function ($attribute, $value, $fail) {
125129
$request->session()->put(self::SESSION_VALIDATION_PASSED, true);
126130
$request->session()->forget(self::SESSION_VALIDATION_MISSING);
127131

128-
return redirect()->route('admin.bulk-upload.index')
129-
->with('success', 'File uploaded. All required columns are present. Click Import to run the import.');
132+
$defaultCreatorEmail = $validated['default_creator_email'] ?? null;
133+
$importPayload = Crypt::encryptString(json_encode([
134+
'path' => $path,
135+
'default_creator_email' => $defaultCreatorEmail,
136+
'disk' => $tempDisk,
137+
]));
138+
139+
$result = new BulkEventImportResult;
140+
$import = new GenericEventsImport($defaultCreatorEmail, $result, true);
141+
Excel::import($import, $path, $tempDisk);
142+
143+
$rowStatuses = [];
144+
foreach ($result->valid as $row => $_) {
145+
$rowStatuses[$row] = ['row' => $row, 'valid' => true];
146+
}
147+
foreach ($result->failures as $row => $reason) {
148+
$rowStatuses[$row] = ['row' => $row, 'valid' => false, 'message' => $reason];
149+
}
150+
ksort($rowStatuses);
151+
152+
return redirect()->route('admin.bulk-upload.preview')
153+
->with('bulk_upload_import_payload', $importPayload)
154+
->with('bulk_upload_row_statuses', array_values($rowStatuses));
155+
}
156+
157+
/**
158+
* Show validation preview: row-by-row green/red status. Payload from flash or session.
159+
*/
160+
public function preview(Request $request): View|RedirectResponse
161+
{
162+
$importPayload = $request->session()->get('bulk_upload_import_payload');
163+
$rowStatuses = $request->session()->get('bulk_upload_row_statuses', []);
164+
165+
if ($importPayload === null || $importPayload === '') {
166+
$filePath = $request->session()->get(self::SESSION_FILE_PATH);
167+
$tempDisk = config('filesystems.bulk_upload_temp_disk', 'local');
168+
if ($filePath) {
169+
$importPayload = Crypt::encryptString(json_encode([
170+
'path' => $filePath,
171+
'default_creator_email' => $request->session()->get(self::SESSION_DEFAULT_CREATOR),
172+
'disk' => $tempDisk,
173+
]));
174+
}
175+
}
176+
177+
if ($importPayload === null || $importPayload === '') {
178+
return redirect()->route('admin.bulk-upload.index')
179+
->withErrors(['import' => 'No validated file found. Please upload and validate a file first.']);
180+
}
181+
182+
return view('admin.bulk-upload.preview', [
183+
'import_payload' => $importPayload,
184+
'row_statuses' => $rowStatuses,
185+
]);
130186
}
131187

132188
/**

app/Imports/GenericEventsImport.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,16 @@ class GenericEventsImport extends BaseEventsImport implements ToModel, WithCusto
2222

2323
protected ?BulkEventImportResult $result = null;
2424

25+
protected bool $previewMode = false;
26+
2527
/** Current Excel row number (2 = first data row after header). */
2628
protected int $currentRow = 2;
2729

28-
public function __construct(?string $defaultCreatorEmail = null, ?BulkEventImportResult $result = null)
30+
public function __construct(?string $defaultCreatorEmail = null, ?BulkEventImportResult $result = null, bool $previewMode = false)
2931
{
3032
$this->defaultCreatorEmail = $defaultCreatorEmail ? trim($defaultCreatorEmail) : null;
3133
$this->result = $result;
34+
$this->previewMode = $previewMode;
3235
}
3336

3437
/**
@@ -311,6 +314,13 @@ public function model(array $row): ?Model
311314
$attrs['contact_person'] = trim($row['contact_email']);
312315
}
313316

317+
// Preview mode: record row as valid and skip persistence
318+
if ($this->previewMode && $this->result) {
319+
$this->result->addValid($rowIndex);
320+
321+
return null;
322+
}
323+
314324
// 8) duplicate check: find existing by title + start_date + country_iso + organizer; if found, update instead of create
315325
$existing = Event::where('title', $attrs['title'])
316326
->where('start_date', $attrs['start_date'])

app/Services/BulkEventImportResult.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,19 @@ class BulkEventImportResult
1212
/** @var array<int, array{id: int, title: string, url: string}> Created events for report links */
1313
public array $created = [];
1414

15+
/** @var array<int, true> Row index (1-based) => valid (for preview) */
16+
public array $valid = [];
17+
1518
public function addFailure(int $rowIndex, string $reason): void
1619
{
1720
$this->failures[$rowIndex] = $reason;
1821
}
1922

23+
public function addValid(int $rowIndex): void
24+
{
25+
$this->valid[$rowIndex] = true;
26+
}
27+
2028
public function addCreated(Event $event): void
2129
{
2230
$this->created[] = [

resources/views/admin/bulk-upload/index.blade.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class="w-full max-w-md px-3 py-2 border rounded"
5252
<div class="flex flex-wrap items-center gap-2 mb-1">
5353
<label for="bulk-upload-file" class="font-medium">Excel / CSV file <span class="text-red-600">*</span></label>
5454
<input type="file" name="file" id="bulk-upload-file" accept=".csv,.xlsx,.xls" required
55-
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">
55+
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">
5656
<span id="bulk-upload-file-name" class="text-sm text-gray-600 italic">No file chosen</span>
5757
<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>
5858
</div>
@@ -91,7 +91,7 @@ class="text-sm file:mr-2 file:py-2 file:px-4 file:rounded-full file:border-0 fil
9191
form.addEventListener('submit', function () {
9292
if (!fileInput.files || fileInput.files.length === 0) return;
9393
btn.disabled = true;
94-
btn.textContent = 'Uploading';
94+
btn.textContent = 'Validating';
9595
});
9696
})();
9797
</script>
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
@extends('layout.base')
2+
3+
@section('content')
4+
<section id="codeweek-bulk-upload-preview-page" class="codeweek-page">
5+
<section class="codeweek-content-header" style="display: flex; justify-content: space-between; align-items: center;">
6+
<h1>Bulk Event Upload – validation preview</h1>
7+
<a href="{{ route('admin.bulk-upload.index') }}" class="px-4 py-2 bg-gray-200 rounded hover:bg-gray-300">Upload another file</a>
8+
</section>
9+
10+
<section class="codeweek-content-wrapper">
11+
<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>
12+
13+
@if ($errors->any())
14+
<div class="mb-4 p-4 rounded bg-red-50 border border-red-200">
15+
<ul class="list-disc list-inside text-red-700">
16+
@foreach ($errors->all() as $error)
17+
<li>{{ $error }}</li>
18+
@endforeach
19+
</ul>
20+
</div>
21+
@endif
22+
23+
<div class="overflow-x-auto mb-6">
24+
<table class="w-full border-collapse border border-gray-300">
25+
<thead>
26+
<tr class="bg-gray-100">
27+
<th class="border border-gray-300 px-3 py-2 text-left">Row</th>
28+
<th class="border border-gray-300 px-3 py-2 text-left">Status</th>
29+
<th class="border border-gray-300 px-3 py-2 text-left">Details</th>
30+
</tr>
31+
</thead>
32+
<tbody>
33+
@forelse ($row_statuses as $item)
34+
<tr class="{{ $item['valid'] ? 'bg-trans-success' : 'bg-trans-danger' }}">
35+
<td class="border border-gray-300 px-3 py-2 font-medium">{{ $item['row'] }}</td>
36+
<td class="border border-gray-300 px-3 py-2">
37+
@if ($item['valid'])
38+
<span class="text-green-800 font-medium">Valid</span>
39+
@else
40+
<span class="text-red-800 font-medium">Problem</span>
41+
@endif
42+
</td>
43+
<td class="border border-gray-300 px-3 py-2 {{ $item['valid'] ? 'text-green-700' : 'text-red-700' }}">
44+
@if ($item['valid'])
45+
46+
@else
47+
{{ $item['message'] ?? 'Validation failed' }}
48+
@endif
49+
</td>
50+
</tr>
51+
@empty
52+
<tr>
53+
<td colspan="3" class="border border-gray-300 px-3 py-4 text-gray-500 text-center">No data rows to show.</td>
54+
</tr>
55+
@endforelse
56+
</tbody>
57+
</table>
58+
</div>
59+
60+
<form method="POST" action="{{ route('admin.bulk-upload.import') }}" class="inline" id="bulk-upload-import-form">
61+
@csrf
62+
<input type="hidden" name="import_payload" value="{{ $import_payload ?? '' }}">
63+
<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>
64+
</form>
65+
<script>
66+
document.getElementById('bulk-upload-import-form').addEventListener('submit', function () {
67+
var btn = document.getElementById('bulk-upload-import-btn');
68+
if (btn) { btn.disabled = true; btn.textContent = 'Importing…'; }
69+
});
70+
</script>
71+
</section>
72+
</section>
73+
@endsection

routes/web.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
Route::middleware(['auth', 'role:super admin'])->group(function () {
7474
Route::get('/admin/bulk-upload', [BulkEventUploadController::class, 'index'])->name('admin.bulk-upload.index');
7575
Route::post('/admin/bulk-upload/validate', [BulkEventUploadController::class, 'validateUpload'])->name('admin.bulk-upload.validate');
76+
Route::get('/admin/bulk-upload/preview', [BulkEventUploadController::class, 'preview'])->name('admin.bulk-upload.preview');
7677
Route::post('/admin/bulk-upload/import', [BulkEventUploadController::class, 'import'])->name('admin.bulk-upload.import');
7778
Route::get('/admin/bulk-upload/import', fn () => redirect()->route('admin.bulk-upload.index'))->name('admin.bulk-upload.import.get');
7879
Route::get('/admin/bulk-upload/report', [BulkEventUploadController::class, 'report'])->name('admin.bulk-upload.report');

0 commit comments

Comments
 (0)