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 @@
- - mdi-bullhorn - Panggil - +
+ + mdi-account-cog-outline + Data Pasien + + + mdi-microphone + Panggil + +
@@ -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; +};