feat: public check-in flow and attendance (Fasa 5)
- AttendanceService: staffCheckin and walkInRegister methods - CheckinController: QR-based check-in (staff & walk-in external) - AttendanceCheckController: semak kehadiran & sijil status - Views: checkin show/success/already/unavailable, semak show/result Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,9 +3,58 @@
|
||||
namespace App\Http\Controllers\Public;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Certificate;
|
||||
use App\Models\Participant;
|
||||
use App\Models\ProgramQrCode;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class AttendanceCheckController extends Controller
|
||||
{
|
||||
//
|
||||
public function show(string $qr_token): View
|
||||
{
|
||||
$qrCode = ProgramQrCode::where('token', $qr_token)->where('is_active', true)->firstOrFail();
|
||||
$program = $qrCode->program;
|
||||
|
||||
abort_if($program->status !== 'published', 404);
|
||||
|
||||
return view('public.semak.show', compact('program', 'qrCode'));
|
||||
}
|
||||
|
||||
public function check(string $qr_token, Request $request): View
|
||||
{
|
||||
$qrCode = ProgramQrCode::where('token', $qr_token)->where('is_active', true)->firstOrFail();
|
||||
$program = $qrCode->program;
|
||||
|
||||
$request->validate([
|
||||
'no_kp' => ['required', 'digits:12'],
|
||||
], [
|
||||
'no_kp.required' => 'Sila masukkan No. Kad Pengenalan anda.',
|
||||
'no_kp.digits' => 'No. Kad Pengenalan mestilah 12 digit tanpa sempang.',
|
||||
]);
|
||||
|
||||
$noKp = preg_replace('/[^0-9]/', '', $request->no_kp);
|
||||
$participant = Participant::where('no_kp', $noKp)->first();
|
||||
|
||||
if (! $participant) {
|
||||
return view('public.semak.result', [
|
||||
'program' => $program,
|
||||
'qrCode' => $qrCode,
|
||||
'found' => false,
|
||||
'participant' => null,
|
||||
'attendance' => null,
|
||||
'certificate' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
$attendance = $participant->attendanceForProgram($program->id);
|
||||
$certificate = $attendance
|
||||
? Certificate::where('program_id', $program->id)
|
||||
->where('participant_id', $participant->id)
|
||||
->first()
|
||||
: null;
|
||||
|
||||
return view('public.semak.result', compact('program', 'qrCode', 'participant', 'attendance', 'certificate'))
|
||||
->with('found', (bool) $attendance);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,118 @@
|
||||
namespace App\Http\Controllers\Public;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ProgramQrCode;
|
||||
use App\Services\AttendanceService;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class CheckinController extends Controller
|
||||
{
|
||||
//
|
||||
public function __construct(private AttendanceService $attendanceService) {}
|
||||
|
||||
public function show(string $qr_token): View|RedirectResponse
|
||||
{
|
||||
$qrCode = ProgramQrCode::where('token', $qr_token)->where('is_active', true)->firstOrFail();
|
||||
$program = $qrCode->program;
|
||||
|
||||
if ($program->status !== 'published') {
|
||||
return view('public.checkin.unavailable', compact('program', 'qrCode'))
|
||||
->with('message', 'Program ini belum dibuka atau sudah ditutup.');
|
||||
}
|
||||
|
||||
// If download period is active → redirect to attendance check page
|
||||
if ($program->isDownloadOpen()) {
|
||||
return redirect()->route('public.semak.show', $qr_token);
|
||||
}
|
||||
|
||||
// Check-in not yet open
|
||||
if ($program->checkin_start_at && now()->lt($program->checkin_start_at)) {
|
||||
return view('public.checkin.unavailable', compact('program', 'qrCode'))
|
||||
->with('message', 'Check-in belum dibuka. Mula pada ' . $program->checkin_start_at->format('d M Y, H:i'));
|
||||
}
|
||||
|
||||
// Check-in already closed
|
||||
if ($program->checkin_end_at && now()->gt($program->checkin_end_at)) {
|
||||
return view('public.checkin.unavailable', compact('program', 'qrCode'))
|
||||
->with('message', 'Tempoh check-in telah tamat.');
|
||||
}
|
||||
|
||||
return view('public.checkin.show', compact('program', 'qrCode'));
|
||||
}
|
||||
|
||||
public function staffCheckin(string $qr_token, Request $request): View|RedirectResponse
|
||||
{
|
||||
$qrCode = ProgramQrCode::where('token', $qr_token)->where('is_active', true)->firstOrFail();
|
||||
$program = $qrCode->program;
|
||||
|
||||
$request->validate([
|
||||
'no_kp' => ['required', 'string', 'max:20'],
|
||||
], [
|
||||
'no_kp.required' => 'Sila masukkan No. Kad Pengenalan anda.',
|
||||
]);
|
||||
|
||||
$result = $this->attendanceService->staffCheckin($program, $request->no_kp, $request);
|
||||
|
||||
return match ($result['status']) {
|
||||
'success' => view('public.checkin.success', [
|
||||
'program' => $program,
|
||||
'participant' => $result['participant'],
|
||||
'attendance' => $result['attendance'],
|
||||
'qrCode' => $qrCode,
|
||||
]),
|
||||
'already_checked_in' => view('public.checkin.already', [
|
||||
'program' => $program,
|
||||
'participant' => $result['participant'],
|
||||
'attendance' => $result['attendance'],
|
||||
]),
|
||||
'not_found' => back()->withInput()
|
||||
->with('error', 'No. Kad Pengenalan tidak dijumpai dalam senarai peserta program ini.')
|
||||
->with('show_external_option', $program->allow_walk_in),
|
||||
'not_registered' => back()->withInput()
|
||||
->with('error', 'No. Kad Pengenalan tidak dijumpai dalam senarai pra-daftar.')
|
||||
->with('show_external_option', $program->allow_walk_in),
|
||||
default => back()->with('error', 'Ralat tidak dijangka. Sila cuba lagi.'),
|
||||
};
|
||||
}
|
||||
|
||||
public function externalRegister(string $qr_token, Request $request): View|RedirectResponse
|
||||
{
|
||||
$qrCode = ProgramQrCode::where('token', $qr_token)->where('is_active', true)->firstOrFail();
|
||||
$program = $qrCode->program;
|
||||
|
||||
if (! $program->allow_walk_in) {
|
||||
return back()->with('error', 'Pendaftaran orang luar tidak dibenarkan untuk program ini.');
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'no_kp' => ['required', 'digits:12'],
|
||||
'email' => ['nullable', 'email', 'max:255'],
|
||||
'phone' => ['nullable', 'string', 'max:20'],
|
||||
'agency' => ['nullable', 'string', 'max:255'],
|
||||
], [
|
||||
'name.required' => 'Sila masukkan nama penuh anda.',
|
||||
'no_kp.required' => 'Sila masukkan No. Kad Pengenalan anda.',
|
||||
'no_kp.digits' => 'No. Kad Pengenalan mestilah 12 digit tanpa sempang.',
|
||||
'email.email' => 'Format emel tidak sah.',
|
||||
]);
|
||||
|
||||
$result = $this->attendanceService->walkInRegister($program, $request->all(), $request);
|
||||
|
||||
return match ($result['status']) {
|
||||
'success' => view('public.checkin.success', [
|
||||
'program' => $program,
|
||||
'participant' => $result['participant'],
|
||||
'attendance' => $result['attendance'],
|
||||
'qrCode' => $qrCode,
|
||||
]),
|
||||
'already_checked_in' => view('public.checkin.already', [
|
||||
'program' => $program,
|
||||
'participant' => $result['participant'],
|
||||
'attendance' => $result['attendance'],
|
||||
]),
|
||||
default => back()->withInput()->with('error', 'Ralat sistem. Sila cuba lagi.'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
123
app/Services/AttendanceService.php
Normal file
123
app/Services/AttendanceService.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Attendance;
|
||||
use App\Models\Participant;
|
||||
use App\Models\Program;
|
||||
use App\Models\ProgramParticipant;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class AttendanceService
|
||||
{
|
||||
public function staffCheckin(Program $program, string $noKp, Request $request): array
|
||||
{
|
||||
$noKp = preg_replace('/[^0-9]/', '', $noKp);
|
||||
|
||||
// Find participant
|
||||
$participant = Participant::where('no_kp', $noKp)->first();
|
||||
if (! $participant) {
|
||||
return ['status' => 'not_found'];
|
||||
}
|
||||
|
||||
// Check if registered in this program
|
||||
$pp = ProgramParticipant::where('program_id', $program->id)
|
||||
->where('participant_id', $participant->id)
|
||||
->first();
|
||||
|
||||
if (! $pp) {
|
||||
return ['status' => 'not_registered', 'participant' => $participant];
|
||||
}
|
||||
|
||||
// Already checked in
|
||||
if (Attendance::where('program_id', $program->id)->where('participant_id', $participant->id)->exists()) {
|
||||
$att = Attendance::where('program_id', $program->id)->where('participant_id', $participant->id)->first();
|
||||
return ['status' => 'already_checked_in', 'participant' => $participant, 'attendance' => $att];
|
||||
}
|
||||
|
||||
// Record attendance
|
||||
$attendance = DB::transaction(function () use ($program, $participant, $pp, $request) {
|
||||
$session = $pp->pre_registered_session ?? $program->default_staff_session ?? 'full_day';
|
||||
|
||||
$pp->update(['status' => 'checked_in']);
|
||||
|
||||
return Attendance::create([
|
||||
'program_id' => $program->id,
|
||||
'participant_id' => $participant->id,
|
||||
'program_participant_id' => $pp->id,
|
||||
'attendance_source' => 'pre_registered_staff',
|
||||
'attendance_session' => $session,
|
||||
'checked_in_at' => now(),
|
||||
'checked_in_ip' => $request->ip(),
|
||||
'user_agent' => substr($request->userAgent() ?? '', 0, 500),
|
||||
]);
|
||||
});
|
||||
|
||||
return ['status' => 'success', 'participant' => $participant, 'attendance' => $attendance];
|
||||
}
|
||||
|
||||
public function walkInRegister(Program $program, array $data, Request $request): array
|
||||
{
|
||||
$noKp = preg_replace('/[^0-9]/', '', $data['no_kp']);
|
||||
|
||||
// Check duplicate in this program
|
||||
$existing = Participant::where('no_kp', $noKp)->first();
|
||||
if ($existing) {
|
||||
$alreadyRegistered = ProgramParticipant::where('program_id', $program->id)
|
||||
->where('participant_id', $existing->id)
|
||||
->exists();
|
||||
if ($alreadyRegistered) {
|
||||
// Check if already attended
|
||||
$att = Attendance::where('program_id', $program->id)
|
||||
->where('participant_id', $existing->id)
|
||||
->first();
|
||||
if ($att) {
|
||||
return ['status' => 'already_checked_in', 'participant' => $existing, 'attendance' => $att];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$result = DB::transaction(function () use ($program, $data, $noKp, $existing, $request) {
|
||||
$participant = $existing ?? Participant::create([
|
||||
'name' => $data['name'],
|
||||
'no_kp' => $noKp,
|
||||
'email' => $data['email'] ?: null,
|
||||
'phone' => $data['phone'] ?: null,
|
||||
'agency' => $data['agency'] ?: null,
|
||||
'participant_type' => 'external',
|
||||
]);
|
||||
|
||||
// Create program_participant if not exists
|
||||
$pp = ProgramParticipant::firstOrCreate(
|
||||
['program_id' => $program->id, 'participant_id' => $participant->id],
|
||||
[
|
||||
'registration_source' => 'walk_in',
|
||||
'is_pre_registered' => false,
|
||||
'pre_registered_session'=> $program->default_external_session,
|
||||
'status' => 'registered',
|
||||
'registered_at' => now(),
|
||||
]
|
||||
);
|
||||
|
||||
$session = $program->default_external_session ?? 'full_day';
|
||||
|
||||
$pp->update(['status' => 'checked_in']);
|
||||
|
||||
$attendance = Attendance::create([
|
||||
'program_id' => $program->id,
|
||||
'participant_id' => $participant->id,
|
||||
'program_participant_id' => $pp->id,
|
||||
'attendance_source' => 'walk_in_external',
|
||||
'attendance_session' => $session,
|
||||
'checked_in_at' => now(),
|
||||
'checked_in_ip' => $request->ip(),
|
||||
'user_agent' => substr($request->userAgent() ?? '', 0, 500),
|
||||
]);
|
||||
|
||||
return ['status' => 'success', 'participant' => $participant, 'attendance' => $attendance];
|
||||
});
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
31
resources/views/public/checkin/already.blade.php
Normal file
31
resources/views/public/checkin/already.blade.php
Normal file
@@ -0,0 +1,31 @@
|
||||
@extends('layouts.public')
|
||||
|
||||
@section('title', 'Sudah Check-In')
|
||||
|
||||
@section('hero')
|
||||
<h4 class="mb-1">{{ $program->title }}</h4>
|
||||
<div class="opacity-75 small"><i class="bi bi-calendar3 me-1"></i>{{ $program->start_date->format('d M Y') }}</div>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="checkin-card card p-4 text-center">
|
||||
<div class="mb-3">
|
||||
<div class="rounded-circle bg-warning bg-opacity-10 d-inline-flex align-items-center justify-content-center mb-3"
|
||||
style="width:80px; height:80px;">
|
||||
<i class="bi bi-exclamation-circle-fill text-warning" style="font-size:2.5rem;"></i>
|
||||
</div>
|
||||
<h5 class="fw-bold text-warning mb-1">Sudah Check-In</h5>
|
||||
<p class="text-muted mb-0">Kehadiran anda sudah direkodkan sebelum ini.</p>
|
||||
</div>
|
||||
<div class="bg-light rounded p-3 text-start">
|
||||
<div class="row g-2">
|
||||
<div class="col-5 text-muted small">Nama</div>
|
||||
<div class="col-7 fw-medium small">{{ $participant->name }}</div>
|
||||
<div class="col-5 text-muted small">Masa Check-In</div>
|
||||
<div class="col-7 small">{{ $attendance->checked_in_at->format('d M Y, H:i') }}</div>
|
||||
<div class="col-5 text-muted small">Sesi</div>
|
||||
<div class="col-7 small"><span class="badge bg-primary">{{ Str::ucfirst($attendance->attendance_session) }}</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
152
resources/views/public/checkin/show.blade.php
Normal file
152
resources/views/public/checkin/show.blade.php
Normal file
@@ -0,0 +1,152 @@
|
||||
@extends('layouts.public')
|
||||
|
||||
@section('title', $program->title . ' — Check-In')
|
||||
|
||||
@section('hero')
|
||||
<h4 class="mb-1">{{ $program->title }}</h4>
|
||||
<div class="opacity-75 small">
|
||||
<i class="bi bi-geo-alt me-1"></i>{{ $program->location }}
|
||||
·
|
||||
<i class="bi bi-calendar3 me-1"></i>{{ $program->start_date->format('d M Y') }}
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
|
||||
@if(session('error'))
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<i class="bi bi-exclamation-triangle-fill me-2"></i>{{ session('error') }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
@if(session('show_external_option') && $program->allow_walk_in)
|
||||
<div class="mt-2">
|
||||
<small>Adakah anda peserta luar? <a href="#external-form" class="alert-link">Daftar di sini</a></small>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Pilihan Jenis Peserta --}}
|
||||
<div class="mb-4" id="type-selector">
|
||||
<h6 class="fw-semibold mb-3 text-center">Sila pilih jenis peserta anda:</h6>
|
||||
<div class="row g-3">
|
||||
<div class="col-6">
|
||||
<button class="btn btn-outline-primary w-100 py-3 h-100 type-btn {{ session('error') && !session('show_external_option') ? 'active' : '' }}"
|
||||
data-target="staff-form" style="border-radius:.75rem;">
|
||||
<i class="bi bi-person-badge fs-3 d-block mb-2"></i>
|
||||
<div class="fw-semibold">Kakitangan</div>
|
||||
<small class="text-muted d-none d-sm-block">Sudah pra-daftar</small>
|
||||
</button>
|
||||
</div>
|
||||
@if($program->allow_walk_in)
|
||||
<div class="col-6">
|
||||
<button class="btn btn-outline-success w-100 py-3 h-100 type-btn {{ session('show_external_option') ? 'active' : '' }}"
|
||||
data-target="external-form" style="border-radius:.75rem;">
|
||||
<i class="bi bi-person-plus fs-3 d-block mb-2"></i>
|
||||
<div class="fw-semibold">Orang Luar</div>
|
||||
<small class="text-muted d-none d-sm-block">Daftar sekarang</small>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Form: Kakitangan --}}
|
||||
<div id="staff-form" class="checkin-card card p-4 mb-3 {{ session('show_external_option') ? 'd-none' : '' }}">
|
||||
<h6 class="fw-semibold mb-3">
|
||||
<i class="bi bi-person-badge me-2 text-primary"></i>Check-In Kakitangan
|
||||
</h6>
|
||||
<form method="POST" action="{{ route('public.checkin.staff', $qrCode->token) }}">
|
||||
@csrf
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-medium">No. Kad Pengenalan <span class="text-danger">*</span></label>
|
||||
<input type="text" name="no_kp"
|
||||
class="form-control form-control-lg @error('no_kp') is-invalid @enderror"
|
||||
placeholder="Contoh: 900101011234"
|
||||
value="{{ old('no_kp') }}"
|
||||
inputmode="numeric" maxlength="12" autocomplete="off">
|
||||
<div class="form-text">12 digit tanpa sempang</div>
|
||||
@error('no_kp')<div class="invalid-feedback">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100 btn-checkin">
|
||||
<i class="bi bi-box-arrow-in-right me-2"></i>Check-In
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{{-- Form: Orang Luar --}}
|
||||
@if($program->allow_walk_in)
|
||||
<div id="external-form" class="checkin-card card p-4 mb-3 {{ session('show_external_option') ? '' : 'd-none' }}"
|
||||
style="anchor-name: --external">
|
||||
<h6 class="fw-semibold mb-3">
|
||||
<i class="bi bi-person-plus me-2 text-success"></i>Daftar & Check-In
|
||||
</h6>
|
||||
<form method="POST" action="{{ route('public.checkin.external', $qrCode->token) }}">
|
||||
@csrf
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-medium">Nama Penuh <span class="text-danger">*</span></label>
|
||||
<input type="text" name="name"
|
||||
class="form-control @error('name') is-invalid @enderror"
|
||||
placeholder="Nama penuh seperti dalam K/P"
|
||||
value="{{ old('name') }}" autocomplete="name">
|
||||
@error('name')<div class="invalid-feedback">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-medium">No. Kad Pengenalan <span class="text-danger">*</span></label>
|
||||
<input type="text" name="no_kp"
|
||||
class="form-control @error('no_kp') is-invalid @enderror"
|
||||
placeholder="900101011234"
|
||||
value="{{ old('no_kp') }}"
|
||||
inputmode="numeric" maxlength="12" autocomplete="off">
|
||||
<div class="form-text">12 digit tanpa sempang</div>
|
||||
@error('no_kp')<div class="invalid-feedback">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-medium">Emel</label>
|
||||
<input type="email" name="email"
|
||||
class="form-control @error('email') is-invalid @enderror"
|
||||
placeholder="nama@email.com"
|
||||
value="{{ old('email') }}" autocomplete="email">
|
||||
<div class="form-text">Untuk penerimaan sijil</div>
|
||||
@error('email')<div class="invalid-feedback">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-medium">No. Telefon</label>
|
||||
<input type="tel" name="phone"
|
||||
class="form-control @error('phone') is-invalid @enderror"
|
||||
placeholder="0123456789"
|
||||
value="{{ old('phone') }}" autocomplete="tel">
|
||||
@error('phone')<div class="invalid-feedback">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="form-label fw-medium">Agensi / Organisasi</label>
|
||||
<input type="text" name="agency"
|
||||
class="form-control @error('agency') is-invalid @enderror"
|
||||
placeholder="Nama syarikat atau agensi"
|
||||
value="{{ old('agency') }}">
|
||||
@error('agency')<div class="invalid-feedback">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success w-100 btn-checkin">
|
||||
<i class="bi bi-person-check me-2"></i>Daftar & Check-In
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@endsection
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.querySelectorAll('.type-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function () {
|
||||
document.querySelectorAll('.type-btn').forEach(b => b.classList.remove('active'));
|
||||
this.classList.add('active');
|
||||
|
||||
const target = this.dataset.target;
|
||||
document.getElementById('staff-form').classList.add('d-none');
|
||||
document.getElementById('external-form')?.classList.add('d-none');
|
||||
document.getElementById(target).classList.remove('d-none');
|
||||
document.getElementById(target).scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@endpush
|
||||
50
resources/views/public/checkin/success.blade.php
Normal file
50
resources/views/public/checkin/success.blade.php
Normal file
@@ -0,0 +1,50 @@
|
||||
@extends('layouts.public')
|
||||
|
||||
@section('title', 'Check-In Berjaya')
|
||||
|
||||
@section('hero')
|
||||
<h4 class="mb-1">{{ $program->title }}</h4>
|
||||
<div class="opacity-75 small">
|
||||
<i class="bi bi-geo-alt me-1"></i>{{ $program->location }}
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
|
||||
<div class="checkin-card card p-4 text-center">
|
||||
<div class="mb-3">
|
||||
<div class="rounded-circle bg-success bg-opacity-10 d-inline-flex align-items-center justify-content-center mb-3"
|
||||
style="width:80px; height:80px;">
|
||||
<i class="bi bi-check-circle-fill text-success" style="font-size:2.5rem;"></i>
|
||||
</div>
|
||||
<h5 class="fw-bold text-success mb-1">Check-In Berjaya!</h5>
|
||||
<p class="text-muted mb-0">Kehadiran anda telah direkodkan.</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-light rounded p-3 text-start mb-4">
|
||||
<div class="row g-2">
|
||||
<div class="col-5 text-muted small">Nama</div>
|
||||
<div class="col-7 fw-medium small">{{ $participant->name }}</div>
|
||||
<div class="col-5 text-muted small">Agensi</div>
|
||||
<div class="col-7 small">{{ $participant->agency ?: '—' }}</div>
|
||||
<div class="col-5 text-muted small">Sesi</div>
|
||||
<div class="col-7 small">
|
||||
<span class="badge bg-primary">{{ Str::ucfirst($attendance->attendance_session) }}</span>
|
||||
</div>
|
||||
<div class="col-5 text-muted small">Masa Check-In</div>
|
||||
<div class="col-7 small">{{ $attendance->checked_in_at->format('d M Y, H:i') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info text-start small mb-0" role="alert">
|
||||
<i class="bi bi-envelope-fill me-2"></i>
|
||||
<strong>Sijil Digital (eCert)</strong> akan dihantar ke emel anda selepas program tamat.
|
||||
@if($participant->email)
|
||||
Emel: <strong>{{ $participant->email }}</strong>
|
||||
@else
|
||||
Sila pastikan emel anda didaftarkan untuk menerima sijil.
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
23
resources/views/public/checkin/unavailable.blade.php
Normal file
23
resources/views/public/checkin/unavailable.blade.php
Normal file
@@ -0,0 +1,23 @@
|
||||
@extends('layouts.public')
|
||||
|
||||
@section('title', 'Tidak Tersedia')
|
||||
|
||||
@section('hero')
|
||||
<h4 class="mb-1">{{ $program->title }}</h4>
|
||||
<div class="opacity-75 small"><i class="bi bi-calendar3 me-1"></i>{{ $program->start_date->format('d M Y') }}</div>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
<div class="checkin-card card p-4 text-center">
|
||||
<div class="mb-3">
|
||||
<div class="rounded-circle bg-secondary bg-opacity-10 d-inline-flex align-items-center justify-content-center mb-3"
|
||||
style="width:80px; height:80px;">
|
||||
<i class="bi bi-clock-history text-secondary" style="font-size:2.5rem;"></i>
|
||||
</div>
|
||||
<h5 class="fw-semibold mb-2">{{ $message ?? 'Tidak Tersedia' }}</h5>
|
||||
<p class="text-muted small mb-0">
|
||||
Sila hubungi penganjur program untuk maklumat lanjut.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
90
resources/views/public/semak/result.blade.php
Normal file
90
resources/views/public/semak/result.blade.php
Normal file
@@ -0,0 +1,90 @@
|
||||
@extends('layouts.public')
|
||||
|
||||
@section('title', 'Hasil Semakan — ' . $program->title)
|
||||
|
||||
@section('hero')
|
||||
<h4 class="mb-1">{{ $program->title }}</h4>
|
||||
<div class="opacity-75 small">
|
||||
<i class="bi bi-calendar3 me-1"></i>{{ $program->start_date->format('d M Y') }}
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
|
||||
@if(! $found)
|
||||
{{-- Not found --}}
|
||||
<div class="checkin-card card p-4 text-center">
|
||||
<div class="rounded-circle bg-danger bg-opacity-10 d-inline-flex align-items-center justify-content-center mx-auto mb-3"
|
||||
style="width:70px; height:70px;">
|
||||
<i class="bi bi-person-x-fill text-danger" style="font-size:2rem;"></i>
|
||||
</div>
|
||||
<h5 class="fw-bold text-danger mb-2">Tiada Rekod Kehadiran</h5>
|
||||
<p class="text-muted small mb-3">
|
||||
No. Kad Pengenalan yang dimasukkan tidak dijumpai dalam senarai kehadiran program ini.
|
||||
</p>
|
||||
<a href="{{ route('public.semak.show', $qrCode->token) }}" class="btn btn-outline-primary btn-sm">
|
||||
<i class="bi bi-arrow-left me-1"></i> Cuba Semula
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@else
|
||||
{{-- Found --}}
|
||||
<div class="checkin-card card p-4 text-center mb-3">
|
||||
<div class="rounded-circle bg-success bg-opacity-10 d-inline-flex align-items-center justify-content-center mx-auto mb-3"
|
||||
style="width:70px; height:70px;">
|
||||
<i class="bi bi-patch-check-fill text-success" style="font-size:2rem;"></i>
|
||||
</div>
|
||||
<h5 class="fw-bold text-success mb-1">Kehadiran Disahkan</h5>
|
||||
<p class="text-muted small mb-3">Rekod kehadiran anda dijumpai.</p>
|
||||
|
||||
<div class="bg-light rounded p-3 text-start">
|
||||
<div class="row g-2">
|
||||
<div class="col-5 text-muted small">Nama</div>
|
||||
<div class="col-7 fw-medium small">{{ $participant->name }}</div>
|
||||
<div class="col-5 text-muted small">Sesi</div>
|
||||
<div class="col-7 small"><span class="badge bg-primary">{{ Str::ucfirst($attendance->attendance_session) }}</span></div>
|
||||
<div class="col-5 text-muted small">Masa Check-In</div>
|
||||
<div class="col-7 small">{{ $attendance->checked_in_at->format('d/m/Y H:i') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Certificate status --}}
|
||||
@if($program->isDownloadOpen())
|
||||
@if($certificate && $certificate->isGenerated())
|
||||
<div class="checkin-card card p-4 text-center">
|
||||
<i class="bi bi-award-fill text-warning fs-1 mb-2"></i>
|
||||
<h6 class="fw-semibold mb-2">Sijil Sedia Dimuat Turun</h6>
|
||||
<p class="text-muted small mb-3">Klik butang di bawah untuk dapatkan sijil anda.</p>
|
||||
<a href="{{ route('public.certificate.show', $certificate->token) }}"
|
||||
class="btn btn-warning btn-checkin w-100">
|
||||
<i class="bi bi-download me-2"></i>Muat Turun Sijil
|
||||
</a>
|
||||
</div>
|
||||
@elseif($certificate && $certificate->status === 'pending')
|
||||
<div class="alert alert-info text-center">
|
||||
<i class="bi bi-hourglass-split me-2"></i>
|
||||
Sijil sedang disediakan. Sila semak emel anda atau cuba lagi sebentar.
|
||||
</div>
|
||||
@else
|
||||
<div class="alert alert-info text-center small">
|
||||
<i class="bi bi-envelope-fill me-2"></i>
|
||||
Link muat turun sijil akan dihantar ke emel anda.
|
||||
@if($participant->email)
|
||||
<strong>{{ $participant->email }}</strong>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<div class="alert alert-secondary text-center small">
|
||||
<i class="bi bi-clock me-2"></i>
|
||||
Sijil belum boleh dimuat turun.
|
||||
@if($program->ecert_download_start_at)
|
||||
Mula pada <strong>{{ $program->ecert_download_start_at->format('d M Y, H:i') }}</strong>.
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@endif
|
||||
|
||||
@endsection
|
||||
45
resources/views/public/semak/show.blade.php
Normal file
45
resources/views/public/semak/show.blade.php
Normal file
@@ -0,0 +1,45 @@
|
||||
@extends('layouts.public')
|
||||
|
||||
@section('title', 'Semak Kehadiran — ' . $program->title)
|
||||
|
||||
@section('hero')
|
||||
<h4 class="mb-1">{{ $program->title }}</h4>
|
||||
<div class="opacity-75 small">
|
||||
<i class="bi bi-calendar3 me-1"></i>{{ $program->start_date->format('d M Y') }}
|
||||
·
|
||||
<i class="bi bi-geo-alt me-1"></i>{{ $program->location }}
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('content')
|
||||
|
||||
@if($program->isDownloadOpen())
|
||||
<div class="alert alert-success mb-4 small">
|
||||
<i class="bi bi-award-fill me-2"></i>
|
||||
<strong>Sijil boleh dimuat turun!</strong>
|
||||
Masukkan No. Kad Pengenalan untuk semak status sijil anda.
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="checkin-card card p-4">
|
||||
<h6 class="fw-semibold mb-3">
|
||||
<i class="bi bi-search me-2 text-primary"></i>Semak Kehadiran & Sijil
|
||||
</h6>
|
||||
<form method="POST" action="{{ route('public.semak.check', $qrCode->token) }}">
|
||||
@csrf
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-medium">No. Kad Pengenalan <span class="text-danger">*</span></label>
|
||||
<input type="text" name="no_kp"
|
||||
class="form-control form-control-lg @error('no_kp') is-invalid @enderror"
|
||||
placeholder="900101011234"
|
||||
inputmode="numeric" maxlength="12" autocomplete="off">
|
||||
<div class="form-text">12 digit tanpa sempang</div>
|
||||
@error('no_kp')<div class="invalid-feedback">{{ $message }}</div>@enderror
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary w-100 btn-checkin">
|
||||
<i class="bi bi-search me-2"></i>Semak Sekarang
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@endsection
|
||||
Reference in New Issue
Block a user