check headr file import peserta

This commit is contained in:
Saufi
2026-05-20 22:07:02 +08:00
parent 7e4bbca2db
commit 9e5ff6b85e
3 changed files with 51 additions and 8 deletions

View File

@@ -13,9 +13,18 @@ use League\Csv\Reader;
class ParticipantImportService class ParticipantImportService
{ {
// Column 1 must match one of these (normalized), Column 2 must match one of these
private const VALID_COL1 = ['name', 'nama'];
private const VALID_COL2 = ['nokp', 'ic', 'nric'];
private function normalizeKey(string $key): string
{
return strtolower(preg_replace('/[^a-z0-9]/i', '', $key));
}
public function import(Program $program, UploadedFile $file, ?string $defaultSession, bool $markAttendance = false): array public function import(Program $program, UploadedFile $file, ?string $defaultSession, bool $markAttendance = false): array
{ {
$result = ['success' => 0, 'duplicates' => 0, 'failed' => 0, 'errors' => [], 'all_empty_ic' => false]; $result = ['success' => 0, 'duplicates' => 0, 'failed' => 0, 'errors' => [], 'all_empty_ic' => false, 'invalid_headers' => false];
$csv = Reader::createFromPath($file->getRealPath(), 'r'); $csv = Reader::createFromPath($file->getRealPath(), 'r');
$csv->setHeaderOffset(0); $csv->setHeaderOffset(0);
@@ -25,12 +34,25 @@ class ParticipantImportService
$csv->addStreamFilter('convert.iconv.UTF-8/UTF-8'); $csv->addStreamFilter('convert.iconv.UTF-8/UTF-8');
} catch (\Throwable) {} } catch (\Throwable) {}
// Validate headers — first two columns are mandatory and must be in order
$rawHeaders = $csv->getHeader();
$normHeaders = array_map(fn($h) => $this->normalizeKey($h), $rawHeaders);
$col1 = $normHeaders[0] ?? '';
$col2 = $normHeaders[1] ?? '';
if (! in_array($col1, self::VALID_COL1) || ! in_array($col2, self::VALID_COL2)) {
$result['invalid_headers'] = true;
$result['found_headers'] = implode(', ', array_slice($rawHeaders, 0, 5));
return $result;
}
// Collect all rows first to detect all_empty_ic // Collect all rows first to detect all_empty_ic
$rows = []; $rows = [];
foreach ($csv->getRecords() as $rowNum => $row) { foreach ($csv->getRecords() as $rowNum => $row) {
$row = array_map('trim', $row); $row = array_map('trim', $row);
$row = array_combine( $row = array_combine(
array_map(fn($k) => strtolower(preg_replace('/[\x{FEFF}\s]/u', '', $k)), array_keys($row)), array_map(fn($k) => $this->normalizeKey($k), array_keys($row)),
array_values($row) array_values($row)
); );
$rows[$rowNum] = $row; $rows[$rowNum] = $row;
@@ -42,7 +64,7 @@ class ParticipantImportService
// If every row has an empty no_kp, offer delete instead // If every row has an empty no_kp, offer delete instead
$noKpValues = array_map( $noKpValues = array_map(
fn($row) => preg_replace('/[^0-9]/', '', $row['no_kp'] ?? $row['nokp'] ?? $row['ic'] ?? ''), fn($row) => preg_replace('/[^0-9]/', '', $row['nokp'] ?? $row['ic'] ?? ''),
$rows $rows
); );
if (count(array_filter($noKpValues)) === 0) { if (count(array_filter($noKpValues)) === 0) {
@@ -55,7 +77,7 @@ class ParticipantImportService
foreach ($rows as $rowNum => $row) { foreach ($rows as $rowNum => $row) {
$data = [ $data = [
'name' => $row['name'] ?? $row['nama'] ?? '', 'name' => $row['name'] ?? $row['nama'] ?? '',
'no_kp' => preg_replace('/[^0-9]/', '', $row['no_kp'] ?? $row['nokp'] ?? $row['ic'] ?? ''), 'no_kp' => preg_replace('/[^0-9]/', '', $row['nokp'] ?? $row['ic'] ?? ''),
'email' => $row['email'] ?? $row['emel'] ?? null, 'email' => $row['email'] ?? $row['emel'] ?? null,
'phone' => $row['phone'] ?? $row['telefon'] ?? null, 'phone' => $row['phone'] ?? $row['telefon'] ?? null,
'agency' => $row['agency'] ?? $row['agensi'] ?? $row['jabatan'] ?? null, 'agency' => $row['agency'] ?? $row['agensi'] ?? $row['jabatan'] ?? null,

View File

@@ -18,8 +18,28 @@
@if(session('import_result')) @if(session('import_result'))
@php $r = session('import_result'); @endphp @php $r = session('import_result'); @endphp
{{-- Invalid headers --}}
@if(!empty($r['invalid_headers']))
<div class="card border-0 shadow-sm mb-4 border-start border-4 border-danger">
<div class="card-body">
<h6 class="fw-semibold mb-2 text-danger">
<i class="bi bi-x-circle me-2"></i>Format Header CSV Tidak Sah
</h6>
<p class="small text-muted mb-2">
Baris pertama fail CSV mesti mengandungi header yang betul mengikut susunan ini:
</p>
<code class="d-block bg-light p-2 rounded small mb-3">name,no_kp,email,phone,agency</code>
@if(!empty($r['found_headers']))
<p class="small text-muted mb-0">
<i class="bi bi-exclamation-triangle text-warning me-1"></i>
Header yang dijumpai: <code>{{ $r['found_headers'] }}</code>
</p>
@endif
</div>
</div>
{{-- All IC empty offer delete --}} {{-- All IC empty offer delete --}}
@if(!empty($r['all_empty_ic'])) @elseif(!empty($r['all_empty_ic']))
<div class="card border-0 shadow-sm mb-4 border-start border-4 border-warning"> <div class="card border-0 shadow-sm mb-4 border-start border-4 border-warning">
<div class="card-body"> <div class="card-body">
<h6 class="fw-semibold mb-2 text-warning"> <h6 class="fw-semibold mb-2 text-warning">
@@ -41,6 +61,7 @@
</div> </div>
</div> </div>
@else @else
{{-- Normal import result --}}
<div class="card border-0 shadow-sm mb-4 border-start border-4 <div class="card border-0 shadow-sm mb-4 border-start border-4
{{ $r['failed'] > 0 ? 'border-warning' : 'border-success' }}"> {{ $r['failed'] > 0 ? 'border-warning' : 'border-success' }}">
<div class="card-body"> <div class="card-body">

View File

@@ -53,13 +53,13 @@ Route::middleware(['auth', 'admin'])->prefix('admin')->name('admin.')->group(fun
Route::get('/', [ParticipantController::class, 'index'])->name('index'); Route::get('/', [ParticipantController::class, 'index'])->name('index');
Route::get('/create', [ParticipantController::class, 'create'])->name('create'); Route::get('/create', [ParticipantController::class, 'create'])->name('create');
Route::post('/', [ParticipantController::class, 'store'])->name('store'); Route::post('/', [ParticipantController::class, 'store'])->name('store');
Route::get('/{pp}/edit', [ParticipantController::class, 'edit'])->name('edit');
Route::put('/{pp}', [ParticipantController::class, 'update'])->name('update');
Route::delete('/{pp}', [ParticipantController::class, 'destroy'])->name('destroy');
Route::get('/import', [ParticipantController::class, 'importForm'])->name('import.form'); Route::get('/import', [ParticipantController::class, 'importForm'])->name('import.form');
Route::post('/import', [ParticipantController::class, 'import'])->name('import'); Route::post('/import', [ParticipantController::class, 'import'])->name('import');
Route::delete('/clear', [ParticipantController::class, 'clearParticipants'])->name('clear'); Route::delete('/clear', [ParticipantController::class, 'clearParticipants'])->name('clear');
Route::get('/export', [ParticipantController::class, 'export'])->name('export'); Route::get('/export', [ParticipantController::class, 'export'])->name('export');
Route::get('/{pp}/edit', [ParticipantController::class, 'edit'])->name('edit');
Route::put('/{pp}', [ParticipantController::class, 'update'])->name('update');
Route::delete('/{pp}', [ParticipantController::class, 'destroy'])->name('destroy');
}); });
// Certificate Template // Certificate Template