refactor: susun semula struktur folder — Laravel source ke src/

This commit is contained in:
Saufi
2026-05-19 15:58:35 +08:00
parent f052251b94
commit bf53c71b45
10806 changed files with 1385379 additions and 121 deletions

View File

@@ -0,0 +1,160 @@
@extends('layouts.admin')
@section('title', 'Dashboard')
@section('header', 'Dashboard')
@section('content')
{{-- Stats Row 1 --}}
<div class="row g-3 mb-4">
<div class="col-sm-6 col-xl-3">
<div class="card stat-card h-100">
<div class="card-body d-flex align-items-center gap-3">
<div class="stat-icon bg-primary bg-opacity-10">
<i class="bi bi-calendar-event-fill text-primary"></i>
</div>
<div>
<div class="text-muted small">Jumlah Program</div>
<div class="fs-3 fw-bold">{{ $stats['total_programs'] }}</div>
<div class="text-success small"><i class="bi bi-circle-fill me-1" style="font-size:.5rem;"></i>{{ $stats['active_programs'] }} aktif</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-xl-3">
<div class="card stat-card h-100">
<div class="card-body d-flex align-items-center gap-3">
<div class="stat-icon bg-success bg-opacity-10">
<i class="bi bi-people-fill text-success"></i>
</div>
<div>
<div class="text-muted small">Jumlah Peserta</div>
<div class="fs-3 fw-bold">{{ $stats['total_participants'] }}</div>
<div class="text-muted small">{{ $stats['total_attendances'] }} kehadiran direkod</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-xl-3">
<div class="card stat-card h-100">
<div class="card-body d-flex align-items-center gap-3">
<div class="stat-icon bg-warning bg-opacity-10">
<i class="bi bi-award-fill text-warning"></i>
</div>
<div>
<div class="text-muted small">Sijil Dijana</div>
<div class="fs-3 fw-bold">{{ $stats['generated_certs'] }}</div>
<div class="text-muted small">{{ $stats['downloaded_certs'] }} dimuat turun</div>
</div>
</div>
</div>
</div>
<div class="col-sm-6 col-xl-3">
<div class="card stat-card h-100">
<div class="card-body d-flex align-items-center gap-3">
<div class="stat-icon bg-info bg-opacity-10">
<i class="bi bi-clipboard2-check-fill text-info"></i>
</div>
<div>
<div class="text-muted small">Soalselidik Dijawab</div>
<div class="fs-3 fw-bold">{{ $stats['total_responses'] }}</div>
@if($stats['pending_emails'] > 0)
<div class="text-warning small"><i class="bi bi-envelope-fill me-1"></i>{{ $stats['pending_emails'] }} emel tertunda</div>
@else
<div class="text-muted small">Tiada emel tertunda</div>
@endif
</div>
</div>
</div>
</div>
</div>
{{-- Recent Programs --}}
<div class="row g-3">
<div class="col-lg-8">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom d-flex justify-content-between align-items-center py-3">
<span class="fw-semibold">Program Terkini</span>
<a href="{{ route('admin.programs.create') }}" class="btn btn-sm btn-primary">
<i class="bi bi-plus-lg me-1"></i> Program Baru
</a>
</div>
<div class="card-body p-0">
@if($recentPrograms->isEmpty())
<div class="text-center py-5 text-muted">
<i class="bi bi-calendar-x fs-1 d-block mb-2 opacity-25"></i>
Belum ada program. <a href="{{ route('admin.programs.create') }}">Tambah sekarang</a>
</div>
@else
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Nama Program</th>
<th>Tarikh</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach($recentPrograms as $program)
<tr>
<td>
<div class="fw-medium">{{ $program->title }}</div>
<small class="text-muted">{{ $program->organizer }}</small>
</td>
<td>
<small>{{ $program->start_date->format('d M Y') }}</small>
</td>
<td>
@include('admin.partials.program-status-badge', ['status' => $program->status])
</td>
<td class="text-end">
<a href="{{ route('admin.programs.show', $program) }}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i>
</a>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
</div>
@if($recentPrograms->isNotEmpty())
<div class="card-footer bg-white text-center py-2">
<a href="{{ route('admin.programs.index') }}" class="text-decoration-none small">
Lihat semua program <i class="bi bi-arrow-right ms-1"></i>
</a>
</div>
@endif
</div>
</div>
<div class="col-lg-4">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white border-bottom py-3">
<span class="fw-semibold">Tindakan Pantas</span>
</div>
<div class="card-body d-flex flex-column gap-2">
<a href="{{ route('admin.programs.create') }}" class="btn btn-outline-primary text-start">
<i class="bi bi-plus-circle me-2"></i> Tambah Program Baru
</a>
<a href="{{ route('admin.questionnaires.create') }}" class="btn btn-outline-secondary text-start">
<i class="bi bi-clipboard2-plus me-2"></i> Cipta Set Soalselidik
</a>
<a href="{{ route('admin.programs.index') }}" class="btn btn-outline-secondary text-start">
<i class="bi bi-list-ul me-2"></i> Semua Program
</a>
<a href="{{ route('admin.questionnaires.index') }}" class="btn btn-outline-secondary text-start">
<i class="bi bi-clipboard2-check me-2"></i> Semua Soalselidik
</a>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,9 @@
@php
$map = [
'draft' => ['secondary', 'Draf'],
'published' => ['success', 'Diterbitkan'],
'closed' => ['dark', 'Ditutup'],
];
[$color, $label] = $map[$status] ?? ['secondary', $status];
@endphp
<span class="badge bg-{{ $color }}">{{ $label }}</span>

View File

@@ -0,0 +1,137 @@
@extends('layouts.admin')
@section('title', 'Profil Saya')
@section('header', 'Profil Saya')
@section('breadcrumb')
<li class="breadcrumb-item active">Profil</li>
@endsection
@section('content')
<div class="row g-4" style="max-width:760px;">
{{-- Account Info --}}
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-body d-flex align-items-center gap-3 py-3">
<div class="rounded-circle bg-primary bg-opacity-10 d-flex align-items-center justify-content-center flex-shrink-0"
style="width:52px;height:52px;">
<i class="bi bi-person-fill text-primary fs-4"></i>
</div>
<div>
<div class="fw-semibold">{{ $user->name }}</div>
<div class="text-muted small">{{ $user->email }}</div>
<span class="badge {{ $user->isSuperAdmin() ? 'bg-danger' : 'bg-primary' }} mt-1">
{{ $user->isSuperAdmin() ? 'Super Admin' : 'Admin Program' }}
</span>
</div>
</div>
</div>
</div>
{{-- Update Email --}}
<div class="col-md-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white py-3">
<h6 class="mb-0 fw-semibold">
<i class="bi bi-envelope me-2 text-primary"></i>Tukar Alamat Emel
</h6>
</div>
<div class="card-body">
@if(session('email_success'))
<div class="alert alert-success small py-2">
<i class="bi bi-check-circle me-1"></i>{{ session('email_success') }}
</div>
@endif
<form method="POST" action="{{ route('admin.profile.update-email') }}">
@csrf @method('PUT')
<div class="mb-3">
<label class="form-label small fw-medium">Kata Laluan Semasa <span class="text-danger">*</span></label>
<input type="password" name="current_password" autocomplete="current-password"
class="form-control form-control-sm @error('current_password', 'email') is-invalid @enderror"
placeholder="••••••••">
@error('current_password', 'email')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label small fw-medium">Emel Baru <span class="text-danger">*</span></label>
<input type="email" name="email" autocomplete="email"
class="form-control form-control-sm @error('email', 'email') is-invalid @enderror"
value="{{ old('email', $user->email) }}"
placeholder="emel@contoh.com">
@error('email', 'email')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<button type="submit" class="btn btn-primary btn-sm w-100">
<i class="bi bi-check-lg me-1"></i> Kemaskini Emel
</button>
</form>
</div>
</div>
</div>
{{-- Update Password --}}
<div class="col-md-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white py-3">
<h6 class="mb-0 fw-semibold">
<i class="bi bi-key me-2 text-primary"></i>Tukar Kata Laluan
</h6>
</div>
<div class="card-body">
@if(session('password_success'))
<div class="alert alert-success small py-2">
<i class="bi bi-check-circle me-1"></i>{{ session('password_success') }}
</div>
@endif
<form method="POST" action="{{ route('admin.profile.update-password') }}">
@csrf @method('PUT')
<div class="mb-3">
<label class="form-label small fw-medium">Kata Laluan Semasa <span class="text-danger">*</span></label>
<input type="password" name="current_password" autocomplete="current-password"
class="form-control form-control-sm @error('current_password', 'password') is-invalid @enderror"
placeholder="••••••••">
@error('current_password', 'password')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label small fw-medium">Kata Laluan Baru <span class="text-danger">*</span></label>
<input type="password" name="password" autocomplete="new-password"
class="form-control form-control-sm @error('password', 'password') is-invalid @enderror"
placeholder="Min. 8 aksara">
@error('password', 'password')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label class="form-label small fw-medium">Sahkan Kata Laluan Baru <span class="text-danger">*</span></label>
<input type="password" name="password_confirmation" autocomplete="new-password"
class="form-control form-control-sm"
placeholder="••••••••">
</div>
<button type="submit" class="btn btn-primary btn-sm w-100">
<i class="bi bi-check-lg me-1"></i> Tukar Kata Laluan
</button>
</form>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,174 @@
{{--
Shared form partial.
Variables expected:
$program Program model (or null for create)
$action form action URL
$method PUT for edit, omit for create (POST)
--}}
<form method="POST" action="{{ $action }}">
@csrf
@isset($method) @method($method) @endisset
<div class="row g-4">
{{-- ── Maklumat Asas ── --}}
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom py-3">
<span class="fw-semibold"><i class="bi bi-info-circle me-2 text-primary"></i>Maklumat Asas</span>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-12">
<label class="form-label fw-medium">Nama Program <span class="text-danger">*</span></label>
<input type="text" name="title" class="form-control @error('title') is-invalid @enderror"
value="{{ old('title', $program->title ?? '') }}"
placeholder="Contoh: Kursus Pengurusan Projek 2025">
@error('title')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Penganjur <span class="text-danger">*</span></label>
<input type="text" name="organizer" class="form-control @error('organizer') is-invalid @enderror"
value="{{ old('organizer', $program->organizer ?? 'MBIP') }}"
placeholder="Contoh: Jabatan Sumber Manusia">
@error('organizer')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Lokasi <span class="text-danger">*</span></label>
<input type="text" name="location" class="form-control @error('location') is-invalid @enderror"
value="{{ old('location', $program->location ?? '') }}"
placeholder="Contoh: Dewan Bandaraya Ipoh">
@error('location')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-12">
<label class="form-label fw-medium">Deskripsi</label>
<textarea name="description" rows="3"
class="form-control @error('description') is-invalid @enderror"
placeholder="Huraian ringkas tentang program...">{{ old('description', $program->description ?? '') }}</textarea>
@error('description')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
</div>
</div>
</div>
</div>
{{-- ── Tarikh & Masa ── --}}
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom py-3">
<span class="fw-semibold"><i class="bi bi-calendar-range me-2 text-primary"></i>Tarikh & Masa</span>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-medium">Tarikh Mula <span class="text-danger">*</span></label>
<input type="date" name="start_date" class="form-control @error('start_date') is-invalid @enderror"
value="{{ old('start_date', isset($program->start_date) ? $program->start_date->format('Y-m-d') : '') }}">
@error('start_date')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Tarikh Tamat <span class="text-danger">*</span></label>
<input type="date" name="end_date" class="form-control @error('end_date') is-invalid @enderror"
value="{{ old('end_date', isset($program->end_date) ? $program->end_date->format('Y-m-d') : '') }}">
@error('end_date')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-12"><hr class="my-1"><p class="text-muted small mb-2">Tempoh Check-In (kosongkan jika tidak ditetapkan)</p></div>
<div class="col-md-6">
<label class="form-label fw-medium">Mula Check-In</label>
<input type="datetime-local" name="checkin_start_at"
class="form-control @error('checkin_start_at') is-invalid @enderror"
value="{{ old('checkin_start_at', isset($program->checkin_start_at) ? $program->checkin_start_at->format('Y-m-d\TH:i') : '') }}">
@error('checkin_start_at')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Tamat Check-In</label>
<input type="datetime-local" name="checkin_end_at"
class="form-control @error('checkin_end_at') is-invalid @enderror"
value="{{ old('checkin_end_at', isset($program->checkin_end_at) ? $program->checkin_end_at->format('Y-m-d\TH:i') : '') }}">
@error('checkin_end_at')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-12"><hr class="my-1"><p class="text-muted small mb-2">Tempoh Download eCert (kosongkan jika tidak ditetapkan)</p></div>
<div class="col-md-6">
<label class="form-label fw-medium">Mula Download Sijil</label>
<input type="datetime-local" name="ecert_download_start_at"
class="form-control @error('ecert_download_start_at') is-invalid @enderror"
value="{{ old('ecert_download_start_at', isset($program->ecert_download_start_at) ? $program->ecert_download_start_at->format('Y-m-d\TH:i') : '') }}">
@error('ecert_download_start_at')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Tamat Download Sijil</label>
<input type="datetime-local" name="ecert_download_end_at"
class="form-control @error('ecert_download_end_at') is-invalid @enderror"
value="{{ old('ecert_download_end_at', isset($program->ecert_download_end_at) ? $program->ecert_download_end_at->format('Y-m-d\TH:i') : '') }}">
@error('ecert_download_end_at')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
</div>
</div>
</div>
</div>
{{-- ── Tetapan Kehadiran ── --}}
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom py-3">
<span class="fw-semibold"><i class="bi bi-person-check me-2 text-primary"></i>Tetapan Kehadiran</span>
</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<div class="form-check form-switch mt-2">
<input type="hidden" name="allow_walk_in" value="0">
<input class="form-check-input" type="checkbox" id="allow_walk_in" name="allow_walk_in" value="1"
{{ old('allow_walk_in', $program->allow_walk_in ?? true) ? 'checked' : '' }}>
<label class="form-check-label fw-medium" for="allow_walk_in">
Benarkan Daftar Walk-in
</label>
</div>
<div class="text-muted small mt-1">Orang luar boleh daftar sendiri semasa program.</div>
</div>
<div class="col-md-4">
<label class="form-label fw-medium">Sesi Default (Kakitangan)</label>
<select name="default_staff_session" class="form-select @error('default_staff_session') is-invalid @enderror">
<option value=""> Pilih Sesi </option>
<option value="pagi" {{ old('default_staff_session', $program->default_staff_session ?? '') === 'pagi' ? 'selected' : '' }}>Pagi</option>
<option value="petang" {{ old('default_staff_session', $program->default_staff_session ?? '') === 'petang' ? 'selected' : '' }}>Petang</option>
<option value="full_day" {{ old('default_staff_session', $program->default_staff_session ?? '') === 'full_day' ? 'selected' : '' }}>Sehari Penuh</option>
</select>
@error('default_staff_session')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-md-4">
<label class="form-label fw-medium">Sesi Default (Peserta Luar)</label>
<select name="default_external_session" class="form-select @error('default_external_session') is-invalid @enderror">
<option value=""> Pilih Sesi </option>
<option value="pagi" {{ old('default_external_session', $program->default_external_session ?? '') === 'pagi' ? 'selected' : '' }}>Pagi</option>
<option value="petang" {{ old('default_external_session', $program->default_external_session ?? '') === 'petang' ? 'selected' : '' }}>Petang</option>
<option value="full_day" {{ old('default_external_session', $program->default_external_session ?? '') === 'full_day' ? 'selected' : '' }}>Sehari Penuh</option>
</select>
@error('default_external_session')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
</div>
</div>
</div>
</div>
{{-- ── Butang Submit ── --}}
<div class="col-12 d-flex justify-content-end gap-2">
<a href="{{ route('admin.programs.index') }}" class="btn btn-outline-secondary">
<i class="bi bi-x-lg me-1"></i> Batal
</a>
<button type="submit" class="btn btn-primary px-4">
<i class="bi bi-check-lg me-1"></i>
{{ isset($program) ? 'Simpan Perubahan' : 'Tambah Program' }}
</button>
</div>
</div>
</form>

View File

@@ -0,0 +1,135 @@
@extends('layouts.admin')
@section('title', 'Sijil — ' . $program->title)
@section('header', 'Pengurusan Sijil')
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.programs.index') }}">Program</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.programs.show', $program) }}">{{ Str::limit($program->title, 30) }}</a></li>
<li class="breadcrumb-item active">Sijil</li>
@endsection
@section('content')
{{-- Stats --}}
<div class="row g-3 mb-4">
<div class="col-6 col-md-3">
<div class="card border-0 bg-secondary bg-opacity-10 text-center p-3">
<div class="fs-3 fw-bold">{{ $stats['total'] }}</div>
<div class="small text-muted">Jumlah</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card border-0 bg-success bg-opacity-10 text-center p-3">
<div class="fs-3 fw-bold text-success">{{ $stats['generated'] }}</div>
<div class="small text-muted">Dijana</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card border-0 bg-warning bg-opacity-10 text-center p-3">
<div class="fs-3 fw-bold text-warning">{{ $stats['pending'] }}</div>
<div class="small text-muted">Menunggu</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card border-0 bg-danger bg-opacity-10 text-center p-3">
<div class="fs-3 fw-bold text-danger">{{ $stats['failed'] }}</div>
<div class="small text-muted">Gagal</div>
</div>
</div>
</div>
{{-- Actions --}}
<div class="d-flex gap-2 mb-4 flex-wrap">
<form method="POST" action="{{ route('admin.programs.certificates.generate-all', $program) }}">
@csrf
<button class="btn btn-primary"
onclick="return confirm('Jana sijil untuk semua peserta hadir?')">
<i class="bi bi-gear me-1"></i> Jana Semua Sijil
</button>
</form>
@if($stats['generated'] > 0)
<form method="POST" action="{{ route('admin.programs.certificates.email-all', $program) }}">
@csrf
<button class="btn btn-outline-success"
onclick="return confirm('Hantar emel sijil kepada semua peserta yang sijilnya sudah sedia?')">
<i class="bi bi-envelope me-1"></i> Hantar Emel Sijil
</button>
</form>
@endif
@if(! $program->certificateTemplate)
<div class="alert alert-warning mb-0 py-2 px-3 small">
<i class="bi bi-exclamation-triangle me-1"></i>
<a href="{{ route('admin.programs.template.show', $program) }}">Upload template sijil</a> dahulu sebelum jana sijil.
</div>
@endif
</div>
{{-- Certificate List --}}
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>Peserta</th>
<th>No. Sijil</th>
<th>Status</th>
<th>Dijana</th>
<th>Muat Turun</th>
<th></th>
</tr>
</thead>
<tbody>
@forelse($certificates as $cert)
<tr>
<td>
<div class="fw-medium small">{{ $cert->participant->name }}</div>
<div class="text-muted" style="font-size:0.75rem;">{{ $cert->participant->agency ?: '—' }}</div>
</td>
<td class="small text-muted">{{ $cert->certificate_no ?? '—' }}</td>
<td>
@if($cert->status === 'generated' || $cert->status === 'downloaded')
<span class="badge bg-success">Sedia</span>
@elseif($cert->status === 'emailed')
<span class="badge bg-info">Diemailkan</span>
@elseif($cert->status === 'pending')
<span class="badge bg-warning text-dark">Menunggu</span>
@elseif($cert->status === 'generating')
<span class="badge bg-secondary">Menjana...</span>
@elseif($cert->status === 'failed')
<span class="badge bg-danger" title="{{ $cert->error_message }}">Gagal</span>
@endif
</td>
<td class="small text-muted">{{ $cert->generated_at?->format('d/m H:i') ?? '—' }}</td>
<td class="small text-muted text-center">{{ $cert->download_count ?: '—' }}</td>
<td>
@if($cert->isGenerated())
<a href="{{ route('public.certificate.show', $cert->token) }}" target="_blank"
class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i>
</a>
@endif
</td>
</tr>
@empty
<tr>
<td colspan="6" class="text-center py-5 text-muted">
<i class="bi bi-award d-block fs-1 mb-3 opacity-25"></i>
Belum ada sijil dijana. Klik "Jana Semua Sijil" untuk mula.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($certificates->hasPages())
<div class="card-footer bg-white">
{{ $certificates->links() }}
</div>
@endif
</div>
@endsection

View File

@@ -0,0 +1,15 @@
@extends('layouts.admin')
@section('title', 'Tambah Program Baru')
@section('header', 'Tambah Program Baru')
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.programs.index') }}">Program</a></li>
<li class="breadcrumb-item active">Tambah Baru</li>
@endsection
@section('content')
@include('admin.programs._form', [
'action' => route('admin.programs.store'),
])
@endsection

View File

@@ -0,0 +1,18 @@
@extends('layouts.admin')
@section('title', 'Edit Program')
@section('header', 'Edit Program')
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.programs.index') }}">Program</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.programs.show', $program) }}">{{ Str::limit($program->title, 30) }}</a></li>
<li class="breadcrumb-item active">Edit</li>
@endsection
@section('content')
@include('admin.programs._form', [
'program' => $program,
'action' => route('admin.programs.update', $program),
'method' => 'PUT',
])
@endsection

View File

@@ -0,0 +1,175 @@
@extends('layouts.admin')
@section('title', 'Senarai Program')
@section('header', 'Senarai Program')
@section('breadcrumb')
<li class="breadcrumb-item active">Program</li>
@endsection
@section('header-actions')
<a href="{{ route('admin.programs.create') }}" class="btn btn-primary btn-sm">
<i class="bi bi-plus-lg me-1"></i> Program Baru
</a>
@endsection
@section('content')
{{-- Filter Bar --}}
<div class="card border-0 shadow-sm mb-4">
<div class="card-body py-3">
<form method="GET" class="row g-2 align-items-end">
<div class="col-sm-6 col-md-5">
<label class="form-label small text-muted mb-1">Carian</label>
<div class="input-group input-group-sm">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" name="search" class="form-control"
placeholder="Nama program, penganjur, lokasi..."
value="{{ request('search') }}">
</div>
</div>
<div class="col-sm-4 col-md-3">
<label class="form-label small text-muted mb-1">Status</label>
<select name="status" class="form-select form-select-sm">
<option value="">Semua Status</option>
<option value="draft" {{ request('status') === 'draft' ? 'selected' : '' }}>Draf</option>
<option value="published" {{ request('status') === 'published' ? 'selected' : '' }}>Diterbitkan</option>
<option value="closed" {{ request('status') === 'closed' ? 'selected' : '' }}>Ditutup</option>
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary btn-sm">Tapis</button>
@if(request()->hasAny(['search', 'status']))
<a href="{{ route('admin.programs.index') }}" class="btn btn-outline-secondary btn-sm ms-1">Reset</a>
@endif
</div>
</form>
</div>
</div>
{{-- Table --}}
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
@if($programs->isEmpty())
<div class="text-center py-5 text-muted">
<i class="bi bi-calendar-x fs-1 d-block mb-2 opacity-25"></i>
@if(request()->hasAny(['search', 'status']))
Tiada program sepadan dengan carian.
@else
Belum ada program. <a href="{{ route('admin.programs.create') }}">Tambah sekarang</a>
@endif
</div>
@else
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>Nama Program</th>
<th>Tarikh</th>
<th class="text-center">Kehadiran</th>
<th class="text-center">Peserta</th>
<th>Status</th>
<th class="text-end">Tindakan</th>
</tr>
</thead>
<tbody>
@foreach($programs as $program)
<tr>
<td>
<div class="fw-medium">{{ $program->title }}</div>
<small class="text-muted">
<i class="bi bi-geo-alt me-1"></i>{{ Str::limit($program->location, 40) }}
</small>
</td>
<td style="min-width:130px;">
<div class="small">{{ $program->start_date->format('d M Y') }}</div>
@if($program->start_date->ne($program->end_date))
<div class="small text-muted">- {{ $program->end_date->format('d M Y') }}</div>
@endif
</td>
<td class="text-center">
<span class="badge bg-light text-dark border">
{{ $program->attendances_count }}
</span>
</td>
<td class="text-center">
<span class="badge bg-light text-dark border">
{{ $program->program_participants_count }}
</span>
</td>
<td>
@include('admin.partials.program-status-badge', ['status' => $program->status])
</td>
<td class="text-end">
<div class="btn-group btn-group-sm">
<a href="{{ route('admin.programs.show', $program) }}"
class="btn btn-outline-primary" title="Lihat">
<i class="bi bi-eye"></i>
</a>
@if($program->status === 'draft')
<a href="{{ route('admin.programs.edit', $program) }}"
class="btn btn-outline-secondary" title="Edit">
<i class="bi bi-pencil"></i>
</a>
@endif
@if($program->status === 'draft' && $program->attendances_count === 0)
<button type="button" class="btn btn-outline-danger"
title="Padam"
onclick="confirmDelete('{{ $program->title }}', '{{ route('admin.programs.destroy', $program) }}')">
<i class="bi bi-trash"></i>
</button>
@endif
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
{{-- Pagination --}}
@if($programs->hasPages())
<div class="px-3 py-3 border-top">
{{ $programs->links() }}
</div>
@endif
@endif
</div>
</div>
{{-- Delete Confirm Modal --}}
<div class="modal fade" id="deleteModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header border-0">
<h6 class="modal-title fw-semibold">Sahkan Pemadaman</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>Adakah anda pasti untuk memadam program <strong id="deleteTitle"></strong>?</p>
<p class="text-muted small mb-0">Tindakan ini tidak boleh dibatalkan.</p>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Batal</button>
<form id="deleteForm" method="POST">
@csrf @method('DELETE')
<button type="submit" class="btn btn-danger btn-sm">
<i class="bi bi-trash me-1"></i> Ya, Padam
</button>
</form>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
function confirmDelete(title, url) {
document.getElementById('deleteTitle').textContent = '"' + title + '"';
document.getElementById('deleteForm').action = url;
new bootstrap.Modal(document.getElementById('deleteModal')).show();
}
</script>
@endpush

View File

@@ -0,0 +1,87 @@
@extends('layouts.admin')
@section('title', 'Tambah Peserta')
@section('header', 'Tambah Peserta')
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.programs.index') }}">Program</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.programs.show', $program) }}">{{ Str::limit($program->title, 25) }}</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.programs.participants.index', $program) }}">Peserta</a></li>
<li class="breadcrumb-item active">Tambah</li>
@endsection
@section('content')
<div class="row justify-content-center">
<div class="col-lg-7">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom py-3">
<span class="fw-semibold">
<i class="bi bi-person-plus me-2 text-primary"></i>Tambah Peserta Pra-Daftar
</span>
</div>
<div class="card-body">
<form method="POST" action="{{ route('admin.programs.participants.store', $program) }}">
@csrf
<div class="row g-3">
<div class="col-12">
<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"
value="{{ old('name') }}" placeholder="Contoh: Ahmad bin Ali">
@error('name')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-md-6">
<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"
value="{{ old('no_kp') }}" placeholder="900101011234"
maxlength="14">
<div class="form-text">Tanpa sempang. 12 digit.</div>
@error('no_kp')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Sesi</label>
<select name="session" class="form-select @error('session') is-invalid @enderror">
<option value=""> Pilih Sesi </option>
<option value="pagi" {{ old('session', $program->default_staff_session) === 'pagi' ? 'selected' : '' }}>Pagi</option>
<option value="petang" {{ old('session', $program->default_staff_session) === 'petang' ? 'selected' : '' }}>Petang</option>
<option value="full_day" {{ old('session', $program->default_staff_session) === 'full_day' ? 'selected' : '' }}>Sehari Penuh</option>
</select>
@error('session')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-md-6">
<label class="form-label fw-medium">Emel</label>
<input type="email" name="email" class="form-control @error('email') is-invalid @enderror"
value="{{ old('email') }}" placeholder="ahmad@jabatan.gov.my">
@error('email')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-md-6">
<label class="form-label fw-medium">No. Telefon</label>
<input type="text" name="phone" class="form-control @error('phone') is-invalid @enderror"
value="{{ old('phone') }}" placeholder="0123456789">
@error('phone')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="col-12">
<label class="form-label fw-medium">Jabatan / Agensi</label>
<input type="text" name="agency" class="form-control @error('agency') is-invalid @enderror"
value="{{ old('agency') }}" placeholder="Jabatan Sumber Manusia">
@error('agency')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
</div>
<div class="d-flex justify-content-end gap-2 mt-4">
<a href="{{ route('admin.programs.participants.index', $program) }}"
class="btn btn-outline-secondary">Batal</a>
<button type="submit" class="btn btn-primary px-4">
<i class="bi bi-check-lg me-1"></i> Tambah Peserta
</button>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,136 @@
@extends('layouts.admin')
@section('title', 'Import Peserta CSV')
@section('header', 'Import Peserta CSV')
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.programs.index') }}">Program</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.programs.show', $program) }}">{{ Str::limit($program->title, 25) }}</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.programs.participants.index', $program) }}">Peserta</a></li>
<li class="breadcrumb-item active">Import CSV</li>
@endsection
@section('content')
<div class="row g-4 justify-content-center">
<div class="col-lg-7">
{{-- Import Result --}}
@if(session('import_result'))
@php $r = session('import_result'); @endphp
<div class="card border-0 shadow-sm mb-4 border-start border-4
{{ $r['failed'] > 0 ? 'border-warning' : 'border-success' }}">
<div class="card-body">
<h6 class="fw-semibold mb-3">
<i class="bi bi-clipboard-check me-2 text-success"></i>Hasil Import
</h6>
<div class="row g-2 text-center mb-3">
<div class="col-4">
<div class="p-2 bg-success bg-opacity-10 rounded">
<div class="fs-4 fw-bold text-success">{{ $r['success'] }}</div>
<div class="small text-muted">Berjaya</div>
</div>
</div>
<div class="col-4">
<div class="p-2 bg-warning bg-opacity-10 rounded">
<div class="fs-4 fw-bold text-warning">{{ $r['duplicates'] }}</div>
<div class="small text-muted">Duplikasi</div>
</div>
</div>
<div class="col-4">
<div class="p-2 bg-danger bg-opacity-10 rounded">
<div class="fs-4 fw-bold text-danger">{{ $r['failed'] }}</div>
<div class="small text-muted">Gagal</div>
</div>
</div>
</div>
@if(!empty($r['errors']))
<div class="mt-2">
<small class="text-muted fw-semibold">Ralat:</small>
<ul class="small text-danger mb-0 ps-3 mt-1">
@foreach(array_slice($r['errors'], 0, 10) as $err)
<li>{{ $err }}</li>
@endforeach
@if(count($r['errors']) > 10)
<li class="text-muted">...dan {{ count($r['errors']) - 10 }} ralat lagi.</li>
@endif
</ul>
</div>
@endif
</div>
</div>
@endif
{{-- Upload Form --}}
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom py-3">
<span class="fw-semibold">
<i class="bi bi-upload me-2 text-primary"></i>Muat Naik Fail CSV
</span>
</div>
<div class="card-body">
<form method="POST" action="{{ route('admin.programs.participants.import', $program) }}"
enctype="multipart/form-data">
@csrf
<div class="mb-3">
<label class="form-label fw-medium">Fail CSV <span class="text-danger">*</span></label>
<input type="file" name="csv_file" accept=".csv,.txt"
class="form-control @error('csv_file') is-invalid @enderror">
<div class="form-text">Saiz maksimum: 5MB. Format: CSV (UTF-8)</div>
@error('csv_file')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="mb-4">
<label class="form-label fw-medium">Sesi Default</label>
<select name="session" class="form-select">
<option value=""> Ikut Tetapan Program </option>
<option value="pagi" {{ $program->default_staff_session === 'pagi' ? 'selected' : '' }}>Pagi</option>
<option value="petang" {{ $program->default_staff_session === 'petang' ? 'selected' : '' }}>Petang</option>
<option value="full_day" {{ $program->default_staff_session === 'full_day' ? 'selected' : '' }}>Sehari Penuh</option>
</select>
<div class="form-text">Sesi yang akan digunakan untuk semua peserta dalam fail ini.</div>
</div>
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-upload me-2"></i>Mula Import
</button>
</form>
</div>
</div>
</div>
{{-- Format Guide --}}
<div class="col-lg-4">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom py-3">
<span class="fw-semibold">
<i class="bi bi-file-earmark-text me-2 text-primary"></i>Format CSV
</span>
</div>
<div class="card-body">
<p class="small text-muted mb-2">Header wajib (baris pertama):</p>
<code class="d-block bg-light p-2 rounded small mb-3">name,no_kp,email,phone,agency</code>
<p class="small text-muted mb-2">Contoh data:</p>
<code class="d-block bg-light p-2 rounded small mb-3" style="font-size:.75rem; word-break:break-all;">
Ahmad Ali,900101011234,ahmad@mbip.gov.my,0123456789,IT<br>
Siti Binti Omar,850505055678,,0198765432,Kewangan
</code>
<div class="small text-muted">
<p class="mb-1"><i class="bi bi-info-circle text-info me-1"></i>No. K/P: 12 digit tanpa sempang.</p>
<p class="mb-1"><i class="bi bi-info-circle text-info me-1"></i>Emel, telefon, agensi: boleh kosong.</p>
<p class="mb-1"><i class="bi bi-shield-check text-success me-1"></i>Duplikasi dalam program akan dilangkau.</p>
<p class="mb-0"><i class="bi bi-shield-check text-success me-1"></i>Ralat satu baris tidak hentikan import keseluruhan.</p>
</div>
<div class="mt-3 pt-3 border-top">
<a href="{{ route('admin.programs.participants.export', $program) }}"
class="btn btn-outline-secondary btn-sm w-100">
<i class="bi bi-download me-1"></i> Muat Turun Senarai Semasa
</a>
</div>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,172 @@
@extends('layouts.admin')
@section('title', 'Peserta — ' . $program->title)
@section('header', 'Senarai Peserta')
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.programs.index') }}">Program</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.programs.show', $program) }}">{{ Str::limit($program->title, 25) }}</a></li>
<li class="breadcrumb-item active">Peserta</li>
@endsection
@section('header-actions')
<div class="d-flex gap-2">
<a href="{{ route('admin.programs.participants.export', $program) }}" class="btn btn-sm btn-outline-success">
<i class="bi bi-download me-1"></i> Export CSV
</a>
<a href="{{ route('admin.programs.participants.import.form', $program) }}" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-upload me-1"></i> Import CSV
</a>
<a href="{{ route('admin.programs.participants.create', $program) }}" class="btn btn-sm btn-primary">
<i class="bi bi-person-plus me-1"></i> Tambah
</a>
</div>
@endsection
@section('content')
{{-- Summary Cards --}}
<div class="row g-3 mb-4">
@foreach([
['label' => 'Jumlah Peserta', 'value' => $counts['total'], 'icon' => 'bi-people', 'color' => 'primary'],
['label' => 'Pra-Daftar', 'value' => $counts['pre_registered'], 'icon' => 'bi-person-check', 'color' => 'info'],
['label' => 'Walk-In', 'value' => $counts['walk_in'], 'icon' => 'bi-person-walking', 'color' => 'warning'],
['label' => 'Hadir', 'value' => $counts['checked_in'], 'icon' => 'bi-patch-check', 'color' => 'success'],
] as $card)
<div class="col-6 col-md-3">
<div class="card border-0 shadow-sm text-center p-3">
<i class="bi {{ $card['icon'] }} text-{{ $card['color'] }} fs-4 mb-1"></i>
<div class="fs-4 fw-bold">{{ $card['value'] }}</div>
<div class="text-muted small">{{ $card['label'] }}</div>
</div>
</div>
@endforeach
</div>
{{-- Filter --}}
<div class="card border-0 shadow-sm mb-3">
<div class="card-body py-3">
<form method="GET" class="row g-2 align-items-end">
<div class="col-sm-5">
<div class="input-group input-group-sm">
<span class="input-group-text"><i class="bi bi-search"></i></span>
<input type="text" name="search" class="form-control"
placeholder="Nama, agensi..." value="{{ request('search') }}">
</div>
</div>
<div class="col-sm-3">
<select name="source" class="form-select form-select-sm">
<option value="">Semua Sumber</option>
<option value="pre_registered" {{ request('source') === 'pre_registered' ? 'selected' : '' }}>Pra-Daftar</option>
<option value="import" {{ request('source') === 'import' ? 'selected' : '' }}>Import</option>
<option value="walk_in" {{ request('source') === 'walk_in' ? 'selected' : '' }}>Walk-In</option>
<option value="admin_manual" {{ request('source') === 'admin_manual' ? 'selected' : '' }}>Manual Admin</option>
</select>
</div>
<div class="col-sm-2">
<select name="status" class="form-select form-select-sm">
<option value="">Semua Status</option>
<option value="registered" {{ request('status') === 'registered' ? 'selected' : '' }}>Berdaftar</option>
<option value="checked_in" {{ request('status') === 'checked_in' ? 'selected' : '' }}>Hadir</option>
<option value="cancelled" {{ request('status') === 'cancelled' ? 'selected' : '' }}>Dibatalkan</option>
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-primary btn-sm">Tapis</button>
@if(request()->hasAny(['search', 'source', 'status']))
<a href="{{ route('admin.programs.participants.index', $program) }}" class="btn btn-outline-secondary btn-sm ms-1">Reset</a>
@endif
</div>
</form>
</div>
</div>
{{-- Table --}}
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
@if($programParticipants->isEmpty())
<div class="text-center py-5 text-muted">
<i class="bi bi-people fs-1 opacity-25 d-block mb-2"></i>
Belum ada peserta.
</div>
@else
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>#</th>
<th>Nama</th>
<th>Agensi</th>
<th>Sesi</th>
<th>Sumber</th>
<th>Status</th>
<th class="text-end">Tindakan</th>
</tr>
</thead>
<tbody>
@foreach($programParticipants as $i => $pp)
@php $p = $pp->participant; @endphp
<tr>
<td class="text-muted small">{{ $programParticipants->firstItem() + $i }}</td>
<td>
<div class="fw-medium">{{ $p->name }}</div>
<small class="text-muted">{{ $p->email ?: '—' }}</small>
</td>
<td><small>{{ $p->agency ?: '—' }}</small></td>
<td>
@if($pp->pre_registered_session)
<span class="badge bg-light text-dark border">
{{ Str::ucfirst($pp->pre_registered_session) }}
</span>
@else
<span class="text-muted"></span>
@endif
</td>
<td>
@php
$sourceMap = [
'pre_registered' => ['secondary', 'Pra-Daftar'],
'import' => ['info', 'Import'],
'walk_in' => ['warning', 'Walk-In'],
'admin_manual' => ['dark', 'Manual'],
];
[$sc, $sl] = $sourceMap[$pp->registration_source] ?? ['light', $pp->registration_source];
@endphp
<span class="badge bg-{{ $sc }}">{{ $sl }}</span>
</td>
<td>
@if($pp->status === 'checked_in')
<span class="badge bg-success"><i class="bi bi-check-lg me-1"></i>Hadir</span>
@elseif($pp->status === 'cancelled')
<span class="badge bg-danger">Dibatalkan</span>
@else
<span class="badge bg-light text-dark border">Berdaftar</span>
@endif
</td>
<td class="text-end">
@if($pp->status !== 'checked_in')
<form method="POST"
action="{{ route('admin.programs.participants.destroy', [$program, $pp]) }}"
onsubmit="return confirm('Keluarkan peserta {{ $p->name }} daripada program?')">
@csrf @method('DELETE')
<button class="btn btn-sm btn-outline-danger" title="Keluarkan">
<i class="bi bi-person-dash"></i>
</button>
</form>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@if($programParticipants->hasPages())
<div class="px-3 py-3 border-top">
{{ $programParticipants->links() }}
</div>
@endif
@endif
</div>
</div>
@endsection

View File

@@ -0,0 +1,171 @@
@extends('layouts.admin')
@section('title', 'QR Code — ' . $program->title)
@section('header', 'QR Code Program')
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.programs.index') }}">Program</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.programs.show', $program) }}">{{ Str::limit($program->title, 30) }}</a></li>
<li class="breadcrumb-item active">QR Code</li>
@endsection
@section('content')
<div class="row g-4 justify-content-center">
<div class="col-lg-6">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom py-3">
<span class="fw-semibold"><i class="bi bi-qr-code me-2 text-primary"></i>QR Code Check-In</span>
</div>
<div class="card-body text-center py-4">
@if($qrCode)
{{-- QR Code Image --}}
<div class="mb-4">
<img src="{{ Storage::disk('public')->url($qrCode->qr_image_path) }}"
alt="QR Code {{ $program->title }}"
class="img-fluid border rounded p-2"
style="max-width: 280px;">
</div>
{{-- Program Info --}}
<h6 class="fw-semibold mb-1">{{ $program->title }}</h6>
<p class="text-muted small mb-3">
{{ $program->start_date->format('d M Y') }}
@if($program->start_date->ne($program->end_date))
{{ $program->end_date->format('d M Y') }}
@endif
&middot; {{ $program->location }}
</p>
{{-- Check-in URL --}}
<div class="bg-light rounded p-3 mb-4 text-start">
<div class="text-muted small mb-1">URL Check-In:</div>
<code class="small text-break" id="checkinUrl">{{ route('public.checkin.show', $qrCode->token) }}</code>
<button class="btn btn-sm btn-outline-secondary ms-2"
onclick="copyUrl()" title="Salin URL">
<i class="bi bi-clipboard" id="copyIcon"></i>
</button>
</div>
{{-- Action Buttons --}}
<div class="d-flex flex-column flex-sm-row gap-2 justify-content-center">
<a href="{{ route('admin.programs.qr.download', $program) }}"
class="btn btn-primary">
<i class="bi bi-download me-2"></i>Muat Turun PNG
</a>
<form method="POST" action="{{ route('admin.programs.qr.generate', $program) }}">
@csrf
<button type="submit" class="btn btn-outline-warning w-100"
onclick="return confirm('Jana semula QR Code? QR Code lama akan dinyahaktifkan.')">
<i class="bi bi-arrow-repeat me-2"></i>Jana Semula
</button>
</form>
<form method="POST" action="{{ route('admin.programs.qr.deactivate', $program) }}">
@csrf
<button type="submit" class="btn btn-outline-danger w-100"
onclick="return confirm('Nyahaktifkan QR Code ini?')">
<i class="bi bi-x-circle me-2"></i>Nyahaktifkan
</button>
</form>
</div>
{{-- Status --}}
<div class="mt-3">
<span class="badge bg-success"><i class="bi bi-circle-fill me-1" style="font-size:.5rem;"></i>Aktif</span>
<small class="text-muted ms-2">Dijana {{ $qrCode->created_at->diffForHumans() }}</small>
</div>
@else
{{-- No QR Code yet --}}
<div class="py-4">
<i class="bi bi-qr-code fs-1 text-muted opacity-25 d-block mb-3"></i>
<h6 class="text-muted mb-3">QR Code belum dijana</h6>
<p class="text-muted small mb-4">
Jana QR Code untuk membolehkan peserta scan dan check-in ke program ini.
</p>
<form method="POST" action="{{ route('admin.programs.qr.generate', $program) }}">
@csrf
<button type="submit" class="btn btn-primary btn-lg">
<i class="bi bi-qr-code me-2"></i>Jana QR Code
</button>
</form>
</div>
@endif
</div>
</div>
</div>
{{-- How to Use --}}
<div class="col-lg-4">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom py-3">
<span class="fw-semibold"><i class="bi bi-info-circle me-2 text-primary"></i>Cara Penggunaan</span>
</div>
<div class="card-body">
<ol class="ps-3 small text-muted" style="line-height: 2;">
<li>Jana QR Code untuk program ini.</li>
<li>Print atau papar QR Code di tempat program.</li>
<li>Peserta scan menggunakan kamera telefon.</li>
<li>Peserta isi maklumat check-in.</li>
<li>Sistem rekod kehadiran secara automatik.</li>
</ol>
<hr>
<div class="small text-muted">
<i class="bi bi-shield-check text-success me-1"></i>
QR Code mengandungi token unik bukan ID program.
</div>
<div class="small text-muted mt-2">
<i class="bi bi-exclamation-triangle text-warning me-1"></i>
Jana semula akan membatalkan QR Code lama.
</div>
</div>
</div>
<div class="card border-0 shadow-sm mt-3">
<div class="card-header bg-white border-bottom py-3">
<span class="fw-semibold"><i class="bi bi-calendar-check me-2 text-primary"></i>Status Program</span>
</div>
<div class="card-body small">
<div class="d-flex justify-content-between mb-2">
<span class="text-muted">Status</span>
@include('admin.partials.program-status-badge', ['status' => $program->status])
</div>
@if($program->checkin_start_at)
<div class="d-flex justify-content-between mb-2">
<span class="text-muted">Check-In Dibuka</span>
<span>{{ $program->checkin_start_at->format('d/m H:i') }}</span>
</div>
<div class="d-flex justify-content-between mb-2">
<span class="text-muted">Check-In Ditutup</span>
<span>{{ $program->checkin_end_at?->format('d/m H:i') ?? '—' }}</span>
</div>
@endif
@if($program->ecert_download_start_at)
<div class="d-flex justify-content-between">
<span class="text-muted">Download Sijil</span>
<span>{{ $program->ecert_download_start_at->format('d/m H:i') }}</span>
</div>
@endif
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
function copyUrl() {
const url = document.getElementById('checkinUrl').textContent.trim();
navigator.clipboard.writeText(url).then(() => {
const icon = document.getElementById('copyIcon');
icon.className = 'bi bi-clipboard-check';
setTimeout(() => icon.className = 'bi bi-clipboard', 2000);
});
}
</script>
@endpush

View File

@@ -0,0 +1,151 @@
@extends('layouts.public')
@section('title', 'Pratonton Soalselidik — ' . $program->title)
@section('hero')
<h4 class="mb-1">{{ $program->title }}</h4>
<div class="opacity-75 small">
<i class="bi bi-clipboard2-check me-1"></i>Borang Penilaian Program
</div>
@endsection
@push('styles')
<style>
.preview-banner {
position: sticky;
top: 0;
z-index: 1000;
background: #fff3cd;
border-bottom: 2px solid #ffc107;
padding: .6rem 1rem;
text-align: center;
font-size: .85rem;
font-weight: 600;
color: #664d03;
}
.preview-banner i { margin-right: .4rem; }
fieldset { border: none; padding: 0; margin: 0; }
</style>
@endpush
@section('content')
<div class="preview-banner">
<i class="bi bi-eye"></i>PRATONTON ADMIN Borang ini tidak akan dihantar
<button type="button" class="btn btn-sm btn-outline-secondary ms-3 py-0" onclick="window.close()">
<i class="bi bi-x-lg me-1"></i>Tutup
</button>
</div>
<div class="checkin-card card p-4 mb-3 mt-3">
<div class="text-center mb-4">
<div class="rounded-circle bg-primary 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-clipboard2-check-fill text-primary" style="font-size:2rem;"></i>
</div>
<h5 class="fw-bold mb-1">{{ $pq->questionnaireSet->title }}</h5>
@if($pq->questionnaireSet->description)
<p class="text-muted small mb-1">{{ $pq->questionnaireSet->description }}</p>
@endif
<p class="text-muted small mb-0">
Sila jawab semua soalan sebelum memuat turun sijil anda, <strong>PESERTA CONTOH</strong>.
</p>
</div>
<fieldset disabled>
@php $qNum = 0; @endphp
@foreach($questions as $q)
@if($q->question_type === 'tajuk')
{{-- ── Section header ─────────────────────────────── --}}
<div class="d-flex align-items-center gap-2 mt-4 mb-3 pb-1 border-bottom">
<span class="fw-bold text-primary">{{ $q->question_text }}</span>
</div>
@foreach($q->children as $child)
@php $qNum++ @endphp
<div class="mb-4">
<label class="form-label fw-medium">
{{ $qNum }}. {{ $child->question_text }}
@if($child->is_required)<span class="text-danger">*</span>@endif
</label>
<div class="d-flex gap-3 flex-wrap">
@for($i = 1; $i <= 5; $i++)
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio"
name="q_{{ $child->id }}" value="{{ $i }}">
<label class="form-check-label">
{{ $i }}
@php $label = $q->rating_labels[$i] ?? $q->rating_labels[strval($i)] ?? ''; @endphp
@if($label)
<small class="text-muted d-block" style="font-size:.7rem;">({{ $label }})</small>
@endif
</label>
</div>
@endfor
</div>
</div>
@endforeach
@else
{{-- ── Standalone question ─────────────────────────── --}}
@php $qNum++ @endphp
<div class="mb-4">
<label class="form-label fw-medium">
{{ $qNum }}. {{ $q->question_text }}
@if($q->is_required)<span class="text-danger">*</span>@endif
</label>
@if($q->question_type === 'rating')
<div class="d-flex gap-3 flex-wrap">
@for($i = 1; $i <= 5; $i++)
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="q_{{ $q->id }}" value="{{ $i }}">
<label class="form-check-label">
{{ $i }}
@if($i === 1)<small class="text-muted">(Sangat Tidak Setuju)</small>
@elseif($i === 5)<small class="text-muted">(Sangat Setuju)</small>
@endif
</label>
</div>
@endfor
</div>
@elseif($q->question_type === 'single_choice')
@foreach($q->options_json ?? [] as $opt)
<div class="form-check">
<input class="form-check-input" type="radio" name="q_{{ $q->id }}" value="{{ $opt }}">
<label class="form-check-label">{{ $opt }}</label>
</div>
@endforeach
@elseif($q->question_type === 'multiple_choice')
@foreach($q->options_json ?? [] as $opt)
<div class="form-check">
<input class="form-check-input" type="checkbox" name="q_{{ $q->id }}[]" value="{{ $opt }}">
<label class="form-check-label">{{ $opt }}</label>
</div>
@endforeach
@elseif($q->question_type === 'short_text')
<input type="text" class="form-control" placeholder="Jawapan anda...">
@elseif($q->question_type === 'long_text')
<textarea class="form-control" rows="4" placeholder="Jawapan anda..."></textarea>
@endif
</div>
@endif
@endforeach
</fieldset>
<div class="alert alert-warning d-flex align-items-center gap-2 mb-0 mt-2">
<i class="bi bi-eye-fill flex-shrink-0"></i>
<span class="small">Ini adalah <strong>pratonton admin</strong>. Borang ini tidak boleh dihantar.</span>
</div>
</div>
@endsection

View File

@@ -0,0 +1,202 @@
@extends('layouts.admin')
@section('title', 'Soalselidik — ' . $program->title)
@section('header', 'Urus Soalselidik Program')
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.programs.index') }}">Program</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.programs.show', $program) }}">{{ Str::limit($program->title, 30) }}</a></li>
<li class="breadcrumb-item active">Soalselidik</li>
@endsection
@section('header-actions')
<div class="d-flex gap-2">
@if($pq && $pq->questionnaireSet)
<a href="{{ route('admin.programs.questionnaire.preview', $program) }}" target="_blank"
class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye me-1"></i> Pratonton
</a>
@endif
<a href="{{ route('admin.programs.show', $program) }}#tab-questionnaire" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i> Kembali
</a>
</div>
@endsection
@section('content')
<div class="row g-4">
{{-- Current Questionnaire --}}
<div class="col-md-7">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white py-3">
<h6 class="mb-0 fw-semibold"><i class="bi bi-clipboard2-check me-2 text-primary"></i>Soalselidik Semasa</h6>
</div>
<div class="card-body">
@if($pq && $pq->questionnaireSet)
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<div class="fw-semibold">{{ $pq->questionnaireSet->title }}</div>
<div class="text-muted small">{{ $pq->questionnaireSet->questions->count() }} soalan</div>
@if($pq->questionnaireSet->description)
<div class="text-muted small mt-1">{{ $pq->questionnaireSet->description }}</div>
@endif
</div>
@if($pq->is_confirmed)
<span class="badge bg-success fs-6 px-3 py-2">
<i class="bi bi-check-circle me-1"></i> Disahkan
</span>
@else
<span class="badge bg-warning text-dark fs-6 px-3 py-2">
<i class="bi bi-exclamation-circle me-1"></i> Belum Disahkan
</span>
@endif
</div>
@if($pq->is_confirmed)
<div class="alert alert-success small mb-3">
<i class="bi bi-info-circle me-1"></i>
Disahkan oleh <strong>{{ $pq->confirmedBy?->name ?? '—' }}</strong>
pada {{ $pq->confirmed_at?->format('d M Y, H:i') }}.
</div>
@else
<div class="alert alert-warning small mb-3">
<i class="bi bi-exclamation-triangle me-1"></i>
Soalselidik perlu <strong>disahkan</strong> sebelum peserta boleh menjawab.
</div>
<form method="POST" action="{{ route('admin.programs.questionnaire.confirm', $program) }}" class="mb-3">
@csrf
<button class="btn btn-success w-100" onclick="return confirm('Sahkan soalselidik ini untuk program?')">
<i class="bi bi-check-circle me-2"></i> Sahkan Soalselidik
</button>
</form>
@endif
{{-- List Questions --}}
<div class="border rounded p-3 bg-light">
<div class="small fw-medium text-muted mb-2">Senarai Soalan:</div>
@php
$allQs = $pq->questionnaireSet->questions->sortBy('sort_order');
$topQs = $allQs->whereNull('parent_id');
$qNum = 0;
@endphp
@foreach($topQs as $q)
@if($q->question_type === 'tajuk')
<div class="d-flex align-items-center gap-2 mt-2 mb-1">
<span class="badge bg-dark" style="font-size:0.6rem;">Tajuk</span>
<div class="small fw-semibold text-dark">{{ $q->question_text }}</div>
</div>
@foreach($allQs->where('parent_id', $q->id)->sortBy('sort_order') as $child)
@php $qNum++ @endphp
<div class="d-flex align-items-start gap-2 mb-1 ps-3">
<span class="badge bg-secondary flex-shrink-0" style="min-width:22px;font-size:0.65rem;">{{ $qNum }}</span>
<div>
<div class="small">{{ $child->question_text }}</div>
<span class="badge bg-light text-dark border" style="font-size:0.6rem;">Rating 15</span>
@if($child->is_required)<span class="badge bg-danger bg-opacity-10 text-danger" style="font-size:0.6rem;">Wajib</span>@endif
</div>
</div>
@endforeach
@else
@php $qNum++ @endphp
<div class="d-flex align-items-start gap-2 mb-2">
<span class="badge bg-secondary flex-shrink-0" style="min-width:22px;font-size:0.65rem;">{{ $qNum }}</span>
<div>
<div class="small">{{ $q->question_text }}</div>
<span class="badge bg-light text-dark border" style="font-size:0.65rem;">
{{ match($q->question_type) {
'rating' => 'Rating 15',
'single_choice' => 'Pilihan Tunggal',
'multiple_choice' => 'Pilihan Berganda',
'short_text' => 'Teks Pendek',
'long_text' => 'Teks Panjang',
default => $q->question_type,
} }}
</span>
@if($q->is_required)<span class="badge bg-danger bg-opacity-10 text-danger" style="font-size:0.65rem;">Wajib</span>@endif
</div>
</div>
@endif
@endforeach
</div>
{{-- Detach --}}
<div class="mt-3">
<form method="POST" action="{{ route('admin.programs.questionnaire.detach', $program) }}"
onsubmit="return confirm('Tanggalkan soalselidik dari program ini?')">
@csrf @method('DELETE')
<button class="btn btn-sm btn-outline-danger">
<i class="bi bi-x-circle me-1"></i> Tanggalkan Soalselidik
</button>
</form>
</div>
@else
<div class="text-center py-4 text-muted">
<i class="bi bi-clipboard2-x d-block fs-1 mb-3 opacity-25"></i>
<p class="mb-0">Belum ada soalselidik dilampirkan untuk program ini.</p>
</div>
@endif
</div>
</div>
</div>
{{-- Right: Attach New --}}
@if(! $pq)
<div class="col-md-5">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white py-3">
<h6 class="mb-0 fw-semibold"><i class="bi bi-clipboard2-plus me-2 text-success"></i>Lampir Soalselidik</h6>
</div>
<div class="card-body">
@if($availableSets->isEmpty())
<div class="alert alert-info small">
<i class="bi bi-info-circle me-1"></i>
Tiada set soalselidik yang diterbitkan.
<a href="{{ route('admin.questionnaires.create') }}">Buat set baru.</a>
</div>
@else
<form method="POST" action="{{ route('admin.programs.questionnaire.attach', $program) }}">
@csrf
<div class="mb-3">
<label class="form-label fw-medium small">Pilih Set Soalselidik <span class="text-danger">*</span></label>
<select name="questionnaire_set_id"
class="form-select @error('questionnaire_set_id') is-invalid @enderror">
<option value=""> Pilih </option>
@foreach($availableSets as $qs)
<option value="{{ $qs->id }}" {{ old('questionnaire_set_id') == $qs->id ? 'selected' : '' }}>
{{ $qs->title }} ({{ $qs->questions_count }} soalan)
</option>
@endforeach
</select>
@error('questionnaire_set_id')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="alert alert-info small">
<i class="bi bi-lightbulb me-1"></i>
Selepas lampir, anda perlu <strong>sahkan</strong> soalselidik sebelum peserta boleh menjawab.
</div>
<button type="submit" class="btn btn-success w-100">
<i class="bi bi-clipboard2-plus me-2"></i> Lampir & Teruskan
</button>
</form>
@endif
<div class="mt-3 text-center">
<a href="{{ route('admin.questionnaires.index') }}" class="small text-muted">
<i class="bi bi-gear me-1"></i> Urus Set Soalselidik
</a>
</div>
</div>
</div>
</div>
@endif
</div>
@endsection

View File

@@ -0,0 +1,285 @@
@extends('layouts.admin')
@section('title', $program->title)
@section('header', $program->title)
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.programs.index') }}">Program</a></li>
<li class="breadcrumb-item active">{{ Str::limit($program->title, 35) }}</li>
@endsection
@section('header-actions')
<div class="d-flex gap-2">
@if($program->status === 'draft')
<a href="{{ route('admin.programs.edit', $program) }}" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-pencil me-1"></i> Edit
</a>
<form method="POST" action="{{ route('admin.programs.publish', $program) }}" class="d-inline">
@csrf
<button class="btn btn-sm btn-success" onclick="return confirm('Terbitkan program ini sekarang?')">
<i class="bi bi-send me-1"></i> Terbitkan
</button>
</form>
@elseif($program->status === 'published')
<form method="POST" action="{{ route('admin.programs.close', $program) }}" class="d-inline">
@csrf
<button class="btn btn-sm btn-outline-danger" onclick="return confirm('Tutup program ini?')">
<i class="bi bi-lock me-1"></i> Tutup Program
</button>
</form>
@endif
</div>
@endsection
@section('content')
{{-- Program Info Card --}}
<div class="row g-3 mb-4">
<div class="col-md-8">
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<h6 class="mb-1 fw-semibold">{{ $program->title }}</h6>
<small class="text-muted">{{ $program->organizer }}</small>
</div>
@include('admin.partials.program-status-badge', ['status' => $program->status])
</div>
<div class="row g-2 text-sm">
<div class="col-6">
<div class="text-muted small">Tarikh</div>
<div>{{ $program->start_date->format('d M Y') }}
@if($program->start_date->ne($program->end_date))
{{ $program->end_date->format('d M Y') }}
@endif
</div>
</div>
<div class="col-6">
<div class="text-muted small">Lokasi</div>
<div>{{ $program->location }}</div>
</div>
@if($program->checkin_start_at)
<div class="col-6">
<div class="text-muted small">Tempoh Check-In</div>
<div class="small">{{ $program->checkin_start_at->format('d/m H:i') }} {{ $program->checkin_end_at?->format('d/m H:i') ?? '—' }}</div>
</div>
@endif
@if($program->ecert_download_start_at)
<div class="col-6">
<div class="text-muted small">Mula Download Sijil</div>
<div class="small">{{ $program->ecert_download_start_at->format('d M Y, H:i') }}</div>
</div>
@endif
</div>
</div>
</div>
</div>
{{-- Stat Cards --}}
<div class="col-md-4">
<div class="row g-2">
<div class="col-6">
<div class="card border-0 bg-primary bg-opacity-10 text-center p-3">
<div class="fs-3 fw-bold text-primary">{{ $stats['total_attendances'] }}</div>
<div class="small text-muted">Hadir</div>
</div>
</div>
<div class="col-6">
<div class="card border-0 bg-success bg-opacity-10 text-center p-3">
<div class="fs-3 fw-bold text-success">{{ $stats['total_participants'] }}</div>
<div class="small text-muted">Peserta</div>
</div>
</div>
<div class="col-6">
<div class="card border-0 bg-warning bg-opacity-10 text-center p-3">
<div class="fs-3 fw-bold text-warning">{{ $stats['total_certificates'] }}</div>
<div class="small text-muted">Sijil</div>
</div>
</div>
<div class="col-6">
<div class="card border-0 bg-info bg-opacity-10 text-center p-3">
<div class="fs-3 fw-bold text-info">{{ $stats['generated_certificates'] }}</div>
<div class="small text-muted">Dijana</div>
</div>
</div>
</div>
</div>
</div>
{{-- Tab Navigation --}}
<ul class="nav nav-tabs mb-0" id="programTabs">
<li class="nav-item">
<a class="nav-link active" data-bs-toggle="tab" href="#tab-participants">
<i class="bi bi-people me-1"></i> Peserta
<span class="badge bg-secondary ms-1">{{ $stats['total_participants'] }}</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#tab-qr">
<i class="bi bi-qr-code me-1"></i> QR Code
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#tab-template">
<i class="bi bi-image me-1"></i> Template Sijil
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#tab-questionnaire">
<i class="bi bi-clipboard2-check me-1"></i> Soalselidik
</a>
</li>
<li class="nav-item">
<a class="nav-link" data-bs-toggle="tab" href="#tab-statistics">
<i class="bi bi-bar-chart me-1"></i> Statistik
</a>
</li>
</ul>
<div class="tab-content border border-top-0 rounded-bottom bg-white p-4 shadow-sm">
{{-- Tab: Peserta --}}
<div class="tab-pane fade show active" id="tab-participants">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<span class="text-muted small">
{{ $stats['pre_registered'] }} pra-daftar &middot;
{{ $stats['walk_in'] }} walk-in
</span>
</div>
<div class="d-flex gap-2">
<a href="{{ route('admin.programs.participants.import.form', $program) }}" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-upload me-1"></i> Import CSV
</a>
<a href="{{ route('admin.programs.participants.create', $program) }}" class="btn btn-sm btn-primary">
<i class="bi bi-person-plus me-1"></i> Tambah Peserta
</a>
</div>
</div>
<div class="text-center py-4 text-muted">
<a href="{{ route('admin.programs.participants.index', $program) }}" class="btn btn-outline-primary">
<i class="bi bi-list-ul me-2"></i> Lihat Senarai Penuh Peserta
</a>
</div>
</div>
{{-- Tab: QR Code --}}
<div class="tab-pane fade" id="tab-qr">
@if($program->qrCode)
<div class="text-center py-3">
<img src="{{ Storage::disk('public')->url($program->qrCode->qr_image_path) }}"
alt="QR Code" class="img-fluid mb-3" style="max-width:220px;">
<div class="d-flex justify-content-center gap-2">
<a href="{{ route('admin.programs.qr.download', $program) }}" class="btn btn-sm btn-outline-primary">
<i class="bi bi-download me-1"></i> Muat Turun PNG
</a>
<form method="POST" action="{{ route('admin.programs.qr.generate', $program) }}">
@csrf
<button class="btn btn-sm btn-outline-warning" onclick="return confirm('Jana semula QR Code?')">
<i class="bi bi-arrow-repeat me-1"></i> Jana Semula
</button>
</form>
</div>
<div class="mt-3">
<small class="text-muted">URL Check-in:</small><br>
<code class="small">{{ route('public.checkin.show', $program->qrCode->token) }}</code>
</div>
</div>
@else
<div class="text-center py-4">
<i class="bi bi-qr-code fs-1 text-muted opacity-25 d-block mb-3"></i>
<p class="text-muted mb-3">QR Code belum dijana untuk program ini.</p>
<form method="POST" action="{{ route('admin.programs.qr.generate', $program) }}">
@csrf
<button class="btn btn-primary">
<i class="bi bi-qr-code me-2"></i> Jana QR Code
</button>
</form>
</div>
@endif
</div>
{{-- Tab: Template Sijil --}}
<div class="tab-pane fade" id="tab-template">
@if($program->certificateTemplate)
<div class="d-flex justify-content-between align-items-center mb-3">
<span class="small text-muted">Template aktif: <strong>{{ $program->certificateTemplate->original_filename }}</strong></span>
<a href="{{ route('admin.programs.template.show', $program) }}" class="btn btn-sm btn-primary">
<i class="bi bi-pencil-square me-1"></i> Urus Template
</a>
</div>
<div class="text-center">
<img src="{{ route('admin.programs.template.preview', $program) }}"
alt="Template" class="img-fluid rounded border" style="max-height:300px;">
</div>
@else
<div class="text-center py-4">
<i class="bi bi-image fs-1 text-muted opacity-25 d-block mb-3"></i>
<p class="text-muted mb-3">Belum ada template sijil untuk program ini.</p>
<a href="{{ route('admin.programs.template.show', $program) }}" class="btn btn-primary">
<i class="bi bi-upload me-2"></i> Upload Template
</a>
</div>
@endif
</div>
{{-- Tab: Soalselidik --}}
<div class="tab-pane fade" id="tab-questionnaire">
@if($program->questionnaire && $program->questionnaire->questionnaireSet)
@php $qs = $program->questionnaire->questionnaireSet; @endphp
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<span class="fw-medium">{{ $qs->title }}</span>
@if($program->questionnaire->is_confirmed)
<span class="badge bg-success ms-2">Disahkan</span>
@else
<span class="badge bg-warning ms-2">Belum Disahkan</span>
@endif
</div>
<a href="{{ route('admin.programs.questionnaire.show', $program) }}" class="btn btn-sm btn-primary">
<i class="bi bi-gear me-1"></i> Urus Soalselidik
</a>
</div>
<p class="text-muted small">{{ $qs->questions->count() }} soalan &middot; {{ $qs->description }}</p>
@else
<div class="text-center py-4">
<i class="bi bi-clipboard2-x fs-1 text-muted opacity-25 d-block mb-3"></i>
<p class="text-muted mb-3">Belum ada soalselidik dilampirkan untuk program ini.</p>
<a href="{{ route('admin.programs.questionnaire.show', $program) }}" class="btn btn-primary">
<i class="bi bi-clipboard2-plus me-2"></i> Lampir Soalselidik
</a>
</div>
@endif
</div>
{{-- Tab: Statistik --}}
<div class="tab-pane fade" id="tab-statistics">
<div class="text-center py-4">
<a href="{{ route('admin.programs.statistics.show', $program) }}" class="btn btn-outline-primary">
<i class="bi bi-bar-chart-line me-2"></i> Lihat Statistik Penuh
</a>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
// Persist active tab in URL hash
document.addEventListener('DOMContentLoaded', function () {
const hash = window.location.hash;
if (hash) {
const tab = document.querySelector('[href="' + hash + '"]');
if (tab) bootstrap.Tab.getOrCreateInstance(tab).show();
}
document.querySelectorAll('#programTabs .nav-link').forEach(el => {
el.addEventListener('shown.bs.tab', () => {
history.replaceState(null, null, el.getAttribute('href'));
});
});
});
</script>
@endpush

View File

@@ -0,0 +1,218 @@
@extends('layouts.admin')
@section('title', 'Statistik — ' . $program->title)
@section('header', 'Statistik Program')
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.programs.index') }}">Program</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.programs.show', $program) }}">{{ Str::limit($program->title, 30) }}</a></li>
<li class="breadcrumb-item active">Statistik</li>
@endsection
@section('header-actions')
<div class="d-flex gap-2">
<a href="{{ route('admin.programs.statistics.export', $program) }}" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-download me-1"></i> Export CSV
</a>
<a href="{{ route('admin.programs.show', $program) }}#tab-statistics" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i> Kembali
</a>
</div>
@endsection
@section('content')
{{-- Summary Cards --}}
<div class="row g-3 mb-4">
<div class="col-6 col-md-3">
<div class="card border-0 bg-primary bg-opacity-10 text-center p-3">
<div class="fs-2 fw-bold text-primary">{{ $summary['total_attendances'] }}</div>
<div class="small text-muted">Jumlah Hadir</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card border-0 bg-success bg-opacity-10 text-center p-3">
<div class="fs-2 fw-bold text-success">{{ $summary['generated_certs'] }}</div>
<div class="small text-muted">Sijil Dijana</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card border-0 bg-warning bg-opacity-10 text-center p-3">
<div class="fs-2 fw-bold text-warning">{{ $summary['downloaded_certs'] }}</div>
<div class="small text-muted">Sijil Dimuat Turun</div>
</div>
</div>
<div class="col-6 col-md-3">
<div class="card border-0 bg-info bg-opacity-10 text-center p-3">
<div class="fs-2 fw-bold text-info">{{ $summary['total_responses'] }}</div>
<div class="small text-muted">Respons Soalselidik</div>
</div>
</div>
</div>
<div class="row g-4">
{{-- Attendance by Session --}}
<div class="col-md-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white py-3">
<h6 class="mb-0 fw-semibold"><i class="bi bi-bar-chart me-2 text-primary"></i>Kehadiran Mengikut Sesi</h6>
</div>
<div class="card-body">
@if(! empty($bySession))
<canvas id="sessionChart" height="200"></canvas>
@else
<div class="text-center py-4 text-muted small">Tiada data kehadiran.</div>
@endif
</div>
</div>
</div>
{{-- Attendance by Source --}}
<div class="col-md-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white py-3">
<h6 class="mb-0 fw-semibold"><i class="bi bi-pie-chart me-2 text-success"></i>Kehadiran Mengikut Jenis</h6>
</div>
<div class="card-body d-flex align-items-center justify-content-center">
@if(! empty($bySource))
<canvas id="sourceChart" height="200"></canvas>
@else
<div class="text-center py-4 text-muted small">Tiada data kehadiran.</div>
@endif
</div>
</div>
</div>
{{-- Certificate Status --}}
<div class="col-md-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white py-3">
<h6 class="mb-0 fw-semibold"><i class="bi bi-award me-2 text-warning"></i>Status Sijil</h6>
</div>
<div class="card-body">
@if(! empty($certStats))
<table class="table table-sm">
@php
$labels = ['pending' => 'Menunggu', 'generating' => 'Sedang Jana', 'generated' => 'Dijana', 'emailed' => 'Diemailkan', 'downloaded' => 'Dimuat Turun', 'failed' => 'Gagal'];
$colors = ['pending' => 'warning', 'generating' => 'secondary', 'generated' => 'success', 'emailed' => 'info', 'downloaded' => 'primary', 'failed' => 'danger'];
@endphp
@foreach($certStats as $status => $count)
<tr>
<td><span class="badge bg-{{ $colors[$status] ?? 'secondary' }}">{{ $labels[$status] ?? $status }}</span></td>
<td class="fw-bold">{{ $count }}</td>
<td class="w-50">
<div class="progress" style="height:8px;">
<div class="progress-bar bg-{{ $colors[$status] ?? 'secondary' }}"
style="width:{{ $summary['total_certificates'] > 0 ? round($count / $summary['total_certificates'] * 100) : 0 }}%"></div>
</div>
</td>
</tr>
@endforeach
</table>
@else
<div class="text-center py-4 text-muted small">Tiada sijil dijana lagi.</div>
@endif
</div>
</div>
</div>
{{-- Response Rate --}}
@if($responseRate !== null)
<div class="col-md-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white py-3">
<h6 class="mb-0 fw-semibold"><i class="bi bi-clipboard2-check me-2 text-info"></i>Kadar Respons Soalselidik</h6>
</div>
<div class="card-body text-center py-4">
<div class="display-4 fw-bold text-primary mb-1">{{ $responseRate }}%</div>
<div class="text-muted small">{{ $summary['total_responses'] }} daripada {{ $summary['total_attendances'] }} peserta hadir</div>
<div class="progress mt-3" style="height:12px;">
<div class="progress-bar bg-primary" style="width:{{ $responseRate }}%"></div>
</div>
</div>
</div>
</div>
@endif
{{-- Question Stats --}}
@foreach($questionStats as $qs)
<div class="col-md-6">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white py-3">
<h6 class="mb-0 fw-semibold small">{{ $qs['text'] }}</h6>
</div>
<div class="card-body">
@if($qs['type'] === 'rating')
<div class="text-center">
<div class="display-6 fw-bold text-warning">{{ $qs['average'] ?? '—' }}</div>
<div class="text-muted small">Purata daripada {{ $qs['count'] }} respons</div>
@php $pct = $qs['average'] ? round($qs['average'] / 5 * 100) : 0; @endphp
<div class="progress mt-2" style="height:10px;">
<div class="progress-bar bg-warning" style="width:{{ $pct }}%"></div>
</div>
<div class="text-muted" style="font-size:0.75rem;">{{ $pct }}% daripada 5</div>
</div>
@elseif(in_array($qs['type'], ['single_choice', 'multiple_choice']))
<table class="table table-sm mb-0">
@foreach($qs['options'] as $opt)
@php $c = $qs['counts'][$opt] ?? 0; $pct = $qs['total'] > 0 ? round($c / $qs['total'] * 100) : 0; @endphp
<tr>
<td class="small text-truncate" style="max-width:180px;">{{ $opt }}</td>
<td class="text-end fw-bold small">{{ $c }}</td>
<td class="w-40">
<div class="progress" style="height:8px;">
<div class="progress-bar bg-info" style="width:{{ $pct }}%"></div>
</div>
</td>
<td class="text-end small text-muted">{{ $pct }}%</td>
</tr>
@endforeach
</table>
@endif
</div>
</div>
</div>
@endforeach
</div>
@endsection
@push('scripts')
<script>
@if(! empty($bySession))
new Chart(document.getElementById('sessionChart'), {
type: 'bar',
data: {
labels: @json(array_map('ucfirst', array_keys($bySession))),
datasets: [{
label: 'Kehadiran',
data: @json(array_values($bySession)),
backgroundColor: 'rgba(26,86,160,0.7)',
borderRadius: 4,
}]
},
options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } } }
});
@endif
@if(! empty($bySource))
const sourceLabels = {
'pre_registered_staff': 'Kakitangan',
'walk_in_external': 'Orang Luar',
};
new Chart(document.getElementById('sourceChart'), {
type: 'doughnut',
data: {
labels: @json(array_map(fn($k) => $sourceLabels[$k] ?? $k, array_keys($bySource))),
datasets: [{
data: @json(array_values($bySource)),
backgroundColor: ['rgba(26,86,160,0.7)', 'rgba(34,197,94,0.7)'],
}]
},
options: { responsive: true, plugins: { legend: { position: 'bottom' } } }
});
@endif
</script>
@endpush

View File

@@ -0,0 +1,418 @@
@extends('layouts.admin')
@section('title', 'Template Sijil — ' . $program->title)
@section('header', 'Template Sijil')
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.programs.index') }}">Program</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.programs.show', $program) }}">{{ Str::limit($program->title, 30) }}</a></li>
<li class="breadcrumb-item active">Template Sijil</li>
@endsection
@section('header-actions')
<a href="{{ route('admin.programs.show', $program) }}#tab-template" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-arrow-left me-1"></i> Kembali
</a>
@endsection
@section('content')
@php
$config = $template?->config_json ?? [];
$fields = $config['fields'] ?? [];
$imgWidth = $config['width'] ?? 0;
$imgHeight = $config['height'] ?? 0;
$isPortrait = $imgHeight > $imgWidth && $imgWidth > 0;
@endphp
{{-- ── Panduan Template (atas, boleh lipat) ────────────────────────────── --}}
<div class="card border-0 bg-light mb-4">
<div class="card-header bg-transparent border-0 py-2 d-flex justify-content-between align-items-center"
role="button" data-bs-toggle="collapse" data-bs-target="#guidePanel" aria-expanded="false">
<span class="fw-semibold small"><i class="bi bi-info-circle me-2 text-primary"></i>Panduan Template</span>
<i class="bi bi-chevron-down small text-muted" id="guideChevron"></i>
</div>
<div class="collapse" id="guidePanel">
<div class="card-body pt-0 pb-3">
<div class="row g-3">
<div class="col-md-4">
<div class="d-flex gap-2">
<i class="bi bi-aspect-ratio text-primary mt-1 flex-shrink-0"></i>
<div class="small text-muted">
Resolusi disyorkan <strong>1754 × 1240px</strong> (A4 landscape 150dpi)
atau <strong>1240 × 1754px</strong> (portrait).
</div>
</div>
</div>
<div class="col-md-4">
<div class="d-flex gap-2">
<i class="bi bi-cursor text-primary mt-1 flex-shrink-0"></i>
<div class="small text-muted">
Koordinat <strong>(0, 0)</strong> adalah sudut kiri atas imej.
X bertambah ke kanan, Y bertambah ke bawah.
</div>
</div>
</div>
<div class="col-md-4">
<div class="d-flex gap-2">
<i class="bi bi-eye text-primary mt-1 flex-shrink-0"></i>
<div class="small text-muted">
Guna butang <strong>Pratonton</strong> untuk semak kedudukan teks
sebelum jana sijil sebenar.
</div>
</div>
</div>
</div>
</div>
</div>
</div>
@if($template)
{{-- ── Template Aktif (kiri) + Konfigurasi (kanan) ─────────────────────── --}}
<div class="row g-4">
{{-- Kiri: Template Aktif --}}
<div class="col-lg-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white py-3 d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center gap-2">
<h6 class="mb-0 fw-semibold"><i class="bi bi-image me-2 text-primary"></i>Template Aktif</h6>
<span id="orientationBadge" class="badge bg-secondary" style="font-size:.7rem;">
{{ $isPortrait ? 'Portrait' : ($imgWidth > 0 ? 'Landscape' : '—') }}
</span>
</div>
<form method="POST" action="{{ route('admin.programs.template.destroy', $program) }}"
onsubmit="return confirm('Padam template sijil ini? Tindakan ini tidak boleh diundur.')">
@csrf @method('DELETE')
<button class="btn btn-sm btn-outline-danger">
<i class="bi bi-trash me-1"></i> Padam
</button>
</form>
</div>
<div class="card-body d-flex flex-column">
{{-- Image viewer tinggi berubah ikut orientasi --}}
<div id="previewWrapper"
class="border rounded overflow-hidden mb-2 d-flex align-items-center justify-content-center bg-light"
style="max-height:{{ $isPortrait ? '520px' : '340px' }}; transition: max-height .3s ease;">
<img src="{{ route('admin.programs.template.preview', $program) }}"
id="templatePreview"
alt="Template Preview"
class="img-fluid"
style="max-width:100%; max-height:{{ $isPortrait ? '520px' : '340px' }}; object-fit:contain;">
</div>
<div class="text-muted small text-center mb-3">
{{ $template->original_filename }}
@if($imgWidth > 0)
&nbsp;·&nbsp;<span id="dimensionLabel">{{ $imgWidth }} × {{ $imgHeight }} px</span>
@endif
</div>
{{-- Jana Pratonton --}}
<div class="border rounded p-3 bg-light mt-auto">
<label class="form-label small fw-medium mb-2">Jana Pratonton</label>
<div class="row g-2">
<div class="col-8">
<input type="text" id="sampleName" class="form-control form-control-sm"
value="NAMA PESERTA CONTOH" placeholder="Nama untuk pratonton">
</div>
<div class="col-4">
<button type="button" id="previewBtn" class="btn btn-sm btn-primary w-100"
onclick="loadPreview()">
<i class="bi bi-eye me-1"></i> Pratonton
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{{-- Kanan: Konfigurasi Kedudukan Teks --}}
<div class="col-lg-6">
<div class="card border-0 shadow-sm h-100">
<div class="card-header bg-white py-3">
<h6 class="mb-0 fw-semibold"><i class="bi bi-sliders me-2 text-warning"></i>Konfigurasi Kedudukan Teks</h6>
</div>
<div class="card-body">
<p class="text-muted small mb-3">
Koordinat X dan Y dikira dari sudut kiri atas imej (piksel).
@if($imgWidth > 0)
Saiz imej: <strong>{{ $imgWidth }} × {{ $imgHeight }} px</strong>.
@endif
</p>
<form method="POST" action="{{ route('admin.programs.template.config', $program) }}">
@csrf @method('PUT')
{{-- Nama Peserta --}}
<div class="card border mb-3">
<div class="card-header py-2 bg-light">
<span class="fw-medium small">Nama Peserta</span>
</div>
<div class="card-body py-3">
<div class="row g-2">
<div class="col-4">
<label class="form-label small">X (px)</label>
<input type="number" name="fields[name][x]" class="form-control form-control-sm"
value="{{ $fields['name']['x'] ?? 800 }}">
</div>
<div class="col-4">
<label class="form-label small">Y (px)</label>
<input type="number" name="fields[name][y]" class="form-control form-control-sm"
value="{{ $fields['name']['y'] ?? 400 }}">
</div>
<div class="col-4">
<label class="form-label small">Saiz Font</label>
<input type="number" name="fields[name][font_size]" class="form-control form-control-sm"
value="{{ $fields['name']['font_size'] ?? 52 }}">
</div>
<div class="col-4">
<label class="form-label small">Warna</label>
<input type="color" name="fields[name][font_color]"
class="form-control form-control-color form-control-sm"
value="{{ $fields['name']['font_color'] ?? '#1a3a6b' }}">
</div>
<div class="col-4">
<label class="form-label small">Saiz Font No IC</label>
<input type="number" name="fields[name][ic_font_size]"
class="form-control form-control-sm" min="8" max="200"
value="{{ $fields['name']['ic_font_size'] ?? (int) round(($fields['name']['font_size'] ?? 52) * 0.7) }}">
</div>
<div class="col-4">
<label class="form-label small">Align</label>
<select name="fields[name][align]" class="form-select form-select-sm">
<option value="left" {{ ($fields['name']['align'] ?? 'center') === 'left' ? 'selected' : '' }}>Kiri</option>
<option value="center" {{ ($fields['name']['align'] ?? 'center') === 'center' ? 'selected' : '' }}>Tengah</option>
<option value="right" {{ ($fields['name']['align'] ?? 'center') === 'right' ? 'selected' : '' }}>Kanan</option>
</select>
</div>
</div>
</div>
</div>
{{-- No. Sijil (pilihan) --}}
<div class="card border mb-4">
<div class="card-header py-2 bg-light d-flex justify-content-between align-items-center">
<span class="fw-medium small">No. Sijil <span class="text-muted">(Pilihan)</span></span>
<div class="form-check form-switch mb-0">
<input class="form-check-input" type="checkbox" id="showCertNo"
onchange="toggleCertNo(this)"
{{ isset($fields['certificate_no']) ? 'checked' : '' }}>
<label class="form-check-label small" for="showCertNo">Papar</label>
</div>
</div>
<div class="card-body py-3" id="certNoFields"
{{ isset($fields['certificate_no']) ? '' : 'style=display:none' }}>
<div class="row g-2">
<div class="col-4">
<label class="form-label small">X (px)</label>
<input type="number" name="fields[certificate_no][x]" class="form-control form-control-sm"
value="{{ $fields['certificate_no']['x'] ?? 800 }}">
</div>
<div class="col-4">
<label class="form-label small">Y (px)</label>
<input type="number" name="fields[certificate_no][y]" class="form-control form-control-sm"
value="{{ $fields['certificate_no']['y'] ?? 460 }}">
</div>
<div class="col-4">
<label class="form-label small">Saiz Font</label>
<input type="number" name="fields[certificate_no][font_size]" class="form-control form-control-sm"
value="{{ $fields['certificate_no']['font_size'] ?? 28 }}">
</div>
<div class="col-4">
<label class="form-label small">Warna</label>
<input type="color" name="fields[certificate_no][font_color]"
class="form-control form-control-color form-control-sm"
value="{{ $fields['certificate_no']['font_color'] ?? '#555555' }}">
</div>
<div class="col-4">
<label class="form-label small">Align</label>
<select name="fields[certificate_no][align]" class="form-select form-select-sm">
<option value="left" {{ ($fields['certificate_no']['align'] ?? 'center') === 'left' ? 'selected' : '' }}>Kiri</option>
<option value="center" {{ ($fields['certificate_no']['align'] ?? 'center') === 'center' ? 'selected' : '' }}>Tengah</option>
<option value="right" {{ ($fields['certificate_no']['align'] ?? 'center') === 'right' ? 'selected' : '' }}>Kanan</option>
</select>
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="bi bi-save me-1"></i> Simpan Konfigurasi
</button>
</form>
</div>
</div>
</div>
</div><!-- /.row -->
@else
{{-- Tiada template form upload --}}
<div class="row g-4">
<div class="col-lg-7">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white py-3">
<h6 class="mb-0 fw-semibold"><i class="bi bi-upload me-2 text-primary"></i>Muat Naik Template Sijil</h6>
</div>
<div class="card-body">
<div class="alert alert-info small mb-4">
<i class="bi bi-lightbulb me-1"></i>
Format <strong>JPG atau PNG</strong>, maksimum <strong>10MB</strong>.
Resolusi disyorkan: <strong>1754 × 1240px</strong> (A4 landscape) atau
<strong>1240 × 1754px</strong> (portrait).
</div>
<form method="POST" action="{{ route('admin.programs.template.store', $program) }}"
enctype="multipart/form-data">
@csrf
<div class="mb-4">
<label class="form-label fw-medium">Fail Template <span class="text-danger">*</span></label>
<input type="file" name="template_image" accept="image/jpeg,image/png"
class="form-control @error('template_image') is-invalid @enderror"
onchange="previewImage(this)">
@error('template_image')<div class="invalid-feedback">{{ $message }}</div>@enderror
<div class="form-text">Format: JPG, PNG Maksimum 10MB</div>
</div>
<div id="imagePreviewBox" class="mb-4 d-none">
<label class="form-label small text-muted">Pratonton:</label>
<div id="uploadPreviewWrapper" class="border rounded overflow-hidden">
<img id="imagePreviewEl" src="" alt="preview" class="img-fluid w-100">
</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="bi bi-upload me-1"></i> Muat Naik
</button>
</form>
</div>
</div>
</div>
</div>
@endif
@endsection
@push('scripts')
<script>
// ── Auto-detect orientasi dan laras tinggi viewer ─────────────────────────────
function applyOrientation(naturalW, naturalH) {
const wrapper = document.getElementById('previewWrapper');
const img = document.getElementById('templatePreview');
const badge = document.getElementById('orientationBadge');
if (!wrapper || !img) return;
const isPortrait = naturalH > naturalW;
const maxH = isPortrait ? '520px' : '340px';
wrapper.style.maxHeight = maxH;
img.style.maxHeight = maxH;
if (badge) {
badge.textContent = isPortrait ? 'Portrait' : 'Landscape';
badge.className = 'badge ' + (isPortrait ? 'bg-info' : 'bg-success');
}
}
// Jalankan sekali apabila imej template dimuatkan
const templateImg = document.getElementById('templatePreview');
if (templateImg) {
if (templateImg.complete && templateImg.naturalWidth) {
applyOrientation(templateImg.naturalWidth, templateImg.naturalHeight);
} else {
templateImg.addEventListener('load', function () {
applyOrientation(this.naturalWidth, this.naturalHeight);
});
}
}
// ── Upload pratonton (form muat naik) ─────────────────────────────────────────
function previewImage(input) {
if (!input.files || !input.files[0]) return;
const reader = new FileReader();
reader.onload = e => {
const el = document.getElementById('imagePreviewEl');
const box = document.getElementById('imagePreviewBox');
el.src = e.target.result;
box.classList.remove('d-none');
// Laras pratonton upload ikut orientasi
el.onload = function () {
const wrap = document.getElementById('uploadPreviewWrapper');
if (wrap) wrap.style.maxHeight = this.naturalHeight > this.naturalWidth ? '520px' : '340px';
};
};
reader.readAsDataURL(input.files[0]);
}
// ── Toggle No. Sijil ──────────────────────────────────────────────────────────
function toggleCertNo(cb) {
document.getElementById('certNoFields').style.display = cb.checked ? '' : 'none';
}
// ── Toggle panduan (ikon chevron) ─────────────────────────────────────────────
document.getElementById('guidePanel')?.addEventListener('show.bs.collapse', () => {
document.getElementById('guideChevron').className = 'bi bi-chevron-up small text-muted';
});
document.getElementById('guidePanel')?.addEventListener('hide.bs.collapse', () => {
document.getElementById('guideChevron').className = 'bi bi-chevron-down small text-muted';
});
// ── Jana Pratonton ────────────────────────────────────────────────────────────
function loadPreview() {
const name = document.getElementById('sampleName').value.trim() || 'NAMA PESERTA CONTOH';
const img = document.getElementById('templatePreview');
const btn = document.getElementById('previewBtn');
const url = "{{ route('admin.programs.template.test', $program) }}";
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1" role="status"></span> Memuatkan...';
const fd = new FormData();
fd.append('_token', '{{ csrf_token() }}');
fd.append('sample_name', name);
// Hantar nilai form semasa — preview guna koordinat terkini walaupun belum simpan
const read = (sel) => document.querySelector(sel)?.value ?? '';
fd.append('fields[name][x]', read('[name="fields[name][x]"]'));
fd.append('fields[name][y]', read('[name="fields[name][y]"]'));
fd.append('fields[name][font_size]', read('[name="fields[name][font_size]"]'));
fd.append('fields[name][font_color]', read('[name="fields[name][font_color]"]'));
fd.append('fields[name][ic_font_size]', read('[name="fields[name][ic_font_size]"]'));
fd.append('fields[name][align]', read('[name="fields[name][align]"]'));
// Sertakan No. Sijil hanya jika toggle aktif
if (document.getElementById('showCertNo')?.checked) {
fd.append('fields[certificate_no][x]', read('[name="fields[certificate_no][x]"]'));
fd.append('fields[certificate_no][y]', read('[name="fields[certificate_no][y]"]'));
fd.append('fields[certificate_no][font_size]', read('[name="fields[certificate_no][font_size]"]'));
fd.append('fields[certificate_no][font_color]', read('[name="fields[certificate_no][font_color]"]'));
fd.append('fields[certificate_no][align]', read('[name="fields[certificate_no][align]"]'));
}
fetch(url, { method: 'POST', body: fd })
.then(r => {
if (!r.ok) return r.json().then(j => { throw new Error(j.error || 'Ralat pelayan (' + r.status + ')'); });
return r.blob();
})
.then(blob => {
const prevSrc = img.src;
img.src = URL.createObjectURL(blob);
if (prevSrc.startsWith('blob:')) URL.revokeObjectURL(prevSrc);
})
.catch(err => {
alert('Gagal jana pratonton: ' + err.message);
})
.finally(() => {
btn.disabled = false;
btn.innerHTML = '<i class="bi bi-eye me-1"></i> Pratonton';
});
}
</script>
@endpush

View File

@@ -0,0 +1,50 @@
@extends('layouts.admin')
@section('title', 'Buat Set Soalselidik Baru')
@section('header', 'Buat Set Soalselidik')
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.questionnaires.index') }}">Soalselidik</a></li>
<li class="breadcrumb-item active">Buat Baru</li>
@endsection
@section('content')
<div class="row justify-content-center">
<div class="col-md-7">
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<form method="POST" action="{{ route('admin.questionnaires.store') }}">
@csrf
<div class="mb-3">
<label class="form-label fw-medium">Tajuk Set <span class="text-danger">*</span></label>
<input type="text" name="title" value="{{ old('title') }}"
class="form-control @error('title') is-invalid @enderror"
placeholder="cth: Borang Penilaian Program 2025">
@error('title')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="mb-4">
<label class="form-label fw-medium">Keterangan</label>
<textarea name="description" rows="3"
class="form-control @error('description') is-invalid @enderror"
placeholder="Keterangan ringkas tentang soalselidik ini (pilihan)">{{ old('description') }}</textarea>
@error('description')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg me-1"></i> Simpan & Tambah Soalan
</button>
<a href="{{ route('admin.questionnaires.index') }}" class="btn btn-outline-secondary">
Batal
</a>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,50 @@
@extends('layouts.admin')
@section('title', 'Edit Set Soalselidik')
@section('header', 'Edit Set Soalselidik')
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.questionnaires.index') }}">Soalselidik</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.questionnaires.show', $set) }}">{{ Str::limit($set->title, 30) }}</a></li>
<li class="breadcrumb-item active">Edit</li>
@endsection
@section('content')
<div class="row justify-content-center">
<div class="col-md-7">
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<form method="POST" action="{{ route('admin.questionnaires.update', $set) }}">
@csrf
@method('PUT')
<div class="mb-3">
<label class="form-label fw-medium">Tajuk Set <span class="text-danger">*</span></label>
<input type="text" name="title" value="{{ old('title', $set->title) }}"
class="form-control @error('title') is-invalid @enderror">
@error('title')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="mb-4">
<label class="form-label fw-medium">Keterangan</label>
<textarea name="description" rows="3"
class="form-control @error('description') is-invalid @enderror">{{ old('description', $set->description) }}</textarea>
@error('description')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg me-1"></i> Kemaskini
</button>
<a href="{{ route('admin.questionnaires.show', $set) }}" class="btn btn-outline-secondary">
Batal
</a>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,107 @@
@extends('layouts.admin')
@section('title', 'Set Soalselidik')
@section('header', 'Set Soalselidik')
@section('breadcrumb')
<li class="breadcrumb-item active">Soalselidik</li>
@endsection
@section('header-actions')
<a href="{{ route('admin.questionnaires.create') }}" class="btn btn-sm btn-primary">
<i class="bi bi-plus-lg me-1"></i> Buat Set Baru
</a>
@endsection
@section('content')
<div class="card border-0 shadow-sm">
<div class="card-header bg-white py-3">
<form method="GET" class="row g-2 align-items-end">
<div class="col-md-5">
<input type="text" name="q" value="{{ request('q') }}"
class="form-control form-control-sm" placeholder="Cari tajuk soalselidik...">
</div>
<div class="col-md-3">
<select name="status" class="form-select form-select-sm">
<option value="">Semua Status</option>
<option value="draft" {{ request('status') === 'draft' ? 'selected' : '' }}>Draf</option>
<option value="published" {{ request('status') === 'published' ? 'selected' : '' }}>Diterbitkan</option>
<option value="archived" {{ request('status') === 'archived' ? 'selected' : '' }}>Diarkib</option>
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-search"></i> Cari
</button>
@if(request()->hasAny(['q', 'status']))
<a href="{{ route('admin.questionnaires.index') }}" class="btn btn-sm btn-link text-muted">Set Semula</a>
@endif
</div>
</form>
</div>
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>Tajuk</th>
<th class="text-center">Soalan</th>
<th>Status</th>
<th>Dicipta Oleh</th>
<th>Tarikh Cipta</th>
<th></th>
</tr>
</thead>
<tbody>
@forelse($sets as $set)
<tr>
<td>
<a href="{{ route('admin.questionnaires.show', $set) }}" class="fw-medium text-decoration-none">
{{ $set->title }}
</a>
@if($set->description)
<div class="text-muted small text-truncate" style="max-width:300px;">{{ $set->description }}</div>
@endif
</td>
<td class="text-center">
<span class="badge bg-secondary">{{ $set->questions_count }}</span>
</td>
<td>
@if($set->status === 'published')
<span class="badge bg-success">Diterbitkan</span>
@elseif($set->status === 'archived')
<span class="badge bg-secondary">Diarkib</span>
@else
<span class="badge bg-warning text-dark">Draf</span>
@endif
</td>
<td class="small text-muted">{{ $set->creator?->name ?? '—' }}</td>
<td class="small text-muted">{{ $set->created_at->format('d/m/Y') }}</td>
<td class="text-end">
<a href="{{ route('admin.questionnaires.show', $set) }}"
class="btn btn-sm btn-outline-primary">
<i class="bi bi-eye"></i>
</a>
</td>
</tr>
@empty
<tr>
<td colspan="6" class="text-center py-4 text-muted">
<i class="bi bi-clipboard2-x d-block fs-2 mb-2 opacity-25"></i>
Tiada set soalselidik dijumpai.
</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($sets->hasPages())
<div class="card-footer bg-white">
{{ $sets->links() }}
</div>
@endif
</div>
@endsection

View File

@@ -0,0 +1,523 @@
@extends('layouts.admin')
@section('title', $set->title)
@section('header', $set->title)
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.questionnaires.index') }}">Soalselidik</a></li>
<li class="breadcrumb-item active">{{ Str::limit($set->title, 35) }}</li>
@endsection
@section('header-actions')
<div class="d-flex gap-2">
<a href="{{ route('admin.questionnaires.edit', $set) }}" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-pencil me-1"></i> Edit
</a>
@if($set->status === 'draft')
<form method="POST" action="{{ route('admin.questionnaires.publish', $set) }}">
@csrf
<button class="btn btn-sm btn-success" onclick="return confirm('Terbitkan set soalselidik ini?')">
<i class="bi bi-send me-1"></i> Terbitkan
</button>
</form>
@elseif($set->status === 'published')
<form method="POST" action="{{ route('admin.questionnaires.archive', $set) }}">
@csrf
<button class="btn btn-sm btn-outline-secondary" onclick="return confirm('Arkibkan set soalselidik ini?')">
<i class="bi bi-archive me-1"></i> Arkib
</button>
</form>
@endif
</div>
@endsection
@push('styles')
<style>
.tajuk-block { border-left: 3px solid #6c757d; }
.tajuk-header { background: #f8f9fa; }
.children-list { border-top: 1px solid #dee2e6; }
.children-list .list-group-item { background: #fff; }
.children-list .list-group-item:last-child { border-bottom: 0; }
.drag-handle { cursor: grab; color: #adb5bd; }
.drag-handle:active { cursor: grabbing; }
.sortable-ghost { opacity: .4; background: #e9f0ff !important; }
</style>
@endpush
@section('content')
<div class="row g-4">
{{-- Left: Questions --}}
<div class="col-md-8">
@if($set->status === 'draft')
<div class="alert alert-warning mb-3 small">
<i class="bi bi-exclamation-triangle me-2"></i>
Set ini masih dalam status <strong>Draf</strong>. Terbitkan setelah siap untuk dilampirkan ke program.
</div>
@elseif($set->status === 'archived')
<div class="alert alert-secondary mb-3 small">
<i class="bi bi-archive me-2"></i>
Set ini telah <strong>diarkibkan</strong> dan tidak boleh dilampirkan ke program baru.
</div>
@endif
{{-- Question List --}}
<div class="card border-0 shadow-sm mb-4">
<div class="card-header bg-white d-flex justify-content-between align-items-center py-3">
<h6 class="mb-0 fw-semibold">
<i class="bi bi-list-ul me-2 text-primary"></i>Senarai Soalan
<span class="badge bg-secondary ms-2">{{ $totalCount }}</span>
</h6>
<small class="text-muted"><i class="bi bi-grip-vertical me-1"></i>Seret untuk susun semula</small>
</div>
@if($totalCount === 0)
<div class="card-body text-center py-5 text-muted">
<i class="bi bi-question-circle d-block fs-1 mb-3 opacity-25"></i>
Belum ada soalan. Tambah soalan menggunakan borang di sebelah kanan.
</div>
@else
<ul class="list-group list-group-flush" id="questionList">
@foreach($topLevel as $q)
@if($q->question_type === 'tajuk')
{{-- ── TAJUK BLOCK ── --}}
<li class="list-group-item p-0 tajuk-block" data-id="{{ $q->id }}">
{{-- Tajuk header row --}}
<div class="tajuk-header d-flex align-items-center gap-2 px-3 py-2">
<i class="bi bi-grip-vertical drag-handle drag-handle-top fs-5"></i>
<span class="badge bg-dark small">Tajuk</span>
<div class="flex-grow-1 fw-semibold">{{ $q->question_text }}</div>
@if($q->rating_labels)
<div class="small text-muted text-nowrap d-none d-md-block">
@php $labels = array_filter($q->rating_labels); @endphp
@if(!empty($labels))
<i class="bi bi-tag me-1"></i>{{ implode(' · ', $labels) }}
@endif
</div>
@endif
<div class="d-flex gap-1 flex-shrink-0">
<button class="btn btn-sm btn-outline-secondary"
onclick="editQuestion({{ $q->id }}, @json($q->question_text), 'tajuk', false, [], null, @json($q->rating_labels ?? []))">
<i class="bi bi-pencil"></i>
</button>
<form method="POST" action="{{ route('admin.questions.destroy', $q) }}"
onsubmit="return confirm('Padam bahagian '{{ addslashes($q->question_text) }}' dan semua soalan di dalamnya?')">
@csrf @method('DELETE')
<button class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i></button>
</form>
</div>
</div>
{{-- Rating labels pills --}}
@if($q->rating_labels && array_filter($q->rating_labels))
<div class="px-3 py-1 bg-light border-bottom d-flex gap-2 flex-wrap d-md-none">
@foreach(array_filter($q->rating_labels) as $val => $lbl)
<span class="badge bg-light text-dark border small">{{ $val }}: {{ $lbl }}</span>
@endforeach
</div>
@endif
{{-- Children list --}}
<ul class="children-list list-group list-group-flush" data-parent-id="{{ $q->id }}">
@foreach($q->children as $child)
<li class="list-group-item d-flex align-items-start gap-2 py-2 ps-4" data-id="{{ $child->id }}">
<i class="bi bi-grip-vertical drag-handle drag-handle-child fs-5 mt-1"></i>
<div class="flex-grow-1">
<div class="d-flex align-items-center gap-2 mb-1">
<span class="badge bg-light text-dark border small">{{ $loop->iteration }}</span>
<span class="badge bg-primary bg-opacity-10 text-primary small">Rating 15</span>
@if($child->is_required)
<span class="badge bg-danger bg-opacity-10 text-danger small">Wajib</span>
@endif
</div>
<div class="small">{{ $child->question_text }}</div>
</div>
<div class="d-flex gap-1 flex-shrink-0">
<button class="btn btn-sm btn-outline-secondary"
onclick="editQuestion({{ $child->id }}, @json($child->question_text), 'rating', {{ $child->is_required ? 'true' : 'false' }}, [], {{ $child->parent_id ?? 'null' }}, [])">
<i class="bi bi-pencil"></i>
</button>
<form method="POST" action="{{ route('admin.questions.destroy', $child) }}"
onsubmit="return confirm('Padam soalan ini?')">
@csrf @method('DELETE')
<button class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i></button>
</form>
</div>
</li>
@endforeach
@if($q->children->isEmpty())
<li class="list-group-item text-muted small text-center py-2 fst-italic ps-4">
<i class="bi bi-arrow-down-short me-1"></i>Tiada soalan dalam bahagian ini
</li>
@endif
</ul>
</li>
@else
{{-- ── STANDALONE QUESTION ── --}}
<li class="list-group-item py-3" data-id="{{ $q->id }}">
<div class="d-flex justify-content-between align-items-start gap-2">
<div class="d-flex align-items-start gap-2 flex-grow-1">
<i class="bi bi-grip-vertical drag-handle drag-handle-top fs-5 mt-1"></i>
<div>
<div class="d-flex align-items-center gap-2 mb-1">
<span class="badge bg-light text-dark border small">{{ $loop->iteration }}</span>
<span class="badge bg-primary bg-opacity-10 text-primary small">
{{ match($q->question_type) {
'rating' => 'Rating 15',
'single_choice' => 'Pilihan Tunggal',
'multiple_choice' => 'Pilihan Berganda',
'short_text' => 'Teks Pendek',
'long_text' => 'Teks Panjang',
default => $q->question_type,
} }}
</span>
@if($q->is_required)
<span class="badge bg-danger bg-opacity-10 text-danger small">Wajib</span>
@endif
</div>
<div class="fw-medium">{{ $q->question_text }}</div>
@if($q->options_json)
<div class="mt-1">
@foreach($q->options_json as $opt)
<span class="badge bg-light text-dark border me-1 small">{{ $opt }}</span>
@endforeach
</div>
@endif
</div>
</div>
<div class="d-flex gap-1 flex-shrink-0">
<button class="btn btn-sm btn-outline-secondary"
onclick="editQuestion({{ $q->id }}, @json($q->question_text), '{{ $q->question_type }}', {{ $q->is_required ? 'true' : 'false' }}, @json($q->options_json ?? []), null, [])">
<i class="bi bi-pencil"></i>
</button>
<form method="POST" action="{{ route('admin.questions.destroy', $q) }}"
onsubmit="return confirm('Padam soalan ini?')">
@csrf @method('DELETE')
<button class="btn btn-sm btn-outline-danger"><i class="bi bi-trash"></i></button>
</form>
</div>
</div>
</li>
@endif
@endforeach
</ul>
@endif
</div>
{{-- Used In Programs --}}
@if($usedInPrograms->count())
<div class="card border-0 shadow-sm">
<div class="card-header bg-white py-3">
<h6 class="mb-0 fw-semibold"><i class="bi bi-diagram-3 me-2 text-secondary"></i>Digunakan Dalam Program</h6>
</div>
<ul class="list-group list-group-flush">
@foreach($usedInPrograms as $program)
<li class="list-group-item d-flex justify-content-between align-items-center py-2">
<a href="{{ route('admin.programs.show', $program) }}" class="text-decoration-none small">
{{ $program->title }}
</a>
@if($program->pivot->is_confirmed ?? false)
<span class="badge bg-success">Disahkan</span>
@else
<span class="badge bg-warning text-dark">Belum Disahkan</span>
@endif
</li>
@endforeach
</ul>
</div>
@endif
</div>
{{-- Right: Add / Edit Question Form --}}
<div class="col-md-4">
<div class="card border-0 shadow-sm sticky-top" style="top:80px;">
<div class="card-header bg-white py-3">
<h6 class="mb-0 fw-semibold" id="formTitle">
<i class="bi bi-plus-circle me-2 text-primary"></i>Tambah Soalan
</h6>
</div>
<div class="card-body">
<form method="POST" id="questionForm" action="{{ route('admin.questions.store', $set) }}">
@csrf
<input type="hidden" name="_method" id="formMethod" value="POST">
<input type="hidden" name="_question_id" id="questionId" value="">
{{-- Question text --}}
<div class="mb-3">
<label class="form-label small fw-medium">Teks Soalan / Tajuk <span class="text-danger">*</span></label>
<textarea name="question_text" id="questionText" rows="3"
class="form-control form-control-sm @error('question_text') is-invalid @enderror"
placeholder="Taip soalan atau nama bahagian..."></textarea>
@error('question_text')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
{{-- Question type --}}
<div class="mb-3">
<label class="form-label small fw-medium">Jenis <span class="text-danger">*</span></label>
<select name="question_type" id="questionType" class="form-select form-select-sm" onchange="onTypeChange()">
<option value="tajuk">Tajuk Bahagian</option>
<option value="rating">Rating (15)</option>
<option value="single_choice">Pilihan Tunggal</option>
<option value="multiple_choice">Pilihan Berganda</option>
<option value="short_text">Teks Pendek</option>
<option value="long_text">Teks Panjang</option>
</select>
</div>
{{-- Required (hidden for tajuk) --}}
<div class="mb-3 form-check d-none" id="requiredSection">
<input type="checkbox" name="is_required" id="isRequired"
class="form-check-input" value="1" checked>
<label class="form-check-label small" for="isRequired">Wajib dijawab</label>
</div>
{{-- Parent selector (rating only) --}}
<div id="parentSection" class="mb-3 d-none">
<label class="form-label small fw-medium">Bahagian (Tajuk) <span class="text-danger">*</span></label>
<select name="parent_id" id="parentId" class="form-select form-select-sm">
<option value=""> Pilih Tajuk </option>
</select>
@error('parent_id')
<div class="text-danger small mt-1">{{ $message }}</div>
@enderror
</div>
{{-- Rating labels (tajuk only) --}}
<div id="ratingLabelsSection" class="mb-3">
<label class="form-label small fw-medium">Label Skala Rating</label>
<div class="d-flex flex-column gap-1">
@for ($i = 1; $i <= 5; $i++)
<div class="input-group input-group-sm">
<span class="input-group-text fw-bold" style="width:32px;justify-content:center;">{{ $i }}</span>
<input type="text" name="rating_labels[{{ $i }}]" id="ratingLabel{{ $i }}"
class="form-control"
placeholder="{{ $i === 1 ? 'cth: Sangat Tidak Setuju' : ($i === 5 ? 'cth: Sangat Setuju' : '') }}">
</div>
@endfor
</div>
<div class="form-text">Kosongkan jika tiada label untuk nilai tersebut.</div>
</div>
{{-- Options (choice types) --}}
<div id="optionsSection" class="mb-3 d-none">
<label class="form-label small fw-medium">Pilihan Jawapan</label>
<div id="optionsList">
<div class="input-group input-group-sm mb-2">
<input type="text" name="options[]" class="form-control" placeholder="Pilihan 1">
<button type="button" class="btn btn-outline-danger" onclick="removeOption(this)"><i class="bi bi-x"></i></button>
</div>
<div class="input-group input-group-sm mb-2">
<input type="text" name="options[]" class="form-control" placeholder="Pilihan 2">
<button type="button" class="btn btn-outline-danger" onclick="removeOption(this)"><i class="bi bi-x"></i></button>
</div>
</div>
<button type="button" class="btn btn-sm btn-outline-secondary w-100" onclick="addOption()">
<i class="bi bi-plus me-1"></i> Tambah Pilihan
</button>
@error('options')
<div class="text-danger small mt-1">{{ $message }}</div>
@enderror
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary btn-sm w-100">
<i class="bi bi-check-lg me-1"></i> <span id="submitLabel">Tambah</span>
</button>
<button type="button" class="btn btn-outline-secondary btn-sm" id="cancelEdit"
style="display:none;" onclick="resetForm()">
Batal
</button>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.3/Sortable.min.js"></script>
<script>
const setId = {{ $set->id }};
const storeUrl = "{{ route('admin.questions.store', $set) }}";
const updateBase = "{{ url('admin/questions') }}/";
const reorderUrl = "{{ route('admin.questions.reorder') }}";
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
// Tajuk questions available as parents for rating questions
const tajukList = @json($topLevel->where('question_type', 'tajuk')->map(fn($q) => ['id' => $q->id, 'text' => $q->question_text])->values());
// ── UI helpers ──────────────────────────────────────────────────────────────
function populateParentDropdown(selectedId) {
const sel = document.getElementById('parentId');
sel.innerHTML = '<option value="">— Pilih Tajuk —</option>';
tajukList.forEach(function(t) {
const opt = document.createElement('option');
opt.value = t.id;
opt.textContent = t.text;
if (selectedId && t.id == selectedId) opt.selected = true;
sel.appendChild(opt);
});
}
function onTypeChange() {
const type = document.getElementById('questionType').value;
const isTajuk = type === 'tajuk';
const isRating = type === 'rating';
const isChoice = ['single_choice', 'multiple_choice'].includes(type);
document.getElementById('requiredSection').classList.toggle('d-none', isTajuk);
document.getElementById('ratingLabelsSection').classList.toggle('d-none', !isTajuk);
document.getElementById('parentSection').classList.toggle('d-none', !isRating);
document.getElementById('optionsSection').classList.toggle('d-none', !isChoice);
if (isRating) populateParentDropdown(null);
}
function addOption() {
const list = document.getElementById('optionsList');
const count = list.querySelectorAll('input').length + 1;
const div = document.createElement('div');
div.className = 'input-group input-group-sm mb-2';
div.innerHTML = `<input type="text" name="options[]" class="form-control" placeholder="Pilihan ${count}">
<button type="button" class="btn btn-outline-danger" onclick="removeOption(this)"><i class="bi bi-x"></i></button>`;
list.appendChild(div);
}
function removeOption(btn) {
const list = document.getElementById('optionsList');
if (list.querySelectorAll('.input-group').length > 1) {
btn.closest('.input-group').remove();
}
}
function editQuestion(id, text, type, required, options, parentId, ratingLabels) {
document.getElementById('formTitle').innerHTML = '<i class="bi bi-pencil me-2 text-warning"></i>Edit Soalan';
document.getElementById('submitLabel').textContent = 'Kemaskini';
document.getElementById('cancelEdit').style.display = '';
document.getElementById('questionId').value = id;
document.getElementById('questionText').value = text;
document.getElementById('questionType').value = type;
document.getElementById('isRequired').checked = required;
const form = document.getElementById('questionForm');
form.action = updateBase + id;
document.getElementById('formMethod').value = 'PUT';
onTypeChange(); // show/hide sections
// Set parent if rating
if (type === 'rating' && parentId) {
populateParentDropdown(parentId);
}
// Set rating labels if tajuk
if (type === 'tajuk') {
for (let i = 1; i <= 5; i++) {
const el = document.getElementById('ratingLabel' + i);
if (el) el.value = (ratingLabels && (ratingLabels[i] || ratingLabels[String(i)])) || '';
}
}
// Options list
const list = document.getElementById('optionsList');
list.innerHTML = '';
if (options && options.length) {
options.forEach(function(opt, i) {
const div = document.createElement('div');
div.className = 'input-group input-group-sm mb-2';
div.innerHTML = `<input type="text" name="options[]" class="form-control" value="${opt}" placeholder="Pilihan ${i+1}">
<button type="button" class="btn btn-outline-danger" onclick="removeOption(this)"><i class="bi bi-x"></i></button>`;
list.appendChild(div);
});
} else {
// Restore default empty options for choice types
list.innerHTML = `<div class="input-group input-group-sm mb-2">
<input type="text" name="options[]" class="form-control" placeholder="Pilihan 1">
<button type="button" class="btn btn-outline-danger" onclick="removeOption(this)"><i class="bi bi-x"></i></button>
</div>
<div class="input-group input-group-sm mb-2">
<input type="text" name="options[]" class="form-control" placeholder="Pilihan 2">
<button type="button" class="btn btn-outline-danger" onclick="removeOption(this)"><i class="bi bi-x"></i></button>
</div>`;
}
form.scrollIntoView({ behavior: 'smooth' });
}
function resetForm() {
document.getElementById('formTitle').innerHTML = '<i class="bi bi-plus-circle me-2 text-primary"></i>Tambah Soalan';
document.getElementById('submitLabel').textContent = 'Tambah';
document.getElementById('cancelEdit').style.display = 'none';
document.getElementById('questionForm').reset();
document.getElementById('questionForm').action = storeUrl;
document.getElementById('formMethod').value = 'POST';
document.getElementById('questionId').value = '';
// Clear rating labels
for (let i = 1; i <= 5; i++) {
const el = document.getElementById('ratingLabel' + i);
if (el) el.value = '';
}
document.getElementById('parentId').value = '';
onTypeChange();
document.getElementById('optionsList').innerHTML = `
<div class="input-group input-group-sm mb-2">
<input type="text" name="options[]" class="form-control" placeholder="Pilihan 1">
<button type="button" class="btn btn-outline-danger" onclick="removeOption(this)"><i class="bi bi-x"></i></button>
</div>
<div class="input-group input-group-sm mb-2">
<input type="text" name="options[]" class="form-control" placeholder="Pilihan 2">
<button type="button" class="btn btn-outline-danger" onclick="removeOption(this)"><i class="bi bi-x"></i></button>
</div>`;
}
// ── Drag & Drop ──────────────────────────────────────────────────────────────
function sendReorder(order, parentId) {
fetch(reorderUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrfToken },
body: JSON.stringify({ order: order, parent_id: parentId }),
});
}
document.addEventListener('DOMContentLoaded', function () {
// Init: show sections for default type (tajuk)
onTypeChange();
// Top-level sortable — sorts tajuk blocks + standalone questions
const topList = document.getElementById('questionList');
if (topList) {
Sortable.create(topList, {
animation: 150,
handle: '.drag-handle-top',
ghostClass: 'sortable-ghost',
onEnd: function() {
const order = [...topList.querySelectorAll(':scope > [data-id]')].map(el => +el.dataset.id);
sendReorder(order, null);
},
});
}
// Per-tajuk sortable — sorts rating children within a group
document.querySelectorAll('.children-list').forEach(function(list) {
Sortable.create(list, {
animation: 150,
handle: '.drag-handle-child',
ghostClass: 'sortable-ghost',
onEnd: function() {
const parentId = +list.dataset.parentId;
const order = [...list.querySelectorAll(':scope > [data-id]')].map(el => +el.dataset.id);
sendReorder(order, parentId);
},
});
});
});
</script>
@endpush

View File

@@ -0,0 +1,76 @@
@extends('layouts.admin')
@section('title', 'Tambah Pengguna')
@section('header', 'Tambah Pengguna Baru')
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.users.index') }}">Pengguna</a></li>
<li class="breadcrumb-item active">Tambah Baru</li>
@endsection
@section('content')
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<form method="POST" action="{{ route('admin.users.store') }}">
@csrf
<div class="mb-3">
<label class="form-label fw-medium">Nama Penuh <span class="text-danger">*</span></label>
<input type="text" name="name" value="{{ old('name') }}"
class="form-control @error('name') is-invalid @enderror"
placeholder="Nama penuh pengguna">
@error('name')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="mb-3">
<label class="form-label fw-medium">Emel <span class="text-danger">*</span></label>
<input type="email" name="email" value="{{ old('email') }}"
class="form-control @error('email') is-invalid @enderror"
placeholder="pengguna@mbip.gov.my">
@error('email')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="mb-3">
<label class="form-label fw-medium">Peranan <span class="text-danger">*</span></label>
<select name="role" class="form-select @error('role') is-invalid @enderror">
<option value="admin" {{ old('role') === 'admin' ? 'selected' : '' }}>
Admin Program Boleh urus program sendiri sahaja
</option>
<option value="super_admin" {{ old('role') === 'super_admin' ? 'selected' : '' }}>
Super Admin Boleh urus semua program & pengguna
</option>
</select>
@error('role')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="mb-3">
<label class="form-label fw-medium">Kata Laluan <span class="text-danger">*</span></label>
<input type="password" name="password"
class="form-control @error('password') is-invalid @enderror"
placeholder="Minimum 8 aksara, huruf besar, huruf kecil, nombor">
@error('password')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="mb-4">
<label class="form-label fw-medium">Sahkan Kata Laluan <span class="text-danger">*</span></label>
<input type="password" name="password_confirmation"
class="form-control"
placeholder="Ulang kata laluan">
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-person-check me-1"></i> Tambah Pengguna
</button>
<a href="{{ route('admin.users.index') }}" class="btn btn-outline-secondary">Batal</a>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,77 @@
@extends('layouts.admin')
@section('title', 'Edit Pengguna — ' . $user->name)
@section('header', 'Edit Pengguna')
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.users.index') }}">Pengguna</a></li>
<li class="breadcrumb-item active">{{ Str::limit($user->name, 30) }}</li>
@endsection
@section('content')
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card border-0 shadow-sm">
<div class="card-body p-4">
<form method="POST" action="{{ route('admin.users.update', $user) }}">
@csrf @method('PUT')
<div class="mb-3">
<label class="form-label fw-medium">Nama Penuh <span class="text-danger">*</span></label>
<input type="text" name="name" value="{{ old('name', $user->name) }}"
class="form-control @error('name') is-invalid @enderror">
@error('name')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="mb-3">
<label class="form-label fw-medium">Emel <span class="text-danger">*</span></label>
<input type="email" name="email" value="{{ old('email', $user->email) }}"
class="form-control @error('email') is-invalid @enderror">
@error('email')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="mb-3">
<label class="form-label fw-medium">Peranan <span class="text-danger">*</span></label>
<select name="role" class="form-select @error('role') is-invalid @enderror"
{{ $user->id === auth()->id() ? 'disabled' : '' }}>
<option value="admin" {{ old('role', $user->role) === 'admin' ? 'selected' : '' }}>
Admin Program
</option>
<option value="super_admin" {{ old('role', $user->role) === 'super_admin' ? 'selected' : '' }}>
Super Admin
</option>
</select>
@if($user->id === auth()->id())
<input type="hidden" name="role" value="{{ $user->role }}">
<div class="form-text text-warning"><i class="bi bi-exclamation-circle me-1"></i>Anda tidak boleh tukar peranan akaun sendiri.</div>
@endif
@error('role')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="mb-3">
<label class="form-label fw-medium">Kata Laluan Baru <span class="text-muted small">(kosongkan jika tidak tukar)</span></label>
<input type="password" name="password"
class="form-control @error('password') is-invalid @enderror"
placeholder="Minimum 8 aksara, huruf besar, huruf kecil, nombor">
@error('password')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="mb-4">
<label class="form-label fw-medium">Sahkan Kata Laluan Baru</label>
<input type="password" name="password_confirmation" class="form-control">
</div>
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg me-1"></i> Simpan Perubahan
</button>
<a href="{{ route('admin.users.index') }}" class="btn btn-outline-secondary">Batal</a>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,83 @@
@extends('layouts.admin')
@section('title', 'Pengurusan Pengguna')
@section('header', 'Pengurusan Pengguna')
@section('breadcrumb')
<li class="breadcrumb-item active">Pengguna</li>
@endsection
@section('header-actions')
<a href="{{ route('admin.users.create') }}" class="btn btn-sm btn-primary">
<i class="bi bi-person-plus me-1"></i> Tambah Pengguna
</a>
@endsection
@section('content')
<div class="card border-0 shadow-sm">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="table-light">
<tr>
<th>Nama</th>
<th>Emel</th>
<th>Peranan</th>
<th class="text-center">Program</th>
<th>Tarikh Daftar</th>
<th></th>
</tr>
</thead>
<tbody>
@forelse($users as $user)
<tr>
<td>
<div class="fw-medium">{{ $user->name }}</div>
@if($user->id === auth()->id())
<span class="badge bg-light text-muted border" style="font-size:.65rem;">Anda</span>
@endif
</td>
<td class="small text-muted">{{ $user->email }}</td>
<td>
@if($user->role === 'super_admin')
<span class="badge bg-danger">Super Admin</span>
@else
<span class="badge bg-primary">Admin Program</span>
@endif
</td>
<td class="text-center">
<span class="badge bg-secondary">{{ $user->programs_count }}</span>
</td>
<td class="small text-muted">{{ $user->created_at->format('d/m/Y') }}</td>
<td class="text-end">
<a href="{{ route('admin.users.edit', $user) }}"
class="btn btn-sm btn-outline-secondary">
<i class="bi bi-pencil"></i>
</a>
@if($user->id !== auth()->id())
<form method="POST" action="{{ route('admin.users.destroy', $user) }}"
class="d-inline"
onsubmit="return confirm('Padam pengguna {{ addslashes($user->name) }}? Program mereka tidak akan terjejas.')">
@csrf @method('DELETE')
<button class="btn btn-sm btn-outline-danger">
<i class="bi bi-trash"></i>
</button>
</form>
@endif
</td>
</tr>
@empty
<tr>
<td colspan="6" class="text-center py-4 text-muted">Tiada pengguna dijumpai.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
@if($users->hasPages())
<div class="card-footer bg-white">{{ $users->links() }}</div>
@endif
</div>
@endsection

View File

@@ -0,0 +1,27 @@
<x-guest-layout>
<div class="mb-4 text-sm text-gray-600">
{{ __('This is a secure area of the application. Please confirm your password before continuing.') }}
</div>
<form method="POST" action="{{ route('password.confirm') }}">
@csrf
<!-- Password -->
<div>
<x-input-label for="password" :value="__('Password')" />
<x-text-input id="password" class="block mt-1 w-full"
type="password"
name="password"
required autocomplete="current-password" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
<div class="flex justify-end mt-4">
<x-primary-button>
{{ __('Confirm') }}
</x-primary-button>
</div>
</form>
</x-guest-layout>

View File

@@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang="ms">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Terlupa Kata Laluan eCert MBIP</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<style>
body { background: linear-gradient(135deg, #1a56a0 0%, #2563eb 100%); min-height: 100vh; }
.card { border-radius: 1rem; border: none; box-shadow: 0 8px 32px rgba(0,0,0,0.2); max-width: 420px; }
</style>
</head>
<body class="d-flex align-items-center justify-content-center py-5">
<div class="w-100 px-3">
<div class="text-center mb-4">
<i class="bi bi-award-fill text-white" style="font-size: 3rem;"></i>
<h4 class="text-white fw-bold mt-2 mb-0">eCert MBIP</h4>
<small class="text-white opacity-75">Sistem Pengurusan Sijil Digital</small>
</div>
<div class="card mx-auto">
<div class="card-body p-4">
<h5 class="card-title fw-semibold mb-1">Terlupa Kata Laluan?</h5>
<p class="text-muted small mb-4">
Masukkan alamat emel anda dan kami akan hantar pautan untuk menetapkan semula kata laluan.
</p>
@if(session('status'))
<div class="alert alert-success small">
<i class="bi bi-check-circle me-1"></i>{{ session('status') }}
</div>
@endif
<form method="POST" action="{{ route('password.email') }}">
@csrf
<div class="mb-3">
<label for="email" class="form-label fw-medium">Alamat Emel</label>
<input id="email" type="email" name="email"
value="{{ old('email') }}" required autofocus autocomplete="email"
class="form-control @error('email') is-invalid @enderror"
placeholder="admin@mbip.gov.my">
@error('email')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<button type="submit" class="btn w-100 text-white fw-semibold mb-3"
style="background: var(--mbip-primary, #1a56a0);">
<i class="bi bi-send me-1"></i> Hantar Pautan Reset
</button>
<div class="text-center">
<a href="{{ route('login') }}" class="small text-decoration-none text-muted">
<i class="bi bi-arrow-left me-1"></i> Kembali ke Log Masuk
</a>
</div>
</form>
</div>
</div>
<div class="text-center mt-3">
<small class="text-white opacity-60">Majlis Bandaraya Ipoh Perak &copy; {{ date('Y') }}</small>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,78 @@
<!DOCTYPE html>
<html lang="ms">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Log Masuk eCert MBIP</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<style>
body { background: linear-gradient(135deg, #1a56a0 0%, #2563eb 100%); min-height: 100vh; }
.login-card { border-radius: 1rem; border: none; box-shadow: 0 8px 32px rgba(0,0,0,0.2); max-width: 420px; }
</style>
</head>
<body class="d-flex align-items-center justify-content-center py-5">
<div class="w-100 px-3">
<div class="text-center mb-4">
<i class="bi bi-award-fill text-white" style="font-size: 3rem;"></i>
<h4 class="text-white fw-bold mt-2 mb-0">eCert MBIP</h4>
<small class="text-white opacity-75">Sistem Pengurusan Sijil Digital</small>
</div>
<div class="card login-card mx-auto">
<div class="card-body p-4">
<h5 class="card-title fw-semibold mb-4">Log Masuk Admin</h5>
@if(session('status'))
<div class="alert alert-info">{{ session('status') }}</div>
@endif
<form method="POST" action="{{ route('login') }}">
@csrf
<div class="mb-3">
<label for="email" class="form-label fw-medium">Alamat Emel</label>
<input id="email" type="email" name="email"
value="{{ old('email') }}" required autofocus autocomplete="username"
class="form-control @error('email') is-invalid @enderror"
placeholder="admin@mbip.gov.my">
@error('email')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<div class="d-flex justify-content-between">
<label for="password" class="form-label fw-medium">Kata Laluan</label>
@if(Route::has('password.request'))
<a href="{{ route('password.request') }}" class="small text-decoration-none">
Terlupa kata laluan?
</a>
@endif
</div>
<input id="password" type="password" name="password"
required autocomplete="current-password"
class="form-control @error('password') is-invalid @enderror"
placeholder="••••••••">
@error('password')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-4 form-check">
<input type="checkbox" name="remember" id="remember" class="form-check-input">
<label class="form-check-label" for="remember">Ingat saya</label>
</div>
<button type="submit" class="btn w-100 text-white fw-semibold" style="background: var(--mbip-primary);">
<i class="bi bi-box-arrow-in-right me-1"></i> Log Masuk
</button>
</form>
</div>
</div>
<div class="text-center mt-3">
<small class="text-white opacity-60">Majlis Bandaraya Ipoh Perak &copy; {{ date('Y') }}</small>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,52 @@
<x-guest-layout>
<form method="POST" action="{{ route('register') }}">
@csrf
<!-- Name -->
<div>
<x-input-label for="name" :value="__('Name')" />
<x-text-input id="name" class="block mt-1 w-full" type="text" name="name" :value="old('name')" required autofocus autocomplete="name" />
<x-input-error :messages="$errors->get('name')" class="mt-2" />
</div>
<!-- Email Address -->
<div class="mt-4">
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required autocomplete="username" />
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<!-- Password -->
<div class="mt-4">
<x-input-label for="password" :value="__('Password')" />
<x-text-input id="password" class="block mt-1 w-full"
type="password"
name="password"
required autocomplete="new-password" />
<x-input-error :messages="$errors->get('password')" class="mt-2" />
</div>
<!-- Confirm Password -->
<div class="mt-4">
<x-input-label for="password_confirmation" :value="__('Confirm Password')" />
<x-text-input id="password_confirmation" class="block mt-1 w-full"
type="password"
name="password_confirmation" required autocomplete="new-password" />
<x-input-error :messages="$errors->get('password_confirmation')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4">
<a class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" href="{{ route('login') }}">
{{ __('Already registered?') }}
</a>
<x-primary-button class="ms-4">
{{ __('Register') }}
</x-primary-button>
</div>
</form>
</x-guest-layout>

View File

@@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="ms">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tetapkan Semula Kata Laluan eCert MBIP</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<style>
body { background: linear-gradient(135deg, #1a56a0 0%, #2563eb 100%); min-height: 100vh; }
.card { border-radius: 1rem; border: none; box-shadow: 0 8px 32px rgba(0,0,0,0.2); max-width: 420px; }
</style>
</head>
<body class="d-flex align-items-center justify-content-center py-5">
<div class="w-100 px-3">
<div class="text-center mb-4">
<i class="bi bi-award-fill text-white" style="font-size: 3rem;"></i>
<h4 class="text-white fw-bold mt-2 mb-0">eCert MBIP</h4>
<small class="text-white opacity-75">Sistem Pengurusan Sijil Digital</small>
</div>
<div class="card mx-auto">
<div class="card-body p-4">
<h5 class="card-title fw-semibold mb-1">Tetapkan Semula Kata Laluan</h5>
<p class="text-muted small mb-4">Masukkan kata laluan baru untuk akaun anda.</p>
<form method="POST" action="{{ route('password.store') }}">
@csrf
<input type="hidden" name="token" value="{{ $request->route('token') }}">
<div class="mb-3">
<label for="email" class="form-label fw-medium">Alamat Emel</label>
<input id="email" type="email" name="email"
value="{{ old('email', $request->email) }}" required autofocus autocomplete="username"
class="form-control @error('email') is-invalid @enderror"
placeholder="admin@mbip.gov.my">
@error('email')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-3">
<label for="password" class="form-label fw-medium">Kata Laluan Baru</label>
<input id="password" type="password" name="password"
required autocomplete="new-password"
class="form-control @error('password') is-invalid @enderror"
placeholder="Min. 8 aksara">
@error('password')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<div class="mb-4">
<label for="password_confirmation" class="form-label fw-medium">Sahkan Kata Laluan Baru</label>
<input id="password_confirmation" type="password" name="password_confirmation"
required autocomplete="new-password"
class="form-control @error('password_confirmation') is-invalid @enderror"
placeholder="••••••••">
@error('password_confirmation')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<button type="submit" class="btn w-100 text-white fw-semibold mb-3"
style="background: var(--mbip-primary, #1a56a0);">
<i class="bi bi-key me-1"></i> Tetapkan Semula Kata Laluan
</button>
<div class="text-center">
<a href="{{ route('login') }}" class="small text-decoration-none text-muted">
<i class="bi bi-arrow-left me-1"></i> Kembali ke Log Masuk
</a>
</div>
</form>
</div>
</div>
<div class="text-center mt-3">
<small class="text-white opacity-60">Majlis Bandaraya Ipoh Perak &copy; {{ date('Y') }}</small>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,31 @@
<x-guest-layout>
<div class="mb-4 text-sm text-gray-600">
{{ __('Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }}
</div>
@if (session('status') == 'verification-link-sent')
<div class="mb-4 font-medium text-sm text-green-600">
{{ __('A new verification link has been sent to the email address you provided during registration.') }}
</div>
@endif
<div class="mt-4 flex items-center justify-between">
<form method="POST" action="{{ route('verification.send') }}">
@csrf
<div>
<x-primary-button>
{{ __('Resend Verification Email') }}
</x-primary-button>
</div>
</form>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
{{ __('Log Out') }}
</button>
</form>
</div>
</x-guest-layout>

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 316 316" xmlns="http://www.w3.org/2000/svg" {{ $attributes }}>
<path d="M305.8 81.125C305.77 80.995 305.69 80.885 305.65 80.755C305.56 80.525 305.49 80.285 305.37 80.075C305.29 79.935 305.17 79.815 305.07 79.685C304.94 79.515 304.83 79.325 304.68 79.175C304.55 79.045 304.39 78.955 304.25 78.845C304.09 78.715 303.95 78.575 303.77 78.475L251.32 48.275C249.97 47.495 248.31 47.495 246.96 48.275L194.51 78.475C194.33 78.575 194.19 78.725 194.03 78.845C193.89 78.955 193.73 79.045 193.6 79.175C193.45 79.325 193.34 79.515 193.21 79.685C193.11 79.815 192.99 79.935 192.91 80.075C192.79 80.285 192.71 80.525 192.63 80.755C192.58 80.875 192.51 80.995 192.48 81.125C192.38 81.495 192.33 81.875 192.33 82.265V139.625L148.62 164.795V52.575C148.62 52.185 148.57 51.805 148.47 51.435C148.44 51.305 148.36 51.195 148.32 51.065C148.23 50.835 148.16 50.595 148.04 50.385C147.96 50.245 147.84 50.125 147.74 49.995C147.61 49.825 147.5 49.635 147.35 49.485C147.22 49.355 147.06 49.265 146.92 49.155C146.76 49.025 146.62 48.885 146.44 48.785L93.99 18.585C92.64 17.805 90.98 17.805 89.63 18.585L37.18 48.785C37 48.885 36.86 49.035 36.7 49.155C36.56 49.265 36.4 49.355 36.27 49.485C36.12 49.635 36.01 49.825 35.88 49.995C35.78 50.125 35.66 50.245 35.58 50.385C35.46 50.595 35.38 50.835 35.3 51.065C35.25 51.185 35.18 51.305 35.15 51.435C35.05 51.805 35 52.185 35 52.575V232.235C35 233.795 35.84 235.245 37.19 236.025L142.1 296.425C142.33 296.555 142.58 296.635 142.82 296.725C142.93 296.765 143.04 296.835 143.16 296.865C143.53 296.965 143.9 297.015 144.28 297.015C144.66 297.015 145.03 296.965 145.4 296.865C145.5 296.835 145.59 296.775 145.69 296.745C145.95 296.655 146.21 296.565 146.45 296.435L251.36 236.035C252.72 235.255 253.55 233.815 253.55 232.245V174.885L303.81 145.945C305.17 145.165 306 143.725 306 142.155V82.265C305.95 81.875 305.89 81.495 305.8 81.125ZM144.2 227.205L100.57 202.515L146.39 176.135L196.66 147.195L240.33 172.335L208.29 190.625L144.2 227.205ZM244.75 114.995V164.795L226.39 154.225L201.03 139.625V89.825L219.39 100.395L244.75 114.995ZM249.12 57.105L292.81 82.265L249.12 107.425L205.43 82.265L249.12 57.105ZM114.49 184.425L96.13 194.995V85.305L121.49 70.705L139.85 60.135V169.815L114.49 184.425ZM91.76 27.425L135.45 52.585L91.76 77.745L48.07 52.585L91.76 27.425ZM43.67 60.135L62.03 70.705L87.39 85.305V202.545V202.555V202.565C87.39 202.735 87.44 202.895 87.46 203.055C87.49 203.265 87.49 203.485 87.55 203.695V203.705C87.6 203.875 87.69 204.035 87.76 204.195C87.84 204.375 87.89 204.575 87.99 204.745C87.99 204.745 87.99 204.755 88 204.755C88.09 204.905 88.22 205.035 88.33 205.175C88.45 205.335 88.55 205.495 88.69 205.635L88.7 205.645C88.82 205.765 88.98 205.855 89.12 205.965C89.28 206.085 89.42 206.225 89.59 206.325C89.6 206.325 89.6 206.325 89.61 206.335C89.62 206.335 89.62 206.345 89.63 206.345L139.87 234.775V285.065L43.67 229.705V60.135ZM244.75 229.705L148.58 285.075V234.775L219.8 194.115L244.75 179.875V229.705ZM297.2 139.625L253.49 164.795V114.995L278.85 100.395L297.21 89.825V139.625H297.2Z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,7 @@
@props(['status'])
@if ($status)
<div {{ $attributes->merge(['class' => 'font-medium text-sm text-green-600']) }}>
{{ $status }}
</div>
@endif

View File

@@ -0,0 +1,3 @@
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500 active:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 transition ease-in-out duration-150']) }}>
{{ $slot }}
</button>

View File

@@ -0,0 +1 @@
<a {{ $attributes->merge(['class' => 'block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out']) }}>{{ $slot }}</a>

View File

@@ -0,0 +1,35 @@
@props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white'])
@php
$alignmentClasses = match ($align) {
'left' => 'ltr:origin-top-left rtl:origin-top-right start-0',
'top' => 'origin-top',
default => 'ltr:origin-top-right rtl:origin-top-left end-0',
};
$width = match ($width) {
'48' => 'w-48',
default => $width,
};
@endphp
<div class="relative" x-data="{ open: false }" @click.outside="open = false" @close.stop="open = false">
<div @click="open = ! open">
{{ $trigger }}
</div>
<div x-show="open"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="absolute z-50 mt-2 {{ $width }} rounded-md shadow-lg {{ $alignmentClasses }}"
style="display: none;"
@click="open = false">
<div class="rounded-md ring-1 ring-black ring-opacity-5 {{ $contentClasses }}">
{{ $content }}
</div>
</div>
</div>

View File

@@ -0,0 +1,9 @@
@props(['messages'])
@if ($messages)
<ul {{ $attributes->merge(['class' => 'text-sm text-red-600 space-y-1']) }}>
@foreach ((array) $messages as $message)
<li>{{ $message }}</li>
@endforeach
</ul>
@endif

View File

@@ -0,0 +1,5 @@
@props(['value'])
<label {{ $attributes->merge(['class' => 'block font-medium text-sm text-gray-700']) }}>
{{ $value ?? $slot }}
</label>

View File

@@ -0,0 +1,78 @@
@props([
'name',
'show' => false,
'maxWidth' => '2xl'
])
@php
$maxWidth = [
'sm' => 'sm:max-w-sm',
'md' => 'sm:max-w-md',
'lg' => 'sm:max-w-lg',
'xl' => 'sm:max-w-xl',
'2xl' => 'sm:max-w-2xl',
][$maxWidth];
@endphp
<div
x-data="{
show: @js($show),
focusables() {
// All focusable element types...
let selector = 'a, button, input:not([type=\'hidden\']), textarea, select, details, [tabindex]:not([tabindex=\'-1\'])'
return [...$el.querySelectorAll(selector)]
// All non-disabled elements...
.filter(el => ! el.hasAttribute('disabled'))
},
firstFocusable() { return this.focusables()[0] },
lastFocusable() { return this.focusables().slice(-1)[0] },
nextFocusable() { return this.focusables()[this.nextFocusableIndex()] || this.firstFocusable() },
prevFocusable() { return this.focusables()[this.prevFocusableIndex()] || this.lastFocusable() },
nextFocusableIndex() { return (this.focusables().indexOf(document.activeElement) + 1) % (this.focusables().length + 1) },
prevFocusableIndex() { return Math.max(0, this.focusables().indexOf(document.activeElement)) -1 },
}"
x-init="$watch('show', value => {
if (value) {
document.body.classList.add('overflow-y-hidden');
{{ $attributes->has('focusable') ? 'setTimeout(() => firstFocusable().focus(), 100)' : '' }}
} else {
document.body.classList.remove('overflow-y-hidden');
}
})"
x-on:open-modal.window="$event.detail == '{{ $name }}' ? show = true : null"
x-on:close-modal.window="$event.detail == '{{ $name }}' ? show = false : null"
x-on:close.stop="show = false"
x-on:keydown.escape.window="show = false"
x-on:keydown.tab.prevent="$event.shiftKey || nextFocusable().focus()"
x-on:keydown.shift.tab.prevent="prevFocusable().focus()"
x-show="show"
class="fixed inset-0 overflow-y-auto px-4 py-6 sm:px-0 z-50"
style="display: {{ $show ? 'block' : 'none' }};"
>
<div
x-show="show"
class="fixed inset-0 transform transition-all"
x-on:click="show = false"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0"
x-transition:enter-end="opacity-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100"
x-transition:leave-end="opacity-0"
>
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
</div>
<div
x-show="show"
class="mb-6 bg-white rounded-lg overflow-hidden shadow-xl transform transition-all sm:w-full {{ $maxWidth }} sm:mx-auto"
x-transition:enter="ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave="ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
x-transition:leave-end="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
{{ $slot }}
</div>
</div>

View File

@@ -0,0 +1,11 @@
@props(['active'])
@php
$classes = ($active ?? false)
? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 text-sm font-medium leading-5 text-gray-900 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out'
: 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out';
@endphp
<a {{ $attributes->merge(['class' => $classes]) }}>
{{ $slot }}
</a>

View File

@@ -0,0 +1,3 @@
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 focus:bg-gray-700 active:bg-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150']) }}>
{{ $slot }}
</button>

View File

@@ -0,0 +1,11 @@
@props(['active'])
@php
$classes = ($active ?? false)
? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 text-start text-base font-medium text-indigo-700 bg-indigo-50 focus:outline-none focus:text-indigo-800 focus:bg-indigo-100 focus:border-indigo-700 transition duration-150 ease-in-out'
: 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300 transition duration-150 ease-in-out';
@endphp
<a {{ $attributes->merge(['class' => $classes]) }}>
{{ $slot }}
</a>

View File

@@ -0,0 +1,3 @@
<button {{ $attributes->merge(['type' => 'button', 'class' => 'inline-flex items-center px-4 py-2 bg-white border border-gray-300 rounded-md font-semibold text-xs text-gray-700 uppercase tracking-widest shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-25 transition ease-in-out duration-150']) }}>
{{ $slot }}
</button>

View File

@@ -0,0 +1,3 @@
@props(['disabled' => false])
<input @disabled($disabled) {{ $attributes->merge(['class' => 'border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm']) }}>

View File

@@ -0,0 +1,17 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Dashboard') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
{{ __("You're logged in!") }}
</div>
</div>
</div>
</div>
</x-app-layout>

View File

@@ -0,0 +1,87 @@
<!DOCTYPE html>
<html lang="ms">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sijil Digital {{ $certificate->program->title }}</title>
<style>
body { font-family: Arial, sans-serif; background: #f4f6f8; margin: 0; padding: 20px; color: #333; }
.container { max-width: 580px; margin: 0 auto; background: #ffffff; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
.header { background: #1a56a0; color: white; padding: 32px 40px; text-align: center; }
.header h1 { margin: 0; font-size: 20px; font-weight: 700; }
.header p { margin: 8px 0 0; font-size: 14px; opacity: 0.85; }
.body { padding: 32px 40px; }
.body p { line-height: 1.6; margin: 0 0 16px; }
.cert-box { background: #f0f7ff; border: 1px solid #c3dafe; border-radius: 6px; padding: 20px; margin: 24px 0; }
.cert-box table { width: 100%; border-collapse: collapse; }
.cert-box td { padding: 6px 0; font-size: 14px; }
.cert-box td:first-child { color: #666; width: 40%; }
.cert-box td:last-child { font-weight: 600; }
.btn { display: inline-block; background: #1a56a0; color: white; text-decoration: none; padding: 14px 32px; border-radius: 6px; font-size: 15px; font-weight: 600; margin: 8px 0; }
.footer { background: #f4f6f8; padding: 20px 40px; text-align: center; font-size: 12px; color: #888; }
.footer a { color: #1a56a0; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🏆 Sijil Digital (eCert)</h1>
<p>{{ $certificate->program->organizer ?? config('app.name') }}</p>
</div>
<div class="body">
<p>Salam sejahtera, <strong>{{ $certificate->participant->name }}</strong>,</p>
<p>
Terima kasih kerana menghadiri program di bawah. Sijil digital anda telah sedia untuk dimuat turun.
</p>
<div class="cert-box">
<table>
<tr>
<td>Program</td>
<td>{{ $certificate->program->title }}</td>
</tr>
@if($certificate->program->start_date)
<tr>
<td>Tarikh Program</td>
<td>{{ $certificate->program->start_date->format('d M Y') }}</td>
</tr>
@endif
@if($certificate->certificate_no)
<tr>
<td>No. Sijil</td>
<td>{{ $certificate->certificate_no }}</td>
</tr>
@endif
</table>
</div>
<p>Klik butang di bawah untuk memuat turun sijil anda:</p>
<div style="text-align: center; margin: 24px 0;">
<a href="{{ route('public.certificate.show', $certificate->token) }}" class="btn">
Muat Turun Sijil Saya
</a>
</div>
<p style="font-size:13px; color:#666;">
Pautan di atas adalah unik untuk anda. Sila simpan emel ini sebagai rujukan.
Jika anda menghadapi sebarang masalah, sila hubungi penganjur program.
</p>
</div>
<div class="footer">
<p>
Emel ini dihantar secara automatik oleh sistem eCert MBIP.<br>
&copy; {{ date('Y') }} {{ config('app.name') }}. Hak cipta terpelihara.
</p>
<p>
Jika anda tidak menjangkakan emel ini,
sila abaikan atau hubungi kami di
<a href="mailto:{{ config('mail.from.address') }}">{{ config('mail.from.address') }}</a>.
</p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,182 @@
<!DOCTYPE html>
<html lang="ms">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>@yield('title', 'eCert MBIP') Admin</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
@stack('styles')
</head>
<body>
<div class="d-flex">
{{-- Sidebar --}}
<nav class="sidebar d-none d-md-flex flex-column" style="min-height:100vh; width:260px; flex-shrink:0;">
<div class="sidebar-brand">
<h5><i class="bi bi-award-fill me-2"></i>eCert MBIP</h5>
<small>Sistem Pengurusan Sijil Digital</small>
</div>
<ul class="nav flex-column mt-2 px-1 flex-grow-1">
<li class="nav-item">
<a href="{{ route('admin.dashboard') }}"
class="nav-link {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}">
<i class="bi bi-grid-1x2-fill"></i> Dashboard
</a>
</li>
<li class="nav-item mt-2">
<small class="text-white-50 px-3" style="font-size:.7rem; text-transform:uppercase; letter-spacing:.8px;">Program</small>
</li>
<li class="nav-item">
<a href="{{ route('admin.programs.index') }}"
class="nav-link {{ request()->routeIs('admin.programs.*') ? 'active' : '' }}">
<i class="bi bi-calendar-event-fill"></i> Senarai Program
</a>
</li>
<li class="nav-item mt-2">
<small class="text-white-50 px-3" style="font-size:.7rem; text-transform:uppercase; letter-spacing:.8px;">Soalselidik</small>
</li>
<li class="nav-item">
<a href="{{ route('admin.questionnaires.index') }}"
class="nav-link {{ request()->routeIs('admin.questionnaires.*') ? 'active' : '' }}">
<i class="bi bi-clipboard2-check-fill"></i> Set Soalselidik
</a>
</li>
@if(auth()->user()->isSuperAdmin())
<li class="nav-item mt-2">
<small class="text-white-50 px-3" style="font-size:.7rem; text-transform:uppercase; letter-spacing:.8px;">Sistem</small>
</li>
<li class="nav-item">
<a href="{{ route('admin.users.index') }}"
class="nav-link {{ request()->routeIs('admin.users.*') ? 'active' : '' }}">
<i class="bi bi-people-fill"></i> Pengurusan Pengguna
</a>
</li>
@endif
</ul>
<div class="px-3 pb-3 mt-auto">
<div class="border-top border-white border-opacity-25 pt-3">
<small class="text-white-50 d-block mb-1">{{ auth()->user()->name }}</small>
<span class="badge {{ auth()->user()->isSuperAdmin() ? 'bg-danger' : 'bg-primary' }} mb-2 d-inline-block">
{{ auth()->user()->isSuperAdmin() ? 'Super Admin' : 'Admin Program' }}
</span>
<div class="d-flex gap-2 mb-2">
<a href="{{ route('admin.profile.show') }}"
class="btn btn-sm btn-outline-light flex-grow-1 {{ request()->routeIs('admin.profile.*') ? 'active' : '' }}">
<i class="bi bi-person-gear me-1"></i> Profil
</a>
</div>
<form method="POST" action="{{ route('logout') }}">
@csrf
<button type="submit" class="btn btn-sm btn-outline-light w-100">
<i class="bi bi-box-arrow-right me-1"></i> Log Keluar
</button>
</form>
</div>
</div>
</nav>
{{-- Main Content --}}
<div class="main-content flex-grow-1">
{{-- Top Navbar (mobile) --}}
<nav class="navbar navbar-light bg-white border-bottom d-md-none px-3">
<span class="navbar-brand fw-bold text-primary mb-0">
<i class="bi bi-award-fill me-1"></i>eCert MBIP
</span>
<button class="btn btn-sm btn-outline-secondary" data-bs-toggle="offcanvas" data-bs-target="#mobileSidebar">
<i class="bi bi-list"></i>
</button>
</nav>
{{-- Page Header --}}
@if (isset($header) || View::hasSection('header'))
<div class="page-header d-flex justify-content-between align-items-center">
<div>
<h5 class="mb-0 fw-semibold">@yield('header')</h5>
@if(View::hasSection('breadcrumb'))
<nav aria-label="breadcrumb">
<ol class="breadcrumb mb-0 mt-1" style="font-size:.82rem;">
<li class="breadcrumb-item"><a href="{{ route('admin.dashboard') }}">Dashboard</a></li>
@yield('breadcrumb')
</ol>
</nav>
@endif
</div>
<div>@yield('header-actions')</div>
</div>
@endif
{{-- Flash Messages --}}
<div class="px-4 pt-3">
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="bi bi-check-circle-fill me-2"></i>{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@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>
</div>
@endif
@if(session('warning'))
<div class="alert alert-warning alert-dismissible fade show" role="alert">
<i class="bi bi-exclamation-circle-fill me-2"></i>{{ session('warning') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
</div>
{{-- Page Content --}}
<div class="p-4">
@yield('content')
</div>
</div>
</div>
{{-- Mobile Offcanvas Sidebar --}}
<div class="offcanvas offcanvas-start" tabindex="-1" id="mobileSidebar" style="width:260px; background: var(--mbip-primary);">
<div class="offcanvas-header border-bottom border-white border-opacity-25">
<h6 class="text-white mb-0"><i class="bi bi-award-fill me-2"></i>eCert MBIP</h6>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas"></button>
</div>
<div class="offcanvas-body p-0">
<ul class="nav flex-column mt-2 px-1">
<li class="nav-item">
<a href="{{ route('admin.dashboard') }}" class="nav-link {{ request()->routeIs('admin.dashboard') ? 'active' : '' }}" style="color:rgba(255,255,255,.85);">
<i class="bi bi-grid-1x2-fill me-2"></i>Dashboard
</a>
</li>
<li class="nav-item">
<a href="{{ route('admin.programs.index') }}" class="nav-link {{ request()->routeIs('admin.programs.*') ? 'active' : '' }}" style="color:rgba(255,255,255,.85);">
<i class="bi bi-calendar-event-fill me-2"></i>Senarai Program
</a>
</li>
<li class="nav-item">
<a href="{{ route('admin.questionnaires.index') }}" class="nav-link {{ request()->routeIs('admin.questionnaires.*') ? 'active' : '' }}" style="color:rgba(255,255,255,.85);">
<i class="bi bi-clipboard2-check-fill me-2"></i>Set Soalselidik
</a>
</li>
@if(auth()->user()->isSuperAdmin())
<li class="nav-item">
<a href="{{ route('admin.users.index') }}" class="nav-link {{ request()->routeIs('admin.users.*') ? 'active' : '' }}" style="color:rgba(255,255,255,.85);">
<i class="bi bi-people-fill me-2"></i>Pengurusan Pengguna
</a>
</li>
@endif
</ul>
</div>
</div>
@stack('scripts')
</body>
</html>

View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Laravel') }}</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans antialiased">
<div class="min-h-screen bg-gray-100">
@include('layouts.navigation')
<!-- Page Heading -->
@isset($header)
<header class="bg-white shadow">
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
{{ $header }}
</div>
</header>
@endisset
<!-- Page Content -->
<main>
{{ $slot }}
</main>
</div>
</body>
</html>

View File

@@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Laravel') }}</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<!-- Scripts -->
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="font-sans text-gray-900 antialiased">
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100">
<div>
<a href="/">
<x-application-logo class="w-20 h-20 fill-current text-gray-500" />
</a>
</div>
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white shadow-md overflow-hidden sm:rounded-lg">
{{ $slot }}
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,100 @@
<nav x-data="{ open: false }" class="bg-white border-b border-gray-100">
<!-- Primary Navigation Menu -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex">
<!-- Logo -->
<div class="shrink-0 flex items-center">
<a href="{{ route('dashboard') }}">
<x-application-logo class="block h-9 w-auto fill-current text-gray-800" />
</a>
</div>
<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
<x-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
{{ __('Dashboard') }}
</x-nav-link>
</div>
</div>
<!-- Settings Dropdown -->
<div class="hidden sm:flex sm:items-center sm:ms-6">
<x-dropdown align="right" width="48">
<x-slot name="trigger">
<button class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-white hover:text-gray-700 focus:outline-none transition ease-in-out duration-150">
<div>{{ Auth::user()->name }}</div>
<div class="ms-1">
<svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</div>
</button>
</x-slot>
<x-slot name="content">
<x-dropdown-link :href="route('profile.edit')">
{{ __('Profile') }}
</x-dropdown-link>
<!-- Authentication -->
<form method="POST" action="{{ route('logout') }}">
@csrf
<x-dropdown-link :href="route('logout')"
onclick="event.preventDefault();
this.closest('form').submit();">
{{ __('Log Out') }}
</x-dropdown-link>
</form>
</x-slot>
</x-dropdown>
</div>
<!-- Hamburger -->
<div class="-me-2 flex items-center sm:hidden">
<button @click="open = ! open" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition duration-150 ease-in-out">
<svg class="h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path :class="{'hidden': open, 'inline-flex': ! open }" class="inline-flex" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
<path :class="{'hidden': ! open, 'inline-flex': open }" class="hidden" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</div>
<!-- Responsive Navigation Menu -->
<div :class="{'block': open, 'hidden': ! open}" class="hidden sm:hidden">
<div class="pt-2 pb-3 space-y-1">
<x-responsive-nav-link :href="route('dashboard')" :active="request()->routeIs('dashboard')">
{{ __('Dashboard') }}
</x-responsive-nav-link>
</div>
<!-- Responsive Settings Options -->
<div class="pt-4 pb-1 border-t border-gray-200">
<div class="px-4">
<div class="font-medium text-base text-gray-800">{{ Auth::user()->name }}</div>
<div class="font-medium text-sm text-gray-500">{{ Auth::user()->email }}</div>
</div>
<div class="mt-3 space-y-1">
<x-responsive-nav-link :href="route('profile.edit')">
{{ __('Profile') }}
</x-responsive-nav-link>
<!-- Authentication -->
<form method="POST" action="{{ route('logout') }}">
@csrf
<x-responsive-nav-link :href="route('logout')"
onclick="event.preventDefault();
this.closest('form').submit();">
{{ __('Log Out') }}
</x-responsive-nav-link>
</form>
</div>
</div>
</div>
</nav>

View File

@@ -0,0 +1,63 @@
<!DOCTYPE html>
<html lang="ms">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>@yield('title', 'eCert MBIP')</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<style>
body { background: #f0f4f8; min-height: 100vh; }
.public-container { max-width: 480px; margin: 0 auto; }
</style>
@stack('styles')
</head>
<body>
{{-- Header Brand --}}
<div class="public-hero">
<div class="public-container px-3 pt-2 pb-3">
<div class="d-flex align-items-center mb-2">
<i class="bi bi-award-fill fs-4 me-2 opacity-75"></i>
<span class="fw-bold" style="font-size:.9rem; letter-spacing:.5px;">eCert MBIP</span>
</div>
@yield('hero')
</div>
</div>
{{-- Main Content --}}
<div class="public-container px-3 py-4">
{{-- Flash Messages --}}
@if(session('success'))
<div class="alert alert-success alert-dismissible fade show mb-4" role="alert">
<i class="bi bi-check-circle-fill me-2"></i>{{ session('success') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@if(session('error'))
<div class="alert alert-danger alert-dismissible fade show mb-4" 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>
</div>
@endif
@if(session('warning'))
<div class="alert alert-warning alert-dismissible fade show mb-4" role="alert">
<i class="bi bi-exclamation-circle-fill me-2"></i>{{ session('warning') }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
@endif
@yield('content')
</div>
{{-- Footer --}}
<div class="text-center py-4">
<small class="text-muted">Majlis Bandaraya Ipoh Perak &copy; {{ date('Y') }}</small>
</div>
@stack('scripts')
</body>
</html>

View File

@@ -0,0 +1,29 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Profile') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 space-y-6">
<div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
<div class="max-w-xl">
@include('profile.partials.update-profile-information-form')
</div>
</div>
<div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
<div class="max-w-xl">
@include('profile.partials.update-password-form')
</div>
</div>
<div class="p-4 sm:p-8 bg-white shadow sm:rounded-lg">
<div class="max-w-xl">
@include('profile.partials.delete-user-form')
</div>
</div>
</div>
</div>
</x-app-layout>

View File

@@ -0,0 +1,55 @@
<section class="space-y-6">
<header>
<h2 class="text-lg font-medium text-gray-900">
{{ __('Delete Account') }}
</h2>
<p class="mt-1 text-sm text-gray-600">
{{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.') }}
</p>
</header>
<x-danger-button
x-data=""
x-on:click.prevent="$dispatch('open-modal', 'confirm-user-deletion')"
>{{ __('Delete Account') }}</x-danger-button>
<x-modal name="confirm-user-deletion" :show="$errors->userDeletion->isNotEmpty()" focusable>
<form method="post" action="{{ route('profile.destroy') }}" class="p-6">
@csrf
@method('delete')
<h2 class="text-lg font-medium text-gray-900">
{{ __('Are you sure you want to delete your account?') }}
</h2>
<p class="mt-1 text-sm text-gray-600">
{{ __('Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.') }}
</p>
<div class="mt-6">
<x-input-label for="password" value="{{ __('Password') }}" class="sr-only" />
<x-text-input
id="password"
name="password"
type="password"
class="mt-1 block w-3/4"
placeholder="{{ __('Password') }}"
/>
<x-input-error :messages="$errors->userDeletion->get('password')" class="mt-2" />
</div>
<div class="mt-6 flex justify-end">
<x-secondary-button x-on:click="$dispatch('close')">
{{ __('Cancel') }}
</x-secondary-button>
<x-danger-button class="ms-3">
{{ __('Delete Account') }}
</x-danger-button>
</div>
</form>
</x-modal>
</section>

View File

@@ -0,0 +1,48 @@
<section>
<header>
<h2 class="text-lg font-medium text-gray-900">
{{ __('Update Password') }}
</h2>
<p class="mt-1 text-sm text-gray-600">
{{ __('Ensure your account is using a long, random password to stay secure.') }}
</p>
</header>
<form method="post" action="{{ route('password.update') }}" class="mt-6 space-y-6">
@csrf
@method('put')
<div>
<x-input-label for="update_password_current_password" :value="__('Current Password')" />
<x-text-input id="update_password_current_password" name="current_password" type="password" class="mt-1 block w-full" autocomplete="current-password" />
<x-input-error :messages="$errors->updatePassword->get('current_password')" class="mt-2" />
</div>
<div>
<x-input-label for="update_password_password" :value="__('New Password')" />
<x-text-input id="update_password_password" name="password" type="password" class="mt-1 block w-full" autocomplete="new-password" />
<x-input-error :messages="$errors->updatePassword->get('password')" class="mt-2" />
</div>
<div>
<x-input-label for="update_password_password_confirmation" :value="__('Confirm Password')" />
<x-text-input id="update_password_password_confirmation" name="password_confirmation" type="password" class="mt-1 block w-full" autocomplete="new-password" />
<x-input-error :messages="$errors->updatePassword->get('password_confirmation')" class="mt-2" />
</div>
<div class="flex items-center gap-4">
<x-primary-button>{{ __('Save') }}</x-primary-button>
@if (session('status') === 'password-updated')
<p
x-data="{ show: true }"
x-show="show"
x-transition
x-init="setTimeout(() => show = false, 2000)"
class="text-sm text-gray-600"
>{{ __('Saved.') }}</p>
@endif
</div>
</form>
</section>

View File

@@ -0,0 +1,64 @@
<section>
<header>
<h2 class="text-lg font-medium text-gray-900">
{{ __('Profile Information') }}
</h2>
<p class="mt-1 text-sm text-gray-600">
{{ __("Update your account's profile information and email address.") }}
</p>
</header>
<form id="send-verification" method="post" action="{{ route('verification.send') }}">
@csrf
</form>
<form method="post" action="{{ route('profile.update') }}" class="mt-6 space-y-6">
@csrf
@method('patch')
<div>
<x-input-label for="name" :value="__('Name')" />
<x-text-input id="name" name="name" type="text" class="mt-1 block w-full" :value="old('name', $user->name)" required autofocus autocomplete="name" />
<x-input-error class="mt-2" :messages="$errors->get('name')" />
</div>
<div>
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" name="email" type="email" class="mt-1 block w-full" :value="old('email', $user->email)" required autocomplete="username" />
<x-input-error class="mt-2" :messages="$errors->get('email')" />
@if ($user instanceof \Illuminate\Contracts\Auth\MustVerifyEmail && ! $user->hasVerifiedEmail())
<div>
<p class="text-sm mt-2 text-gray-800">
{{ __('Your email address is unverified.') }}
<button form="send-verification" class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
{{ __('Click here to re-send the verification email.') }}
</button>
</p>
@if (session('status') === 'verification-link-sent')
<p class="mt-2 font-medium text-sm text-green-600">
{{ __('A new verification link has been sent to your email address.') }}
</p>
@endif
</div>
@endif
</div>
<div class="flex items-center gap-4">
<x-primary-button>{{ __('Save') }}</x-primary-button>
@if (session('status') === 'profile-updated')
<p
x-data="{ show: true }"
x-show="show"
x-transition
x-init="setTimeout(() => show = false, 2000)"
class="text-sm text-gray-600"
>{{ __('Saved.') }}</p>
@endif
</div>
</form>
</section>

View File

@@ -0,0 +1,101 @@
@extends('layouts.public')
@section('title', 'Sijil Digital — ' . $program->title)
@section('hero')
<h4 class="mb-1">{{ $program->title }}</h4>
<div class="opacity-75 small">
<i class="bi bi-award me-1"></i>Sijil Digital (eCert)
</div>
@endsection
@section('content')
@if(! $certificate->isGenerated())
{{-- Not ready yet --}}
<div class="checkin-card card p-4 text-center">
<div class="rounded-circle bg-warning 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-hourglass-split text-warning" style="font-size:2rem;"></i>
</div>
<h5 class="fw-bold mb-2">Sijil Belum Sedia</h5>
<p class="text-muted small mb-3">
Sijil anda sedang disediakan. Sila semak semula sebentar atau tunggu emel dari penganjur program.
</p>
@if($certificate->status === 'failed')
<div class="alert alert-danger text-start small">
<i class="bi bi-exclamation-circle me-1"></i>
Penjanaan sijil gagal. Sila hubungi penganjur program untuk bantuan.
</div>
@endif
</div>
@else
{{-- Questionnaire gate --}}
@if($needsQuestionnaire && ! $hasAnswered)
<div class="checkin-card card p-4 text-center">
<div class="rounded-circle bg-primary 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-clipboard2-check-fill text-primary" style="font-size:2rem;"></i>
</div>
<h5 class="fw-bold mb-2">Jawab Borang Penilaian Dahulu</h5>
<p class="text-muted small mb-4">
Sebelum memuat turun sijil, anda perlu melengkapkan borang penilaian program.
</p>
@if($qrCode)
<a href="{{ route('public.questionnaire.show', [$qrCode->token, $participant->uuid]) }}"
class="btn btn-primary btn-checkin w-100">
<i class="bi bi-clipboard2 me-2"></i>Isi Borang Penilaian
</a>
@else
<div class="alert alert-warning small">
Sila dapatkan pautan borang penilaian dari penganjur program.
</div>
@endif
</div>
@else
{{-- Certificate ready to download --}}
<div class="checkin-card card p-4 text-center">
<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-award-fill text-success" style="font-size:2rem;"></i>
</div>
<h5 class="fw-bold text-success mb-1">Sijil Sedia Dimuat Turun</h5>
<p class="text-muted small mb-3">
Tahniah, <strong>{{ $participant->name }}</strong>! Sijil digital anda untuk program ini telah sedia.
</p>
<div class="bg-light rounded p-3 text-start mb-4">
<div class="row g-2">
<div class="col-5 text-muted small">Program</div>
<div class="col-7 small fw-medium">{{ $program->title }}</div>
@if($certificate->certificate_no)
<div class="col-5 text-muted small">No. Sijil</div>
<div class="col-7 small">{{ $certificate->certificate_no }}</div>
@endif
<div class="col-5 text-muted small">Tarikh Jana</div>
<div class="col-7 small">{{ $certificate->generated_at?->format('d M Y') ?? '—' }}</div>
@if($certificate->download_count > 0)
<div class="col-5 text-muted small">Dimuat Turun</div>
<div class="col-7 small">{{ $certificate->download_count }} kali</div>
@endif
</div>
</div>
<form method="POST" action="{{ route('public.certificate.download', $certificate->token) }}">
@csrf
<button type="submit" class="btn btn-success btn-checkin w-100 mb-2">
<i class="bi bi-download me-2"></i>Muat Turun Sijil (JPG)
</button>
</form>
<div class="text-muted" style="font-size:0.75rem;">
<i class="bi bi-info-circle me-1"></i>Sijil dalam format JPEG. Boleh dimuat turun berulang kali.
</div>
</div>
@endif
@endif
@endsection

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,35 @@
@extends('layouts.public')
@section('title', 'Sudah Dihantar — ' . $program->title)
@section('hero')
<h4 class="mb-1">{{ $program->title }}</h4>
<div class="opacity-75 small">
<i class="bi bi-clipboard2-check me-1"></i>Borang Penilaian
</div>
@endsection
@section('content')
<div class="checkin-card card p-4 text-center">
<div class="rounded-circle bg-info 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-info-circle-fill text-info" style="font-size:2rem;"></i>
</div>
<h5 class="fw-bold mb-2">Sudah Dihantar</h5>
<p class="text-muted small mb-4">
Maklum balas anda untuk program ini sudah pernah dihantar sebelum ini.
Setiap peserta hanya boleh menghantar satu maklum balas.
</p>
<div class="alert alert-info text-start small mb-4">
<i class="bi bi-award me-2"></i>
Sijil Digital (eCert) akan dihantar ke emel anda setelah program tamat.
</div>
<a href="{{ route('public.semak.show', $qrCode->token) }}" class="btn btn-outline-primary btn-sm">
<i class="bi bi-search me-1"></i> Semak Status Sijil
</a>
</div>
@endsection

View File

@@ -0,0 +1,142 @@
@extends('layouts.public')
@section('title', 'Borang Penilaian — ' . $program->title)
@section('hero')
<h4 class="mb-1">{{ $program->title }}</h4>
<div class="opacity-75 small">
<i class="bi bi-clipboard2-check me-1"></i>Borang Penilaian Program
</div>
@endsection
@section('content')
<div class="checkin-card card p-4 mb-3">
<div class="text-center mb-4">
<div class="rounded-circle bg-primary 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-clipboard2-check-fill text-primary" style="font-size:2rem;"></i>
</div>
<h5 class="fw-bold mb-1">{{ $pq->questionnaireSet->title }}</h5>
<p class="text-muted small mb-0">
Sila jawab semua soalan sebelum memuat turun sijil anda, {{ $participant->name }}.
</p>
</div>
<form method="POST" action="{{ route('public.questionnaire.submit', [$qrCode->token, $participant->uuid]) }}">
@csrf
@php $qNum = 0; @endphp
@foreach($questions as $q)
@if($q->question_type === 'tajuk')
{{-- ── Section header ─────────────────────────────── --}}
<div class="d-flex align-items-center gap-2 mt-4 mb-3 pb-1 border-bottom">
<span class="fw-bold text-primary">{{ $q->question_text }}</span>
</div>
@foreach($q->children as $child)
@php $qNum++ @endphp
<div class="mb-4">
<label class="form-label fw-medium">
{{ $qNum }}. {{ $child->question_text }}
@if($child->is_required)<span class="text-danger">*</span>@endif
</label>
@error('q_' . $child->id)
<div class="text-danger small mb-1"><i class="bi bi-exclamation-circle me-1"></i>{{ $message }}</div>
@enderror
<div class="d-flex gap-3 flex-wrap">
@for($i = 1; $i <= 5; $i++)
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio"
name="q_{{ $child->id }}" id="q{{ $child->id }}_{{ $i }}"
value="{{ $i }}" {{ old('q_'.$child->id) == $i ? 'checked' : '' }}>
<label class="form-check-label" for="q{{ $child->id }}_{{ $i }}">
{{ $i }}
@php $label = $q->rating_labels[$i] ?? $q->rating_labels[strval($i)] ?? ''; @endphp
@if($label)
<small class="text-muted d-block" style="font-size:.7rem;">({{ $label }})</small>
@endif
</label>
</div>
@endfor
</div>
</div>
@endforeach
@else
{{-- ── Standalone question ─────────────────────────── --}}
@php $qNum++ @endphp
<div class="mb-4">
<label class="form-label fw-medium">
{{ $qNum }}. {{ $q->question_text }}
@if($q->is_required)<span class="text-danger">*</span>@endif
</label>
@error('q_' . $q->id)
<div class="text-danger small mb-1"><i class="bi bi-exclamation-circle me-1"></i>{{ $message }}</div>
@enderror
@if($q->question_type === 'rating')
<div class="d-flex gap-3 flex-wrap">
@for($i = 1; $i <= 5; $i++)
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio"
name="q_{{ $q->id }}" id="q{{ $q->id }}_{{ $i }}"
value="{{ $i }}" {{ old('q_'.$q->id) == $i ? 'checked' : '' }}>
<label class="form-check-label" for="q{{ $q->id }}_{{ $i }}">
{{ $i }}
@if($i === 1)<small class="text-muted">(Sangat Tidak Setuju)</small>
@elseif($i === 5)<small class="text-muted">(Sangat Setuju)</small>
@endif
</label>
</div>
@endfor
</div>
@elseif($q->question_type === 'single_choice')
@foreach($q->options_json ?? [] as $opt)
<div class="form-check">
<input class="form-check-input" type="radio"
name="q_{{ $q->id }}" id="q{{ $q->id }}_{{ $loop->index }}"
value="{{ $opt }}" {{ old('q_'.$q->id) === $opt ? 'checked' : '' }}>
<label class="form-check-label" for="q{{ $q->id }}_{{ $loop->index }}">{{ $opt }}</label>
</div>
@endforeach
@elseif($q->question_type === 'multiple_choice')
@foreach($q->options_json ?? [] as $opt)
<div class="form-check">
<input class="form-check-input" type="checkbox"
name="q_{{ $q->id }}[]" id="q{{ $q->id }}_{{ $loop->index }}"
value="{{ $opt }}"
{{ in_array($opt, (array)(old('q_'.$q->id) ?? [])) ? 'checked' : '' }}>
<label class="form-check-label" for="q{{ $q->id }}_{{ $loop->index }}">{{ $opt }}</label>
</div>
@endforeach
@elseif($q->question_type === 'short_text')
<input type="text" name="q_{{ $q->id }}"
class="form-control @error('q_'.$q->id) is-invalid @enderror"
value="{{ old('q_'.$q->id) }}" placeholder="Jawapan anda...">
@elseif($q->question_type === 'long_text')
<textarea name="q_{{ $q->id }}" rows="4"
class="form-control @error('q_'.$q->id) is-invalid @enderror"
placeholder="Jawapan anda...">{{ old('q_'.$q->id) }}</textarea>
@endif
</div>
@endif
@endforeach
<button type="submit" class="btn btn-primary w-100 btn-checkin">
<i class="bi bi-send me-2"></i>Hantar Maklum Balas
</button>
</form>
</div>
@endsection

View File

@@ -0,0 +1,41 @@
@extends('layouts.public')
@section('title', 'Terima Kasih — ' . $program->title)
@section('hero')
<h4 class="mb-1">{{ $program->title }}</h4>
<div class="opacity-75 small">
<i class="bi bi-clipboard2-check me-1"></i>Maklum Balas Diterima
</div>
@endsection
@section('content')
<div class="checkin-card card p-4 text-center">
<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-heart-fill text-success" style="font-size:2rem;"></i>
</div>
<h5 class="fw-bold text-success mb-2">Terima Kasih!</h5>
<p class="text-muted small mb-4">
Maklum balas anda telah berjaya dihantar, <strong>{{ $participant->name }}</strong>.
Kami menghargai pandangan anda.
</p>
<div class="alert alert-info text-start small mb-4">
<i class="bi bi-award me-2"></i>
<strong>Sijil Digital (eCert)</strong> anda akan dihantar ke emel
@if($participant->email)
<strong>{{ $participant->email }}</strong>
@else
yang didaftarkan
@endif
setelah program tamat.
</div>
<a href="{{ route('public.semak.show', $qrCode->token) }}" class="btn btn-outline-primary btn-sm">
<i class="bi bi-search me-1"></i> Semak Status Sijil
</a>
</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

File diff suppressed because one or more lines are too long