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:
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`.
|
||||
Reference in New Issue
Block a user