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:
Saufi
2026-05-16 20:20:27 +08:00
parent 32428733d6
commit d0ebaf8433
9 changed files with 674 additions and 2 deletions

View 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

View 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 }}
&nbsp;&middot;&nbsp;
<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

View 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

View 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

View 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

View 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') }}
&nbsp;&middot;&nbsp;
<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