# Planning — Modul Baharu: Aduan & Maklum Balas (`content-aduan`)

> Dikemas kini: 23 Okt 2026 · Status: **cadangan (belum mula)** · Saiz: **L** (3 fasa)
> Rujukan: `docs/module/planning-pending-modules.md` · `docs/module/planning-media-menu.md`

---

## 0. Konsep dalam bahasa mudah (baca ini dahulu)

### 0.1 Satu ayat

> **Admin "taip" borang dalam CMS → CMS jana borang itu di portal NRES → pelawat isi →
> jawapan masuk peti masuk admin → admin balas (aduan) atau buat laporan (kajian).**

### 0.2 Analogi paling mudah

Fikir begini: modul ini = **Google Forms + Google Sheet + peti masuk e-mel**,
tetapi **semuanya di dalam portal NRES** dan diurus dari **satu tempat**:

| Google (sekarang) | Modul ini |
|---|---|
Google Form (borang) | borang dipapar **di portal NRES sendiri** (tema & bahasa sama) |
Google Sheet (jawapan) | jawapan masuk **pangkalan data CMS** |
Baca satu-satu dalam Sheet | **peti masuk** dengan badge status + penapis + carian |
Tiada balasan rasmi | **utas balasan** admin ↔ pengadu + No. Tiket |
Kira peratus manual | **laporan agregat** automatik + eksport Excel |

### 0.3 SEBELUM vs SELEPAS

| | SEKARANG | SELEPAS |
|---|---|---|
Pelawat | klik → **keluar** dari portal ke `docs.google.com` | isi borang **dalam portal NRES** |
Data | dalam Google Drive (luar kawalan NRES) | dalam pangkalan data CMS |
Jejak aduan | tiada No. tiket | **No. Tiket** + boleh semak status |
Admin | buka Google Sheet | peti masuk CMS + balasan + status |
Laporan kajian | kira manual | peratus & purata automatik |
Tema / bahasa | tak boleh ubah | ikut reka bentuk site (BM/EN, mod gelap) |

### 0.4 Aliran penuh — jenis **ADUAN**

```
[ADMIN]    buat borang "Aduan" dalam CMS
              │
              ▼
[SISTEM]   jana borang itu di portal  →  /nres/aduan
              │
              ▼
[PELAWAT]  isi & hantar
              │
              ▼
[SISTEM]   simpan + jana NO. TIKET + hantar e-mel pengesahan
              │
              ▼
[PELAWAT]  terima "No. Tiket: NRES/AD/2026/0001"
           (boleh semak status bila-bila: No. Tiket + e-mel)
              │
              ▼
[ADMIN]    peti masuk:  NEW → IN_REVIEW → balas → REPLIED → CLOSED
              │
              ▼
[PELAWAT]  semak status & baca balasan admin
```

### 0.5 Aliran penuh — jenis **KAJIAN**

```
[ADMIN]    buat borang "Kajian Kepuasan Pelanggan 2026"
           (3 seksyen + 11 soalan + ~40 pilihan)
              │
              ▼
[PELAWAT]  isi 12 soalan  →  hantar
              │
              ▼
[SISTEM]   simpan jawapan (tiada No. tiket, tiada balasan)
              │
              ▼
[ADMIN]    LAPORAN:  "72% jawab Baik · purata skala 4.2 · 138 responden"
           + eksport Excel untuk laporan rasmi
```

### 0.6 Konsep "satu enjin, banyak borang" (ini bahagian paling penting)

**Jangan tulis kod berasingan untuk setiap borang.** Sebaliknya admin **susun** borang
dalam CMS, dan CMS jana HTML-nya sendiri:

```
BORANG: "Kajian Kepuasan Pelanggan 2026"
 ├── SEKSYEN: "BAHAGIAN A: DEMOGRAFI RESPONDEN"
 │     ├── FIELD: Jantina      (jenis: Radio)    → Lelaki, Perempuan
 │     ├── FIELD: Umur         (jenis: Radio)    → <20, 21–40, 41–60, 61>
 │     ├── FIELD: Bangsa       (jenis: Radio)    → Melayu, Cina, India, Bumiputera
 │     └── FIELD: Pekerjaan    (jenis: Radio)    → 5 pilihan
 ├── SEKSYEN: "BAHAGIAN B: KAEDAH PERHUBUNGAN"
 │     ├── FIELD: Kaedah Berurusan (Checkbox)    → 8 pilihan
 │     ├── FIELD: Bahagian/Unit   (Radio)        → dari ref NRES_DEPARTMENT (17)
 │     └── FIELD: Tujuan Urusan   (Radio)        → 6 pilihan
 └── SEKSYEN: "BAHAGIAN C: TAHAP KEPUASAN"
       ├── FIELD: 9 pernyataan   (Skala 1–5)
       └── FIELD: Cadangan       (Perenggan)
```

**Kebaikan konsep ini:**
- Bila NRES tukar borang tahun depan ("Kajian 2027"), admin **edit dalam CMS** — tak perlu panggil developer.
- Borang aduan & kajian guna **enjin yang sama** — tiada kod berganda.
- Setiap jawapan disimpan sebagai baris `content_aduan_answer` (satu baris = satu jawapan) → mudah buat laporan & eksport.

### 0.7 Apa yang PELAWAT nampak (portal)

| Halaman | Isi |
|---|---|
`/nres/aduan` | senarai kad borang yang **sedang dibuka** (Aduan · Maklum Balas · Kajian Kepuasan) |
`/nres/aduan/aduan` | borang aduan — Nama*, E-mel*, Telefon, Bahagian/Unit, Kategori*, Subjek*, Butiran*, Lampiran |
Selepas hantar | kad "Aduan diterima" + **No. Tiket** besar + butang salin + pautan semak status |
`/nres/aduan/tiket` | masuk **No. Tiket + e-mel** → nampak status terkini + utas balasan admin |

### 0.8 Apa yang ADMIN nampak (CMS)

| Menu | Isi |
|---|---|
**Aduan & Maklum Balas** → Peti Masuk | senarai tiket: No. Tiket · Nama · Kategori · Unit · Status (badge warna) · Tarikh · penapis + carian |
Butiran tiket | semua jawapan · lampiran (muat turun) · **utas balasan** · tukar status · assign pegawai · keutamaan |
**Borang** (P2) | builder: susun seksyen → field → pilihan (drag & drop) |
**Laporan** (P2) | peratus jawapan per soalan · purata skala · eksport Excel/PDF |

### 0.9 Kalau anda mahu yang paling mudah dahulu (cadangan saya)

> **P1 sahaja** sudah cukup untuk menggantikan 2 Google Form yang boleh dibaca:
> - 1 borang **Aduan & Maklum Balas** (field tetap)
> - 1 borang **Kajian Kepuasan Pelanggan 2026** (field tetap)
> - Peti masuk admin + balasan + No. Tiket + eksport + e-mel
>
> Nampak hasil dalam **1–2 sesi**. Builder borang (P2) hanya bila borang perlu **berubah kerap**.

---

## 1. Latar belakang & objektif

Portal NRES ada halaman **Aduan dan Maklum Balas**
(`/ms-my/Pages/AduandanMaklumbalas.aspx`) yang pada masa ini **hanya memaut ke borang
Google Form**. Matlamat modul ini: memindahkan keseluruhan aliran itu **ke dalam CMS** —
borang awam + peti masuk admin + balasan + laporan — supaya tiada data pelawat tersimpan
di luar sistem dan tiada kebergantungan pada Google Forms.

### 1.1 Apa yang sudah ada (jangan ulang kerja)

| Modul | Status | Nota |
|---|---|---|
`content-feedback` | ✅ **admin siap** | `content_feedback_main` (`ticket_no`, `name`, `email`, `type`, `cat`, `subject`, `feedback`, `status`) + `content_feedback_messages` (utas balasan) · controller CRUD + **export Excel/PDF** · `FeedbackSubmittedMail` · ref `FEEDBACK_STATUS`, `FEEDBACK_TYPE` · views `contentFeedback/{index,create,edit,export-pdf}` |
**Kekurangan** | ❌ | **Tiada borang awam**, tiada page/komponen/menu, tiada field NRES (bahagian/unit, negeri, lampiran), tiada laporan kajian |

### 1.2 Tiga jenis pada halaman NRES (disahkan dari banner + pautan)

Halaman `AduandanMaklumbalas.aspx` memaparkan **3 banner** (bukan borang dalaman):

| # | Banner | Pautan sebenar | Jenis | Ciri penting |
|---|---|---|---|---|
1 | `Banner_SISPAA.jpg` | `https://nres.spab.gov.my/eApps/system/index.do` | **Aduan Awam** | 🚫 **PAUTAN LUAR SAHAJA — tiada modul** (kekal guna SISPAA) |
2 | `Banner_AduanIntegriti.jpg` | Google Form `1FAIpQLSfZM88BBqBE…` | **Aduan Integriti** | ⚠️ Perlu **Google sign-in** → sensitif, **akses terhad** |
3 | (banner ke-3) | Google Form `1FAIpQLSc4PVD1o0OX…` | **Kajian Kepuasan Pelanggan NRES 2026** | 12 item — borang kajian |

Borang tambahan yang diberikan pengguna (tidak dipautkan pada halaman ini):
`1FAIpQLSeTjzM5i1tW…` = **KAJIAN KEPUASAN PERKHIDMATAN DIGITAL 2026** (8 item)

> **Kesimpulan reka bentuk:** ketiga-tiga jenis **berkongsi enjin yang sama** (borang → jawapan →
> peti masuk). Yang berbeza hanya `type`, field, dan 3 tingkah laku: **pautan luar** (SISPAA),
> **akses terhad** (Integriti), dan **laporan agregat** (Kajian). → **1 modul mencukupi.**

---

## 2. Keputusan reka bentuk

### 2.1 Satu modul, empat jenis borang (form-driven)

Kedua-dua borang yang boleh dibaca ialah **KAJIAN KEPUASAN**, bukan borang aduan. Jika kita
bina dua modul berasingan (aduan + kajian) kita akan tulis **enjin borang dua kali**.
Cadangan: **satu modul berasaskan-borang** — enjin sama, berbeza `type`:

| `aduan_form_type` | Kegunaan | Ciri |
|---|---|---|
`ADUAN` | Aduan rasmi | No. tiket + utas balasan + SLA + lampiran |
`MAKLUM_BALAS` | Cadangan / maklum balas | No. tiket + utas balasan |
`PERTANYAAN` | Pertanyaan umum | No. tiket + balasan |
`KAJIAN` | Kajian kepuasan | Tiada balasan · **laporan agregat %** |

### 2.2 Tiga fasa (jangan bina semuanya sekali)

> **Skop akhir (disahkan 23 Okt 2026):** SISPAA **tidak** dimasukkan ke modul —
> ia kekal **pautan luar** pada halaman. Modul ini mengendalikan **3 borang dalaman**:
> Aduan Integriti · Kajian Kepuasan Pelanggan 2026 · Kajian Kepuasan Perkhidmatan Digital 2026.

| Fasa | Skop | Saiz |
|---|---|---|
**F1** | **Asas — boleh guna.** Semua 9 table + enjin render + 3 borang (via seeder) + hantar + No. tiket + e-mel + peti masuk admin + balas + status + eksport | M |
**F2** | **Laporan & pengurusan borang.** Laporan agregat (peratus/purata) + UI admin edit borang/field/pilihan + duplikasi borang (Kajian tahun berikutnya) | M |
**F3** | **Frontend portal.** Page + komponen + menu BM/EN + kad SISPAA (pautan luar) + semak status tiket | M |

> Rasional: schema **lengkap dalam F1** (elak migration berulang); UI admin bertambah
> beransur-ansur di F2; F3 hanya bila data & aliran sudah stabil.

### 2.3 Hubungan dengan `content-feedback` sedia ada

| Pilihan | Keputusan |
|---|---|
(A) Kekalkan `content-feedback` **tidak diusik**, modul baharu `content-aduan` berasingan | ✅ **dipilih** |
(B) Guna semula `content-feedback` + tambah kolum | ❌ tidak dipilih (field NRES terlalu berbeza) |
(C) Migrasi data lama `content_feedback_main` → `content_aduan_submission` | opsyen P3 (skrip sekali jalan) |


---

## 3. Skema P1 — enjin hantar (4 table)

### 3.1 `content_aduan_submission`
```php
$table->id();
$table->foreignId('aduan_form_fk')->nullable();              // null pada P1 (borang tetap)
$table->string('aduan_ticket_no', 40)->unique();             // cth NRES/AD/2026/0001
$table->string('aduan_ref_no', 60)->nullable();              // no. rujukan agensi / SISPAA
$table->string('aduan_name', 255);
$table->string('aduan_email', 255);
$table->string('aduan_phone', 50)->nullable();
$table->string('aduan_org', 255)->nullable();                // syarikat / organisasi
$table->string('aduan_state', 50)->nullable();               // ref STATE
$table->string('aduan_type', 40);                            // ADUAN|MAKLUM_BALAS|PERTANYAAN|KAJIAN
$table->string('aduan_category', 60)->nullable();            // ref ADUAN_CATEGORY
$table->string('aduan_unit', 100)->nullable();               // ref NRES_DEPARTMENT (17 unit)
$table->string('aduan_subject', 255)->nullable();
$table->text('aduan_body')->nullable();                      // butiran / aduan
$table->string('aduan_status', 40)->default('NEW');          // ref ADUAN_STATUS
$table->string('aduan_priority', 20)->default('NORMAL');     // ref ADUAN_PRIORITY
$table->unsignedBigInteger('aduan_assigned_to')->nullable(); // FK admin users
$table->string('aduan_channel', 20)->default('WEB');         // WEB|SISPAA|EMAIL|COUNTER
$table->string('aduan_locale', 10)->default('ms');
$table->string('aduan_ip_hash', 64)->nullable();
$table->string('aduan_user_agent', 255)->nullable();
$table->timestamp('aduan_submitted_at')->nullable();
$table->timestamp('aduan_replied_at')->nullable();
$table->timestamp('aduan_closed_at')->nullable();
$table->timestamps();
$table->unsignedBigInteger('created_by')->nullable();
$table->unsignedBigInteger('updated_by')->nullable();
```

### 3.2 `content_aduan_answer`
```php
$table->id();
$table->foreignId('aduan_submission_fk');                    // cascade delete
$table->string('aduan_field_code', 60);                      // cth 'jantina','unit','skala_ui'
$table->string('aduan_field_label', 255);                    // label semasa (untuk eksport)
$table->text('aduan_answer_text')->nullable();               // teks / pilihan (dipisah koma)
$table->decimal('aduan_answer_scale', 5, 2)->nullable();     // skala 1–5
$table->timestamps();
$table->index(['aduan_submission_fk', 'aduan_field_code']);
```

### 3.3 `content_aduan_attachment`
```php
$table->id();
$table->foreignId('aduan_submission_fk');
$table->string('aduan_field_code', 60)->nullable();          // null = lampiran am
$table->string('aduan_file_path');                           // aduan/files/xxxx.pdf
$table->string('aduan_file_name', 255);
$table->unsignedBigInteger('aduan_file_size')->nullable();
$table->string('aduan_file_mime', 100)->nullable();
$table->timestamps();
```

### 3.4 `content_aduan_message` (utas balasan)
```php
$table->id();
$table->foreignId('aduan_submission_fk');
$table->text('aduan_message_body');
$table->string('aduan_message_from', 10)->default('ADMIN');  // ADMIN|USER
$table->string('aduan_read_status', 20)->default('UNREAD');
$table->string('aduan_sent_to', 255)->nullable();            // e-mel penerima
$table->timestamps();
$table->unsignedBigInteger('created_by')->nullable();
```

---

## 4. Skema P2 — builder borang (5 table tambahan)

### 4.1 `content_aduan_form`
```php
$table->id();
$table->string('aduan_form_code', 60)->unique();       // cth 'kajian-kepuasan-pelanggan-2026'
$table->string('aduan_form_type', 40);                 // ref ADUAN_FORM_TYPE
$table->string('aduan_form_slug', 80)->unique();       // laluan awam: /aduan/{slug}
$table->date('aduan_form_start')->nullable();
$table->date('aduan_form_end')->nullable();
$table->string('aduan_form_status', 20)->default('DRAFT');   // ref ADUAN_FORM_STATUS
$table->boolean('aduan_form_captcha')->default(true);
$table->boolean('aduan_form_attachment')->default(false);
$table->boolean('aduan_form_reply')->default(true);          // benarkan utas balasan
$table->boolean('aduan_form_report')->default(false);        // papar laporan agregat
$table->text('aduan_form_thanks')->nullable();               // mesej selepas hantar
$table->string('aduan_form_external_url', 255)->nullable();  // jika diisi → kad jadi PAUTAN LUAR (cth SISPAA)
$table->boolean('aduan_form_anonymous')->default(false);     // benarkan hantar tanpa identiti
$table->json('aduan_form_access')->nullable();               // had akses per borang (cth Aduan Integriti = Unit Integriti sahaja)
$table->integer('aduan_form_sort')->default(0);
$table->timestamps();
$table->unsignedBigInteger('created_by')->nullable();
$table->unsignedBigInteger('updated_by')->nullable();
```

### 4.2 `content_aduan_form_translation`
```php
$table->id();
$table->foreignId('aduan_form_fk');
$table->string('language', 5);            // ms|en
$table->string('title', 255);
$table->text('description')->nullable();
$table->boolean('main')->default(false);
$table->timestamps();
```

### 4.3 `content_aduan_section` — BAHAGIAN A / B / C
```php
$table->id();
$table->foreignId('aduan_form_fk');
$table->string('aduan_section_title', 255);
$table->text('aduan_section_desc')->nullable();
$table->integer('aduan_section_sort')->default(0);
$table->timestamps();
```

### 4.4 `content_aduan_field`
```php
$table->id();
$table->foreignId('aduan_form_fk');
$table->foreignId('aduan_section_fk')->nullable();
$table->string('aduan_field_code', 60);                      // unik dalam satu borang
$table->string('aduan_field_label', 255);
$table->string('aduan_field_help', 255)->nullable();
$table->string('aduan_field_type', 20);                      // ref ADUAN_FIELD_TYPE
$table->boolean('aduan_field_required')->default(false);
$table->string('aduan_field_ref_cat', 60)->nullable();       // pilihan dari `ref`
$table->decimal('aduan_field_scale_min', 5, 2)->nullable();
$table->decimal('aduan_field_scale_max', 5, 2)->nullable();
$table->json('aduan_field_scale_labels')->nullable();        // {"1":"Sangat tidak setuju",…}
$table->integer('aduan_field_sort')->default(0);
$table->boolean('aduan_field_status')->default(true);
$table->timestamps();
$table->unique(['aduan_form_fk', 'aduan_field_code']);
```

### 4.5 `content_aduan_field_option`
```php
$table->id();
$table->foreignId('aduan_field_fk');
$table->string('aduan_option_label', 255);
$table->string('aduan_option_value', 100)->nullable();
$table->integer('aduan_option_sort')->default(0);
$table->timestamps();
```

> **Nota reka bentuk penting**
> - "Grid 9 pernyataan" (FORM B) dimodelkan sebagai **9 field `SCALE` dalam satu seksyen** —
>   lebih mudah dari cipta jenis GRID khas, dan laporan agregat tetap berfungsi per-pernyataan.
> - `aduan_field_ref_cat` membolehkan pilihan datang dari `ref` (cth. 17 unit NRES, negeri)
>   tanpa perlu taip semula — ikut konvensyen projek (dropdown dari `ref`, `descr_en` fallback `descr`).

---

## 5. Ref data baharu (`RefDataSeeder`)

| Cat | Kod | Kegunaan |
|---|---|---|
`ADUAN_FORM_TYPE` | `ADUAN`, `MAKLUM_BALAS`, `PERTANYAAN`, `KAJIAN` | jenis borang |
`ADUAN_FORM_STATUS` | `DRAFT`, `OPEN`, `CLOSED` | status borang |
`ADUAN_STATUS` | `NEW`, `IN_REVIEW`, `PENDING_INFO`, `REPLIED`, `CLOSED`, `REJECTED` | status tiket |
`ADUAN_PRIORITY` | `LOW`, `NORMAL`, `HIGH`, `URGENT` | keutamaan |
`ADUAN_CATEGORY` | `PERHUTANAN`, `BIODIVERSITI`, `MINERAL_GEOSAINS`, `ALAM_SEKITAR`, `PERUBAHAN_IKLIM`, `TANAH_UKUR`, `PENGURUSAN`, `LAIN_LAIN` | kategori aduan |
`NRES_DEPARTMENT` | 17 unit (dari FORM B) | dropdown Bahagian/Unit |
`ADUAN_FIELD_TYPE` | `TEXT`, `TEXTAREA`, `EMAIL`, `PHONE`, `SELECT`, `RADIO`, `CHECKBOX`, `SCALE`, `DATE`, `FILE` | jenis field |

> `STATE` (negeri) — guna sedia ada jika ada; jika tiada, tambah dalam seeder yang sama.

---

## 6. Struktur fail

### 6.1 P1 — enjin hantar
| Fail | Kerja |
|---|---|
`database/migrations/*_create_content_aduan_tables.php` | 4 table (§3) |
`app/Models/Backend/ContentAduanSubmission.php` + `ContentAduanAnswer.php` + `ContentAduanAttachment.php` + `ContentAduanMessage.php` | model + relasi |
`app/Http/Controllers/Frontend/AduanController.php` | `index()` (senarai borang) · `show($slug)` · `store()` · `ticket()` (semak status) |
`app/Http/Requests/Frontend/AduanSubmissionRequest.php` | validasi + captcha + honeypot |
`app/Http/Controllers/Backend/ContentAduanController.php` | index (peti masuk) · show · reply · status · assign · destroy · export |
`app/Http/Requests/Backend/ContentAduanReplyRequest.php` | validasi balasan |
`app/Exports/AduanExport.php` + `AduanPdfExport.php` | eksport Excel/PDF |
`app/Mail/AduanSubmittedMail.php` + `AduanRepliedMail.php` | notifikasi |
`resources/views/backend/module/contentAduan/{index,show,edit,export-pdf}.blade.php` | UI admin |
`resources/views/emails/aduan/*.blade.php` | templat e-mel |

### 6.2 P2 — builder borang
| Fail | Kerja |
|---|---|
`database/migrations/*_create_content_aduan_form_tables.php` | 5 table (§4) |
`app/Models/Backend/ContentAduanForm.php` + `ContentAduanFormTranslation.php` + `ContentAduanSection.php` + `ContentAduanField.php` + `ContentAduanFieldOption.php` | model |
`app/Http/Controllers/Backend/ContentAduanFormController.php` | CRUD borang + duplikasi |
`app/Http/Controllers/Backend/ContentAduanFieldController.php` | CRUD seksyen/field/pilihan (ajax sortable) |
`app/Http/Controllers/Backend/ContentAduanReportController.php` | laporan agregat (peratus per field/option/scale) |
`resources/views/backend/module/contentAduanForm/{index,create,edit,_fields}.blade.php` | builder UI |
`resources/views/backend/module/contentAduanReport/{index,show}.blade.php` | laporan |

### 6.3 P3 — frontend
| Fail | Kerja |
|---|---|
`storage/app/private/PHP/aduan_web.blade.php` | komponen: senarai borang + render dinamik + papar No. tiket |
`frontend_pages` (site `nres`, slug `aduan-web`) + `frontend_page_components` | page |
`frontend_menu` | menu "Aduan & Maklum Balas" (BM/EN) |
`routes/web.php` | `/{site}/aduan` · `/{site}/aduan/{slug}` (GET+POST) · `/{site}/aduan/tiket` (GET) |
`tests/Feature/AduanSubmissionTest.php` + `ContentAduanAdminTest.php` | ujian |
`docs/module/content-aduan.md` | dokumen modul |

---

## 7. Permission & menu admin

| Permission | Kegunaan |
|---|---|
`content-aduan.view` | lihat peti masuk + tiket |
`content-aduan.reply` | balas tiket / tukar status |
`content-aduan.update` | kemas kini butiran tiket |
`content-aduan.delete` | padam tiket |
`content-aduan.export` | eksport Excel/PDF |
`content-aduan.form` | urus borang (P2) |
`content-aduan.report` | laporan kajian (P2) |

Baris `backend_menu` + `backend_role_mapping` (ikut pola `BackendMenuSeeder`):
**Content → Aduan & Maklum Balas** (`content-aduan.index`) · submenu **Borang** (`content-aduan.form.index`, P2) · submenu **Laporan** (`content-aduan.report.index`, P2).

---

## 8. Aliran kerja & notifikasi

### 8.1 State machine tiket (`aduan_status`)
```
NEW ──► IN_REVIEW ──► REPLIED ──► CLOSED
          │                │
          └─► PENDING_INFO ┘        (menunggu maklumat pengadu)
                    │
                    └─► REJECTED     (spam / di luar bidang kuasa)
```
- `aduan_replied_at` diisi bila status → `REPLIED`; `aduan_closed_at` bila → `CLOSED`.
- `aduan_assigned_to` — pegawai yang bertanggungjawab (dropdown admin users).

### 8.2 Notifikasi e-mel (gunakan `email_templates` sedia ada)
| Peristiwa | Penerima | Templat |
|---|---|---|
Tiket baharu dihantar | Pengadu | `ADUAN_SUBMITTED` — No. tiket + ringkasan + pautan semak status |
Tiket baharu | Pegawai/admin (unit terlibat) | `ADUAN_NEW_ADMIN` |
Admin balas | Pengadu | `ADUAN_REPLIED` — teks balasan + No. tiket |
Status ditutup | Pengadu | `ADUAN_CLOSED` |

### 8.3 Anti-spam & keselamatan
- **Honeypot** field tersembunyi + **captcha** (opsyenal per borang) + **throttle** `throttle:5,1` pada route POST.
- Simpan `aduan_ip_hash` (sha256 + `APP_KEY`) — **jangan** simpan IP mentah.
- Lampiran: had saiz (cth 10 MB), whitelist mime (`pdf,jpg,jpeg,png,docx,xlsx`), simpan di `storage/app/public/aduan/files`, muat turun guna `url('storage/…')`.
- XSS: semua output `{{ }}`; butiran aduan (HTML) di-escape.

---

## 9. Frontend (P3)

### 9.1 Halaman
| | |
|---|---|
Slug | `aduan-web` (site `nres`) |
Komponen | `aduan_web` (satu fail Blade, pola sama `parliament_qa_web`) |
Menu | "Aduan & Maklum Balas" / "Complaints & Feedback" |
Sub-halaman | Senarai borang → borang → halaman "Terima kasih" (No. tiket) → Semak Status Tiket |

### 9.2 Reka bentuk borang (ikut reka bentuk site NRES sedia ada)
- Guna token sedia ada (`--teal`, `--fade`, `--line`, `card-soft`) + sokongan **mod gelap**.
- Seksyen sebagai `<fieldset>` + `<legend>` (BAHAGIAN A/B/C).
- Field wajib: label + `*`, ralat inline (`is-invalid` / `invalid-feedback`).
- **Skala 1–5**: butang segmen radio (label: "Sangat tidak setuju" → "Sangat setuju").
- Grid 9 pernyataan: senarai pernyataan × baris skala (susun menegak pada mudah alih).
- Lampiran: FilePond (`storeAsFile: true`) — sama seperti modul sedia ada.
- Butang: **Hantar Aduan** (teal) + **Set Semula**.
- Selepas hantar: papar kad "Aduan diterima" dengan **No. Tiket** besar + butang salin + pautan semak status.

### 9.3 Semak status tiket
`GET /{site}/aduan/tiket?no=NRES/AD/2026/0001&email=…` → papar status, tarikh, dan **utas balasan**
(hanya jika `no` + `email` padan — elak orang lain membaca aduan orang lain).

---

## 10. Ujian (`tests/Feature`)

### `AduanSubmissionTest.php` (P1)
- [ ] Hantar aduan sah → 302 + rekod `content_aduan_submission` + `aduan_ticket_no` berformat
- [ ] Field wajib kosong → ralat validasi (nama, e-mel, kategori, butiran)
- [ ] Honeypot diisi → ditolak (tiada rekod)
- [ ] Throttle: 6 permintaan/minit → 429
- [ ] Jawapan disimpan ke `content_aduan_answer` (setiap field ada 1 baris)
- [ ] Lampiran disimpan + baris `content_aduan_attachment`
- [ ] `AduanSubmittedMail` dihantar (Mail::fake)
- [ ] Semak tiket: no+email betul → papar; salah → 404/ralat

### `ContentAduanAdminTest.php` (P1/P2)
- [ ] Permission: tanpa `content-aduan.view` → 403
- [ ] Admin balas → `content_aduan_message` (`from=ADMIN`) + status → `REPLIED` + `aduan_replied_at`
- [ ] Tukar status → `aduan_closed_at` diisi bila `CLOSED`
- [ ] Eksport Excel/PDF → 200 + bilangan baris betul
- [ ] (P2) CRUD borang/seksyen/field/pilihan + render borang awam ikut susunan
- [ ] (P2) Laporan agregat: peratus option + purata skala betul
- [ ] Borang `CLOSED` / luar tarikh → tidak boleh dihantar

---

## 11. Fasa, anggaran & acceptance

| Fasa | Skop | Saiz | Acceptance ringkas |
|---|---|---|---|
**P1** | 4 table + peti masuk admin + borang tetap (2 borang: Aduan & Kajian Kepuasan Pelanggan) + balasan + eksport + e-mel | **M** | Pelawat boleh hantar; admin boleh balas; No. tiket berfungsi; ujian lulus |
**P2** | Builder borang (5 table) + render dinamik + laporan kajian | **L** | Admin boleh cipta borang baharu tanpa developer; laporan agregat betul |
**P3** | Frontend page + komponen + menu + BM/EN + semak status tiket | **M** | Menu portal berfungsi; borang boleh guna pada telefon; status tiket boleh disemak |

**Jumlah anggaran: L (4–7 sesi)**

---

## 12. Risiko & keputusan terbuka

| # | Isu | Cadangan / keputusan diperlukan |
|---|---|---|
1 | **FORM A tidak dapat dibaca** (perlu Google sign-in) | Perlu senarai field dari pengguna — adakah ia borang aduan sebenar atau borang dalaman? |
2 | Borang C (Perkhidmatan Digital) **tidak** dipautkan pada halaman NRES | Sahkan sama ada perlu dimasukkan ke modul |
3 | Grid 9 pernyataan — perlu jenis GRID khas? | Tidak — guna 9 field `SCALE` (§4 nota) |
4 | Adakah perlu **SISPAA** (Sistem Pengurusan Aduan Awam) disambung? | Fasa akan datang — simpan `aduan_channel` + `aduan_ref_no` supaya mudah disambung |
5 | SLA / tempoh balasan (cth 7 hari bekerja) | Perlu angka SLA untuk paparan awam + peringatan admin |
6 | Data lama `content_feedback_main` | Migrasi ke modul baharu (P3) atau biarkan berasingan? |
7 | Captcha — jenis apa? | Cadangan: honeypot + captcha matematik ringan (tiada kebergantungan luar) atau reCAPTCHA v2 jika ada kunci |
8 | Modul bilingual (BM/EN) | Borang perlu 2 bahasa? Cadangan: label borang dari `*_translation`, jawapan pengguna disimpan seperti yang dihantar |

---

## 13. Pemetaan field Google Forms → schema (bukti reka bentuk padan)

### Skop akhir: 3 borang dalaman + 1 pautan luar

| Item NRES | Cara | `aduan_form_type` | `external_url` | `access` | Tiket | Balasan | Laporan |
|---|---|---|---|---|---|---|---|
Aduan Awam (SISPAA) | 🚫 **pautan luar sahaja — TIADA modul** | — | — | — | — | — | — |
Aduan Integriti | borang dalaman | `ADUAN_INTEGRITI` | — | **Unit Integriti sahaja** | ✅ | ✅ | — |
Kajian Kepuasan Pelanggan 2026 | borang dalaman | `KAJIAN` | — | semua | — | — | ✅ |
Kajian Kepuasan Digital 2026 | borang dalaman | `KAJIAN` | — | semua | — | — | ✅ |
*(pilihan)* Maklum Balas umum | borang dalaman | `MAKLUM_BALAS` | — | semua | ✅ | ✅ | — |

> Satu borang = **satu rekod** dalam `content_aduan_form`. Menambah jenis/borang ke-4 nanti =
> tambah satu rekod dalam CMS ( **tiada kod baharu** ).

### FORM 1 — KAJIAN KEPUASAN PELANGGAN NRES 2026 (12 item · lengkap)

| # | Item borang | Jenis | Schema |
|---|---|---|---|
1 | BAHAGIAN A: DEMOGRAFI RESPONDEN *(nota: "Sila tandakan di ruangan yang disediakan")* | seksyen | `content_aduan_section` #1 |
2 | Jantina — Lelaki, Perempuan | radio | field `jantina` + 2 option |
3 | Umur — <20 · 21–40 · 41–60 · 61> | radio | field `umur` + 4 option |
4 | Bangsa — Melayu, Cina, India, Bumiputera | radio | field `bangsa` + 4 option |
5 | Pekerjaan — Sektor awam, swasta, bekerja sendiri, pembekal, pelajar | radio | field `pekerjaan` + 5 option |
6 | BAHAGIAN B: KAEDAH PERHUBUNGAN | seksyen | section #2 |
7 | Kaedah/Cara Berurusan **(multi)** — Bersemuka · Kaunter · Telefon · E-mel · Surat · Media Sosial NRES · Forum rasmi (Lawatan/Mesyuarat/Program/Pameran) · **SISPAA** | checkbox | field `kaedah` + 8 option |
8 | Bahagian/Unit Yang Dihubungi **(17)** | radio | field `unit` — `aduan_field_ref_cat = 'NRES_DEPARTMENT'` |
9 | Tujuan Urusan (6) — Menyertai aktiviti pameran · Mendapatkan maklumat · Urusan pejabat · Urusan perniagaan (Tender/Kontrak/Bayaran) · Urusan rasmi (Mesyuarat/Libat urus) · Urusan peribadi | radio | field `tujuan` + 6 option |
10 | BAHAGIAN C: TAHAP KEPUASAN PELANGGAN TERHADAP PERKHIDMATAN | seksyen | section #3 |
11 | **Penilaian Perkhidmatan — GRID 9 baris × 5 lajur** | grid | 9 field `SCALE` (lihat senarai baris di bawah) |
12 | Cadangan penambahbaikan kualiti perkhidmatan NRES | perenggan | field `cadangan` (`TEXTAREA`) |

**Lajur grid (5):** Cemerlang · Baik · Memuaskan · Kurang memuaskan · Tidak memuaskan
**Baris grid (9 — disahkan dari borang):**
`a.` Layanan yang diterima daripada pegawai ·
`b.` Kepakaran dan profesionalisme pegawai ·
`c.` Masa menunggu sebelum berurusan ·
`d.` Maklumbalas/penerangan yang diberi oleh pegawai ·
`e.` Tempoh masa maklumbalas yang diberi ·
`f.` Pengurusan fasiliti yang disediakan ·
`g.` Portal rasmi Kementerian (www.nres.gov.my) ·
`h.` Kandungan media sosial Kementerian (@nresmalaysia) ·
`i.` Secara keseluruhan saya berpuas hati dengan perkhidmatan Kementerian

### FORM 3 — KAJIAN KEPUASAN PERKHIDMATAN DIGITAL 2026 (8 item · lengkap)

| # | Item borang | Jenis | Schema |
|---|---|---|---|
1 | Nyatakan sektor pekerjaan anda? — Kerajaan/Badan Berkanun · Sektor Swasta · Orang Awam/Individu | radio | field `sektor` + 3 option |
2 | Nyatakan nama perkhidmatan digital Kementerian yang anda gunakan:*** | teks (**wajib**) | field `perkhidmatan` (`TEXT`, `required=1`) |
3 | TAJUK + arahan: *"Pilih 1 atau 2 jika sangat tidak setuju… Pilih 4 atau 5 jika setuju… Pilih 3 jika kurang pasti"* | tajuk seksyen (jenis 6) | `content_aduan_section` #1 |
4 | Antara muka pengguna (User Interface) mudah digunakan dan tidak mengelirukan | skala 1–5 | field `d1` |
5 | Butiran maklumat/soalan berkaitan dengan urusan yang ingin diselesaikan | skala 1–5 | field `d2` |
6 | Urusan saya dikira selesai selepas menggunakan perkhidmatan digital | skala 1–5 | field `d3` |
7 | Saya mungkin akan mengesyorkan orang lain menggunakan perkhidmatan digital ini | skala 1–5 | field `d4` |
8 | ⚠️ *(perenggan — label SALAH: teksnya salinan soalan #7)* | perenggan | field `cadangan` — **label perlu dibetulkan** |

**Skala:** `scale_min=1`, `scale_max=5`, `scale_labels` = 1 Sangat tidak setuju · 2 Tidak setuju · 3 Kurang pasti · 4 Setuju · 5 Sangat setuju

> ⚠️ **Dua penemuan penting:**
> 1. **FORM 2 (Aduan Integriti) = HTTP 401** — borang **terhad** (perlu Google sign-in + kebenaran).
>    Field **tidak boleh dibaca** tanpa akaun yang diberi akses → **perlu input pengguna**.
> 2. **FORM 3 soalan terakhir (#8)** labelnya salah — teksnya **salinan soalan #7**, patutnya
>    "Cadangan penambahbaikan" (soalan perenggan untuk cadangan).

> **Nota jenis item Google Form:** `2`=radio · `4`=checkbox · `5`=skala · `6` & `8`=tajuk seksyen ·
> `7`=grid · `0`=teks · `1`=perenggan. Jenis 6 & 8 kedua-duanya → `content_aduan_section`.

> **Kesimpulan:** reka bentuk §3 + §4 mampu menampung **kesemua** item dalam FORM B & C
> **tanpa** jenis field khas — bukti enjin borang ini cukup generik.

---

## 14. Plan pelaksanaan (langkah demi langkah)

### Skop akhir (disahkan)

| Item | Cara | Modul? |
|---|---|---|
Aduan Awam (**SISPAA**) | **pautan luar sahaja** — kad butang pada halaman | 🚫 **tiada modul** |
**Aduan Integriti** | borang dalaman + No. tiket + balasan + **akses terhad** | ✅ |
**Kajian Kepuasan Pelanggan 2026** | borang dalaman (12 item) + laporan | ✅ |
**Kajian Kepuasan Perkhidmatan Digital 2026** | borang dalaman (8 item) + laporan | ✅ |

---

### F1 — Asas (M) · "boleh guna"

| # | Kerja | Hasil |
|---|---|---|
1 | Migration **9 table** (§3 + §4) | schema lengkap, tiada migration berulang |
2 | Model (9) + relasi (`submission → answers/attachments/messages`, `form → sections/fields/options`) | asas data |
3 | `RefDataSeeder` + 7 cat (`ADUAN_FORM_TYPE`, `ADUAN_FORM_STATUS`, `ADUAN_STATUS`, `ADUAN_PRIORITY`, `ADUAN_CATEGORY`, `NRES_DEPARTMENT`, `ADUAN_FIELD_TYPE`) | dropdown |
4 | **`AduanFormSeeder`** — 3 borang + seksyen + field + pilihan (data dari Google Form) | 3 borang sedia ada dalam DB |
5 | Enjin render borang (Blade partial ikut `aduan_field_type`) | borang terjana sendiri |
6 | `AduanController@show/store` + `AduanSubmissionRequest` + No. tiket + e-mel | pelawat boleh hantar |
7 | Admin **peti masuk** (`index`) + **butiran tiket** (`show`) + balas + tukar status + assign | admin boleh urus |
8 | Eksport Excel + PDF (pola `FeedbackExport`) | laporan pantas |
9 | 7 permission + `backend_menu` + role mapping | akses dikawal |
10 | `tests/Feature/AduanSubmissionTest.php` + `ContentAduanAdminTest.php` | ujian |
11 | `docs/module/content-aduan.md` | dokumen |

**Acceptance F1:** pelawat boleh hantar Aduan Integriti & 2 Kajian · No. tiket berfungsi ·
admin (dengan akses) boleh balas & tukar status · borang Integriti **hanya** kelihatan kepada
Unit Integriti · ujian lulus.

---

### F2 — Laporan & pengurusan borang (M)

| # | Kerja | Hasil |
|---|---|---|
12 | `ContentAduanReportController` — peratus per option, purata skala, bilangan responden | laporan kajian |
13 | Paparan laporan (kad + bar peratus + jadual) + eksport Excel/PDF | boleh dihantar sebagai laporan rasmi |
14 | UI admin **edit borang/seksyen/field/pilihan** (tanpa drag-drop fancy) | admin boleh ubah borang |
15 | **Duplikasi borang** (untuk "Kajian Kepuasan 2027") | tak perlu taip semula |

**Acceptance F2:** admin boleh cipta Kajian tahun baharu dengan menduplikasi borang lama ·
laporan agregat betul (peratus jumlah 100%, purata skala tepat).

---

### F3 — Frontend portal (M)

| # | Kerja | Hasil |
|---|---|---|
16 | Page `aduan-web` (site `nres`) + komponen `aduan_web` | halaman awam |
17 | Menu **Aduan & Maklum Balas** (BM/EN) | navigasi |
18 | **Kad SISPAA** (pautan luar, `target=_blank`) | saluran rasmi dikekalkan |
19 | Halaman **semak status tiket** (`No. Tiket` + e-mel) | pelawat boleh semak |
20 | Ujian + sahkan live di `/nres/home` | selesai |

**Acceptance F3:** menu portal berfungsi · borang boleh guna pada telefon (responsif) ·
kad SISPAA membuka `nres.spab.gov.my` · semak status hanya berjaya jika no+email padan.

---

### Jumlah: **M + M + M ≈ 4–6 sesi**

---

## 15. Yang saya perlukan sebelum mula F1

1. ✅ Skop — **disahkan** (SISPAA = pautan luar; 3 borang dalaman)
2. ❓ **Field Aduan Integriti** — borang Google-nya perlu sign-in. Cadangan field:
   `Nama*` · `E-mel*` · `Telefon` · `Kategori integriti*` (Rasuah · Salah guna kuasa · Pecah amanah ·
   Konflik kepentingan · Lain-lain) · `Subjek*` · `Butiran*` · `Lampiran` · `Hantar sebagai anonim` (checkbox)
   → **sahkan atau betulkan**
3. ❓ **Akses Aduan Integriti** — siapa boleh baca? (cadangan: permission baharu
   `content-aduan.integrity` untuk **Unit Integriti** sahaja)
4. ❓ **Maklum Balas umum** — perlu ada borang ini juga (jenis `MAKLUM_BALAS`), atau 3 borang itu cukup?




