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