From 61772105fd8facdd5acb2927988e422fe0e656ee Mon Sep 17 00:00:00 2001 From: Fanrouver Date: Thu, 2 Jul 2026 11:39:07 +0700 Subject: [PATCH] feat: implement queue store with clinic API integration, WebSocket sync, and patient data management --- composables/useQueueAPI.ts | 213 +++-------- composables/useQueueSync.ts | 237 ++++++++++++ composables/useWebSocket.ts | 10 +- docs/DEVLOG.md | 50 +++ refactor.py | 54 +++ stores/doctorStore.js | 15 +- stores/{queueStore.js => queueStore.ts} | 467 ++++++------------------ types/queue.ts | 48 +++ 8 files changed, 553 insertions(+), 541 deletions(-) create mode 100644 composables/useQueueSync.ts create mode 100644 refactor.py rename stores/{queueStore.js => queueStore.ts} (89%) create mode 100644 types/queue.ts diff --git a/composables/useQueueAPI.ts b/composables/useQueueAPI.ts index b18686c..15666f9 100644 --- a/composables/useQueueAPI.ts +++ b/composables/useQueueAPI.ts @@ -1,193 +1,62 @@ -// composables/useQueueAPI.ts -// Composable untuk API calls terkait antrian pasien - -export interface Patient { - no: number; - jamPanggil: string; - barcode: string; - noAntrian: string; - shift: string; - klinik: string; - fastTrack: string; - pembayaran: string; - status: 'anjungan' | 'pending' | 'di-loket' | 'di-klinik' | 'selesai' | 'terlambat'; - processStage: 'loket' | 'klinik' | 'penunjang'; - createdAt: string; - registrationType?: 'online' | 'onsite'; - visitType?: string; - visitDate?: string; -} - -export interface QueueAPIResponse { - success: boolean; - data?: T; - message?: string; - error?: string; -} +import { useRuntimeConfig } from '#app'; +import type { QueuePatient } from '@/types/queue'; export const useQueueAPI = () => { const config = useRuntimeConfig(); - const baseURL = config.public.apiBaseUrl || '/api/queue'; + const verificationApiBase = config.public.verificationApiBaseUrl || 'http://10.10.123.140:8089/api/v1'; + const externalApiBase = config.public.externalApiBaseUrl; - /** - * Fetch all patients from database - */ - const fetchAllPatients = async (): Promise => { + const fetchRawLoketPatients = async (loketId: string | number) => { try { - const response = await $fetch>(`${baseURL}/patients`, { - method: 'GET', - }); - - if (response.success && response.data) { - return response.data; + const rawData: any = await $fetch(`${verificationApiBase}/loket/${loketId}`); + if (rawData.metadata && rawData.metadata.code !== 200) { + throw new Error(rawData.message || 'API returned error status'); } - throw new Error(response.message || 'Failed to fetch patients'); + return rawData.data || []; } catch (error: any) { - console.error('❌ Error fetching patients:', error); - throw error; + throw new Error(`HTTP error! status: ${error.response?.status || error.message}`); } }; - /** - * Fetch single patient by ID or barcode - */ - const fetchPatient = async (idOrBarcode: string): Promise => { + const fetchRawClinicPatients = async (clinicId: string | number) => { + const url = `${externalApiBase}/visit?klinik_id=${clinicId}&limit=500`; try { - const response = await $fetch>(`${baseURL}/patients/${idOrBarcode}`, { - method: 'GET', - }); + const rawResponse: any = await $fetch(url); + return rawResponse?.data || []; + } catch (error: any) { + throw new Error(`HTTP error! status: ${error.response?.status || error.message}`); + } + }; - if (response.success && response.data) { - return response.data; + const updateTicketStatus = async (barcode: string, statuspasien: string, statuspasien2: string, idklinikstatus: string, idklinikstatus2: string) => { + return await $fetch(`${verificationApiBase}/tiket/update`, { + method: 'POST', + body: { + barcode: barcode || "", + statuspasien, + statuspasien2, + idklinikstatus, + idklinikstatus2 } - return null; - } catch (error: any) { - console.error('❌ Error fetching patient:', error); - return null; - } + }); }; - /** - * Create new patient (register from Anjungan) - */ - const createPatient = async (patientData: Partial): Promise => { - try { - const response = await $fetch>(`${baseURL}/patients`, { - method: 'POST', - body: patientData, - }); - - if (response.success && response.data) { - return response.data; + const completeTicketStatus = async (idloket: string, barcode: string, statuspasien: string, idklinikstatus: string) => { + return await $fetch(`${verificationApiBase}/tiket/selesai`, { + method: 'POST', + body: { + idloket: String(idloket || ""), + barcode: barcode || "", + statuspasien, + idklinikstatus } - throw new Error(response.message || 'Failed to create patient'); - } catch (error: any) { - console.error('❌ Error creating patient:', error); - throw error; - } - }; - - /** - * Update patient status (check-in, process, etc) - */ - const updatePatient = async ( - idOrBarcode: string, - updates: Partial - ): Promise => { - try { - const response = await $fetch>( - `${baseURL}/patients/${idOrBarcode}`, - { - method: 'PATCH', - body: updates, - } - ); - - if (response.success && response.data) { - return response.data; - } - throw new Error(response.message || 'Failed to update patient'); - } catch (error: any) { - console.error('❌ Error updating patient:', error); - throw error; - } - }; - - /** - * Check-in patient (update status to di-loket) - */ - const checkInPatient = async (idOrBarcode: string): Promise => { - return updatePatient(idOrBarcode, { status: 'di-loket' }); - }; - - /** - * Process patient at loket (update status and processStage) - */ - const processPatientAtLoket = async ( - idOrBarcode: string, - updates: { status?: string; processStage?: string } - ): Promise => { - return updatePatient(idOrBarcode, updates as Partial); - }; - - /** - * Sync local state with database - */ - const syncWithDatabase = async (localPatients: Patient[]): Promise => { - try { - // Fetch latest from database - const dbPatients = await fetchAllPatients(); - - // Merge strategy: prefer database data, but keep local if newer - const merged = new Map(); - - // Add database patients - dbPatients.forEach(patient => { - merged.set(patient.barcode, patient); - }); - - // Add local patients that don't exist in DB or are newer - localPatients.forEach(localPatient => { - const existing = merged.get(localPatient.barcode); - if (!existing || new Date(localPatient.createdAt) > new Date(existing.createdAt)) { - merged.set(localPatient.barcode, localPatient); - } - }); - - return Array.from(merged.values()); - } catch (error: any) { - console.error('❌ Error syncing with database:', error); - // Return local patients as fallback - return localPatients; - } - }; - - /** - * Batch sync: save multiple patients to database - */ - const batchSyncPatients = async (patients: Patient[]): Promise => { - try { - const response = await $fetch(`${baseURL}/patients/batch`, { - method: 'POST', - body: { patients }, - }); - - return response.success || false; - } catch (error: any) { - console.error('❌ Error batch syncing patients:', error); - return false; - } + }); }; return { - fetchAllPatients, - fetchPatient, - createPatient, - updatePatient, - checkInPatient, - processPatientAtLoket, - syncWithDatabase, - batchSyncPatients, + fetchRawLoketPatients, + fetchRawClinicPatients, + updateTicketStatus, + completeTicketStatus }; }; - diff --git a/composables/useQueueSync.ts b/composables/useQueueSync.ts new file mode 100644 index 0000000..c67fc59 --- /dev/null +++ b/composables/useQueueSync.ts @@ -0,0 +1,237 @@ +import { ref } from 'vue'; +import { useRuntimeConfig } from '#app'; +import { useWebSocket } from '@/composables/useWebSocket'; +import type { QueuePatient } from '@/types/queue'; + +export interface QueueSyncDeps { + allPatients: Ref; + currentProcessingPatient: Ref>; + activeLoketInterest: Ref>; + activeClinicInterest: Ref>; + globalInterestCount: Ref; + fetchPatientsForLoket: (id: string | number, force?: boolean) => void; + fetchPatientsForClinic: (id: string, force?: boolean) => void; + fetchAllPatients: () => void; +} + +export const useQueueSync = (deps: QueueSyncDeps) => { + const isWsConnected = ref(false); + const wsClientId = ref(`client-${Math.random().toString(36).substring(7)}`); + const lastGlobalCall = ref(null); + const lastKlinikCall = ref(null); + + const onWsMessage = (data: any) => { + // Robust data extraction: some relays wrap data in another 'data' property + let messageData = data?.data || data; + if (messageData?.data && !messageData.callKlinikEvent && !messageData.callEvent) { + messageData = messageData.data; // Double wrap check + } + + const targetLoketId = messageData?.loketId || messageData?.idloket; + const targetKlinikId = messageData?.klinikId || messageData?.idklinik; + + // Handle Call Events and WS messages + if (messageData?.triggerRefresh) { + if (messageData.klinikId) { + // console.log(`🔄 [queueSync] Received refresh trigger for clinic ${messageData.klinikId}`); + + // Handle current processing update if provided + if (messageData.currentProcessingUpdate) { + // console.log(`🎯 [queueSync] Applying current processing update:`, messageData.currentProcessingUpdate); + + Object.keys(messageData.currentProcessingUpdate).forEach(key => { + deps.currentProcessingPatient.value[key] = messageData.currentProcessingUpdate[key]; + + // Also patch the status in allPatients if possible + const processingPatient = messageData.currentProcessingUpdate[key]; + if (processingPatient && processingPatient.no) { + const idx = deps.allPatients.value.findIndex(p => p.no === processingPatient.no); + if (idx !== -1) { + deps.allPatients.value[idx] = { ...deps.allPatients.value[idx], status: 'di-loket' }; + } + } + }); + } + + deps.fetchPatientsForClinic(messageData.klinikId, true); + } + } + if (messageData?.callEvent) { + lastGlobalCall.value = messageData.callEvent; + } + + // Handle Klinik Call Events (cross-device sync for AntrianKlinikRuang display) + if (messageData?.callKlinikEvent) { + const ev = messageData.callKlinikEvent; + // console.log('🏥 [queueSync] Klinik call event received:', ev); + + // PERSISTENCE FIX: Save to lastKlinikCall for displays to watch + lastKlinikCall.value = ev; + + // Find the patient in allPatients and patch directly for immediate UI update + const idx = deps.allPatients.value.findIndex(p => + p.processStage === 'klinik-ruang' && + p.kodeKlinik === ev.kodeKlinik && + ( + (p.barcode && String(p.barcode) === String(ev.barcode)) || + (p.noAntrian && p.noAntrian.split(' |')[0] === ev.noantrian) + ) + ); + + if (idx !== -1) { + const updatedPatient = { + ...deps.allPatients.value[idx], + tipeLayanan: ev.tipeLayanan, + lastCalledAt: ev.lastCalledAt || new Date().toISOString(), + lastCalledTipeLayanan: ev.tipeLayanan, + status: 'di-loket' as any, + calledPemeriksaanAwal: ev.tipeLayanan === 'Pemeriksaan Awal' ? true : deps.allPatients.value[idx].calledPemeriksaanAwal, + calledTindakan: ev.tipeLayanan === 'Tindakan' ? true : deps.allPatients.value[idx].calledTindakan + }; + + deps.allPatients.value[idx] = updatedPatient; + // console.log(`✅ [queueSync] Successfully patched patient ${ev.noantrian} status to di-loket (lastCalledAt: ${updatedPatient.lastCalledAt})`); + } else { + console.warn(`⚠️ [queueSync] Patient ${ev.noantrian} not found in store for clinic ${ev.kodeKlinik}.`); + } + } + // TRIGGER STRATEGIC REFRESHES + let refreshedSomething = false; + + if (targetLoketId) { + deps.fetchPatientsForLoket(targetLoketId, true); + refreshedSomething = true; + } + + if (targetKlinikId) { + const interestingClinics = Object.keys(deps.activeClinicInterest.value); + if (interestingClinics.includes(String(targetKlinikId)) || targetKlinikId === 'broadcast') { + const clinicToFetch = targetKlinikId === 'broadcast' ? interestingClinics[0] : targetKlinikId; + deps.fetchPatientsForClinic(clinicToFetch, true); + refreshedSomething = true; + } + } + + if (deps.globalInterestCount.value > 0) { + deps.fetchAllPatients(); + refreshedSomething = true; + } + + // ALWAYS refresh our own active interests when a WebSocket message is received + const interestingLokets = Object.keys(deps.activeLoketInterest.value); + const interestingClinics = Object.keys(deps.activeClinicInterest.value); + + if (interestingLokets.length > 0) { + interestingLokets.forEach(loketId => { + if (String(loketId) !== String(targetLoketId)) { + deps.fetchPatientsForLoket(loketId, true); + refreshedSomething = true; + } + }); + } + + if (interestingClinics.length > 0) { + interestingClinics.forEach(kodeKlinik => { + if (String(kodeKlinik) !== String(targetKlinikId)) { + deps.fetchPatientsForClinic(kodeKlinik, true); + refreshedSomething = true; + } + }); + } + + if (!refreshedSomething) { + // console.log(`🔕 [queueSync] WS trigger received but no active interest matched. Skipping.`); + } + }; + + const config = useRuntimeConfig(); + const wsBaseUrl = config.public?.wsBaseUrl || "ws://10.10.123.135:8084/api/v1/ws"; + + const { connect, disconnect, sendViaPost, isConnected } = useWebSocket({ + url: wsBaseUrl, + clientId: wsClientId, + fallbackPostUrl: `${config.public.externalApiBaseUrl}/ws`, + reconnectInterval: 2000, + maxReconnectAttempts: 9999, + onOpen: () => { + // console.log('✅ [queueSync] WebSocket connected'); + isWsConnected.value = true; + }, + onClose: () => { + // console.log('❌ [queueSync] WebSocket disconnected'); + isWsConnected.value = false; + }, + onError: (err: any) => { + console.error('⚠️ [queueSync] WebSocket error:', err); + isWsConnected.value = false; + }, + onMessage: onWsMessage + }); + + let _autoSyncInterval: any = null; + + const startAutoSync = () => { + if (typeof window === 'undefined') return; + if (_autoSyncInterval) return; + + // console.log('🔄 [queueSync] Starting store-level auto-sync (30s interval)'); + + _autoSyncInterval = setInterval(async () => { + const hasLoketInterest = Object.keys(deps.activeLoketInterest.value).length > 0; + const hasClinicInterest = Object.keys(deps.activeClinicInterest.value).length > 0; + const hasGlobalInterest = deps.globalInterestCount.value > 0; + + if (hasGlobalInterest) { + deps.fetchAllPatients(); + } else { + if (hasLoketInterest) { + Object.keys(deps.activeLoketInterest.value).forEach(loketId => { + deps.fetchPatientsForLoket(loketId, true); + }); + } + if (hasClinicInterest) { + Object.keys(deps.activeClinicInterest.value).forEach(kodeKlinik => { + deps.fetchPatientsForClinic(kodeKlinik, true); + }); + } + } + }, 30000); // 30 seconds + }; + + const stopAutoSync = () => { + if (_autoSyncInterval) { + clearInterval(_autoSyncInterval); + _autoSyncInterval = null; + // console.log('⏹️ [queueSync] Store-level auto-sync stopped'); + } + }; + + const initWebSocket = (customClientId: string | null = null) => { + if (isConnected.value && customClientId === wsClientId.value) { + // console.log('🔌 [queueSync] WebSocket already connected with same ID.'); + startAutoSync(); + return; + } + + if (customClientId) { + wsClientId.value = customClientId; + disconnect(); + } + + // console.log(`🔌 [queueSync] Connecting to WebSocket: ${wsBaseUrl} as ${wsClientId.value}`); + connect(); + startAutoSync(); + }; + + return { + isWsConnected, + wsClientId, + lastGlobalCall, + lastKlinikCall, + initWebSocket, + disconnectWebSocket: disconnect, + sendViaPost, + startAutoSync, + stopAutoSync + }; +}; diff --git a/composables/useWebSocket.ts b/composables/useWebSocket.ts index 317800a..bc2bccc 100644 --- a/composables/useWebSocket.ts +++ b/composables/useWebSocket.ts @@ -64,7 +64,7 @@ export const useWebSocket = (config: WebSocketConfig) => { // Close existing connection if any, and CLEAN HANDLERS to prevent recursion if (ws.value) { if (ws.value.readyState !== WebSocket.CLOSED) { - console.log('🔌 Closing existing WebSocket before new connection...') + // console.log('🔌 Closing existing WebSocket before new connection...') clearHandlers(ws.value) ws.value.close() } @@ -75,7 +75,7 @@ export const useWebSocket = (config: WebSocketConfig) => { ws.value = new WebSocket(connectionUrl) ws.value.onopen = () => { - console.log('✅ WebSocket connected:', currentClientId.value) + // console.log('✅ WebSocket connected:', currentClientId.value) isConnected.value = true reconnectAttempts.value = 0 config.onOpen?.() @@ -91,7 +91,7 @@ export const useWebSocket = (config: WebSocketConfig) => { } ws.value.onclose = () => { - console.log('❌ WebSocket closed:', currentClientId.value) + // console.log('❌ WebSocket closed:', currentClientId.value) isConnected.value = false config.onClose?.() @@ -100,7 +100,7 @@ export const useWebSocket = (config: WebSocketConfig) => { if (reconnectAttempts.value < (config.maxReconnectAttempts || 5)) { reconnectAttempts.value++ const interval = config.reconnectInterval || 3000 - console.log(`⏳ Reconnecting in ${interval}ms... Attempt ${reconnectAttempts.value}`) + // console.log(`⏳ Reconnecting in ${interval}ms... Attempt ${reconnectAttempts.value}`) reconnectTimer.value = setTimeout(() => { connect() }, interval) @@ -125,7 +125,7 @@ export const useWebSocket = (config: WebSocketConfig) => { reconnectTimer.value = null } if (ws.value) { - console.log('🔌 Manual disconnect: Cleaning handlers and closing...') + // console.log('🔌 Manual disconnect: Cleaning handlers and closing...') clearHandlers(ws.value) ws.value.close() ws.value = null diff --git a/docs/DEVLOG.md b/docs/DEVLOG.md index b02fbe4..f1a36e4 100644 --- a/docs/DEVLOG.md +++ b/docs/DEVLOG.md @@ -63,6 +63,56 @@ Format output: +## 2026-06-23 — Refactoring queueStore.js ke TypeScript (Phase 2: WebSocket & Typings) + +**Sprint/Phase:** Phase 6 (Verifikasi Akun & Kiosk) / Refactoring Technical Debt +**Durasi:** 1 jam +**Status:** ✅ Done + +### Yang Dikerjakan +- Membuat composable mandiri `composables/useQueueSync.ts` dan memindahkan lebih dari 200 baris logika *WebSocket* (`onWsMessage`, `connect`, `startAutoSync`) dari dalam *store*. +- Menjalankan *scripting* untuk me-resolve sebagian besar error peringatan `implicit any type` dan `catch(error: any)` di dalam `queueStore.ts`. +- Menerapkan injeksi dependencies dari `queueStore` ke dalam parameter `useQueueSync()` tanpa memutus *reactivity* dari Pinia state. + +### Keputusan Teknis +> **Dependency Injection pada Composable:** Karena fungsionalitas WebSocket membutuhkan akses terhadap state (`allPatients`, `currentProcessingPatient`) dan actions (`fetchPatientsForLoket`, dll), maka variabel-variabel tersebut dilempar (injected) melalui parameter `deps` ke dalam fungsi `useQueueSync(deps)`. Hal ini mencegah masalah *circular dependencies* di ekosistem Nuxt/Pinia. + +### Masalah & Solusi +| Masalah | Solusi | Referensi | +|---------|--------|-----------| +| Pinia-Plugin-PersistedState *type mismatch* pada `paths` config | Menambahkan flag `// @ts-ignore` untuk properti `paths` karena kompatibel secara *runtime* namun melanggar validasi tipe di rilis plugin yang terpasang. | QMD: Technical Debt | + +### Besok +- Menguji secara manual interaksi antar *terminal* (Anjungan, Loket, Klinik) untuk memverifikasi fungsionalitas `useQueueSync`. +- Menulis Unit Tests (jika platform sudah siap). + +--- + +## 2026-06-23 — Refactoring queueStore.js ke TypeScript (Phase 1) + +**Sprint/Phase:** Phase 6 (Verifikasi Akun & Kiosk) / Refactoring Technical Debt +**Durasi:** 1 jam +**Status:** 🔄 In Progress + +### Yang Dikerjakan +- Mengubah ekstensi file `queueStore.js` menjadi `queueStore.ts`. +- Membuat file definisi tipe antrean di `types/queue.ts` (`QueuePatient`). +- Mengekstrak raw fetch API calls (Loket, Visit, Update, Selesai Tiket) ke composable mandiri `composables/useQueueAPI.ts`. +- Melakukan transisi pada fungsi-fungsi besar (`fetchPatientsForLoket`, `callNext`, `callMultiplePatients`, `processPatient`) di dalam `queueStore.ts` untuk menggunakan `useQueueAPI` guna memangkas boilerplate fetch. + +### Keputusan Teknis +> **Incremental Refactoring:** Menghindari pecahnya `queueStore` secara drastis dengan tetap mempertahankan satu *store instance* (`useQueueStore`) namun mengabstraksi beban logikanya keluar (*API* & *Sync layer*). Ini meminimalisasi *blast radius* / *breaking changes* ke komponen UI yang sudah jalan. + +### Masalah & Solusi +| Masalah | Solusi | Referensi | +|---------|--------|-----------| +| Pembengkakan ukuran *store file* (>3500 baris, 145KB). | Mulai memisahkan logika pemanggilan API eksternal dan mendefinisikan strukturnya dengan TypeScript. | QMD: Technical Debt | + +### Besok +- Membuat tipe yang strict untuk `state` dan seluruh *method signatures* di `queueStore.ts`. +- Mengekstraksi fungsionalitas WebSocket ke `composables/useQueueSync.ts`. +- Jika sudah ringan, memecah *store* menjadi `loketQueueStore` dan `clinicQueueStore`. + --- ## 2026-06-19 — Fitur Detail Akun Verifikasi diff --git a/refactor.py b/refactor.py new file mode 100644 index 0000000..871ee21 --- /dev/null +++ b/refactor.py @@ -0,0 +1,54 @@ +import re +import sys + +path = r'e:\antrean operasi\web-antrean\stores\queueStore.ts' +try: + with open(path, 'r', encoding='utf-8') as f: + content = f.read() + + # 1. Add import for useQueueSync at the top + if 'useQueueSync' not in content: + content = content.replace("import { useQueueAPI } from '@/composables/useQueueAPI';", "import { useQueueAPI } from '@/composables/useQueueAPI';\nimport { useQueueSync } from '@/composables/useQueueSync';") + + # 2. Remove the WEBSOCKET INTEGRATION block + ws_pattern = re.compile(r' // WEBSOCKET INTEGRATION \(CENTRALIZED\).*? const disconnectWebSocket = \(\) => \{\n disconnect\(\);\n \};', re.DOTALL) + + content = ws_pattern.sub(' // WS and AutoSync logic extracted to composables/useQueueSync.ts', content) + + # 3. Add the initialization right before return + init_code = """ + // Initialize Queue Sync + const queueSync = useQueueSync({ + allPatients, + currentProcessingPatient, + activeLoketInterest, + activeClinicInterest, + globalInterestCount, + fetchPatientsForLoket, + fetchPatientsForClinic, + fetchAllPatients + }); + + const { + isWsConnected, + wsClientId, + lastGlobalCall, + lastKlinikCall, + initWebSocket, + disconnectWebSocket, + sendViaPost, + startAutoSync, + stopAutoSync + } = queueSync; + + return { +""" + + content = content.replace(" return {\n // State\n allPatients,", init_code + " // State\n allPatients,") + + with open(path, 'w', encoding='utf-8') as f: + f.write(content) + print("Success") +except Exception as e: + print("Error:", e) + sys.exit(1) diff --git a/stores/doctorStore.js b/stores/doctorStore.js index c120544..bbfc864 100644 --- a/stores/doctorStore.js +++ b/stores/doctorStore.js @@ -94,6 +94,14 @@ export const useDoctorStore = defineStore('doctor', () => { break; // Success! } catch (err) { lastError = err; + const status = err.response ? err.response.status : (err.status || 'unknown'); + + // Do not retry for 500 (Internal Server Error) or 404 (Not Found) + // as these are likely permanent failures for this specific clinic + if (status === 500 || status === 404) { + break; + } + if (i < retries) { const delay = 500 * (i + 1); console.warn(`⚠️ [doctorStore] Retry ${i+1}/${retries} for klinik ${idklinik} after ${delay}ms...`); @@ -130,7 +138,12 @@ export const useDoctorStore = defineStore('doctor', () => { return doctorNames; } catch (error) { const status = error.response ? error.response.status : (error.status || 'unknown'); - console.error(`❌ [doctorStore] Gagal mengambil dokter untuk klinik ID ${idklinik} (${clinic.name}) - Status: ${status}`); + + if (status === 500 || status === 404) { + console.warn(`⚠️ [doctorStore] Data dokter kosong/tidak ditemukan untuk klinik ID ${idklinik} (${clinic.name}) - Status: ${status}`); + } else { + console.error(`❌ [doctorStore] Gagal mengambil dokter untuk klinik ID ${idklinik} (${clinic.name}) - Status: ${status}`); + } // Track failure for blacklisting if (!failedClinics.value[idklinik]) { diff --git a/stores/queueStore.js b/stores/queueStore.ts similarity index 89% rename from stores/queueStore.js rename to stores/queueStore.ts index 03cb349..35e0083 100644 --- a/stores/queueStore.js +++ b/stores/queueStore.ts @@ -2,43 +2,48 @@ import { defineStore } from 'pinia'; import { ref, computed, watch } from 'vue'; import { useClinicStore } from './clinicStore'; -import { usePenunjangStore } from './penunjangStore'; import { useLoketStore } from './loketStore'; +import { usePenunjangStore } from './penunjangStore'; import { useWebSocket } from '@/composables/useWebSocket'; +import { useQueueAPI } from '@/composables/useQueueAPI'; +import { useQueueSync } from '@/composables/useQueueSync'; +import type { QueuePatient } from '@/types/queue'; export const useQueueStore = defineStore('queue', () => { + const config = useRuntimeConfig(); const clinicStore = useClinicStore(); const penunjangStore = usePenunjangStore(); const loketStore = useLoketStore(); + const queueAPI = useQueueAPI(); // ============================================ // API INTEGRATION FOR LOKET PATIENTS // ============================================ // State untuk API patient data per loket - const allPatients = ref([]); - const apiPatientsPerLoket = ref({}); - const isLoadingPatients = ref(false); - const apiPatientsError = ref(null); - const quotaUsed = ref(5); - const currentProcessingPatient = ref({}); + const allPatients = ref([]); + const apiPatientsPerLoket = ref>({}); + const isLoadingPatients = ref(false); + const apiPatientsError = ref(null); + const quotaUsed = ref(5); + const currentProcessingPatient = ref>({}); - const lastUpdated = ref(Date.now()); - const lastFetchTime = ref({}); - const lastGlobalFetchTime = ref(0); // Cooldown for bulk refreshes + const lastUpdated = ref(Date.now()); + const lastFetchTime = ref>({}); + const lastGlobalFetchTime = ref(0); // Cooldown for bulk refreshes // Scoped Refresh Logic: track which lokets are currently being viewed - const activeLoketInterest = ref({}); // { [loketId]: count } - const activeClinicInterest = ref({}); // { [kodeKlinik]: count } - const globalInterestCount = ref(0); // Tracks pages that need ALL loket data (e.g. CheckInPasien) + const activeLoketInterest = ref>({}); // { [loketId]: count } + const activeClinicInterest = ref>({}); // { [kodeKlinik]: count } + const globalInterestCount = ref(0); // Tracks pages that need ALL loket data (e.g. CheckInPasien) - const registerInterest = (loketId) => { + const registerInterest = (loketId: string | number) => { if (!loketId) return; const id = String(loketId); activeLoketInterest.value[id] = (activeLoketInterest.value[id] || 0) + 1; }; - const unregisterInterest = (loketId) => { + const unregisterInterest = (loketId: string | number) => { if (!loketId) return; const id = String(loketId); if (activeLoketInterest.value[id]) { @@ -49,13 +54,13 @@ export const useQueueStore = defineStore('queue', () => { } }; - const registerClinicInterest = (kodeKlinik) => { + const registerClinicInterest = (kodeKlinik: string) => { if (!kodeKlinik) return; const code = String(kodeKlinik); activeClinicInterest.value[code] = (activeClinicInterest.value[code] || 0) + 1; }; - const unregisterClinicInterest = (kodeKlinik) => { + const unregisterClinicInterest = (kodeKlinik: string) => { if (!kodeKlinik) return; const code = String(kodeKlinik); if (activeClinicInterest.value[code]) { @@ -74,7 +79,7 @@ export const useQueueStore = defineStore('queue', () => { globalInterestCount.value = Math.max(0, globalInterestCount.value - 1); }; - const fetchPatientsForClinic = async (kodeKlinik, force = false) => { + const fetchPatientsForClinic = async (kodeKlinik: string, force: boolean = false) => { if (!kodeKlinik) return { success: false, message: 'Kode Klinik diperlukan' }; isLoadingPatients.value = true; @@ -86,7 +91,7 @@ export const useQueueStore = defineStore('queue', () => { const timeSinceLastFetch = now - lastFetch; if (!force && timeSinceLastFetch < 2000) { - console.log(`⏭️ [queueStore] Skipping fetch for clinic ${kodeKlinik} (last fetched ${Math.round(timeSinceLastFetch/1000)}s ago)`); + // console.log(`⏭️ [queueStore] Skipping fetch for clinic ${kodeKlinik} (last fetched ${Math.round(timeSinceLastFetch/1000)}s ago)`); isLoadingPatients.value = false; return { success: true, message: 'Using cache' }; } @@ -100,14 +105,9 @@ export const useQueueStore = defineStore('queue', () => { throw new Error(`Klinik ID tidak ditemukan untuk kode: ${kodeKlinik}`); } - const url = `${config.public.externalApiBaseUrl}/visit?klinik_id=${clinic.id}&limit=500`; - console.log(`🔄 [queueStore] Fetching patients for clinic ${kodeKlinik} (ID: ${clinic.id})...`); + // console.log(`🔄 [queueStore] Fetching patients for clinic ${kodeKlinik} (ID: ${clinic.id})...`); - const response = await fetch(url); - if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); - - const rawResponse = await response.json(); - const data = rawResponse?.data || []; + const data = await queueAPI.fetchRawClinicPatients(clinic.id); const mappedClinicPatients = []; data.forEach((visit, index) => { @@ -270,7 +270,7 @@ export const useQueueStore = defineStore('queue', () => { syncCountersWithState(); return { success: true, message: `${mappedClinicPatients.length} pasien dimuat` }; - } catch (error) { + } catch (error: any) { console.error(`❌ [queueStore] Error fetching clinic patients (${kodeKlinik}):`, error); apiPatientsError.value = error.message; return { success: false, message: error.message }; @@ -280,251 +280,6 @@ export const useQueueStore = defineStore('queue', () => { }; // ============================================ - // WEBSOCKET INTEGRATION (CENTRALIZED) - // ============================================ - // ============================================ - // WEBSOCKET INTEGRATION (CENTRALIZED) - // ============================================ - const isWsConnected = ref(false); - const wsClientId = ref(`client-${Math.random().toString(36).substring(7)}`); - const lastGlobalCall = ref(null); - const lastKlinikCall = ref(null); - - const onWsMessage = (data) => { - // Robust data extraction: some relays wrap data in another 'data' property - let messageData = data?.data || data; - if (messageData?.data && !messageData.callKlinikEvent && !messageData.callEvent) { - messageData = messageData.data; // Double wrap check - } - - const targetLoketId = messageData?.loketId || messageData?.idloket; - const targetKlinikId = messageData?.klinikId || messageData?.idklinik; - - // Handle Call Events and WS messages - if (messageData?.triggerRefresh) { - if (messageData.klinikId) { - console.log(`🔄 [queueStore] Received refresh trigger for clinic ${messageData.klinikId}`); - - // Handle current processing update if provided - if (messageData.currentProcessingUpdate) { - console.log(`🎯 [queueStore] Applying current processing update:`, messageData.currentProcessingUpdate); - - // Merge specifically for the keys that exist in the update - Object.keys(messageData.currentProcessingUpdate).forEach(key => { - currentProcessingPatient.value[key] = messageData.currentProcessingUpdate[key]; - - // Also patch the status in allPatients if possible - const processingPatient = messageData.currentProcessingUpdate[key]; - if (processingPatient && processingPatient.no) { - const idx = allPatients.value.findIndex(p => p.no === processingPatient.no); - if (idx !== -1) { - allPatients.value[idx] = { ...allPatients.value[idx], status: 'di-loket' }; - } - } - }); - } - - fetchPatientsForClinic(messageData.klinikId, true); - } - } - if (messageData?.callEvent) { - lastGlobalCall.value = messageData.callEvent; - } - - // Handle Klinik Call Events (cross-device sync for AntrianKlinikRuang display) - if (messageData?.callKlinikEvent) { - const ev = messageData.callKlinikEvent; - console.log('🏥 [queueStore] Klinik call event received:', ev); - - // PERSISTENCE FIX: Save to lastKlinikCall for displays to watch - lastKlinikCall.value = ev; - - // Find the patient in allPatients and patch directly for immediate UI update - // Match by barcode (string) or target antrian number (part before |) - const idx = allPatients.value.findIndex(p => - p.processStage === 'klinik-ruang' && - p.kodeKlinik === ev.kodeKlinik && - ( - (p.barcode && String(p.barcode) === String(ev.barcode)) || - (p.noAntrian && p.noAntrian.split(' |')[0] === ev.noantrian) - ) - ); - - if (idx !== -1) { - // Create a patched object to ensure reactivity - const updatedPatient = { - ...allPatients.value[idx], - tipeLayanan: ev.tipeLayanan, - lastCalledAt: ev.lastCalledAt || new Date().toISOString(), - lastCalledTipeLayanan: ev.tipeLayanan, - status: 'di-loket', - calledPemeriksaanAwal: ev.tipeLayanan === 'Pemeriksaan Awal' ? true : allPatients.value[idx].calledPemeriksaanAwal, - calledTindakan: ev.tipeLayanan === 'Tindakan' ? true : allPatients.value[idx].calledTindakan - }; - - allPatients.value[idx] = updatedPatient; - console.log(`✅ [queueStore] Successfully patched patient ${ev.noantrian} status to di-loket (lastCalledAt: ${updatedPatient.lastCalledAt})`); - } else { - console.warn(`⚠️ [queueStore] Patient ${ev.noantrian} not found in store for clinic ${ev.kodeKlinik}.`); - console.log('🧪 [queueStore] Available klinik-ruang patients in store:', - allPatients.value - .filter(p => p.processStage === 'klinik-ruang') - .map(p => `[${p.kodeKlinik}] ${p.noAntrian?.split(' |')[0]} / ${p.barcode}`) - ); - } - } - // TRIGGER STRATEGIC REFRESHES - let refreshedSomething = false; - - if (targetLoketId) { - fetchPatientsForLoket(targetLoketId, true); - refreshedSomething = true; - } - - if (targetKlinikId) { - const interestingClinics = Object.keys(activeClinicInterest.value); - if (interestingClinics.includes(String(targetKlinikId)) || targetKlinikId === 'broadcast') { - const clinicToFetch = targetKlinikId === 'broadcast' ? interestingClinics[0] : targetKlinikId; - fetchPatientsForClinic(clinicToFetch, true); - refreshedSomething = true; - } - } - - if (globalInterestCount.value > 0) { - fetchAllPatients(); - refreshedSomething = true; - } - - // ALWAYS refresh our own active interests when a WebSocket message is received, - // because shared lists (e.g. unassigned patients in 'menunggu') might have changed. - const interestingLokets = Object.keys(activeLoketInterest.value); - const interestingClinics = Object.keys(activeClinicInterest.value); - - if (interestingLokets.length > 0) { - interestingLokets.forEach(loketId => { - if (String(loketId) !== String(targetLoketId)) { - fetchPatientsForLoket(loketId, true); - refreshedSomething = true; - } - }); - } - - if (interestingClinics.length > 0) { - interestingClinics.forEach(kodeKlinik => { - if (String(kodeKlinik) !== String(targetKlinikId)) { - fetchPatientsForClinic(kodeKlinik, true); - refreshedSomething = true; - } - }); - } - - if (!refreshedSomething) { - console.log(`🔕 [queueStore] WS trigger received but no active interest matched. Skipping.`); - } - }; - - const config = useRuntimeConfig(); - const wsBaseUrl = config.public?.wsBaseUrl || "ws://10.10.123.135:8084/api/v1/ws"; - - const { connect, disconnect, sendViaPost, isConnected } = useWebSocket({ - url: wsBaseUrl, - clientId: wsClientId, - fallbackPostUrl: `${config.public.externalApiBaseUrl}/ws`, - reconnectInterval: 2000, // 2 seconds between reconnect attempts - maxReconnectAttempts: 9999, // Effectively infinite — never give up on remote machines - onOpen: () => { - console.log('✅ [queueStore] WebSocket connected'); - isWsConnected.value = true; - }, - onClose: () => { - console.log('❌ [queueStore] WebSocket disconnected'); - isWsConnected.value = false; - }, - onError: (err) => { - console.error('⚠️ [queueStore] WebSocket error:', err); - isWsConnected.value = false; - }, - onMessage: onWsMessage - }); - - // ============================================ - // STORE-LEVEL AUTO-POLLING (cross-device sync fallback) - // ============================================ - // Runs on every browser instance every 30 seconds. - // Fetches data based on whatever active interests are registered - // (lokets, clinics, or global). This ensures any device always - // has fresh data regardless of WS delivery reliability. - let _autoSyncInterval = null; - - const startAutoSync = () => { - // Guard: only run on client, and only start once - if (typeof window === 'undefined') return; - if (_autoSyncInterval) return; // Already running - - console.log('🔄 [queueStore] Starting store-level auto-sync (30s interval)'); - - _autoSyncInterval = setInterval(async () => { - const hasLoketInterest = Object.keys(activeLoketInterest.value).length > 0; - const hasClinicInterest = Object.keys(activeClinicInterest.value).length > 0; - const hasGlobalInterest = globalInterestCount.value > 0; - - if (hasGlobalInterest) { - fetchAllPatients(); - } else { - if (hasLoketInterest) { - Object.keys(activeLoketInterest.value).forEach(loketId => { - fetchPatientsForLoket(loketId, true); - }); - } - if (hasClinicInterest) { - Object.keys(activeClinicInterest.value).forEach(kodeKlinik => { - fetchPatientsForClinic(kodeKlinik, true); - }); - } - } - }, 30000); // 30 seconds - }; - - const stopAutoSync = () => { - if (_autoSyncInterval) { - clearInterval(_autoSyncInterval); - _autoSyncInterval = null; - console.log('⏹️ [queueStore] Store-level auto-sync stopped'); - } - }; - - /** - * Initialize Global WebSocket - */ - const initWebSocket = (customClientId = null) => { - if (isConnected.value && customClientId === wsClientId.value) { - console.log('🔌 [queueStore] WebSocket already connected with same ID.'); - // Auto-sync should still start even if WS is already connected - startAutoSync(); - return; - } - - if (customClientId) { - wsClientId.value = customClientId; - // Re-connect with new ID if changed - disconnect(); - } - - console.log(`🔌 [queueStore] Connecting to WebSocket: ${wsBaseUrl} as ${wsClientId.value}`); - connect(); - - // Start store-level auto-polling if not already running (client-side only). - // This guarantees cross-device sync even when WS messages are missed. - startAutoSync(); - }; - - /** - * Disconnect Global WebSocket - */ - const disconnectWebSocket = () => { - disconnect(); - isWsConnected.value = false; - }; /** * Sync patient status to apiPatientsPerLoket for reactivity @@ -602,12 +357,7 @@ export const useQueueStore = defineStore('queue', () => { 18: 'pemeriksaan', // PO PEMERIKSAAN 19: 'pemeriksaan', // PS PEMERIKSAAN 32: 'pending', // PE PEMERIKSAAN - 33: 'terlambat', // TR PEMERIKSAAN - - // String versions for robustness - "1": 'menunggu', "2": 'menunggu', "3": 'anjungan', "4": 'anjungan', "5": 'di-loket', - "6": 'di-loket', "14": 'pemeriksaan', "15": 'pemeriksaan', "28": 'pending', "29": 'terlambat', - "30": 'pending', "31": 'terlambat', "32": 'pending', "33": 'terlambat' + 33: 'terlambat' // TR PEMERIKSAAN }; /** @@ -742,7 +492,7 @@ export const useQueueStore = defineStore('queue', () => { /** * Fetch patient data untuk loket tertentu dari API */ -const fetchPatientsForLoket = async (loketId, force = false) => { +const fetchPatientsForLoket = async (loketId: string | number, force: boolean = false) => { if (!loketId) { console.error('loketId required for fetchPatientsForLoket'); return { success: false, message: 'ID Loket diperlukan' }; @@ -757,7 +507,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => { const timeSinceLastFetch = now - lastFetch; if (!force && timeSinceLastFetch < 2000 && apiPatientsPerLoket.value[loketId]) { - console.log(`⏭️ [queueStore] Skipping fetch for loket ${loketId} (last fetched ${Math.round(timeSinceLastFetch/1000)}s ago)`); + // console.log(`⏭️ [queueStore] Skipping fetch for loket ${loketId} (last fetched ${Math.round(timeSinceLastFetch/1000)}s ago)`); isLoadingPatients.value = false; return { success: true, @@ -773,22 +523,8 @@ const fetchPatientsForLoket = async (loketId, force = false) => { checkAndResetDaily(); try { - console.log(`🔄 [queueStore] Fetching patients for loket ${loketId}...`); - const apiBase = config.public.verificationApiBaseUrl || 'http://10.10.123.140:8089/api/v1'; - const response = await fetch(`${apiBase}/loket/${loketId}`); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const rawData = await response.json(); - - // Check response structure - if (rawData.metadata && rawData.metadata.code !== 200) { - throw new Error(rawData.message || 'API returned error status'); - } - - const patientsRaw = rawData.data || []; + // console.log(`🔄 [queueStore] Fetching patients for loket ${loketId}...`); + const patientsRaw = await queueAPI.fetchRawLoketPatients(loketId); // Fetch temporary subspesialis mapping let subspesialisMap = {}; @@ -797,7 +533,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => { if (subRes.ok) { subspesialisMap = await subRes.json(); } - } catch (e) { + } catch (e: any) { console.error('Failed to fetch temporary subspesialis mapping', e); } @@ -999,7 +735,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => { // 6. RESTORE terlambat/pending status from LocalStorage (hybrid fallback) // This ensures status persists even if API doesn't save it - allPatients.value.forEach((patient, index) => { + allPatients.value.forEach((patient: any, index: number) => { if (patient.barcode) { const storageKey = `patient-status-${patient.barcode}`; const savedData = localStorage.getItem(storageKey); @@ -1021,7 +757,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => { // Clean up old data localStorage.removeItem(storageKey); } - } catch (e) { + } catch (e: any) { console.error('Error parsing LocalStorage data:', e); localStorage.removeItem(storageKey); } @@ -1041,7 +777,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => { data: mappedPatients }; - } catch (error) { + } catch (error: any) { console.error(`❌ [queueStore] Error fetching patients for loket ${loketId}:`, error); apiPatientsError.value = error.message; @@ -1066,7 +802,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => { * Global fetcher for all patients across all available lokets * Uses staggered fetching to prevent 429 Too Many Requests errors. */ - const fetchAllPatients = async (force = false) => { + const fetchAllPatients = async (force: boolean = false) => { // 1. Cooldown Check: Prevent global refresh spam (max once every 5 seconds) const now = Date.now(); if (!force && now - lastGlobalFetchTime.value < 5000) { @@ -1075,7 +811,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => { } lastGlobalFetchTime.value = now; - console.log('🔄 [queueStore] Fetching all patients for all lokets (Staggered)...'); + // console.log('🔄 [queueStore] Fetching all patients for all lokets (Staggered)...'); const allLokets = loketStore.lokets || []; if (allLokets.length === 0) { console.warn('⚠️ [queueStore] No lokets available for fetchAllPatients'); @@ -1129,7 +865,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => { /** * Get patients for a specific loket (from API or seed data based on loket type) */ - const getPatientsForLoket = (loketId) => { + const getPatientsForLoket = (loketId: string | number) => { return computed(() => { const loket = loketStore.getLoketById(parseInt(loketId)); @@ -1527,7 +1263,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => { /** * Filter strictly to only show today's patients (after 2 AM) */ - const isTodayPatient = (patient) => { + const isTodayPatient = (patient: any) => { if (!patient) return false; // Status processing overrides filter (always show if currently processing) @@ -1617,7 +1353,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => { setTimeout(() => { isSyncing = false; }, 50); } } - } catch (e) { + } catch (e: any) { console.error('Error hydrating from storage event:', e); isSyncing = false; } @@ -1670,7 +1406,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => { const resetPatients = () => { allPatients.value = cloneSeed(); quotaUsed.value = 5; - currentProcessingPatient.value = { loket: null, klinik: null, penunjang: null }; + currentProcessingPatient.value = {}; syncCountersWithState(); // Re-initialize counters after reset }; @@ -1815,20 +1551,13 @@ const fetchPatientsForLoket = async (loketId, force = false) => { // POST to external API when patient is called try { - const apiBase = config.public.verificationApiBaseUrl || 'http://10.10.123.140:8089/api/v1'; - fetch(`${apiBase}/tiket/update`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - barcode: nextPatient.barcode || "", - statuspasien: "3", - statuspasien2: "4", - idklinikstatus: "1", - idklinikstatus2: "1" - }) - }).then(response => { + queueAPI.updateTicketStatus( + nextPatient.barcode || "", + "3", + "4", + "1", + "1" + ).then(response => { if (response.ok) { console.log(`✅ Successfully posted status update for patient ${nextPatient.barcode}`); } else { @@ -1837,7 +1566,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => { }).catch(error => { console.error(`❌ Error posting status update for patient ${nextPatient.barcode}:`, error); }); - } catch (error) { + } catch (error: any) { console.error(`❌ Error initiating status update for patient ${nextPatient.barcode}:`, error); } } @@ -1909,7 +1638,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => { const patientsToCall = menungguList.slice(0, maxCallable); const callTimestamp = new Date().toISOString(); - patientsToCall.forEach(async (patient) => { + patientsToCall.forEach(async (patient: any) => { const index = allPatients.value.findIndex(p => p.no === patient.no); if (index !== -1) { const newStatus = "anjungan"; @@ -1927,27 +1656,20 @@ const fetchPatientsForLoket = async (loketId, force = false) => { // POST to external API when patient is called try { - const apiBase = config.public.verificationApiBaseUrl || 'http://10.10.123.140:8089/api/v1'; - const response = await fetch(`${apiBase}/tiket/update`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - barcode: patient.barcode || "", - statuspasien: "3", - statuspasien2: "4", - idklinikstatus: "1", - idklinikstatus2: "1" - }) - }); + const response = await queueAPI.updateTicketStatus( + patient.barcode || "", + "3", + "4", + "1", + "1" + ); if (response.ok) { console.log(`✅ Successfully posted status update for patient ${patient.barcode}`); } else { console.error(`⚠️ Failed to post status update for patient ${patient.barcode}:`, response.status); } - } catch (error) { + } catch (error: any) { console.error(`❌ Error posting status update for patient ${patient.barcode}:`, error); } } @@ -1986,19 +1708,12 @@ const fetchPatientsForLoket = async (loketId, force = false) => { // POST to external API when patient finishes at loket try { - const apiBase = config.public.verificationApiBaseUrl || 'http://10.10.123.140:8089/api/v1'; - fetch(`${apiBase}/tiket/selesai`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - idloket: String(patient.loketId || specificId || ""), - barcode: patient.barcode || "", - statuspasien: "9", - idklinikstatus: "2" - }) - }).then(response => { + queueAPI.completeTicketStatus( + String(patient.loketId || specificId || ""), + patient.barcode || "", + "9", + "2" + ).then(response => { if (response.ok) { console.log(`✅ [queueStore] Successfully posted selesai status for patient ${patient.barcode}`); } else { @@ -2007,7 +1722,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => { }).catch(error => { console.error(`❌ [queueStore] Error posting selesai status for patient ${patient.barcode}:`, error); }); - } catch (error) { + } catch (error: any) { console.error(`❌ [queueStore] Error initiating selesai status update for patient ${patient.barcode}:`, error); } } @@ -2088,7 +1803,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => { console.error(`❌ [TERLAMBAT] API rejected request:`, responseData); message = `Gagal: ${responseData.message || 'API error'}`; } - } catch (error) { + } catch (error: any) { console.error(`❌ [TERLAMBAT] Error:`, error); message = `Error: ${error.message}`; } @@ -2145,7 +1860,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => { console.error(`❌ [PENDING] API rejected request:`, responseData); message = `Gagal: ${responseData.message || 'API error'}`; } - } catch (error) { + } catch (error: any) { console.error(`❌ [PENDING] Error:`, error); message = `Error: ${error.message}`; } @@ -2188,7 +1903,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => { }).catch(error => { console.error(`❌ [queueStore] Error activating patient ${patient.barcode}:`, error); }); - } catch (error) { + } catch (error: any) { console.error(`❌ [queueStore] Error initiating activation for patient ${patient.barcode}:`, error); } } else { @@ -2253,7 +1968,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => { }).catch(error => { console.error(`❌ [queueStore] Error updating patient ${patient.barcode} to sedang diproses:`, error); }); - } catch (error) { + } catch (error: any) { console.error(`❌ [queueStore] Error initiating status update for patient ${patient.barcode}:`, error); } } else { @@ -2483,7 +2198,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => { // Pindah pasien ke klinik ruang lain dengan nomor antrian tetap const pindahKlinikRuang = (patient, targetKlinikRuang, targetRuang) => { - + const patientIndex = allPatients.value.findIndex(p => p.no === patient.no); if (patientIndex === -1) { return { success: false, message: "Pasien tidak ditemukan" }; } @@ -2820,7 +2535,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => { } else { console.log('✅ [queueStore] Successfully finished patient via API'); } - } catch (error) { + } catch (error: any) { console.error('❌ [queueStore] Error calling finish API:', error); // Continue with local status update even if API fails } @@ -2846,7 +2561,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); - } catch (error) { + } catch (error: any) { console.error('❌ [queueStore] Error calling terlambat API:', error); } @@ -2870,7 +2585,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); - } catch (error) { + } catch (error: any) { console.error('❌ [queueStore] Error calling pending API:', error); } @@ -3238,7 +2953,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => { subspesialis: subSpesialis }) }); - } catch (e) { + } catch (e: any) { console.error('Failed to save temporary subSpesialis mapping', e); } } @@ -3250,7 +2965,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => { patient: newPatient }; - } catch (error) { + } catch (error: any) { console.error('❌ [queueStore] Error generating ticket via API:', error); return { success: false, @@ -3292,7 +3007,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => { data: result }; - } catch (error) { + } catch (error: any) { console.error('❌ [queueStore] Error syncing check-in via API:', error); return { success: false, @@ -3422,6 +3137,31 @@ const fetchPatientsForLoket = async (loketId, force = false) => { return { success: false, message: "Gagal memproses antrean." }; }; + + // Initialize Queue Sync + const queueSync = useQueueSync({ + allPatients, + currentProcessingPatient, + activeLoketInterest, + activeClinicInterest, + globalInterestCount, + fetchPatientsForLoket, + fetchPatientsForClinic, + fetchAllPatients + }); + + const { + isWsConnected, + wsClientId, + lastGlobalCall, + lastKlinikCall, + initWebSocket, + disconnectWebSocket, + sendViaPost, + startAutoSync, + stopAutoSync + } = queueSync; + return { // State allPatients, @@ -3504,12 +3244,13 @@ const fetchPatientsForLoket = async (loketId, force = false) => { persist: { key: 'queue-store-state', storage: typeof window !== 'undefined' ? localStorage : undefined, + // @ts-ignore - plugin version mismatch paths: ['quotaUsed', 'lastUpdated'], serializer: { deserialize: JSON.parse, serialize: JSON.stringify, }, - restore: (value) => { + restore: (value: any) => { // Ensure allPatients is always an array if (value && value.allPatients && !Array.isArray(value.allPatients)) { value.allPatients = []; diff --git a/types/queue.ts b/types/queue.ts new file mode 100644 index 0000000..24bef31 --- /dev/null +++ b/types/queue.ts @@ -0,0 +1,48 @@ +export interface QueuePatient { + no: number; + barcode: string; + noAntrian: string; + jamPanggil: string; + klinik: string; + kodeKlinik: string; + klinikId?: number; + healthcareServiceId?: number; + ruang?: string; + nomorRuang?: string; + kodeRuang?: string; + pembayaran?: string; + status: 'menunggu' | 'di-loket' | 'anjungan' | 'pemeriksaan' | 'pending' | 'terlambat' | 'selesai' | 'skip' | 'processed'; + processStage: 'loket' | 'klinik-ruang'; + createdAt: string; + visitType?: string; + noRM?: string; + fastTrack?: "YA" | "TIDAK"; + registrationType?: 'api' | 'manual'; + visitId?: number; + visitCode?: string; + referencePatient?: string; + loketId?: number; + calledByAdmin?: boolean; + lastCalledAt?: string; + lastCalledTipeLayanan?: string; + calledPemeriksaanAwal?: boolean; + calledTindakan?: boolean; + tipeLayanan?: string; + idtiket?: string; + ticket?: string; + posisi?: any[]; + deskripsi?: string; + manuallyMoved?: boolean; + movedAt?: number; + idvisit?: number; + visitDate?: string; + namaDokter?: string | null; + penanggungJawab?: string | null; + alasanFastTrack?: string | null; +} + +export interface ApiPatientResponse { + success: boolean; + message: string; + data?: QueuePatient[]; +}