chore: initial Laravel 13 project setup for eCert MBIP
- Laravel 13.9 + PHP 8.5 + MySQL - Bootstrap 5.3 + jQuery 3.7 + Chart.js (replacing Alpine/Tailwind) - Packages: intervention/image, dompdf, simple-qrcode, league/csv, laravel/breeze, laravel/boost - 17 database migrations: users, programs, qr_codes, participants, attendances, certificates, questionnaires, email_logs, audit_logs - 13 Eloquent models with full relationships - Admin layout (Bootstrap 5 sidebar) + public layout (mobile-first) - Rate limiters: checkin (60/min), certificate (30/min) - Admin seeder: admin@mbip.gov.my - Storage directories + symlink configured Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
201
docs/architecture.md
Normal file
201
docs/architecture.md
Normal file
@@ -0,0 +1,201 @@
|
||||
# eCert MBIP — System Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
Sistem pengurusan sijil digital (eCert) untuk program-program yang dianjurkan oleh MBIP (Majlis Bandaraya Ipoh Perak). Sistem membolehkan admin urus program, kehadiran peserta, soalselidik, dan penjanaan sijil digital secara automatik.
|
||||
|
||||
---
|
||||
|
||||
## Technology Stack
|
||||
|
||||
| Komponen | Pilihan | Versi |
|
||||
|----------|---------|-------|
|
||||
| Backend Framework | Laravel | 12.x (latest stable) |
|
||||
| PHP | PHP | 8.5.x |
|
||||
| Database | MySQL | 8.x |
|
||||
| Frontend CSS | Bootstrap | 5.3 |
|
||||
| Frontend JS | jQuery | 3.7 |
|
||||
| Template Engine | Blade | (built-in) |
|
||||
| Asset Pipeline | Vite | (built-in Laravel) |
|
||||
| Queue Driver | Database (→ Redis production) | — |
|
||||
| Storage | Laravel Storage (local disk) | — |
|
||||
|
||||
---
|
||||
|
||||
## Composer Packages (Cadangan)
|
||||
|
||||
### Core / Wajib
|
||||
|
||||
| Package | Tujuan | Sebab Pilih |
|
||||
|---------|--------|-------------|
|
||||
| `intervention/image:^3.0` | Image manipulation untuk generate sijil | GD sudah ada, sokongan penuh Malay font TTF |
|
||||
| `barryvdh/laravel-dompdf:^3.0` | Generate PDF sijil dari HTML | Standard Laravel, senang maintain |
|
||||
| `simplesoftwareio/simple-qrcode:^4.0` | Generate QR Code PNG | Wrapper BaconQRCode, Laravel-friendly |
|
||||
| `league/csv:^9.0` | Import CSV peserta | Ringan, tanpa dependency besar, handle UTF-8 BOM |
|
||||
| `laravel/breeze:^2.0` | Admin auth scaffolding | Minimal, Blade stack, senang customise |
|
||||
|
||||
### Optional (Cadang Fasa Lanjut)
|
||||
|
||||
| Package | Tujuan | Bila Perlukan |
|
||||
|---------|--------|---------------|
|
||||
| `maatwebsite/excel:^3.1` | Import Excel (.xlsx) | Jika perlu format Excel (bukan CSV) |
|
||||
| `spatie/laravel-activitylog:^4.0` | Audit log | Jika audit trail perlu lebih structured |
|
||||
|
||||
### Kenapa Tidak Pakai
|
||||
|
||||
- **Imagick**: Tidak ada sebagai PHP extension dalam environment ini — guna GD sahaja.
|
||||
- **Laravel Sanctum/Passport**: Tidak perlu — public peserta akses via token, bukan API JWT.
|
||||
- **Livewire/Vue/React**: Tidak dalam requirement — kekal Blade + jQuery.
|
||||
- **Spatie Media Library**: Over-engineered untuk keperluan ini — guna Laravel Storage terus.
|
||||
|
||||
---
|
||||
|
||||
## Laravel Boost — Perlu Penjelasan
|
||||
|
||||
> **NOTA**: "Laravel Boost" tidak jelas merujuk kepada package mana. Kemungkinan:
|
||||
> 1. **Laravel Breeze** — Auth scaffolding (cadangan saya untuk admin auth)
|
||||
> 2. **Package dalaman** — Jika ada package khusus MBIP/organisasi
|
||||
> 3. **Laravel Octane** — Performance boost (tidak diperlukan untuk sistem ini)
|
||||
>
|
||||
> **Sila sahkan** apa yang dimaksudkan dengan "Laravel Boost" sebelum Fasa 1 dimulakan.
|
||||
|
||||
---
|
||||
|
||||
## Application Architecture
|
||||
|
||||
```
|
||||
eCert MBIP
|
||||
├── Admin Module (authenticated, /admin/*)
|
||||
│ ├── Program Management
|
||||
│ ├── QR Code Management
|
||||
│ ├── Participant Management (pre-register + import)
|
||||
│ ├── Certificate Template Management
|
||||
│ ├── Questionnaire Management
|
||||
│ └── Statistics & Reports
|
||||
│
|
||||
├── Public Module (token-based, /p/* dan /certificate/*)
|
||||
│ ├── QR Scan → Check-in Page
|
||||
│ ├── Staff Check-in Flow
|
||||
│ ├── Walk-in Registration Flow
|
||||
│ ├── Questionnaire Page
|
||||
│ └── Certificate Download
|
||||
│
|
||||
└── Background Jobs (Queue)
|
||||
├── GenerateCertificateJob
|
||||
├── SendCertificateEmailJob
|
||||
└── BlastCertificateLinkJob
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Design Decisions
|
||||
|
||||
### 1. Token-Based Public Access
|
||||
- Peserta awam tidak login.
|
||||
- Semua akses public guna `token` atau Laravel Signed URL.
|
||||
- QR code bawa ke `/p/{qr_token}` — token UUID 64 char, bukan ID.
|
||||
- Certificate download guna `/certificate/{cert_token}` — UUID unik per sijil.
|
||||
- Questionnaire link guna `participant.uuid` dalam signed URL.
|
||||
|
||||
### 2. Participant Identity
|
||||
- `no_kp` adalah pengecam unik peserta dalam sistem.
|
||||
- Jika peserta hadir program berbeza, rekod `participants` dikongsi (cari by `no_kp`).
|
||||
- Satu peserta boleh ada banyak `program_participants` dan `attendances`.
|
||||
|
||||
### 3. Certificate Generation Strategy
|
||||
- **On-demand**: Sijil dijana semasa peserta minta download buat pertama kali.
|
||||
- Flow: Soalselidik selesai → dispatch `GenerateCertificateJob` → simpan PNG → wrap PDF.
|
||||
- Jika `certificate.file_path` sudah ada, terus serve — tidak jana semula.
|
||||
- Gagal generate: simpan `status = failed`, log error ringkas.
|
||||
|
||||
### 4. Queue Strategy
|
||||
- Development: `QUEUE_CONNECTION=database` (simpan dalam `jobs` table).
|
||||
- Production: cadang `QUEUE_CONNECTION=redis` (perlu Redis server).
|
||||
- Queue worker perlu jalan sebagai service (Supervisor di Linux / Task Scheduler di Windows).
|
||||
|
||||
### 5. File Storage
|
||||
- Semua fail dalam `storage/app/private/` — tidak boleh akses direct.
|
||||
- Download melalui controller dengan `Storage::download()`.
|
||||
- QR code images dalam `storage/app/public/qrcodes/` (boleh akses via symlink).
|
||||
- Certificate template dalam `storage/app/private/templates/`.
|
||||
- Generated certificates dalam `storage/app/private/certificates/{program_uuid}/`.
|
||||
|
||||
### 6. Font untuk Certificate
|
||||
- Guna `.ttf` font file dalam `storage/app/fonts/` atau `resources/fonts/`.
|
||||
- Cadang bundel **Noto Sans** (Google Fonts, open source, sokong Malay charset).
|
||||
- Font size perlu auto-scale berdasarkan panjang nama peserta.
|
||||
|
||||
### 7. Admin Auth
|
||||
- Standard Laravel auth (email + password).
|
||||
- Guna Laravel Breeze (Blade stack) untuk scaffolding.
|
||||
- Semua `/admin/*` route dilindungi middleware `auth`.
|
||||
- Satu role sahaja buat masa ini: `admin` (users table dengan is_admin flag atau role enum).
|
||||
|
||||
---
|
||||
|
||||
## Security Architecture
|
||||
|
||||
```
|
||||
Request Layer:
|
||||
- Rate limiting: /p/* (60/min), /certificate/* (30/min), /admin/* (unlimited)
|
||||
- CSRF protection: semua POST form
|
||||
- Input validation: FormRequest classes
|
||||
|
||||
Auth Layer:
|
||||
- Admin: session-based Laravel auth
|
||||
- Public: token validation + signed URL verification
|
||||
|
||||
Data Layer:
|
||||
- no_kp: TIDAK dalam URL — guna UUID/token sahaja
|
||||
- Sensitive files: dalam storage/app/private (tidak boleh direct access)
|
||||
- Database: prepared statements (Eloquent ORM)
|
||||
|
||||
Audit Layer:
|
||||
- audit_logs table: admin actions (create/update program, upload template, dll)
|
||||
- email_logs table: semua email attempt
|
||||
- certificates table: track generated_at, emailed_at, downloaded_at
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Directory Structure (Cadangan)
|
||||
|
||||
```
|
||||
app/
|
||||
├── Http/
|
||||
│ ├── Controllers/
|
||||
│ │ ├── Admin/ ← semua admin controller
|
||||
│ │ └── Public/ ← semua public controller
|
||||
│ ├── Requests/ ← FormRequest validation
|
||||
│ └── Middleware/
|
||||
├── Models/
|
||||
├── Jobs/
|
||||
│ ├── GenerateCertificateJob.php
|
||||
│ └── SendCertificateEmailJob.php
|
||||
├── Mail/
|
||||
│ └── CertificateReadyMail.php
|
||||
├── Services/
|
||||
│ ├── CertificateService.php ← image manipulation logic
|
||||
│ ├── QrCodeService.php
|
||||
│ └── AttendanceService.php
|
||||
└── Imports/
|
||||
└── ParticipantImport.php ← CSV import logic
|
||||
|
||||
resources/
|
||||
├── views/
|
||||
│ ├── admin/ ← semua admin views
|
||||
│ ├── public/ ← public check-in, questionnaire, download
|
||||
│ ├── emails/ ← email templates
|
||||
│ └── layouts/
|
||||
│ ├── admin.blade.php
|
||||
│ └── public.blade.php
|
||||
├── fonts/ ← TTF fonts untuk certificate
|
||||
└── js/ + css/
|
||||
|
||||
storage/app/
|
||||
├── public/qrcodes/ ← QR code images (accessible via symlink)
|
||||
├── private/
|
||||
│ ├── templates/ ← certificate template images
|
||||
│ ├── certificates/ ← generated certificates
|
||||
│ └── imports/ ← temporary CSV uploads
|
||||
```
|
||||
404
docs/database-design.md
Normal file
404
docs/database-design.md
Normal file
@@ -0,0 +1,404 @@
|
||||
# eCert MBIP — Database Design (Final)
|
||||
|
||||
## Entity Relationship Summary
|
||||
|
||||
```
|
||||
users ← admin accounts only
|
||||
programs ← program data
|
||||
├── program_qr_codes ← QR token per program
|
||||
├── program_participants ← enrollment (pre-reg + walk-in)
|
||||
├── attendances ← actual check-in records
|
||||
├── certificate_templates ← uploaded template image + config
|
||||
├── certificates ← generated certificates per participant
|
||||
└── program_questionnaires ← link program ke questionnaire set
|
||||
|
||||
participants ← master data peserta (no_kp unique)
|
||||
├── program_participants
|
||||
├── attendances
|
||||
├── certificates
|
||||
└── questionnaire_responses
|
||||
|
||||
questionnaire_sets ← reusable question sets
|
||||
├── questionnaire_questions
|
||||
├── program_questionnaires
|
||||
└── questionnaire_responses
|
||||
└── questionnaire_answers
|
||||
|
||||
email_logs ← semua email attempt
|
||||
audit_logs ← admin action tracking
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Table Definitions
|
||||
|
||||
### 1. `users`
|
||||
Laravel default. Tambah kolum berikut:
|
||||
```
|
||||
is_admin boolean default true ← semua user = admin buat masa ini
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. `programs`
|
||||
|
||||
| Kolum | Type | Keterangan |
|
||||
|-------|------|-----------|
|
||||
| id | bigint PK | — |
|
||||
| uuid | char(36) UNIQUE | public identifier, pakai dalam URL |
|
||||
| title | varchar(255) | nama program |
|
||||
| description | text nullable | deskripsi program |
|
||||
| organizer | varchar(255) | penganjur |
|
||||
| location | varchar(500) | lokasi program |
|
||||
| start_date | date | tarikh mula |
|
||||
| end_date | date | tarikh tamat |
|
||||
| checkin_start_at | datetime nullable | mula boleh check-in |
|
||||
| checkin_end_at | datetime nullable | tamat check-in |
|
||||
| ecert_download_start_at | datetime nullable | mula boleh download sijil |
|
||||
| ecert_download_end_at | datetime nullable | tamat download sijil |
|
||||
| status | enum(draft,published,closed) | default: draft |
|
||||
| allow_walk_in | boolean | default: true |
|
||||
| default_staff_session | enum(pagi,petang,full_day) nullable | sesi default untuk staff |
|
||||
| default_external_session | enum(pagi,petang,full_day) nullable | sesi default untuk orang luar |
|
||||
| created_by | bigint FK users.id | — |
|
||||
| timestamps | — | created_at, updated_at |
|
||||
|
||||
Index: `status`, `uuid`
|
||||
|
||||
---
|
||||
|
||||
### 3. `program_qr_codes`
|
||||
|
||||
| Kolum | Type | Keterangan |
|
||||
|-------|------|-----------|
|
||||
| id | bigint PK | — |
|
||||
| program_id | bigint FK | — |
|
||||
| token | varchar(64) UNIQUE | random token, bukan UUID biasa |
|
||||
| qr_image_path | varchar(500) nullable | path dalam storage |
|
||||
| is_active | boolean | default: true |
|
||||
| timestamps | — | — |
|
||||
|
||||
Index: `token`, `program_id`
|
||||
|
||||
---
|
||||
|
||||
### 4. `participants`
|
||||
|
||||
| Kolum | Type | Keterangan |
|
||||
|-------|------|-----------|
|
||||
| id | bigint PK | — |
|
||||
| uuid | char(36) UNIQUE | public identifier |
|
||||
| name | varchar(255) | nama penuh |
|
||||
| no_kp | varchar(20) UNIQUE | no kad pengenalan (tanpa sempang) |
|
||||
| email | varchar(255) nullable | emel |
|
||||
| phone | varchar(20) nullable | no telefon |
|
||||
| agency | varchar(255) nullable | jabatan / agensi |
|
||||
| participant_type | enum(staff,external) | default: external |
|
||||
| timestamps | — | — |
|
||||
|
||||
Index: `no_kp`, `email`, `uuid`
|
||||
|
||||
> **Nota PDPA**: `no_kp` disimpan dalam DB tetapi TIDAK dipaparkan dalam URL. Akses public guna `uuid` atau `certificate.token`.
|
||||
|
||||
---
|
||||
|
||||
### 5. `program_participants`
|
||||
|
||||
| Kolum | Type | Keterangan |
|
||||
|-------|------|-----------|
|
||||
| id | bigint PK | — |
|
||||
| program_id | bigint FK | — |
|
||||
| participant_id | bigint FK | — |
|
||||
| registration_source | enum(pre_registered,walk_in,admin_manual,import) | — |
|
||||
| is_pre_registered | boolean | default: false |
|
||||
| pre_registered_session | enum(pagi,petang,full_day) nullable | sesi yang ditetapkan semasa pre-reg |
|
||||
| status | enum(registered,checked_in,cancelled) | default: registered |
|
||||
| registered_at | timestamp nullable | — |
|
||||
| timestamps | — | — |
|
||||
|
||||
Unique: `(program_id, participant_id)`
|
||||
Index: `program_id`, `participant_id`, `status`
|
||||
|
||||
---
|
||||
|
||||
### 6. `attendances`
|
||||
|
||||
| Kolum | Type | Keterangan |
|
||||
|-------|------|-----------|
|
||||
| id | bigint PK | — |
|
||||
| program_id | bigint FK | — |
|
||||
| participant_id | bigint FK | — |
|
||||
| program_participant_id | bigint FK nullable | link ke enrollment |
|
||||
| attendance_source | enum(pre_registered_staff,walk_in_external,admin_manual) | — |
|
||||
| attendance_session | enum(pagi,petang,full_day) | sesi hadir |
|
||||
| checked_in_at | timestamp | masa check-in |
|
||||
| checked_in_ip | varchar(45) nullable | IP address |
|
||||
| user_agent | varchar(500) nullable | browser/device |
|
||||
| notes | varchar(500) nullable | nota tambahan |
|
||||
| timestamps | — | — |
|
||||
|
||||
Unique: `(program_id, participant_id)`
|
||||
Index: `program_id`, `participant_id`, `attendance_source`, `attendance_session`
|
||||
|
||||
---
|
||||
|
||||
### 7. `certificate_templates`
|
||||
|
||||
| Kolum | Type | Keterangan |
|
||||
|-------|------|-----------|
|
||||
| id | bigint PK | — |
|
||||
| program_id | bigint FK | — |
|
||||
| original_filename | varchar(255) | nama fail asal |
|
||||
| image_path | varchar(500) | path dalam storage |
|
||||
| config_json | json | koordinat dan style teks |
|
||||
| is_active | boolean | default: true |
|
||||
| uploaded_by | bigint FK users.id | — |
|
||||
| timestamps | — | — |
|
||||
|
||||
**Contoh `config_json`:**
|
||||
```json
|
||||
{
|
||||
"fields": {
|
||||
"name": {
|
||||
"x": 1240,
|
||||
"y": 850,
|
||||
"font_size": 72,
|
||||
"font_size_min": 36,
|
||||
"font_family": "NotoSans-Bold",
|
||||
"color": "#1a1a1a",
|
||||
"align": "center",
|
||||
"max_width": 1600
|
||||
},
|
||||
"no_kp": {
|
||||
"x": 1240,
|
||||
"y": 960,
|
||||
"font_size": 48,
|
||||
"font_size_min": 36,
|
||||
"font_family": "NotoSans-Regular",
|
||||
"color": "#333333",
|
||||
"align": "center",
|
||||
"max_width": 1200
|
||||
},
|
||||
"program_title": {
|
||||
"x": 1240,
|
||||
"y": 620,
|
||||
"font_size": 52,
|
||||
"font_size_min": 28,
|
||||
"font_family": "NotoSans-Bold",
|
||||
"color": "#1a1a1a",
|
||||
"align": "center",
|
||||
"max_width": 1800,
|
||||
"enabled": false
|
||||
},
|
||||
"program_date": {
|
||||
"x": 1240,
|
||||
"y": 700,
|
||||
"font_size": 36,
|
||||
"font_family": "NotoSans-Regular",
|
||||
"color": "#555555",
|
||||
"align": "center",
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
"canvas_dpi": 150
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 8. `certificates`
|
||||
|
||||
| Kolum | Type | Keterangan |
|
||||
|-------|------|-----------|
|
||||
| id | bigint PK | — |
|
||||
| uuid | char(36) UNIQUE | — |
|
||||
| program_id | bigint FK | — |
|
||||
| participant_id | bigint FK | — |
|
||||
| certificate_template_id | bigint FK | — |
|
||||
| certificate_no | varchar(100) UNIQUE nullable | no sijil rasmi (auto-generate) |
|
||||
| file_path | varchar(500) nullable | path PDF dalam storage |
|
||||
| token | varchar(64) UNIQUE | token untuk download link |
|
||||
| status | enum(pending,generating,generated,emailed,failed) | default: pending |
|
||||
| error_message | text nullable | jika gagal |
|
||||
| generated_at | timestamp nullable | — |
|
||||
| emailed_at | timestamp nullable | — |
|
||||
| downloaded_at | timestamp nullable | tarikh pertama download |
|
||||
| download_count | int | default: 0 |
|
||||
| timestamps | — | — |
|
||||
|
||||
Unique: `(program_id, participant_id)`
|
||||
Index: `token`, `status`, `program_id`, `participant_id`
|
||||
|
||||
**Format `certificate_no`**: `MBIP/{YEAR}/{PROGRAM_ID}/{SEQUENCE}` — contoh: `MBIP/2025/001/0042`
|
||||
|
||||
---
|
||||
|
||||
### 9. `questionnaire_sets`
|
||||
|
||||
| Kolum | Type | Keterangan |
|
||||
|-------|------|-----------|
|
||||
| id | bigint PK | — |
|
||||
| title | varchar(255) | tajuk set |
|
||||
| description | text nullable | — |
|
||||
| status | enum(draft,published,archived) | default: draft |
|
||||
| created_by | bigint FK users.id | — |
|
||||
| timestamps | — | — |
|
||||
|
||||
---
|
||||
|
||||
### 10. `questionnaire_questions`
|
||||
|
||||
| Kolum | Type | Keterangan |
|
||||
|-------|------|-----------|
|
||||
| id | bigint PK | — |
|
||||
| questionnaire_set_id | bigint FK | — |
|
||||
| question_text | text | teks soalan |
|
||||
| question_type | enum(rating,single_choice,multiple_choice,short_text,long_text) | — |
|
||||
| options_json | json nullable | untuk single/multiple choice dan rating |
|
||||
| is_required | boolean | default: true |
|
||||
| sort_order | int | default: 0 |
|
||||
| timestamps | — | — |
|
||||
|
||||
**Contoh `options_json`:**
|
||||
```json
|
||||
// single_choice / multiple_choice:
|
||||
{ "options": ["Sangat Berpuas Hati", "Berpuas Hati", "Sederhana", "Tidak Berpuas Hati"] }
|
||||
|
||||
// rating:
|
||||
{ "min": 1, "max": 5, "labels": { "1": "Sangat Lemah", "5": "Cemerlang" } }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 11. `program_questionnaires`
|
||||
|
||||
| Kolum | Type | Keterangan |
|
||||
|-------|------|-----------|
|
||||
| id | bigint PK | — |
|
||||
| program_id | bigint FK | — |
|
||||
| questionnaire_set_id | bigint FK | — |
|
||||
| is_confirmed | boolean | default: false |
|
||||
| confirmed_at | timestamp nullable | — |
|
||||
| confirmed_by | bigint FK users.id nullable | — |
|
||||
| timestamps | — | — |
|
||||
|
||||
Unique: `(program_id, questionnaire_set_id)`
|
||||
|
||||
---
|
||||
|
||||
### 12. `questionnaire_responses`
|
||||
|
||||
| Kolum | Type | Keterangan |
|
||||
|-------|------|-----------|
|
||||
| id | bigint PK | — |
|
||||
| program_id | bigint FK | — |
|
||||
| participant_id | bigint FK | — |
|
||||
| questionnaire_set_id | bigint FK | — |
|
||||
| submitted_at | timestamp | — |
|
||||
| ip_address | varchar(45) nullable | — |
|
||||
| user_agent | varchar(500) nullable | — |
|
||||
| timestamps | — | — |
|
||||
|
||||
Unique: `(program_id, participant_id, questionnaire_set_id)`
|
||||
Index: `program_id`, `participant_id`
|
||||
|
||||
---
|
||||
|
||||
### 13. `questionnaire_answers`
|
||||
|
||||
| Kolum | Type | Keterangan |
|
||||
|-------|------|-----------|
|
||||
| id | bigint PK | — |
|
||||
| questionnaire_response_id | bigint FK | — |
|
||||
| questionnaire_question_id | bigint FK | — |
|
||||
| answer_value | json nullable | nilai jawapan (flexibel untuk semua jenis) |
|
||||
| timestamps | — | — |
|
||||
|
||||
**Contoh `answer_value`:**
|
||||
```json
|
||||
// rating: { "value": 4 }
|
||||
// single: { "value": "Berpuas Hati" }
|
||||
// multiple: { "value": ["A", "B"] }
|
||||
// short_text: { "value": "Teks ringkas" }
|
||||
// long_text: { "value": "Teks panjang..." }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 14. `email_logs`
|
||||
|
||||
| Kolum | Type | Keterangan |
|
||||
|-------|------|-----------|
|
||||
| id | bigint PK | — |
|
||||
| program_id | bigint FK nullable | — |
|
||||
| participant_id | bigint FK nullable | — |
|
||||
| certificate_id | bigint FK nullable | — |
|
||||
| recipient_email | varchar(255) | alamat emel penerima |
|
||||
| subject | varchar(500) | subjek emel |
|
||||
| email_type | enum(certificate_ready,reminder,test) | jenis emel |
|
||||
| status | enum(pending,sent,failed) | default: pending |
|
||||
| error_message | text nullable | ralat SMTP/queue |
|
||||
| sent_at | timestamp nullable | — |
|
||||
| timestamps | — | — |
|
||||
|
||||
Index: `status`, `program_id`, `participant_id`
|
||||
|
||||
---
|
||||
|
||||
### 15. `audit_logs`
|
||||
|
||||
| Kolum | Type | Keterangan |
|
||||
|-------|------|-----------|
|
||||
| id | bigint PK | — |
|
||||
| user_id | bigint FK nullable | admin yang buat action |
|
||||
| action | varchar(100) | nama action: program.created, template.uploaded, dll |
|
||||
| auditable_type | varchar(255) nullable | model class |
|
||||
| auditable_id | bigint nullable | ID rekod berkaitan |
|
||||
| old_values | json nullable | nilai sebelum (REDACT data sensitif) |
|
||||
| new_values | json nullable | nilai selepas (REDACT data sensitif) |
|
||||
| ip_address | varchar(45) nullable | — |
|
||||
| user_agent | varchar(500) nullable | — |
|
||||
| timestamps | — | — |
|
||||
|
||||
Index: `user_id`, `action`, `auditable_type + auditable_id`
|
||||
|
||||
**Actions yang perlu diaudit:**
|
||||
- `program.created`, `program.updated`, `program.deleted`
|
||||
- `template.uploaded`, `template.updated`
|
||||
- `questionnaire.confirmed`, `questionnaire.attached`
|
||||
- `certificate.generated`, `certificate.downloaded`
|
||||
- `participant.imported`, `participant.added`
|
||||
|
||||
---
|
||||
|
||||
## Migration Order (Dependency-safe)
|
||||
|
||||
```
|
||||
1. users (no dependency)
|
||||
2. programs (FK: users)
|
||||
3. program_qr_codes (FK: programs)
|
||||
4. participants (no dependency)
|
||||
5. program_participants (FK: programs, participants)
|
||||
6. attendances (FK: programs, participants, program_participants)
|
||||
7. certificate_templates (FK: programs, users)
|
||||
8. questionnaire_sets (FK: users)
|
||||
9. questionnaire_questions (FK: questionnaire_sets)
|
||||
10. program_questionnaires (FK: programs, questionnaire_sets, users)
|
||||
11. certificates (FK: programs, participants, certificate_templates)
|
||||
12. questionnaire_responses (FK: programs, participants, questionnaire_sets)
|
||||
13. questionnaire_answers (FK: questionnaire_responses, questionnaire_questions)
|
||||
14. email_logs (FK: programs, participants, certificates)
|
||||
15. audit_logs (FK: users)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Integrity Rules
|
||||
|
||||
1. `participants.no_kp` — UNIQUE global (satu orang = satu rekod).
|
||||
2. `attendances (program_id, participant_id)` — UNIQUE (tidak boleh check-in dua kali program sama).
|
||||
3. `certificates (program_id, participant_id)` — UNIQUE (satu sijil per peserta per program).
|
||||
4. `questionnaire_responses (program_id, participant_id, questionnaire_set_id)` — UNIQUE.
|
||||
5. `program_participants (program_id, participant_id)` — UNIQUE.
|
||||
6. Jika program `status = closed` atau `published`, rekod tidak boleh padam (soft delete sahaja jika perlu).
|
||||
7. `no_kp` dalam participants tidak dipaparkan dalam URL atau log — pakai `uuid` atau `token`.
|
||||
434
docs/execution-plan.md
Normal file
434
docs/execution-plan.md
Normal file
@@ -0,0 +1,434 @@
|
||||
# eCert MBIP — Execution Plan (Phased)
|
||||
|
||||
## Pre-requisites (Sebelum Fasa 1)
|
||||
|
||||
- [ ] Sahkan "Laravel Boost" — package apa yang dimaksudkan
|
||||
- [ ] Sahkan MySQL server credentials (host, port, db name, user, password)
|
||||
- [ ] Sahkan SMTP settings untuk email
|
||||
- [ ] Sahkan storage path boleh write
|
||||
- [ ] Sahkan Git SSH key berfungsi ke git.mbip.my
|
||||
|
||||
---
|
||||
|
||||
## Fasa 1: Foundation & Authentication
|
||||
**Anggaran: 1-2 jam**
|
||||
|
||||
### Tasks
|
||||
- [ ] Install Laravel 12 via `composer create-project`
|
||||
- [ ] Setup `.env` (DB, queue, mail, app URL)
|
||||
- [ ] Install Laravel Breeze (Blade stack)
|
||||
- [ ] Install Composer packages (intervention/image, simple-qrcode, dompdf, league/csv)
|
||||
- [ ] Buat semua 15 migrations
|
||||
- [ ] Buat admin layout Blade (sidebar, navbar) dengan Bootstrap 5
|
||||
- [ ] Buat public layout Blade (mobile-first) dengan Bootstrap 5
|
||||
- [ ] Setup Queue (database driver → create `jobs` table)
|
||||
- [ ] Setup Storage symlink
|
||||
- [ ] Buat Seeder: admin user
|
||||
- [ ] Commit: `chore: initial Laravel project setup`
|
||||
- [ ] Commit: `feat: admin authentication and layout`
|
||||
|
||||
### Files Utama
|
||||
```
|
||||
app/Http/Controllers/Admin/DashboardController.php
|
||||
resources/views/layouts/admin.blade.php
|
||||
resources/views/layouts/public.blade.php
|
||||
resources/views/admin/dashboard.blade.php
|
||||
database/migrations/* (15 migration files)
|
||||
database/seeders/AdminSeeder.php
|
||||
```
|
||||
|
||||
### Commands Selepas Fasa 1
|
||||
```bash
|
||||
php artisan migrate
|
||||
php artisan db:seed --class=AdminSeeder
|
||||
php artisan storage:link
|
||||
php artisan queue:table && php artisan migrate
|
||||
```
|
||||
|
||||
### Manual Test
|
||||
- [ ] Login admin berjaya
|
||||
- [ ] Dashboard papar (walaupun kosong)
|
||||
- [ ] Logout berjaya
|
||||
|
||||
---
|
||||
|
||||
## Fasa 2: Program Management
|
||||
**Anggaran: 2-3 jam**
|
||||
|
||||
### Tasks
|
||||
- [ ] `ProgramController` — CRUD penuh
|
||||
- [ ] `StoreProgramRequest` + `UpdateProgramRequest` — validation
|
||||
- [ ] Program index view (table + badge status)
|
||||
- [ ] Program create/edit form
|
||||
- [ ] Program show view (tab: details, participants, qr, template, questionnaire, stats)
|
||||
- [ ] Status management (draft → published → closed)
|
||||
- [ ] Protect delete jika ada kehadiran
|
||||
- [ ] Commit: `feat: program management`
|
||||
|
||||
### Files Utama
|
||||
```
|
||||
app/Http/Controllers/Admin/ProgramController.php
|
||||
app/Http/Requests/Admin/StoreProgramRequest.php
|
||||
app/Http/Requests/Admin/UpdateProgramRequest.php
|
||||
app/Models/Program.php
|
||||
resources/views/admin/programs/*
|
||||
```
|
||||
|
||||
### Manual Test
|
||||
- [ ] Tambah program
|
||||
- [ ] Edit program
|
||||
- [ ] Tukar status
|
||||
- [ ] Cuba padam program yang ada kehadiran (mesti gagal)
|
||||
|
||||
---
|
||||
|
||||
## Fasa 3: QR Code Generation
|
||||
**Anggaran: 1-2 jam**
|
||||
|
||||
### Tasks
|
||||
- [ ] `QrCodeController` — generate, show, download
|
||||
- [ ] `QrCodeService` — generate token, buat QR image, simpan storage
|
||||
- [ ] QR code preview dalam admin
|
||||
- [ ] Download QR sebagai PNG
|
||||
- [ ] Commit: `feat: qr code generation`
|
||||
|
||||
### Files Utama
|
||||
```
|
||||
app/Http/Controllers/Admin/QrCodeController.php
|
||||
app/Services/QrCodeService.php
|
||||
app/Models/ProgramQrCode.php
|
||||
resources/views/admin/programs/qr.blade.php
|
||||
```
|
||||
|
||||
### Manual Test
|
||||
- [ ] Generate QR code untuk program
|
||||
- [ ] QR code papar dalam admin
|
||||
- [ ] Download QR sebagai PNG berjaya
|
||||
- [ ] Scan QR code → bawa ke /p/{token} (walaupun page belum siap)
|
||||
|
||||
---
|
||||
|
||||
## Fasa 4: Participant Management & CSV Import
|
||||
**Anggaran: 2-3 jam**
|
||||
|
||||
### Tasks
|
||||
- [ ] `ParticipantController` — add manual, import, list, export
|
||||
- [ ] `ParticipantImport` service — parse CSV, validate, bulk insert
|
||||
- [ ] Import summary: berjaya, duplicate, gagal
|
||||
- [ ] Export CSV senarai peserta
|
||||
- [ ] Commit: `feat: participant management`
|
||||
|
||||
### Files Utama
|
||||
```
|
||||
app/Http/Controllers/Admin/ParticipantController.php
|
||||
app/Services/ParticipantImportService.php
|
||||
app/Models/Participant.php
|
||||
app/Models/ProgramParticipant.php
|
||||
resources/views/admin/programs/participants/*
|
||||
```
|
||||
|
||||
### CSV Template Headers
|
||||
```
|
||||
name,no_kp,email,phone,agency
|
||||
```
|
||||
|
||||
### Manual Test
|
||||
- [ ] Tambah peserta manual
|
||||
- [ ] Import CSV (normal)
|
||||
- [ ] Import CSV dengan duplicate (summary papar betul)
|
||||
- [ ] Import CSV dengan row kosong/invalid
|
||||
- [ ] Export CSV peserta
|
||||
|
||||
---
|
||||
|
||||
## Fasa 5: Public Check-in Flow
|
||||
**Anggaran: 3-4 jam**
|
||||
|
||||
### Tasks
|
||||
- [ ] `CheckinController` — show, staffCheckin, externalRegister
|
||||
- [ ] `AttendanceService` — rekod kehadiran, cegah duplicate
|
||||
- [ ] Public check-in page (mobile-first, Bootstrap 5)
|
||||
- [ ] Staff check-in form + validation
|
||||
- [ ] Walk-in registration form + validation
|
||||
- [ ] Status page selepas check-in
|
||||
- [ ] Rate limiting pada routes
|
||||
- [ ] Commit: `feat: participant registration and attendance`
|
||||
|
||||
### Files Utama
|
||||
```
|
||||
app/Http/Controllers/Public/CheckinController.php
|
||||
app/Services/AttendanceService.php
|
||||
resources/views/public/checkin/*
|
||||
```
|
||||
|
||||
### Manual Test
|
||||
- [ ] Scan QR → buka page check-in
|
||||
- [ ] Staff check-in dengan no_kp betul
|
||||
- [ ] Staff check-in dengan no_kp salah
|
||||
- [ ] Staff check-in yang sudah hadir (mesej duplicate)
|
||||
- [ ] Walk-in daftar baru
|
||||
- [ ] Walk-in dengan no_kp sama → error duplicate
|
||||
- [ ] Test rate limit (cuba submit banyak kali)
|
||||
|
||||
---
|
||||
|
||||
## Fasa 6: Questionnaire Management
|
||||
**Anggaran: 3-4 jam**
|
||||
|
||||
### Tasks
|
||||
- [ ] `QuestionnaireSetController` — CRUD
|
||||
- [ ] `QuestionController` — CRUD, reorder
|
||||
- [ ] `ProgramQuestionnaireController` — attach, confirm, detach
|
||||
- [ ] Public questionnaire form (semua jenis soalan)
|
||||
- [ ] Submit response + answers
|
||||
- [ ] Semak sudah jawab (cegah double submit)
|
||||
- [ ] Commit: `feat: questionnaire management`
|
||||
|
||||
### Files Utama
|
||||
```
|
||||
app/Http/Controllers/Admin/QuestionnaireSetController.php
|
||||
app/Http/Controllers/Admin/QuestionController.php
|
||||
app/Http/Controllers/Admin/ProgramQuestionnaireController.php
|
||||
app/Http/Controllers/Public/QuestionnaireController.php
|
||||
app/Models/QuestionnaireSet.php
|
||||
app/Models/QuestionnaireQuestion.php
|
||||
app/Models/QuestionnaireResponse.php
|
||||
app/Models/QuestionnaireAnswer.php
|
||||
resources/views/admin/questionnaires/*
|
||||
resources/views/public/questionnaire/*
|
||||
```
|
||||
|
||||
### Manual Test
|
||||
- [ ] Cipta questionnaire set
|
||||
- [ ] Tambah pelbagai jenis soalan
|
||||
- [ ] Attach questionnaire ke program
|
||||
- [ ] Confirm questionnaire
|
||||
- [ ] Peserta boleh akses questionnaire (selepas check-in)
|
||||
- [ ] Submit soalselidik berjaya
|
||||
- [ ] Cuba submit semula → error (sudah jawab)
|
||||
|
||||
---
|
||||
|
||||
## Fasa 7: Certificate Template & Generation
|
||||
**Anggaran: 4-5 jam**
|
||||
|
||||
### Tasks
|
||||
- [ ] `CertificateTemplateController` — upload, config, preview, test generate
|
||||
- [ ] `CertificateService` — image overlay (Intervention Image), PDF wrap (DOMPDF)
|
||||
- [ ] Font auto-scale berdasarkan panjang nama
|
||||
- [ ] Preview template dalam admin
|
||||
- [ ] Test generate dengan nama sample
|
||||
- [ ] `GenerateCertificateJob` — queue job
|
||||
- [ ] `AttendanceCheckController` — public semak kehadiran
|
||||
- [ ] `CertificateController` — public, show, download gate
|
||||
- [ ] Certificate gate: soalselidik dijawab + masa download aktif
|
||||
- [ ] Rekod downloaded_at dan download_count
|
||||
- [ ] Commit: `feat: certificate template upload`
|
||||
- [ ] Commit: `feat: certificate generation and download`
|
||||
|
||||
### Files Utama
|
||||
```
|
||||
app/Http/Controllers/Admin/CertificateTemplateController.php
|
||||
app/Http/Controllers/Public/CertificateController.php
|
||||
app/Http/Controllers/Public/AttendanceCheckController.php
|
||||
app/Services/CertificateService.php
|
||||
app/Jobs/GenerateCertificateJob.php
|
||||
app/Models/Certificate.php
|
||||
app/Models/CertificateTemplate.php
|
||||
resources/views/admin/programs/template/*
|
||||
resources/views/public/certificate/*
|
||||
resources/fonts/ ← bundel NotoSans TTF
|
||||
```
|
||||
|
||||
### Manual Test
|
||||
- [ ] Upload template imej
|
||||
- [ ] Set koordinat nama + no_kp
|
||||
- [ ] Preview template
|
||||
- [ ] Test generate → papar hasil
|
||||
- [ ] Peserta scan QR (masa download) → semak kehadiran
|
||||
- [ ] Peserta download sijil (selepas soalselidik)
|
||||
- [ ] Peserta cuba download sebelum jawab soalselidik → redirect
|
||||
|
||||
---
|
||||
|
||||
## Fasa 8: Email & Queue
|
||||
**Anggaran: 2-3 jam**
|
||||
|
||||
### Tasks
|
||||
- [ ] `CertificateReadyMail` — Mailable class
|
||||
- [ ] Email template Blade (HTML + text)
|
||||
- [ ] `SendCertificateEmailJob` — queue job
|
||||
- [ ] `BlastCertificateLinkJob` — mass email untuk semua peserta hadir
|
||||
- [ ] Admin trigger: "Email semua peserta" button
|
||||
- [ ] `email_logs` rekod semua attempt
|
||||
- [ ] Commit: `feat: email certificate link`
|
||||
|
||||
### Files Utama
|
||||
```
|
||||
app/Mail/CertificateReadyMail.php
|
||||
app/Jobs/SendCertificateEmailJob.php
|
||||
app/Jobs/BlastCertificateLinkJob.php
|
||||
resources/views/emails/certificate-ready.blade.php
|
||||
resources/views/emails/certificate-ready.text.blade.php
|
||||
```
|
||||
|
||||
### Manual Test
|
||||
- [ ] Trigger email untuk satu peserta
|
||||
- [ ] Email diterima dengan link betul
|
||||
- [ ] Link dalam email bawa ke certificate page
|
||||
- [ ] `email_logs` rekod status sent
|
||||
- [ ] Test email SMTP failure → status failed dalam email_logs
|
||||
|
||||
---
|
||||
|
||||
## Fasa 9: Statistics Dashboard
|
||||
**Anggaran: 2-3 jam**
|
||||
|
||||
### Tasks
|
||||
- [ ] `StatisticsController` — dashboard + per-program
|
||||
- [ ] Dashboard cards (counts)
|
||||
- [ ] Per-program breakdown (attendance by type, session, soalselidik)
|
||||
- [ ] Chart.js integration (rating chart, attendance trend)
|
||||
- [ ] Export CSV statistik
|
||||
- [ ] Commit: `feat: admin statistics dashboard`
|
||||
|
||||
### Files Utama
|
||||
```
|
||||
app/Http/Controllers/Admin/StatisticsController.php
|
||||
app/Http/Controllers/Admin/DashboardController.php (update)
|
||||
resources/views/admin/dashboard.blade.php (update)
|
||||
resources/views/admin/programs/statistics.blade.php
|
||||
```
|
||||
|
||||
### Manual Test
|
||||
- [ ] Dashboard papar kiraan betul
|
||||
- [ ] Per-program statistik betul
|
||||
- [ ] Chart.js render tanpa error
|
||||
- [ ] Export CSV berjaya
|
||||
|
||||
---
|
||||
|
||||
## Fasa 10: Security Hardening & Audit Log
|
||||
**Anggaran: 1-2 jam**
|
||||
|
||||
### Tasks
|
||||
- [ ] `AuditLogService` — log admin actions
|
||||
- [ ] Hook audit log ke key events
|
||||
- [ ] Review semua route ada proper validation
|
||||
- [ ] Semak file upload hanya benarkan jpg/png
|
||||
- [ ] Semak no_kp tidak expose dalam URL
|
||||
- [ ] Rate limiting final review
|
||||
- [ ] Commit: `feat: audit logging and security hardening`
|
||||
|
||||
---
|
||||
|
||||
## Fasa 11: Testing
|
||||
**Anggaran: 3-4 jam**
|
||||
|
||||
### Tasks
|
||||
- [ ] Setup Pest
|
||||
- [ ] Admin can create program
|
||||
- [ ] QR token valid opens check-in page
|
||||
- [ ] Staff pre-registered can check-in
|
||||
- [ ] External participant can register and check-in
|
||||
- [ ] Duplicate no_kp for same program rejected
|
||||
- [ ] Participant cannot download certificate before questionnaire
|
||||
- [ ] Participant can download certificate after questionnaire
|
||||
- [ ] Admin can view basic statistics
|
||||
- [ ] Commit: `test: core attendance and certificate flow`
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables Checklist
|
||||
|
||||
```env
|
||||
APP_NAME="eCert MBIP"
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=ecert_mbip
|
||||
DB_USERNAME=root
|
||||
DB_PASSWORD=
|
||||
|
||||
QUEUE_CONNECTION=database
|
||||
|
||||
MAIL_MAILER=smtp
|
||||
MAIL_HOST=
|
||||
MAIL_PORT=587
|
||||
MAIL_USERNAME=
|
||||
MAIL_PASSWORD=
|
||||
MAIL_ENCRYPTION=tls
|
||||
MAIL_FROM_ADDRESS=noreply@mbip.gov.my
|
||||
MAIL_FROM_NAME="eCert MBIP"
|
||||
|
||||
FILESYSTEM_DISK=local
|
||||
|
||||
SESSION_DRIVER=database
|
||||
SESSION_LIFETIME=120
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Git Commit Sequence
|
||||
|
||||
```bash
|
||||
# Fasa 1
|
||||
git commit -m "chore: initial Laravel project setup"
|
||||
git commit -m "feat: admin authentication and layout"
|
||||
|
||||
# Fasa 2
|
||||
git commit -m "feat: program management"
|
||||
|
||||
# Fasa 3
|
||||
git commit -m "feat: qr code generation"
|
||||
|
||||
# Fasa 4
|
||||
git commit -m "feat: participant management and csv import"
|
||||
|
||||
# Fasa 5
|
||||
git commit -m "feat: public check-in flow and attendance"
|
||||
|
||||
# Fasa 6
|
||||
git commit -m "feat: questionnaire management"
|
||||
|
||||
# Fasa 7
|
||||
git commit -m "feat: certificate template upload"
|
||||
git commit -m "feat: certificate generation and download"
|
||||
|
||||
# Fasa 8
|
||||
git commit -m "feat: email certificate link"
|
||||
|
||||
# Fasa 9
|
||||
git commit -m "feat: admin statistics dashboard"
|
||||
|
||||
# Fasa 10
|
||||
git commit -m "feat: audit logging and security hardening"
|
||||
|
||||
# Fasa 11
|
||||
git commit -m "test: core attendance and certificate flow"
|
||||
|
||||
# Production push (HANYA apabila diarahkan)
|
||||
# git push -u origin master
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Risiko Teknikal
|
||||
|
||||
| # | Risiko | Kemungkinan | Impak | Mitigasi |
|
||||
|---|--------|-------------|-------|----------|
|
||||
| 1 | GD font rendering — perlu TTF, tidak ada jika lupa bundel | Sederhana | Tinggi | Bundel Noto Sans TTF dalam `resources/fonts/`, test awal |
|
||||
| 2 | Queue worker tidak berjalan di Windows production | Tinggi | Tinggi | Guna Task Scheduler Windows atau switch ke Linux server |
|
||||
| 3 | CSV import gagal handle UTF-8 BOM (Excel export) | Tinggi | Sederhana | Detect dan strip BOM dalam import service |
|
||||
| 4 | SMTP kerajaan — TLS/SSL config berbeza | Sederhana | Tinggi | Test awal dengan real SMTP, fallback ke log driver |
|
||||
| 5 | Concurrent check-in race condition | Rendah | Sederhana | DB unique constraint + try-catch pada insert |
|
||||
| 6 | PDF certificate kualiti rendah jika gambar template kecil | Sederhana | Sederhana | Minta admin upload template minimum 1240px lebar |
|
||||
| 7 | "Laravel Boost" tidak jelas | — | Rendah | Perlu penjelasan sebelum install |
|
||||
| 8 | Session tidak persistent untuk public (no login) | — | Sederhana | Guna certificate token + signed URL, bukan session |
|
||||
| 9 | Storage permissions pada Windows production | Sederhana | Tinggi | Pastikan `storage/` dan `bootstrap/cache/` writable |
|
||||
| 10 | PDPA — no_kp exposure dalam log/debug | Rendah | Tinggi | Disable query log production, redact no_kp dalam audit_logs |
|
||||
268
docs/route-plan.md
Normal file
268
docs/route-plan.md
Normal file
@@ -0,0 +1,268 @@
|
||||
# eCert MBIP — Route Plan
|
||||
|
||||
## Admin Routes (Authenticated)
|
||||
|
||||
```
|
||||
Prefix: /admin
|
||||
Middleware: auth, verified (optional)
|
||||
```
|
||||
|
||||
### Auth
|
||||
```
|
||||
GET /login → Auth\AuthenticatedSessionController@create
|
||||
POST /login → Auth\AuthenticatedSessionController@store
|
||||
POST /logout → Auth\AuthenticatedSessionController@destroy
|
||||
```
|
||||
|
||||
### Dashboard
|
||||
```
|
||||
GET /admin/dashboard → Admin\DashboardController@index
|
||||
```
|
||||
|
||||
### Programs
|
||||
```
|
||||
GET /admin/programs → Admin\ProgramController@index
|
||||
GET /admin/programs/create → Admin\ProgramController@create
|
||||
POST /admin/programs → Admin\ProgramController@store
|
||||
GET /admin/programs/{program:uuid} → Admin\ProgramController@show
|
||||
GET /admin/programs/{program:uuid}/edit → Admin\ProgramController@edit
|
||||
PUT /admin/programs/{program:uuid} → Admin\ProgramController@update
|
||||
DELETE /admin/programs/{program:uuid} → Admin\ProgramController@destroy
|
||||
POST /admin/programs/{program:uuid}/publish → Admin\ProgramController@publish
|
||||
POST /admin/programs/{program:uuid}/close → Admin\ProgramController@close
|
||||
```
|
||||
|
||||
### QR Code
|
||||
```
|
||||
GET /admin/programs/{program:uuid}/qr → Admin\QrCodeController@show
|
||||
POST /admin/programs/{program:uuid}/qr/generate → Admin\QrCodeController@generate
|
||||
GET /admin/programs/{program:uuid}/qr/download → Admin\QrCodeController@download
|
||||
POST /admin/programs/{program:uuid}/qr/deactivate → Admin\QrCodeController@deactivate
|
||||
```
|
||||
|
||||
### Participants (Pre-registered)
|
||||
```
|
||||
GET /admin/programs/{program:uuid}/participants → Admin\ParticipantController@index
|
||||
GET /admin/programs/{program:uuid}/participants/create → Admin\ParticipantController@create
|
||||
POST /admin/programs/{program:uuid}/participants → Admin\ParticipantController@store
|
||||
DELETE /admin/programs/{program:uuid}/participants/{pp} → Admin\ParticipantController@destroy
|
||||
GET /admin/programs/{program:uuid}/participants/import → Admin\ParticipantController@importForm
|
||||
POST /admin/programs/{program:uuid}/participants/import → Admin\ParticipantController@import
|
||||
GET /admin/programs/{program:uuid}/participants/export → Admin\ParticipantController@export
|
||||
```
|
||||
|
||||
### Certificate Template
|
||||
```
|
||||
GET /admin/programs/{program:uuid}/template → Admin\CertificateTemplateController@show
|
||||
POST /admin/programs/{program:uuid}/template → Admin\CertificateTemplateController@store
|
||||
PUT /admin/programs/{program:uuid}/template/config → Admin\CertificateTemplateController@updateConfig
|
||||
DELETE /admin/programs/{program:uuid}/template → Admin\CertificateTemplateController@destroy
|
||||
GET /admin/programs/{program:uuid}/template/preview → Admin\CertificateTemplateController@preview
|
||||
POST /admin/programs/{program:uuid}/template/test → Admin\CertificateTemplateController@testGenerate
|
||||
```
|
||||
|
||||
### Questionnaire (Attachment to Program)
|
||||
```
|
||||
GET /admin/programs/{program:uuid}/questionnaire → Admin\ProgramQuestionnaireController@show
|
||||
POST /admin/programs/{program:uuid}/questionnaire/attach → Admin\ProgramQuestionnaireController@attach
|
||||
POST /admin/programs/{program:uuid}/questionnaire/confirm → Admin\ProgramQuestionnaireController@confirm
|
||||
DELETE /admin/programs/{program:uuid}/questionnaire/detach → Admin\ProgramQuestionnaireController@detach
|
||||
```
|
||||
|
||||
### Statistics
|
||||
```
|
||||
GET /admin/programs/{program:uuid}/statistics → Admin\StatisticsController@show
|
||||
GET /admin/programs/{program:uuid}/statistics/export → Admin\StatisticsController@export
|
||||
```
|
||||
|
||||
### Questionnaire Sets (Reusable)
|
||||
```
|
||||
GET /admin/questionnaires → Admin\QuestionnaireSetController@index
|
||||
GET /admin/questionnaires/create → Admin\QuestionnaireSetController@create
|
||||
POST /admin/questionnaires → Admin\QuestionnaireSetController@store
|
||||
GET /admin/questionnaires/{set} → Admin\QuestionnaireSetController@show
|
||||
GET /admin/questionnaires/{set}/edit → Admin\QuestionnaireSetController@edit
|
||||
PUT /admin/questionnaires/{set} → Admin\QuestionnaireSetController@update
|
||||
DELETE /admin/questionnaires/{set} → Admin\QuestionnaireSetController@destroy
|
||||
POST /admin/questionnaires/{set}/publish → Admin\QuestionnaireSetController@publish
|
||||
POST /admin/questionnaires/{set}/archive → Admin\QuestionnaireSetController@archive
|
||||
```
|
||||
|
||||
### Questionnaire Questions
|
||||
```
|
||||
POST /admin/questionnaires/{set}/questions → Admin\QuestionController@store
|
||||
PUT /admin/questions/{question} → Admin\QuestionController@update
|
||||
DELETE /admin/questions/{question} → Admin\QuestionController@destroy
|
||||
POST /admin/questions/reorder → Admin\QuestionController@reorder
|
||||
```
|
||||
|
||||
### Certificate Generation (Admin Trigger)
|
||||
```
|
||||
POST /admin/programs/{program:uuid}/certificates/generate-all → Admin\CertificateController@generateAll
|
||||
POST /admin/programs/{program:uuid}/certificates/email-all → Admin\CertificateController@emailAll
|
||||
GET /admin/programs/{program:uuid}/certificates → Admin\CertificateController@index
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Public Routes (Token-based, No Auth)
|
||||
|
||||
```
|
||||
Prefix: none
|
||||
Middleware: throttle:60,1 (check-in), throttle:30,1 (download)
|
||||
```
|
||||
|
||||
### Check-in Flow
|
||||
```
|
||||
GET /p/{qr_token} → Public\CheckinController@show
|
||||
↑ Papar: nama program, status, pilihan jenis peserta
|
||||
↑ Redirect ke questionnaire/download jika masa download aktif
|
||||
|
||||
POST /p/{qr_token}/staff → Public\CheckinController@staffCheckin
|
||||
↑ Input: no_kp, email
|
||||
↑ Semak pre-registered, rekod attendance
|
||||
|
||||
POST /p/{qr_token}/external → Public\CheckinController@externalRegister
|
||||
↑ Input: name, no_kp, email, phone, agency
|
||||
↑ Daftar + rekod attendance
|
||||
```
|
||||
|
||||
### Questionnaire (Public)
|
||||
```
|
||||
GET /p/{qr_token}/questionnaire/{participant_uuid}
|
||||
→ Public\QuestionnaireController@show
|
||||
↑ Semak: attendance exists, questionnaire published, belum jawab
|
||||
|
||||
POST /p/{qr_token}/questionnaire/{participant_uuid}
|
||||
→ Public\QuestionnaireController@submit
|
||||
↑ Simpan responses, redirect ke certificate page
|
||||
```
|
||||
|
||||
### Semakan Kehadiran (via QR masa download)
|
||||
```
|
||||
GET /p/{qr_token}/semak → Public\AttendanceCheckController@show
|
||||
↑ Papar form: masukkan no_kp untuk semak
|
||||
|
||||
POST /p/{qr_token}/semak → Public\AttendanceCheckController@check
|
||||
↑ Semak kehadiran, papar status sijil
|
||||
```
|
||||
|
||||
### Certificate Download
|
||||
```
|
||||
GET /certificate/{cert_token} → Public\CertificateController@show
|
||||
↑ Semak: token valid, masa download aktif, soalselidik dijawab
|
||||
↑ Jika semua OK: papar download button / auto-download
|
||||
|
||||
POST /certificate/{cert_token}/download → Public\CertificateController@download
|
||||
↑ Generate if not exists, serve file, increment download_count
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Route Logic / Decision Tree
|
||||
|
||||
### GET /p/{qr_token}
|
||||
```
|
||||
QR token valid?
|
||||
NO → 404
|
||||
YES →
|
||||
Program published?
|
||||
NO → Papar: "Program belum dibuka"
|
||||
YES →
|
||||
Masa download aktif (ecert_download_start_at <= now)?
|
||||
YES → Redirect ke /p/{qr_token}/semak
|
||||
NO →
|
||||
Masa check-in aktif (checkin_start_at <= now <= checkin_end_at)?
|
||||
YES → Papar check-in form
|
||||
NO → Papar: "Check-in belum dibuka" atau "Check-in sudah ditutup"
|
||||
```
|
||||
|
||||
### POST /p/{qr_token}/staff
|
||||
```
|
||||
no_kp + email valid format?
|
||||
NO → Validation error
|
||||
YES →
|
||||
Jumpa dalam program_participants (pre_registered)?
|
||||
NO → "Tidak dijumpai dalam senarai" + pilihan daftar luar
|
||||
YES →
|
||||
Sudah check-in (attendances exists)?
|
||||
YES → "Anda sudah check-in" + papar masa check-in
|
||||
NO →
|
||||
Rekod attendance
|
||||
Papar: "Check-in berjaya! Sijil akan dihantar ke emel selepas program."
|
||||
```
|
||||
|
||||
### GET /certificate/{cert_token}
|
||||
```
|
||||
Token valid (dalam certificates table)?
|
||||
NO → 404
|
||||
YES →
|
||||
Masa download aktif?
|
||||
NO → Papar: "Sijil belum boleh dimuat turun. Mula dari: {datetime}"
|
||||
YES →
|
||||
Soalselidik diperlukan dan belum dijawab?
|
||||
YES → Redirect ke /p/{qr_token}/questionnaire/{participant_uuid}
|
||||
NO →
|
||||
Sijil sudah generated?
|
||||
NO → Dispatch GenerateCertificateJob, papar: "Sijil sedang disediakan..."
|
||||
YES → Papar download page / auto-download PDF
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Named Routes
|
||||
|
||||
```php
|
||||
// Admin
|
||||
'admin.dashboard'
|
||||
'admin.programs.index'
|
||||
'admin.programs.create'
|
||||
'admin.programs.show'
|
||||
'admin.programs.edit'
|
||||
'admin.programs.qr.show'
|
||||
'admin.programs.qr.generate'
|
||||
'admin.programs.qr.download'
|
||||
'admin.programs.participants.index'
|
||||
'admin.programs.participants.import'
|
||||
'admin.programs.template.show'
|
||||
'admin.programs.questionnaire.show'
|
||||
'admin.programs.statistics.show'
|
||||
'admin.questionnaires.index'
|
||||
'admin.questions.store'
|
||||
|
||||
// Public
|
||||
'public.checkin.show' → /p/{qr_token}
|
||||
'public.checkin.staff' → POST /p/{qr_token}/staff
|
||||
'public.checkin.external' → POST /p/{qr_token}/external
|
||||
'public.questionnaire.show' → /p/{qr_token}/questionnaire/{uuid}
|
||||
'public.questionnaire.submit' → POST /p/{qr_token}/questionnaire/{uuid}
|
||||
'public.semak.show' → /p/{qr_token}/semak
|
||||
'public.semak.check' → POST /p/{qr_token}/semak
|
||||
'public.certificate.show' → /certificate/{cert_token}
|
||||
'public.certificate.download' → POST /certificate/{cert_token}/download
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Middleware Stack
|
||||
|
||||
```php
|
||||
// web.php
|
||||
Route::middleware('auth')->prefix('admin')->name('admin.')->group(function () {
|
||||
// semua admin routes
|
||||
});
|
||||
|
||||
Route::middleware(['throttle:checkin'])->prefix('p')->name('public.')->group(function () {
|
||||
// check-in routes
|
||||
});
|
||||
|
||||
Route::middleware(['throttle:certificate'])->prefix('certificate')->name('public.certificate.')->group(function () {
|
||||
// download routes
|
||||
});
|
||||
```
|
||||
|
||||
Rate limiter dalam `AppServiceProvider`:
|
||||
```php
|
||||
RateLimiter::for('checkin', fn($req) => Limit::perMinute(60)->by($req->ip()));
|
||||
RateLimiter::for('certificate', fn($req) => Limit::perMinute(30)->by($req->ip()));
|
||||
```
|
||||
Reference in New Issue
Block a user