- 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>
181 lines
6.6 KiB
PHP
181 lines
6.6 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Admin;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Participant;
|
|
use App\Models\Program;
|
|
use App\Models\ProgramParticipant;
|
|
use App\Services\AuditLogService;
|
|
use App\Services\ParticipantImportService;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\View\View;
|
|
use League\Csv\Writer;
|
|
use SplTempFileObject;
|
|
|
|
class ParticipantController extends Controller
|
|
{
|
|
public function index(Program $program, Request $request): View
|
|
{
|
|
$query = $program->programParticipants()
|
|
->with('participant')
|
|
->latest();
|
|
|
|
if ($request->filled('search')) {
|
|
$query->whereHas('participant', function ($q) use ($request) {
|
|
$q->where('name', 'like', '%' . $request->search . '%')
|
|
->orWhere('agency', 'like', '%' . $request->search . '%');
|
|
});
|
|
}
|
|
|
|
if ($request->filled('source')) {
|
|
$query->where('registration_source', $request->source);
|
|
}
|
|
|
|
if ($request->filled('status')) {
|
|
$query->where('status', $request->status);
|
|
}
|
|
|
|
$programParticipants = $query->paginate(20)->withQueryString();
|
|
|
|
$counts = [
|
|
'total' => $program->programParticipants()->count(),
|
|
'pre_registered' => $program->programParticipants()->where('is_pre_registered', true)->count(),
|
|
'walk_in' => $program->programParticipants()->where('registration_source', 'walk_in')->count(),
|
|
'checked_in' => $program->programParticipants()->where('status', 'checked_in')->count(),
|
|
];
|
|
|
|
return view('admin.programs.participants.index', compact('program', 'programParticipants', 'counts'));
|
|
}
|
|
|
|
public function create(Program $program): View
|
|
{
|
|
return view('admin.programs.participants.create', compact('program'));
|
|
}
|
|
|
|
public function store(Program $program, Request $request): RedirectResponse
|
|
{
|
|
$request->validate([
|
|
'name' => ['required', 'string', 'max:255'],
|
|
'no_kp' => ['required', 'string', 'regex:/^\d{6}-?\d{2}-?\d{4}$|^\d{12}$/'],
|
|
'email' => ['nullable', 'email', 'max:255'],
|
|
'phone' => ['nullable', 'string', 'max:20'],
|
|
'agency' => ['nullable', 'string', 'max:255'],
|
|
'session' => ['nullable', 'in:pagi,petang,full_day'],
|
|
]);
|
|
|
|
$noKp = preg_replace('/[^0-9]/', '', $request->no_kp);
|
|
|
|
// Check duplicate in this program
|
|
$existing = Participant::where('no_kp', $noKp)->first();
|
|
if ($existing && $program->programParticipants()->where('participant_id', $existing->id)->exists()) {
|
|
return back()->withInput()->with('error', 'Peserta dengan No. K/P ini sudah didaftarkan dalam program ini.');
|
|
}
|
|
|
|
DB::transaction(function () use ($program, $request, $noKp, $existing) {
|
|
$participant = $existing ?? Participant::create([
|
|
'name' => $request->name,
|
|
'no_kp' => $noKp,
|
|
'email' => $request->email,
|
|
'phone' => $request->phone,
|
|
'agency' => $request->agency,
|
|
'participant_type' => 'staff',
|
|
]);
|
|
|
|
$program->programParticipants()->create([
|
|
'participant_id' => $participant->id,
|
|
'registration_source' => 'admin_manual',
|
|
'is_pre_registered' => true,
|
|
'pre_registered_session'=> $request->session ?? $program->default_staff_session,
|
|
'status' => 'registered',
|
|
'registered_at' => now(),
|
|
]);
|
|
|
|
AuditLogService::log('participant.added', $participant);
|
|
});
|
|
|
|
return back()->with('success', 'Peserta berjaya ditambah.');
|
|
}
|
|
|
|
public function destroy(Program $program, ProgramParticipant $pp): RedirectResponse
|
|
{
|
|
if ($pp->program_id !== $program->id) {
|
|
abort(403);
|
|
}
|
|
|
|
if ($pp->attendance()->exists()) {
|
|
return back()->with('error', 'Peserta tidak boleh dikeluarkan kerana sudah ada rekod kehadiran.');
|
|
}
|
|
|
|
$pp->delete();
|
|
|
|
return back()->with('success', 'Peserta berjaya dikeluarkan daripada program.');
|
|
}
|
|
|
|
public function importForm(Program $program): View
|
|
{
|
|
return view('admin.programs.participants.import', compact('program'));
|
|
}
|
|
|
|
public function import(Program $program, Request $request, ParticipantImportService $importer): RedirectResponse
|
|
{
|
|
$request->validate([
|
|
'csv_file' => ['required', 'file', 'mimes:csv,txt', 'max:5120'],
|
|
'session' => ['nullable', 'in:pagi,petang,full_day'],
|
|
]);
|
|
|
|
$result = $importer->import(
|
|
$program,
|
|
$request->file('csv_file'),
|
|
$request->input('session', $program->default_staff_session)
|
|
);
|
|
|
|
AuditLogService::log('participant.imported', $program, [], [
|
|
'success' => $result['success'],
|
|
'duplicates' => $result['duplicates'],
|
|
'failed' => $result['failed'],
|
|
]);
|
|
|
|
return back()->with('import_result', $result);
|
|
}
|
|
|
|
public function export(Program $program): \Symfony\Component\HttpFoundation\StreamedResponse
|
|
{
|
|
$headers = [
|
|
'Content-Type' => 'text/csv; charset=UTF-8',
|
|
'Content-Disposition' => 'attachment; filename="peserta_' . $program->uuid . '_' . now()->format('Ymd') . '.csv"',
|
|
];
|
|
|
|
return response()->stream(function () use ($program) {
|
|
$handle = fopen('php://output', 'w');
|
|
|
|
// UTF-8 BOM for Excel compatibility
|
|
fputs($handle, "\xEF\xBB\xBF");
|
|
|
|
fputcsv($handle, ['Nama', 'No K/P', 'Emel', 'Telefon', 'Agensi', 'Sesi', 'Sumber', 'Status', 'Tarikh Daftar']);
|
|
|
|
$program->programParticipants()
|
|
->with('participant')
|
|
->lazy()
|
|
->each(function ($pp) use ($handle) {
|
|
$p = $pp->participant;
|
|
fputcsv($handle, [
|
|
$p->name,
|
|
$p->no_kp,
|
|
$p->email,
|
|
$p->phone,
|
|
$p->agency,
|
|
$pp->pre_registered_session ?? '—',
|
|
$pp->registration_source,
|
|
$pp->status,
|
|
$pp->registered_at?->format('d/m/Y H:i') ?? '—',
|
|
]);
|
|
});
|
|
|
|
fclose($handle);
|
|
}, 200, $headers);
|
|
}
|
|
}
|