Files
eCert-MBIP/docs/database-design.md
Saufi 5b85822b78 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>
2026-05-16 15:44:19 +08:00

405 lines
13 KiB
Markdown

# 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`.