feat: participant management and csv import
- 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>
This commit is contained in:
101
app/Services/ParticipantImportService.php
Normal file
101
app/Services/ParticipantImportService.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user