- ParticipantController: list (search/filter), add manual, remove, export CSV (UTF-8 BOM) - ParticipantImportService: League\Csv, strip BOM, normalise headers, per-row validation, duplicate detection, transaction per row (single failure does not abort import), summary report - Participant index: counts (total/pre-reg/walk-in/hadir), filter by source+status, pagination - Participant create: inline no_kp validation, session picker pre-filled from program default - Import page: result summary (success/duplicates/failed), error list, format guide Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
102 lines
3.9 KiB
PHP
102 lines
3.9 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Participant;
|
|
use App\Models\Program;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Validator;
|
|
use League\Csv\Reader;
|
|
|
|
class ParticipantImportService
|
|
{
|
|
public function import(Program $program, UploadedFile $file, ?string $defaultSession): array
|
|
{
|
|
$result = ['success' => 0, 'duplicates' => 0, 'failed' => 0, 'errors' => []];
|
|
|
|
$csv = Reader::createFromPath($file->getRealPath(), 'r');
|
|
$csv->setHeaderOffset(0);
|
|
|
|
// Strip UTF-8 BOM if present (Excel-exported CSV)
|
|
$csv->setOutputBOM('');
|
|
try {
|
|
$csv->addStreamFilter('convert.iconv.UTF-8/UTF-8');
|
|
} catch (\Throwable) {}
|
|
|
|
foreach ($csv->getRecords() as $rowNum => $row) {
|
|
$row = array_map('trim', $row);
|
|
|
|
// Normalise header keys (lowercase, strip BOM)
|
|
$row = array_combine(
|
|
array_map(fn($k) => strtolower(preg_replace('/[\x{FEFF}\s]/u', '', $k)), array_keys($row)),
|
|
array_values($row)
|
|
);
|
|
|
|
$data = [
|
|
'name' => $row['name'] ?? $row['nama'] ?? '',
|
|
'no_kp' => preg_replace('/[^0-9]/', '', $row['no_kp'] ?? $row['nokp'] ?? $row['ic'] ?? ''),
|
|
'email' => $row['email'] ?? $row['emel'] ?? null,
|
|
'phone' => $row['phone'] ?? $row['telefon'] ?? $row['phone'] ?? null,
|
|
'agency' => $row['agency'] ?? $row['agensi'] ?? $row['jabatan'] ?? null,
|
|
];
|
|
|
|
// Validate row
|
|
$validator = Validator::make($data, [
|
|
'name' => ['required', 'string', 'max:255'],
|
|
'no_kp' => ['required', 'digits:12'],
|
|
'email' => ['nullable', 'email', 'max:255'],
|
|
'phone' => ['nullable', 'string', 'max:20'],
|
|
]);
|
|
|
|
if ($validator->fails()) {
|
|
$result['failed']++;
|
|
$result['errors'][] = 'Baris ' . ($rowNum + 2) . ': ' . implode(', ', $validator->errors()->all());
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
DB::transaction(function () use ($program, $data, $defaultSession, &$result) {
|
|
// Find or create participant by no_kp
|
|
$participant = Participant::firstOrCreate(
|
|
['no_kp' => $data['no_kp']],
|
|
[
|
|
'name' => $data['name'],
|
|
'email' => $data['email'] ?: null,
|
|
'phone' => $data['phone'] ?: null,
|
|
'agency' => $data['agency'] ?: null,
|
|
'participant_type' => 'staff',
|
|
]
|
|
);
|
|
|
|
// Check duplicate in this program
|
|
$exists = $program->programParticipants()
|
|
->where('participant_id', $participant->id)
|
|
->exists();
|
|
|
|
if ($exists) {
|
|
$result['duplicates']++;
|
|
return;
|
|
}
|
|
|
|
$program->programParticipants()->create([
|
|
'participant_id' => $participant->id,
|
|
'registration_source' => 'import',
|
|
'is_pre_registered' => true,
|
|
'pre_registered_session' => $defaultSession,
|
|
'status' => 'registered',
|
|
'registered_at' => now(),
|
|
]);
|
|
|
|
$result['success']++;
|
|
});
|
|
} catch (\Throwable $e) {
|
|
$result['failed']++;
|
|
$result['errors'][] = 'Baris ' . ($rowNum + 2) . ': Ralat sistem — ' . $e->getMessage();
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
}
|