# Content Parliament Q&A — Soal Jawab Parlimen

Modul admin untuk seksyen **Parlimen** portal: **Soal Jawab Parlimen**, **Kamar Khas**, **MQT** (Ministerial Question Time) dan **RUU** (Rang Undang-Undang). Struktur **sama seperti `content-media`**: parent table + translation table berasingan (bukan self-referencing).

> Portal asal: `nres.gov.my/Parlimen/Pages/default.aspx` (tab: *Soal Jawab Parlimen · Kamar Khas · Pertanyaan Ministerial Question Time (MQT) · Rang Undang-Undang (RUU) · Carian Parlimen · Arkib*).
> Keempat-empat jenis dikongsi **satu modul** kerana strukturnya sama (Dewan → Parlimen/Penggal/Mesyuarat → Tarikh Persidangan → Soalan → Jawapan → Lampiran PDF), dibezakan oleh kolum `parliament_qa_type`.

## 1. Table

**`content_parliament_qa`** (PK `parliament_qa_id`)

| Kolum | Jenis | Nota |
|---|---|---|
`parliament_qa_type` | varchar(50) null | **wajib** — `ref` `PARLIAMENT_QA_TYPE` (`SOAL_JAWAB`, `KAMAR_KHAS`, `MQT`, `RUU`) |
`parliament_qa_question_type` | varchar(50) null | `ref` `PARLIAMENT_QUESTION_TYPE` — **wajib hanya untuk `SOAL_JAWAB`** |
`parliament_qa_dewan` | varchar(50) null | **wajib** — `ref` `PARLIAMENT_DEWAN` (`DEWAN_RAKYAT`, `DEWAN_NEGARA`) |
`parliament_qa_parlimen` | int null | **wajib** — nombor Parlimen (cth. `15`) |
`parliament_qa_penggal` | int null | **wajib** — Penggal (cth. `5`) |
`parliament_qa_sesi` | int null | **wajib** — Mesyuarat (1–9) |
`parliament_qa_date` | date null | **wajib** — Tarikh Persidangan |
`parliament_qa_year` | int null | **wajib** — Tahun Persidangan; asas tapisan/tab tahun arkib |
`parliament_qa_no` | varchar(20) null | Nombor Soalan |
`parliament_qa_member` | varchar(255) null | Ahli / Senator |
`parliament_qa_area` | varchar(255) null | Kawasan (DR) / negeri (DN) |
`parliament_qa_minister` | varchar(255) null | Kementerian yang menjawab |
`parliament_qa_file` | varchar(255) null | **Jawapan PDF** (fail utama, opsional) |
`parliament_qa_status` | varchar(20) null | `ref` `PARLIAMENT_QA_STATUS` (`PUBLISHED`, `DRAFT`, `ARCHIVED`) |
`parliament_qa_sort` | int default 0 | susunan dalam kumpulan sesi; `0` → auto sambung di hujung kumpulan |
`parliament_qa_sector` | varchar(100) null | **disediakan** untuk fasa sekatan akses sektor (belum ada UI/guard) |
`created_by`, `updated_by`, `created_at`, `updated_at` | | |

Index: `parliament_qa_type`, `parliament_qa_question_type`, `parliament_qa_dewan`, `parliament_qa_date`, `parliament_qa_year`, `parliament_qa_sector`.

**`content_parliament_qa_translations`** (PK `parliament_qa_translation_id`)

| Kolum | Jenis | Nota |
|---|---|---|
`parliament_qa_translation_parent_id` | int null | FK → `parliament_qa_id` |
`parliament_qa_translation_title` | varchar(255) null | tajuk (main-language wajib) |
`parliament_qa_translation_question` | text null | teks soalan penuh |
`parliament_qa_translation_answer` | longText null | teks jawapan penuh |
`parliament_qa_translation_main` | int null | `1` = bahasa utama |
`parliament_qa_translation_language` | varchar(255) null | kod bahasa (`ref` `LANGUAGE`) |
`created_by`, `updated_by`, `created_at`, `updated_at` | | |

Index: `parliament_qa_translation_parent_id` (nama `parliament_qa_translations_parent_index` — had 64 aksara MySQL).

**`content_parliament_qa_categories`** (PK `parliament_qa_category_id`)

| Kolum | Jenis | Nota |
|---|---|---|
`parliament_qa_category_parent_id` | int null | FK → `parliament_qa_id` |
`parliament_qa_category_code` | varchar(100) null | kod `ref` `PARLIAMENT_CATEGORY` |
`parliament_qa_category_sort` | int null | susunan paparan |
`created_by`, `updated_by`, `created_at`, `updated_at` | | |

Index: `parliament_qa_categories_parent_index`, `parliament_qa_categories_code_index`.

> Portal asal menyimpan kategori sebagai nilai berbilang (`;#Mineral;#Perhutanan;#`). Disimpan sebagai baris berasingan supaya boleh ditapis (`whereHas`) dan dikira — bukan kolum teks yang perlu `LIKE`.

**`content_parliament_qa_documents`** (PK `parliament_qa_document_id`)

| Kolum | Jenis | Nota |
|---|---|---|
`parliament_qa_document_parent_id` | int null | FK → `parliament_qa_id` |
`parliament_qa_document_label` | varchar(255) null | label (auto dari nama fail) |
`parliament_qa_document_file` | varchar(255) null | laluan fail lampiran |
`parliament_qa_document_sort` | int null | susunan |
`created_by`, `updated_by`, `created_at`, `updated_at` | | |

Index: `parliament_qa_documents_parent_index`.

## 2. Fail

| Jenis | Path |
|---|---|
Model | `app/Models/Backend/ContentParliamentQa.php`, `ContentParliamentQaTranslation.php`, `ContentParliamentQaCategory.php`, `ContentParliamentQaDocument.php` |
Request | `app/Http/Requests/Backend/ContentParliamentQaRequest.php` |
Controller | `app/Http/Controllers/Backend/ContentParliamentQaController.php` |
Views | `resources/views/backend/module/parliamentQa/{index,create,edit,_form}.blade.php` + `partials/{_select,_date-input}.blade.php` |
Storan | `storage/app/public/content-parliament-qa/{files,attachments}` |
Migration | `database/migrations/2026_10_23_100001_create_content_parliament_qa_table.php`, `…_100002_…_translations_…`, `…_100003_…_categories_…`, `…_100004_…_documents_…` |
Test | `tests/Feature/ContentParliamentQaTest.php` |
Menu/Permission | `database/seeders/BackendMenuSeeder.php` (**Content → Parliament Q&A**) |
Ref | `database/seeders/RefDataSeeder.php` (`PARLIAMENT_QA_TYPE`, `PARLIAMENT_QUESTION_TYPE`, `PARLIAMENT_DEWAN`, `PARLIAMENT_CATEGORY`, `PARLIAMENT_QA_STATUS`) |

## 3. Routes

| Method | URI | Permission | Name |
|---|---|---|---|
GET | `/content-parliament-qa` | `content-parliament-qa.view` | `content-parliament-qa.index` |
GET | `/content-parliament-qa/create` | `.create` | `content-parliament-qa.create` |
POST | `/content-parliament-qa` | `.create` | `content-parliament-qa.store` |
GET | `/content-parliament-qa/{id}/edit` | `.update` | `content-parliament-qa.edit` |
PUT | `/content-parliament-qa/{id}` | `.update` | `content-parliament-qa.update` |
DELETE | `/content-parliament-qa/{id}` | `.delete` | `content-parliament-qa.destroy` |
POST | `/content-parliament-qa/reorder` | `.update` | `content-parliament-qa.reorder` |
DELETE | `/content-parliament-qa/documents/{id}` | `.delete` | `content-parliament-qa.documents.destroy` |

## 4. Admin UI

**Senarai (index)**

- Tapisan (satu baris, `sm:flex-nowrap`): **Type · House (Dewan) · Category · Year · Status** + **carian** (`parliament_qa_no`, `parliament_qa_member`, `parliament_qa_area`, tajuk & teks soalan).
- Kolum: `#` (drag handle) · Title (+ bilangan bahasa) · Type (+ badge kecil Question Type) · House · Session (`15 / 5 / 1`, tooltip `Parlimen 15 · Penggal 5 · Mesyuarat 1` dari `sessionLabel()`) · Category (2 pertama + `+n`) · Member (+ Area) · Date · Status · Sort · Actions.
- **Drag & drop** aktif hanya bila tapisan **Type** dipilih dan senarai > 1 baris (ikon hijau = boleh susun, kuning = tambah ≥2 item, merah = pilih Type dahulu). Susunan disimpan oleh `reorder()` dalam skop Type (+ Dewan + Tahun).
- Tanpa tapisan Type, senarai diisih: Tahun (menurun) → Type → Sort → ID.

**Borang (create/edit)** — baris demi baris mengikut corak `content-media`:

| Seksyen | Kandungan |
|---|---|
Translations | Pemilih bahasa (Alpine) · **Title*** · **Question** · **Answer** · checkbox *Main Translation* |
Parliament Information | `Type*` · `Question Type` · `House (Dewan)*` → `Parliament No.*` · `Term (Penggal)*` · `Meeting (Sesi)*` → `Parliament Date*` · `Parliament Year*` · `Question No.` → `Member / Senator` · `Area / Constituency` · `Ministry` → **Categories** (checkbox grid) → `Status*` · `Sort` |
Answer File | **Answer PDF** (FilePond, `.pdf`, max 15MB) — pautan fail lama dipaparkan bila edit |
Attachments | Senarai lampiran sedia ada (dengan butang padam) + FilePond `attachments[]` (multiple, sehingga 20 fail) |

- **Tahun Persidangan** diisi automatik dari Tarikh Persidangan bila medan tahun masih kosong.
- Penanda `*` pada **Question Type** bertukar dinamik (`data-required-mark`) ikut pilihan Type — sama corak dengan `Source` dalam `content-media`.
- `Sort = 0` → sistem letak di hujung kumpulan sesi yang sama (`nextSort()`).
- Borang lampiran padam berada **di luar** borang utama (HTML tidak benarkan form bertindih) dan dihantar melalui route `…documents.destroy` dengan pengesahan SweetAlert.

## 5. Pengesahan

`ContentParliamentQaRequest`:

| Medan | Peraturan |
|---|---|
`parliament_qa_type` | `required|string|max:50` |
`parliament_qa_question_type` | `nullable` — **wajib bila `parliament_qa_type = SOAL_JAWAB`** (mesej: *The question type is required for Parliament Question & Answer.*) |
`parliament_qa_dewan` | `required|string|max:50` |
`parliament_qa_parlimen` | `required|integer|min:1|max:99` |
`parliament_qa_penggal` | `required|integer|min:1|max:99` |
`parliament_qa_sesi` | `required|integer|min:1|max:9` |
`parliament_qa_date` | `required|date` |
`parliament_qa_year` | `required|integer|min:1959|max:2100` |
`parliament_qa_no` / `_member` / `_area` / `_minister` | `nullable|string` (`no` max 20, lain-lain max 255) |
`parliament_qa_file` | `nullable|file|mimes:pdf|max:15360` |
`parliament_qa_status` | `required|string|max:20` |
`categories[]` | `nullable|array` + setiap kod `string|max:100`; kod yang **tiada dalam ref** `PARLIAMENT_CATEGORY` diabaikan (`syncCategories()`) |
`attachments[]` | `nullable|array` + `file|mimes:pdf,doc,docx,xls,xlsx,zip|max:15360` |
`translations[lang][title]` | **tajuk bahasa utama wajib** (bahasa utama = `main = 1`, jika tiada → bahasa pertama) |
`translations[lang][question]` / `[answer]` | `nullable|string` |

Semua mesej ralat dalam **Bahasa Inggeris** dan pengesahan sepenuhnya di **server** (tiada atribut HTML `required`).

## 6. Ref

| Cat | Kod |
|---|---|
`PARLIAMENT_QA_TYPE` | `SOAL_JAWAB`, `KAMAR_KHAS`, `MQT`, `RUU` |
`PARLIAMENT_QUESTION_TYPE` | `LISAN`, `BERTULIS` |
`PARLIAMENT_DEWAN` | `DEWAN_RAKYAT`, `DEWAN_NEGARA` |
`PARLIAMENT_CATEGORY` | `PERHUTANAN`, `BIODIVERSITI`, `PERUBAHAN_IKLIM`, `MINERAL`, `GEOSAINS`, `ALAM_SEKITAR`, `TANAH`, `UKUR_PEMETAAN`, `GEOSPATIAL` |
`PARLIAMENT_QA_STATUS` | `PUBLISHED`, `DRAFT`, `ARCHIVED` |

Label dropdown guna `descr_en` fallback `descr` (`ContentParliamentQaController::refOptions()`). Kategori diambil terus dari halaman portal asal (9 kategori) — tambah kod baru di halaman **Ref** tanpa ubah kod.

## 7. Skop portal

| Halaman portal | Modul |
|---|---|
Soal Jawab Parlimen (`/Parlimen/Pages/default.aspx` + sesi `{tahun}-DR{n}.aspx` / `{tahun}-DN{n}.aspx`) | **`content-parliament-qa`** (`SOAL_JAWAB`) |
Kamar Khas (`KamarKhas.aspx`) | **`content-parliament-qa`** (`KAMAR_KHAS`) |
Pertanyaan Ministerial Question Time (`MQT.aspx`) | **`content-parliament-qa`** (`MQT`) |
Rang Undang-Undang (`RUU.aspx`) | **`content-parliament-qa`** (`RUU`) |
Arkib Soal Jawab Parlimen (`ArkibSoalJawabParlimen.aspx`, tab tahun) | **`content-parliament-qa`** (`parliament_qa_year`) |
Butiran jawapan (`papar-Jawapan-Parlimen.aspx?ID=…`) | `?id=` pada halaman frontend (fasa P4) |

**Nota bukti portal:** halaman butiran memaparkan `Pertanyaan Lisan/Bertulis · Dewan · Parlimen · Penggal · Sesi · Tarikh Persidangan · Tahun Persidangan · Kategori · Nombor Soalan · Soalan · Jawapan · Attachments` — kesemua medan ini ada dalam modul (kategori sebagai multi-value, attachments sebagai `content_parliament_qa_documents`). Nama ahli & kawasan pada portal asal berada **di dalam teks soalan**; modul ini mengasingkannya (`parliament_qa_member`, `parliament_qa_area`) supaya boleh ditapis dan dicari.

## 8. Ujian

`tests/Feature/ContentParliamentQaTest.php` — **22 ujian (134 assertions)**:

- index & create render; borang memaparkan semua medan Parlimen
- penanda `*` pada **Question Type** dinamik (`data-required-mark`) dan **tiada** `setAttribute('required')`
- store: simpan fail (`content-parliament-qa/files`) + lampiran (`…/attachments`) + 2 translation + kategori
- pengesahan: `type` wajib · Dewan/Parlimen/Penggal/Sesi/Tarikh/Tahun/Status wajib · **question type wajib hanya untuk `SOAL_JAWAB`** · RUU lulus tanpa question type & tanpa fail · tajuk bahasa utama wajib
- kod kategori yang tiada dalam ref diabaikan
- `Sort = 0` → auto sambung di hujung kumpulan sesi
- update: ganti translation + kategori, fail lama kekal bila tidak diganti · ganti fail jawapan + tambah lampiran
- destroy: translation + kategori + lampiran + fail terpadam
- tapisan **Type / House / Category / Year / Status** · carian no. soalan & nama ahli
- reorder: kemas `parliament_qa_sort` dalam skop Type sahaja · `type` wajib (422)
- padam lampiran: fail terpadam + redirect ke halaman edit
- `sessionLabel()` format portal · halaman edit memaparkan lampiran sedia ada
- baris tapisan kekal satu baris (`sm:flex-nowrap`, `min-w-0 flex-1`, `lg:w-56 shrink-0`)

## 9. Frontend — komponen `parliament_qa_web`

Komponen frontend sudah dibina dan **dipasang pada page 23** (`Parliament QA`, slug `parliament-qa`, site `nres`). URL portal: **`/nres/parliament-qa`**.

| Item | Nilai |
|---|---|
Komponen (DB) | `frontend_components.code = 'parliament_qa_web'` · name `Parliament QA Web` · type `PHP` · category `Content` · status `1` |
`content` | `@include('frontend.components.parliament-qa-web')` |
Sync | `storage/app/private/PHP/parliament_qa_web.blade.php` (melalui `BladeSyncService`) |
View | `resources/views/frontend/components/parliament-qa-web.blade.php` |
Partial | `resources/views/frontend/components/partials/parliament-qa-item.blade.php` (kad satu rekod) |
Page | `frontend_pages` id **23** · `frontend_page_components` (page 23 → component, `sort = 0`) · `page_content = {!! $parliament_qa_web !!}` → `storage/app/private/PAGE_CONTENT/nres/parliament-qa.blade.php` |

**Reka bentuk** mengikut portal asal (`/Parlimen/Pages/default.aspx`): 6 tab seksyen, tajuk **Dewan Sesi** + kumpulan sesi ikut Dewan, kad rekod gaya portal (`Jenis Soalan : … | Kategori : …`, `No. Soalan : …`, teks soalan, butang *Papar Jawapan*), halaman butiran dengan semua label portal + Soalan + Jawapan + Attachments, tab tahun **Arkib**, dan borang **Carian Parlimen**. CSS self-contained (kelas `.parliament-*`) — tidak bergantung pada framework site, jadi ia berfungsi pada layout mana-mana site.

**Parameter URL**

| Parameter | Nilai | Fungsi |
|---|---|---|
`tab` | `soaljawab` (lalai) · `kamarkhas` · `mqt` · `ruu` · `carian` · `arkib` | Pilih seksyen |
`dewan` | kod `PARLIAMENT_DEWAN` | Tapisan sesi |
`parlimen` · `penggal` · `sesi` | integer | Pilih sesi — bila ketiga-tiganya ada, senarai item sesi dipaparkan |
`id` | `parliament_qa_id` | Papar soalan + jawapan penuh + lampiran |
`q` | teks | Carian: no. soalan, ahli, kawasan, tajuk, soalan, jawapan |
`jenis` | `LISAN` / `BERTULIS` | Tapisan carian |
`category` | kod `PARLIAMENT_CATEGORY` | Tapisan carian |
`year` | tahun | Tapisan carian & tab Arkib |

**Aliran** (contoh URL):

| Skrin | URL |
|---|---|
Senarai Dewan Sesi | `/nres/parliament-qa?tab=soaljawab` |
Senarai item satu sesi | `?tab=soaljawab&dewan=DEWAN_NEGARA&parlimen=15&penggal=5&sesi=1` |
Butiran soalan + jawapan | `?tab=soaljawab&dewan=DEWAN_NEGARA&parlimen=15&penggal=5&sesi=1&id=1` |
Carian Parlimen | `?tab=carian&q=Net+Zero` |
Arkib (tab tahun) | `?tab=arkib&year=2026` |

**Guna pada page lain:** pilih komponen **Parliament QA Web** (`parliament_qa_web`) pada page editor, kemudian letak `{!! $parliament_qa_web !!}` dalam **Page Content**. Untuk cipta semula komponen: **Frontend → Components** di admin, atau `php artisan tinker` →

```php
App\Models\Backend\FrontendComponent::updateOrCreate(
    ['code' => 'parliament_qa_web'],
    ['name' => 'Parliament QA Web', 'content' => "@include('frontend.components.parliament-qa-web')",
     'type' => 'PHP', 'category' => 'Content', 'status' => 1]
);
app(App\Services\BladeSyncService::class)->syncAll();
```

**Pengesahan:** dirender tanpa ralat untuk 7 senario (senarai sesi · item sesi · butiran · carian kosong · carian berhasil · arkib · RUU kosong) dan diuji melalui pelayan dev — `HTTP 200` + kandungan betul untuk `/nres/parliament-qa`, `…&sesi=1` (kad `No. Soalan : 1`) dan `…&id=1` (teks jawapan).

## 10. Fasa seterusnya (belum dibuat)

1. **Import data lama (P5)** — portal asal menyimpan 2017–2026 (ratusan rekod) dalam SharePoint; skrip rujukan `storage/app/tmp_parliament_import.php` (fetch halaman butiran ikut ID → parse → insert, idempotent + `PARLIAMENT_DRY_RUN=1`). Untuk produksi: jadikan `ContentParliamentQaImportController` (precedent: `ContentDirectoryStaffImportController`).
2. **Menu & quicklink** — daftar `frontend_menu` bawah **Warga → Perkhidmatan Digital** + quicklink **e-Perkhidmatan** (halaman sudah boleh diakses tanpa menu).
3. **Sekatan akses sektor** — isi `parliament_qa_sector` + guard peringkat baris (trait `SectorScoped`).



