diff --git a/app.vue b/app.vue
index be705dd..fa3c801 100644
--- a/app.vue
+++ b/app.vue
@@ -13,7 +13,7 @@ nuxtApp.hook('page:finish', () => {
// Naikkan versi ini setiap kali ada perubahan struktur state (schema migration).
// Saat versi tidak cocok, semua cache Pinia akan dibersihkan otomatis.
-const STORE_SCHEMA_VERSION = 3;
+const STORE_SCHEMA_VERSION = 4;
const VERSION_KEY = 'app-store-version';
// Daftar semua localStorage key yang dikelola oleh Pinia stores
diff --git a/components/features/queue/CurrentPatientCard.vue b/components/features/queue/CurrentPatientCard.vue
index 7386991..37e45df 100644
--- a/components/features/queue/CurrentPatientCard.vue
+++ b/components/features/queue/CurrentPatientCard.vue
@@ -3,17 +3,31 @@
@@ -138,11 +152,30 @@
+
+
+
diff --git a/composables/useQueueSync.ts b/composables/useQueueSync.ts
index c67fc59..96e5636 100644
--- a/composables/useQueueSync.ts
+++ b/composables/useQueueSync.ts
@@ -170,6 +170,11 @@ export const useQueueSync = (deps: QueueSyncDeps) => {
let _autoSyncInterval: any = null;
+ /**
+ * Memulai interval auto-sync (fallback) untuk memastikan data tetap segar
+ * meskipun koneksi WebSocket terputus atau gagal menerima event.
+ * Berjalan setiap 30 detik berdasarkan active interest.
+ */
const startAutoSync = () => {
if (typeof window === 'undefined') return;
if (_autoSyncInterval) return;
diff --git a/docs/DEVLOG.md b/docs/DEVLOG.md
index 3a98ede..aaa98ea 100644
--- a/docs/DEVLOG.md
+++ b/docs/DEVLOG.md
@@ -63,6 +63,36 @@ Format output:
+## 2026-07-10 — Fitur Hubungkan Data Pasien ke SIMRS
+
+**Sprint/Phase:** Phase 7 — UI/UX Feature
+**Durasi:** ~1.5 jam
+**Status:** ✅ Done
+
+### Yang Dikerjakan
+- Membuat UI Pop-up Modal baru `PatientDataDialog.vue` untuk mengaitkan nomor antrean yang berjalan dengan data dari SIMRS.
+- Menambahkan tombol **Data Pasien** (ikon *user-cog*) di komponen `CurrentPatientCard.vue` berdampingan dengan tombol **Panggil**.
+- Mengimplementasikan state management lokal untuk pop-up, termasuk simulasi proses API (*loading bar*, *mock data*, dan *success notification*).
+- Menyesuaikan warna komponen secara presisi menggunakan hex code `#003482` dengan *SCSS override* untuk menembus batasan tema *default*.
+- Meningkatkan *border-radius* modal ke `xl` untuk sudut yang lebih melengkung (*rounded*) sesuai desain UI/UX.
+- Mengubah logika *mock data* pada *method* `searchPatient` menjadi `$fetch` (*API call*) asinkron yang menembak internal API Nuxt (`/api/simrs/patient/[id]`).
+- Mengimplementasikan endpoint API backend (menggunakan *Nitro/H3*) di folder `server/api/simrs/patient/[id].get.ts` sebagai *Dummy JSON Provider* untuk di-*replace* dengan layanan asli nanti.
+- Melengkapi fungsi-fungsi yang baru dibuat dengan komentar JSDoc.
+
+### Keputusan Teknis
+> **Dummy API via Nitro**: Dengan memindahkan *mock data* dari dalam komponen Vue ke `server/api/`, arsitektur kode simulasi jadi lebih mirip dengan *production*. Saat pindah ke *Service API* sungguhan nanti, front-end developer hanya perlu mengganti URL string `$fetch` tanpa harus membongkar ulang struktur logika _async_ `try-catch`-nya.
+> **Pemisahan Komponen**: Daripada menggabungkan form SIMRS ke dalam `CurrentPatientCard`, diputuskan untuk membuat file baru `PatientDataDialog.vue`. Ini menjaga agar file loket tidak terlalu membengkak (modular) dan mudah dikelola atau diganti endpoint API-nya nanti.
+
+### Masalah & Solusi
+| Masalah | Solusi | Referensi |
+|---------|--------|-----------|
+| Warna tombol garis luar (*outlined*) selalu kembali ke warna hitam (*fallback*) dan tidak menyerap properti `color="primary"`. | Menambahkan *style SCSS* kustom secara paksa (`!important`) untuk mengisi `border-color` dan `color` sesuai `hex code` `#003482` dari desain. | `CurrentPatientCard.vue`, `PatientDataDialog.vue` |
+
+### Besok
+- [ ] Mengganti endpoint `/api/simrs/patient/` internal ini dengan URL *Service API* SIMRS yang sebenarnya saat endpoint backend sudah di-*deploy*.
+
+---
+
## 2026-07-10 — Migrasi API Endpoint & Project Knowledge Base
**Sprint/Phase:** Phase 7 — Infrastruktur & Dokumentasi
@@ -720,6 +750,22 @@ Format output:
---
+## 2026-07-13 — Konsolidasi Loket Eksekutif
+
+**Sprint/Phase:** Phase 7
+**Status:** ✅ Done
+
+### Yang Dikerjakan
+- Mengubah `localLoketData` pada `loketStore.js` dari 14 loket menjadi 1 loket tunggal (`LOKET EKS`) untuk melayani semua klinik eksekutif (ID 1000 hingga 1022).
+- Menaikkan `STORE_SCHEMA_VERSION` pada `app.vue` (menjadi v4) untuk memicu pembersihan *cache localStorage* pada pengguna agar konfigurasi loket tunggal segera diterapkan.
+
+### Keputusan Teknis
+- Mengubah default pembuatan array untuk `localLoketData` menjadi 1 objek.
+- Properti `pelayanan` (hak akses layanan) diberikan semua ID layanan dari 1000 - 1022.
+- Tidak ada penambahan/perubahan logika UI karena antrean dan grid UI otomatis menyesuaikan jumlah `loketStore`.
+
+---
+
---
diff --git a/docs/DEVPLAN.md b/docs/DEVPLAN.md
index 5b08de0..08d08d9 100644
--- a/docs/DEVPLAN.md
+++ b/docs/DEVPLAN.md
@@ -258,5 +258,6 @@ docs: update DEVPLAN
| Versi | Tanggal | Author | Perubahan |
|-------|---------|--------|-----------|
+| 1.1.1 | 2026-07-13 | Agent | Refactor `loketStore.js` untuk menggabungkan loket eksekutif menjadi 1 loket tunggal (melayani semua) |
| 1.1.0 | 2026-07-10 | Akbar | Phase 7: migrasi API, knowledge base, update docs |
| 1.0.0 | 2026-05-25 | Akbar | Initial plan — direkonstruksi berdasarkan timeline aktual |
diff --git a/docs/hakakses implementation_plan_v2.md b/docs/hakakses implementation_plan_v2.md
new file mode 100644
index 0000000..4410637
--- /dev/null
+++ b/docs/hakakses implementation_plan_v2.md
@@ -0,0 +1,181 @@
+# Integrasi Hak Akses (Permissions) di UI beserta Konfigurasi CRUD
+
+## ✅ Hasil Pengecekan Kesiapan Project
+1. **Navigasi (*Menu/Routing*)**: Sudah tersedia di `stores/navItems1.ts` (`defaultNavItems`). Array ini akan menjadi sumber daftar halaman di dialog "Edit Hak Akses" — dengan catatan penting di bagian struktur data (lihat poin 2 di bawah).
+2. **Library Komponen**: Vuetify 3 sudah tersedia, tinggal memasang `` / `` di tabel dialog Edit Hak Akses.
+3. **Mock Backend API**: `server/api/hak-akses/index.ts` sudah bisa menahan format data CRUD yang akan dikirim dari UI.
+
+Karena API backend sesungguhnya belum siap, kita pakai **Nuxt Local API (Mock Backend)** sebagai *Backend-for-Frontend* sementara, dengan arsitektur *toggling* API sejak awal agar migrasi ke backend asli mulus.
+
+> ⚠️ **Catatan penting**: Toggle mock/real API ini menyelesaikan masalah *sumber data*, bukan masalah *keamanan*. Semua pengecekan `v-permission` dan middleware di bawah ini berjalan di client — lihat bagian **Keamanan** sebelum dianggap selesai.
+
+---
+
+## Bagan Alur Sistem Hak Akses (Permission Flow)
+
+```mermaid
+flowchart TD
+ User([Pengguna]) --> Login(Berhasil Login via Keycloak)
+ Login --> Auth[useAuth.ts: Dapat Data Roles & Groups]
+ Auth --> PermStore[permissionStore.ts memuat Permissions]
+
+ PermStore --> CheckFlag{USE_MOCK_API ?}
+
+ CheckFlag -- TRUE --> MockAPI[Nuxt Local API: /api/hak-akses]
+ CheckFlag -- FALSE --> RealAPI[Real Backend: /api/v1/permission]
+
+ MockAPI --> FetchFail{Fetch gagal?}
+ RealAPI --> FetchFail
+ FetchFail -- Ya --> DenyDefault[Deny-by-default: anggap tanpa izin]
+ FetchFail -- Tidak --> PermState(Permission State Disimpan)
+
+ PermState --> Router[Vue Router Middleware - client]
+ PermState --> ServerCheck[Server Middleware/Plugin - SSR guard]
+ PermState --> Sidebar[Sidebar Navigation]
+
+ Router -- canAccess: False --> Deny[Redirect ke Error/403]
+ Router -- canAccess: True --> Page[Buka Halaman]
+ ServerCheck -- canAccess: False --> Deny
+
+ Sidebar -- canView: False --> HideMenu[Sembunyikan Menu]
+
+ Page --> Directive[v-permission directive pada Komponen]
+ Directive -- canDelete: False --> HideBtn[Tombol Hapus Hilang/Disabled]
+ Directive -- canAdd: True --> ShowBtn[Tombol Tambah Tampil]
+
+ Page -.-> BackendGuard[[Backend API tetap validasi ulang izin]]
+```
+
+---
+
+## Proposed Changes
+
+### 1. Seamless Backend Migration Strategy (Persiapan API Asli)
+- `stores/permissionStore.ts` dan composable pemanggil membaca konfigurasi *runtime* (`useRuntimeConfig()`).
+- Tambahkan flag `USE_MOCK_PERMISSION_API: true`.
+- Saat backend siap, cukup ubah `NUXT_PUBLIC_USE_MOCK_PERMISSION_API=false` di `.env`.
+- **Tambahan — contract test**: buat satu skema/interface TypeScript (idealnya divalidasi dengan `zod`) yang dipakai bersama oleh mock API dan dipakai untuk memvalidasi response real API nanti. Ini memastikan klaim "tinggal ganti flag" benar-benar teruji, bukan asumsi.
+
+### 2. Upgrade Local API Mock (`server/api/hak-akses/index.ts`)
+- Perbarui `data/mock/hakAkses.json` agar field `hakAksesMenu` berisi boolean: `canAccess`, `canView`, `canAdd`, `canEdit`, `canDelete`.
+- **Perubahan struktur — pakai key stabil, bukan label**: setiap entri `hakAksesMenu` menyimpan `menuKey` (mengacu ke `key`/`routeName` unik di `navItems1.ts`), bukan `name` (label tampilan). Label bisa berubah/di-rename tanpa memutus mapping izin.
+
+```json
+{
+ "menuKey": "master-klinik-ruang",
+ "name": "Master Klinik Ruang",
+ "canAccess": true,
+ "canView": true,
+ "canAdd": false,
+ "canEdit": false,
+ "canDelete": false
+}
+```
+
+- Validasi payload masuk dengan skema (zod) di endpoint mock, supaya struktur tidak diam-diam berubah antara UI dan backend.
+
+### 3. Memperbarui `Setting/HakAkses.vue` (UI Konfigurasi CRUD)
+- Dialog Edit melooping `navItems1.ts`, menampilkan matriks checkbox untuk `canAccess`, `canView`, `canAdd`, `canEdit`, `canDelete`, dikunci ke `menuKey` masing-masing.
+- Payload POST mengarah ke API Mock/Real sesuai flag.
+- **Aturan precedence eksplisit** (harus didefinisikan sebelum coding, karena ada `role`, `group`, dan `isGroupBased` sekaligus):
+ 1. Jika user punya override individual (role-based) → pakai itu.
+ 2. Jika tidak ada override individual dan `isGroupBased: true` → pakai izin dari group.
+ 3. Jika keduanya tidak ada → deny-by-default.
+ - Tuliskan aturan ini sebagai komentar di `permissionStore.ts`, bukan hanya di dokumen, supaya tidak jadi sumber bug tersembunyi saat logic berkembang.
+
+### 4. Vue Custom Directive `v-permission`
+- File baru `plugins/permission.ts` untuk registrasi directive.
+- Dukungan penggunaan:
+ - Single: `Edit`
+ - Multiple (AND): `...`
+- **Mode hide vs disable**: tambahkan modifier, misal `v-permission:disable="'canDelete'"`, agar tombol bisa di-disable dengan tooltip ("Anda tidak punya izin") alih-alih hilang total tanpa penjelasan — pilih sesuai konteks UX per halaman.
+
+### 5. `stores/permissionStore.ts` (State Management)
+- Mengambil data izin dari Local/Real API saat login.
+- Menyimpan state global.
+- **Deny-by-default**: jika fetch permission gagal (network error, token expired), state dianggap "tanpa izin sama sekali", bukan default terbuka.
+- **Refresh strategy**: tentukan apakah perubahan hak akses oleh admin berlaku langsung (polling/refetch berkala) atau baru berlaku setelah re-login. Pilih salah satu secara eksplisit dan dokumentasikan, jangan dibiarkan implisit.
+
+### 6. Middleware & Dynamic Sidebar
+- **Client middleware (`middleware/permissions.ts`)**: mencegat rute jika `canAccess` false.
+- **Server-side guard**: karena project ini SSR (Nuxt), tambahkan pengecekan di server middleware/plugin juga — bukan hanya client — untuk mencegah *flash of unauthorized content* (halaman sempat ter-render sebelum redirect).
+- **Sidebar (`stores/navItems1.ts`)**: filter menu berdasarkan `canView`, dikunci ke `menuKey`.
+
+### 7. Keamanan Backend (wajib, non-negotiable)
+- `v-permission` dan middleware di atas adalah **UX**, bukan kontrol akses sesungguhnya — keduanya berjalan di client dan bisa dilewati siapa saja yang memanggil API langsung.
+- Backend API asli **wajib** memvalidasi ulang setiap permission di server berdasarkan identitas user dari token, tidak pernah mempercayai payload/izin yang dikirim dari frontend.
+
+---
+
+## Verification Plan
+
+### 1. Verifikasi API Lokal via cURL
+Pastikan API Lokal mampu membaca dan menyimpan JSON berformat CRUD dengan `menuKey`:
+
+```bash
+curl -X POST "http://localhost:3000/api/hak-akses" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "role": "admin",
+ "group": "LOKET",
+ "namaTipeUser": "Admin Loket",
+ "isGroupBased": true,
+ "hakAksesMenu": [
+ {
+ "menuKey": "master-klinik-ruang",
+ "name": "Master Klinik Ruang",
+ "canAccess": true,
+ "canView": true,
+ "canAdd": false,
+ "canEdit": false,
+ "canDelete": false
+ }
+ ]
+ }'
+```
+
+### 2. Manual UI Verification
+1. Buka **Setting > Hak Akses**, atur hak untuk Role tertentu (centang `canView`, matikan `canDelete`).
+2. Login sebagai akun dengan Role tersebut.
+3. Buka halaman target.
+4. **Validasi**: Halaman terbuka, tombol "Delete" tersembunyi/disabled sesuai mode `v-permission`.
+5. **Validasi fetch gagal**: Simulasikan permission API error (mis. matikan endpoint sementara) → pastikan sistem deny-by-default, bukan default terbuka.
+6. **Validasi kesiapan API asli**: Cek logika precedence dan toggle flag di `permissionStore.ts`.
+
+### 3. Automated Testing (baru)
+- **Unit test** untuk `permissionStore.ts`: precedence role vs group, deny-by-default saat fetch gagal, evaluasi `canAccess`/`canView`/dst.
+- **Contract test**: bandingkan skema response Mock API vs Real API (setelah Real API tersedia) menggunakan skema TypeScript/zod yang sama, untuk memastikan switch flag benar-benar tanpa perubahan kode lain.
+- **Directive test**: pastikan `v-permission` menyembunyikan/disable elemen dengan benar untuk kombinasi single dan multiple permission.
+
+---
+
+## Strategi Selama Backend Asli Belum Tersedia
+
+Karena tim belum bisa mengimplementasikan validasi izin di server sungguhan, mock API diperlakukan sebagai **kontrak (contract-first)**, bukan sekadar penyimpanan data sementara. Tujuannya: frontend sudah teruji terhadap semua skenario yang nanti jadi tanggung jawab backend asli, dan tidak perlu dirombak saat migrasi.
+
+### 1. Mock API mengikuti skema, bukan menerima apa saja
+- Definisikan interface TypeScript / skema `zod` untuk request dan response `hak-akses` **sekarang**, bukan menunggu backend asli.
+- Mock API menolak (400) payload yang tidak sesuai skema.
+- Skema ini menjadi kontrak yang wajib dipatuhi backend asli nanti — perbedaan struktur akan ketahuan lewat contract test, bukan saat production.
+
+### 2. Simulasikan tanggung jawab yang nanti dipegang backend
+- Tambahkan endpoint mock `/api/hak-akses/check` yang bisa mensimulasikan penolakan server (403) karena user tidak punya izin — supaya UI dan middleware sudah teruji menangani penolakan dari server, bukan hanya dari state client.
+- Simulasikan juga kegagalan fetch (delay/error 500) untuk memverifikasi deny-by-default benar-benar berjalan.
+
+### 3. Tandai eksplisit bagian yang "sementara tidak aman"
+Beri komentar `TODO(security)` di titik-titik yang wajib diperkuat saat backend asli terpasang, contoh:
+```ts
+// TODO(security): saat backend asli terpasang, endpoint ini WAJIB
+// memvalidasi ulang permission dari token JWT/session di server,
+// jangan percaya payload role/group yang dikirim dari client.
+```
+Ini mencegah asumsi keliru saat handoff bahwa "karena UI sudah mengatur tampilan sesuai izin, backend tidak perlu memvalidasi ulang".
+
+### 4. Definition of Done — Migrasi ke Backend Asli
+Checklist ini harus tercentang semua **sebelum** flag `USE_MOCK_PERMISSION_API` dimatikan (`false`) di production:
+- [ ] Endpoint real API memvalidasi permission berdasarkan identitas dari token (JWT/session), bukan dari body request yang dikirim client.
+- [ ] Response real API lolos contract test terhadap skema yang sama dengan mock API (field, tipe data, struktur `hakAksesMenu` identik).
+- [ ] Skenario penolakan server (403) dan fetch gagal (500/timeout) sudah diuji terhadap real API, tidak hanya terhadap mock.
+- [ ] Ada audit log untuk setiap perubahan hak akses (siapa mengubah, kapan, dari-ke apa) — direkomendasikan mengingat ini fitur kontrol akses.
+- [ ] Rate limiting pada endpoint pengubahan hak akses, untuk mencegah penyalahgunaan.
+- [ ] Semua komentar `TODO(security)` di kode sudah diselesaikan atau dipindahkan menjadi tiket tersendiri yang dilacak.
diff --git a/pages/AdminKlinikRuang/index.vue b/pages/AdminKlinikRuang/index.vue
index f85e066..8920b73 100644
--- a/pages/AdminKlinikRuang/index.vue
+++ b/pages/AdminKlinikRuang/index.vue
@@ -103,11 +103,47 @@ const ruangStore = useRuangStore();
const loading = ref(false);
+/**
+ * Computed property untuk mendapatkan daftar klinik dengan menggabungkan
+ * semua poli Eksekutif menjadi satu entitas "GRAND PAVILIUN".
+ * @returns {Array} Daftar klinik yang sudah digabung dan diurutkan.
+ */
const klinikRuangList = computed(() => {
const list = masterStore.ruangData || [];
- return [...list].sort((a, b) => a.namaKlinik.localeCompare(b.namaKlinik));
+ const regulerList = list.filter(k => k.jenisLayanan !== 'Eksekutif');
+ const eksekutifList = list.filter(k => k.jenisLayanan === 'Eksekutif');
+
+ const result = [...regulerList];
+
+ if (eksekutifList.length > 0) {
+ // Combine all Eksekutif rooms for the preview
+ const allRooms = eksekutifList.flatMap(k => k.ruangList || []);
+ // Filter unique room names
+ const uniqueRoomNames = new Set();
+ const uniqueRooms = [];
+ allRooms.forEach(r => {
+ if (!uniqueRoomNames.has(r.namaRuang)) {
+ uniqueRoomNames.add(r.namaRuang);
+ uniqueRooms.push(r);
+ }
+ });
+
+ result.push({
+ kodeKlinik: 'EKS',
+ namaKlinik: 'GRAND PAVILIUN',
+ jenisLayanan: 'Eksekutif',
+ ruangList: uniqueRooms
+ });
+ }
+
+ return result.sort((a, b) => a.namaKlinik.localeCompare(b.namaKlinik));
});
+/**
+ * Navigasi ke halaman detail antrean klinik ruang tertentu.
+ * @param {string} kodeKlinik - Kode unik klinik (contoh: 'AN', 'EKS').
+ * @param {string} jenisLayanan - Jenis layanan ('Reguler' atau 'Eksekutif').
+ */
const navigateToKlinik = (kodeKlinik, jenisLayanan) => {
// Include jenisLayanan as query parameter to differentiate same clinic code with different service types
router.push({
@@ -116,6 +152,9 @@ const navigateToKlinik = (kodeKlinik, jenisLayanan) => {
});
};
+/**
+ * Navigasi ke halaman master setting klinik ruang.
+ */
const navigateToSettings = () => {
router.push('/setting/masterklinikruang');
};
diff --git a/pages/AdminLoket/[id].vue b/pages/AdminLoket/[id].vue
index b4d8a40..33e81ab 100644
--- a/pages/AdminLoket/[id].vue
+++ b/pages/AdminLoket/[id].vue
@@ -37,6 +37,7 @@
@call="handleCallPatient"
@open-klinik-ruang="openKlinikRuangDialog"
@open-penunjang="openPenunjangDialog"
+ @linked="handlePatientLinked"
/>
{
snackbarColor.value = result.success ? "success" : "error";
snackbar.value = true;
+
if (result.success) {
broadcastUpdate();
}
closeKlinikRuangDialog();
};
+
+// --- SIMRS Integration Handlers ---
+const handlePatientLinked = (message) => {
+ snackbarText.value = message;
+ snackbarColor.value = "success";
+ snackbar.value = true;
+};