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:
Saufi
2026-05-16 15:44:19 +08:00
commit 5b85822b78
159 changed files with 18351 additions and 0 deletions

201
docs/architecture.md Normal file
View 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
View 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
View 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
View 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()));
```