# SANGGRALOKA ONE — Laporan QC Integrator (8 Jul 2026)

Gerbang kualitas akhir sebelum serah terima. Referensi:
`docs/PM-PLAN-SANGGRALOKA-ONE.md` §6 (QC gate) + `docs/DESIGN-SYSTEM.md` §8.
Lingkup: `backend/` — modul FIX (existing) + CREW (`hr_`) + SELL (`mkt_`).

Metode: lint shell (`php -l`), migrasi via MySQL CLI di DB uji terpisah
(`qc_sanggraloka_one`, dibuat & di-DROP di akhir sesi — tidak menyentuh
`hotelfix`/`hotelfix2` yang sudah ada), grep untuk gate design/ACL, dan smoke
test CLI (bootstrap manual + config sementara di scratchpad — **tidak
menimpa** `backend/config/config.php` yang sudah ada).

---

## Ringkasan per butir checklist

| # | Butir | Hasil |
|---|---|---|
| 1 | Lint PHP (`src/` + `public/`, 217+96 file) | **LOLOS** — 0 error |
| 2 | Migrasi 0001→0012 di DB uji kosong | **LOLOS** — bersih, 0012 juga jalan tanpa perbaikan |
| 3 | Gap `broadcasts.app_source` | **DIPERBAIKI** — migrasi baru `0013` |
| 4 | Nav↔File (Acl↔public/) | **LOLOS** — semua href Acl punya file; file tanpa nav semuanya wajar (detail page/system page) |
| 5 | Thin file → kelas Page ada | **LOLOS** — semua `public/*.php` memanggil kelas `App\Page\*Page` yang benar-benar ada |
| 6 | Smoke render CLI (UiBuilder + TaskBoardService) | **LOLOS** — semua method callable, board() gabung task CREW+SELL+WO FIX dari seed demo |
| 7 | Gate design (`<div`/`style=` di Hr*/Mkt*, hex di app.css) | **DIPERBAIKI** — 5 pelanggaran `<div` ditemukan & diperbaiki; sekarang 0. Hex di app.css sudah 0 (kecuali `#fff`, diizinkan) |
| 8 | Zona merah payroll (`$allowedRoles`) | **LOLOS** — `Role::PAYROLL_ROLES = [hr_admin, fc, gm]`, dipakai persis oleh `HrPayrollPage`/`HrPayrollParamsPage` |
| 9 | DB uji + file sementara dibersihkan | **SELESAI** |

Tambahan yang ditemukan sendiri saat QC (di luar checklist asli, lihat P1 di bawah):
**gap besar** — `user_property_access.role` ENUM tidak pernah diperluas untuk
10 role baru CREW/SELL dan tidak ada seed user dengan role tersebut sama
sekali → sebelum migrasi `0014`, mustahil login sebagai gm/hr_admin/hr_staff/
fc/supervisor/employee/sales_manager/sales/ewm/design. **Sudah diperbaiki**
(migrasi ditambahkan; lihat P1 di bawah untuk sisa pekerjaan non-teknis).

---

## Perbaikan yang dilakukan

### 1. Migrasi `0013_broadcast_app_source.sql` (butir 3, ditugaskan)
`ALTER TABLE broadcasts ADD COLUMN app_source ENUM('fix','hrd','sell') NOT NULL
DEFAULT 'fix'`. Diuji jalan bersih di DB uji. **Tidak** mengubah
`HrEngagementPage.php`/`engagement.php` — menambah filter `app_source` di
`Broadcast::listForProperty()` + pemanggilnya bukan perubahan sepele (butuh
keputusan default filter per halaman pemanggil lain juga), jadi hanya
skema yang ditambahkan; pemakaian filter dicatat sebagai P3 di bawah.

### 2. Migrasi `0014_crew_sell_role_enum_seed.sql` (temuan QC sendiri, P1)
Ditemukan saat menyiapkan smoke test login: `user_property_access.role` ENUM
masih persis 6 nilai lama (`super_admin,property_admin,dept_head,technician,
reporter,viewer`) walau `PM-PLAN-SANGGRALOKA-ONE.md` §2 + `src/Auth/Role.php`
+ `src/Auth/Acl.php` + SEMUA `$allowedRoles` di halaman Hr*/Mkt* sudah
mengasumsikan 10 role baru. Tidak ada satu pun baris di 0006-0011 yang
memberi role baru ke user manapun — modul CREW & SELL secara fungsional
**tidak bisa dites lewat browser sama sekali** sebelum ini.

Migrasi `0014`:
- **Bagian A** — `ALTER TABLE user_property_access MODIFY role ENUM(...)`
  menambah 10 nilai baru (role lama tidak berubah).
- **Bagian B** — 10 user **DEV-ONLY** (id 900-909, `qc.gm`, `qc.hr_admin`,
  dst; password `password`, hash sama dgn `0002_seed_master.sql`), satu per
  role baru, khusus supaya QC/UAT bisa login. **Bukan** penetapan role staf
  Sanggraloka sungguhan (lihat P1 — itu keputusan HR/GM, di luar wewenang QC;
  tidak mengarang fakta personel).

Diverifikasi: migrasi jalan bersih; smoke test CLI membuktikan
`password_verify('password', ...)` cocok dan `Acl::visibleMenu()` memberi
menu yang benar (mis. `qc.gm` → 5 grup/37 item, `qc.sales_manager` → 4
grup/19 item, `qc.hr_admin` → 4 grup/28 item) untuk ketiga role yang dicoba.

### 3. Gate design — 5 pelanggaran `<div`/`style=` diperbaiki (butir 7)
Ditemukan HTML mentah langsung di kelas `Page` (bukan lewat `UiBuilder`):

| File | Sebelum | Sesudah |
|---|---|---|
| `HrBriefingDailyPage.php:160` | `'<div class="form__actions">...btn--primary...Publikasikan briefing...'` | `$this->ui->formClose('Publikasikan briefing')` |
| `HrBriefingDailyPage.php:173` | idem, varian `btn--default` | `$this->ui->formClose('Acknowledge kehadiran', 'default')` |
| `HrEmployeeListPage.php:113` | idem, varian `btn--default` | `$this->ui->formClose('Jadwalkan reminder', 'default')` |
| `HrRosterPage.php:105` | idem, varian `btn--primary` | `$this->ui->formClose('Publikasikan minggu ini')` |
| `HrRosterPage.php:158` | `'<div class="table-scroll"><table class="data-table">...'` (grid roster bespoke, kolom dinamis + mini-form per sel) | `$this->ui->tableRaw($head, $body)` |

Perubahan di `UiBuilder.php` (dua penambahan kecil, backward-compatible):
- `formClose(string $submitLabel = 'Simpan', string $variant = 'primary')`
  — tambah parameter opsional `$variant` (43 pemanggil lain di seluruh
  `src/Page/*.php` tidak terpengaruh, semuanya pakai default `'primary'`).
- `tableRaw(string $theadRowHtml, string $tbodyHtml)` — pembungkus tabel
  untuk grid bespoke yang tak cocok bentuk flat `table()`/`tableSortable()`
  (dipakai `HrRosterPage` untuk grid roster mingguan dgn kontrol per sel).
  Markup identik dengan yang sudah dipakai `table()` sendiri — tidak
  menambah pola visual baru.

Setelah perbaikan: `grep -n "<div" src/Page/Hr*.php src/Page/Mkt*.php` dan
`grep -n "style=" src/Page/Hr*.php src/Page/Mkt*.php` → **0 hasil**. Lint
ulang ke-3 file Page + `UiBuilder.php` → bersih. Smoke test UiBuilder
diulang → semua method masih callable.

---

## Detail temuan per butir (yang tidak butuh perbaikan)

**Butir 4 (Nav↔File):** semua `href` di `Acl::allowedHrefs()` (semua role,
termasuk 10 role baru) punya file `.php` yang ada di `backend/public/`. File
yang ADA tapi TIDAK ada di nav manapun — semuanya wajar (dicatat, tidak
diperbaiki): halaman detail yang diakses lewat link dari daftar
(`asset.php`, `asset-form.php`, `booking.php`, `calendar-day.php`,
`employee.php`, `location.php`, `partner.php`, `task.php`, `work-order.php`,
`change-password.php`, `checklist-builder.php`, `pm-plan.php`,
`reports-export.php`, `file.php`), halaman sistem (`login.php`, `logout.php`,
`router.php`, `select-property.php`), dan `styleguide.php` (dev tool,
`$allowedRoles` sendiri).

**Butir 5 (Thin file):** seluruh 76 file `public/*.php` (halaman web) yang
memanggil kelas diverifikasi memanggil `App\Page\<Nama>Page` yang filenya
memang ada di `src/Page/`. File lain di `public/` (endpoint API, `router.php`,
`file.php`, `logout.php`, `reports-export.php`) memang bukan pola thin-Page
by design (endpoint non-HTML/khusus).

**Butir 6 (Smoke render):** `StyleguidePage` ada & `$allowedRoles` sendiri.
Ketujuh method `UiBuilder` baru (`pageHeader, metricCard, kanbanBoard,
calendarMonth, guestCard, wizardSteps, emptyState`) dipanggil dengan data
contoh via CLI — semua mengembalikan HTML non-kosong tanpa fatal.
`TaskBoardService::createTask()` dicoba 2x (source_app crew & sell) lalu
`board()` ditarik untuk property 1: task CREW & SELL yang baru dibuat
muncul di kolom yang benar, dan **work order FIX** dari seed demo (0004)
ikut tampil di papan (kolom open/in_progress) sebagaimana kontrak §"work
order terbuka -> kartu FIX read-only". Kolom `done` kosong pada smoke test
ini — **bukan bug**: `board()` sengaja hanya menampilkan WO dengan status
`OPEN_STATUSES` (`scheduled/reported/in_progress/waiting_parts`), WO yang
sudah `done/verified` memang tidak dimaksudkan tampil di papan gabungan
(lihat komentar "work_orders terbuka" di `TaskBoardService::board()`).

**Butir 8 (Zona merah):** `Role::PAYROLL_ROLES = [HR_ADMIN, FC, GM]`
(`src/Auth/Role.php:39`), dipakai langsung sebagai `$allowedRoles` di
`HrPayrollPage.php:22` dan `HrPayrollParamsPage.php:22`. Tidak ada
penyimpangan.

---

## Isu tersisa (prioritas)

**P1 — Blocker**
1. **Assignment role staf Sanggraloka sungguhan.** Migrasi `0014` hanya
   membuka kapabilitas teknis (ENUM + 10 akun QC dummy). Belum ada
   keputusan siapa staf nyata (di antara user id 501-592, hasil
   `0006_seed_real_users.sql`) yang sebenarnya berperan `gm`, `hr_admin`,
   `hr_staff`, `fc`, `supervisor`, `sales_manager`, `sales`, `ewm`, `design`.
   Ini keputusan HR/GM (di luar wewenang QC) — perlu migrasi susulan
   `UPDATE user_property_access SET role=... WHERE user_id=...` setelah
   org chart per-role dikonfirmasi. Sebelum itu, gunakan 10 akun `qc.*`
   (password `password`) untuk UAT fungsional.

**P2 — Penting**
1. **Filter `app_source` di `engagement.php` belum dipakai.** Kolom
   sudah ada (migrasi 0013) tapi `Broadcast::listForProperty()` /
   `HrEngagementPage::render()` belum memfilternya — pengumuman modul lain
   masih tercampur di daftar engagement. Perlu keputusan default per
   halaman pemanggil (broadcast.php FIX vs engagement.php CREW), sedikit
   di luar "perbaikan kecil" QC.
2. **`must_change_password`** — semua user hasil `0002`/`0006` ditandai
   wajib ganti sandi pertama kali (migrasi 0007). Konfirmasi alur
   `ChangePasswordPage` tidak menghalangi skenario UAT cepat (mis. reset
   massal via SQL bila perlu demo cepat tanpa ganti sandi satu-satu).

**P3 — Nice to have**
1. Halaman detail/system yang tidak muncul di nav manapun (lihat daftar di
   Butir 4) — sudah sesuai desain (diakses via link), tidak perlu aksi,
   hanya dicatat untuk transparansi audit nav.

---

## Cara menjalankan aplikasi (XAMPP)

1. **Config**: `backend/config/config.php` **sudah ada** (menunjuk DB
   `hotelfix2`, `127.0.0.1:3306`, user `root`). Untuk lingkungan baru:
   `copy backend\config\config.sample.php backend\config\config.php` lalu
   isi kredensial DB nyata.
2. **Migrasi**: dari `backend/`, jalankan
   `C:\xampp\php\php.exe bin\migrate.php` (idempoten, mencatat status di
   tabel `schema_migrations`; `--status` untuk cek tanpa eksekusi). Migrasi
   0001→0014 harus jalan berurutan tanpa error (sudah diverifikasi QC di DB
   uji terpisah).
3. **Jalankan server** (dev, built-in PHP):
   `C:\xampp\php\php.exe -S localhost:8080 -t backend\public backend\public\router.php`
   — atau via Apache XAMPP dengan vhost menunjuk `backend/public` sebagai
   document root (pastikan `.htaccess` aktif untuk `/api/v1/*` pretty path).
4. **Login seed** (password semua user: `password`, kecuali sudah diganti):
   - Role FIX asli (dari `0002_seed_master.sql`): `super.admin`,
     `property.admin`, `depthead.eng`, `tech.candra`, `hk.reporter`,
     `gm.viewer`.
   - Staf nyata Sanggraloka (`0006_seed_real_users.sql`): role FIX lama saja
     (viewer/reporter/dept_head/technician) — lihat P1, belum ada role
     CREW/SELL.
   - **Role CREW/SELL baru** (migrasi `0014`, khusus QC/UAT):
     `qc.gm`, `qc.hr_admin`, `qc.hr_staff`, `qc.fc`, `qc.supervisor`,
     `qc.employee`, `qc.sales_manager`, `qc.sales`, `qc.ewm`, `qc.design`
     — satu-satunya cara login & menguji modul CREW/SELL saat ini.
