diff --git a/components/features/queue/CurrentPatientCard.vue b/components/features/queue/CurrentPatientCard.vue index 1e70028..7386991 100644 --- a/components/features/queue/CurrentPatientCard.vue +++ b/components/features/queue/CurrentPatientCard.vue @@ -84,6 +84,11 @@
{{ patient.klinik }} | {{ patient.pembayaran }}
+
+ mdi-link + Ref. Loket: + {{ patient.referencePatient }} +
@@ -342,6 +347,22 @@ defineEmits(['action', 'change-klinik', 'process-next', 'call', 'open-klinik-rua font-weight: 600; } +.patient-reference { + font-size: 12px; + line-height: 18px; + color: var(--color-neutral-700); +} + +.reference-label { + font-weight: 600; + color: var(--color-neutral-700); +} + +.reference-value { + font-weight: 700; + font-size: 13px; +} + .create-queue-buttons { margin-top: 0; } diff --git a/nuxt.config.ts b/nuxt.config.ts index 89a2cdf..9aaf281 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -80,7 +80,7 @@ export default defineNuxtConfig({ authUrl: process.env.AUTH_ORIGIN, // authUrl: process.env.AUTH_ORIGIN || "http://10.10.150.175:3001", // authUrl: process.env.AUTH_ORIGIN || "http://localhost:3001", - wsBaseUrl: process.env.WS_BASE_URL || 'ws://10.10.150.100:8084/api/v1/ws', + wsBaseUrl: process.env.WS_BASE_URL || 'ws://10.10.123.135:8084/api/v1/ws', ekstrakExpertiseUrl: process.env.EKSTRAK_EXPERTISE_URL || 'http://10.10.123.218/ekstrakexpertise', verificationApiBaseUrl: process.env.VERIFICATION_API_BASE_URL || 'http://10.10.123.140:8089/api/v1', }, diff --git a/pages/AdminKlinikRuang/[kodeKlinik].vue b/pages/AdminKlinikRuang/[kodeKlinik].vue index f05077e..114ccc1 100644 --- a/pages/AdminKlinikRuang/[kodeKlinik].vue +++ b/pages/AdminKlinikRuang/[kodeKlinik].vue @@ -1471,29 +1471,25 @@ const handleCallPatientFromList = async (ruang, patient, tipeLayanan) => { // Send via WebSocket to Anjungan clients try { - // Get all possible client IDs for this klinik (anjungan screens) + // Robust Klinik Code: Use patient's code first, then clinic data, then route + const patientKlinikKode = updateData.kodeKlinik || updateData.idKlinik; + const dataKlinikKode = klinikData.value?.kodeKlinik; + const targetKlinikKode = String(patientKlinikKode || dataKlinikKode || kodeKlinik.value); + + console.log('๐Ÿฅ [AdminKlinikRuang] Final Target Klinik Kode:', targetKlinikKode); + const anjunganClientIds = []; - // Always send to client ID without screen number (broadcast to all screens for this klinik) - anjunganClientIds.push(`anjungan-klinik-ruang-${klinikData.value.kodeKlinik}`); + // 1. Always send to base client ID (broadcast to all screens for this klinik) + anjunganClientIds.push(`anjungan-klinik-ruang-${targetKlinikKode}`); - // Also send to specific screen if ruang has nomorScreen + // 2. Also send to specific screen if this room has a nomorScreen if (ruang.nomorScreen) { - const specificScreenId = `anjungan-klinik-ruang-${klinikData.value.kodeKlinik}-screen-${ruang.nomorScreen}`; - if (!anjunganClientIds.includes(specificScreenId)) { - anjunganClientIds.push(specificScreenId); - } + anjunganClientIds.push(`anjungan-klinik-ruang-${targetKlinikKode}-screen-${ruang.nomorScreen}`); } - // Also send to all other screens for this klinik (from ruangList) - ruangList.value.forEach(r => { - if (r.nomorScreen) { - const screenId = `anjungan-klinik-ruang-${klinikData.value.kodeKlinik}-screen-${r.nomorScreen}`; - if (!anjunganClientIds.includes(screenId)) { - anjunganClientIds.push(screenId); - } - } - }); + // NOTE: Removed redundant loop to all other screens. + // Screens should either be Klinik-wide (using base ID) or Room-specific (using screen param in URL). console.log('๐Ÿ“‹ All target client IDs:', anjunganClientIds); @@ -1504,6 +1500,10 @@ const handleCallPatientFromList = async (ruang, patient, tipeLayanan) => { console.log('๐Ÿ“ Target clients:', anjunganClientIds); console.log('๐Ÿ“ฆ Nomor antrian:', nomorAntrian); console.log('๐Ÿ“ฆ Tipe layanan:', tipeLayanan); + console.log('๐Ÿ•™ Call timestamp:', updateData.lastCalledAt); + console.log('๐Ÿฅ [AdminKlinikRuang] debug - Route kodeKlinik:', kodeKlinik.value); + console.log('๐Ÿฅ [AdminKlinikRuang] debug - Data kodeKlinik:', klinikData.value?.kodeKlinik); + console.log('๐Ÿฅ [AdminKlinikRuang] debug - Data id:', klinikData.value?.id); // Send to all relevant clients in parallel for speed await Promise.all(anjunganClientIds.map(async (clientId) => { @@ -1511,18 +1511,27 @@ const handleCallPatientFromList = async (ruang, patient, tipeLayanan) => { to_client: clientId, data: { noantrian: nomorAntrian, - klinikId: klinikData.value?.kodeKlinik, + klinikId: kodeKlinik.value, // DONT USE Data kodeKlinik, use Route param tipeLayanan: tipeLayanan, - triggerRefresh: true + triggerRefresh: true, + // Include full call event so Anjungan can patch patient state directly + callKlinikEvent: { + noantrian: nomorAntrian, + barcode: updateData.barcode, + kodeKlinik: kodeKlinik.value, // DONT USE Data kodeKlinik, use Route param + tipeLayanan: tipeLayanan, + lastCalledAt: updateData.lastCalledAt, + nomorRuang: String(ruang.nomorRuang) + } }, }; - console.log(`๐Ÿ“จ Sending to ${clientId}:`, message); + console.log(`๐Ÿ“จ [AdminKlinikRuang] Triggering broadcast to ${clientId}:`, JSON.stringify(message.data.callKlinikEvent)); const result = await sendViaPost(message); - console.log(`โœ… Response from ${clientId}:`, result); + console.log(`โœ… [AdminKlinikRuang] Response from ${clientId}:`, result); })); - console.log('โœ… All WebSocket messages sent successfully'); + console.log('โœ… [AdminKlinikRuang] All WebSocket messages sent successfully'); } catch (error) { console.error('โŒ Error sending WebSocket message:', error); // Don't block the UI if WebSocket fails @@ -1698,6 +1707,31 @@ const handleCallPatientByTipe = async (ruang, tipeLayanan) => { queueStore.allPatients[patientIndex].barcode || '-'; + // --- API call: update visit status to idvisit 16 (PG PEMERIKSAAN AWAL) or 17 (PG PEMERIKSAAN) --- + try { + const visitStatusId = tipeLayanan === 'Pemeriksaan Awal' ? 16 : 17; + const patient = queueStore.allPatients[patientIndex]; + const apiPayload = { + patient_visit_healthcare_service_id: patient.healthcareServiceId, + visit_code: patient.barcode || patient.visitCode, + visit_status_id: [visitStatusId] + }; + console.log(`๐Ÿ“ค [AdminKlinikRuang] Sending call status (idvisit ${visitStatusId}) to API:`, apiPayload); + const apiResponse = await fetch('http://10.10.123.135:8084/api/v1/visit/status/finish', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(apiPayload) + }); + if (apiResponse.ok) { + console.log(`โœ… [AdminKlinikRuang] Call status ${visitStatusId} saved to API`); + } else { + console.warn(`โš ๏ธ [AdminKlinikRuang] API returned error for call status:`, apiResponse.status); + } + } catch (apiError) { + console.error('โŒ [AdminKlinikRuang] Error sending call status to API:', apiError); + // Continue even if API fails โ€” local UI still updates + } + try { const anjunganClientIds = []; anjunganClientIds.push(`anjungan-klinik-ruang-${klinikData.value.kodeKlinik}`); @@ -1771,7 +1805,7 @@ const handlePatientAction = async (ruang, action) => { }; // Handle proses pasien (untuk pasien yang belum diproses) -const handleProcessPatient = (ruang, patient) => { +const handleProcessPatient = async (ruang, patient) => { const patientIndex = queueStore.allPatients.findIndex(p => p.no === patient.no); if (patientIndex === -1) { showSnackbar('Pasien tidak ditemukan', 'error'); @@ -1779,7 +1813,7 @@ const handleProcessPatient = (ruang, patient) => { } // Set sebagai current processing - const result = queueStore.processPatientKlinikRuang( + const result = await queueStore.processPatientKlinikRuang( queueStore.allPatients[patientIndex], 'proses', klinikData.value.kodeKlinik, @@ -1792,7 +1826,7 @@ const handleProcessPatient = (ruang, patient) => { } }; -const handleProcessPendingPatient = (ruang, patient) => { +const handleProcessPendingPatient = async (ruang, patient) => { // Proses kembali pasien yang dipending const patientIndex = queueStore.allPatients.findIndex(p => p.no === patient.no); if (patientIndex === -1) { @@ -1807,7 +1841,7 @@ const handleProcessPendingPatient = (ruang, patient) => { }; // Set sebagai current processing - const result = queueStore.processPatientKlinikRuang( + const result = await queueStore.processPatientKlinikRuang( queueStore.allPatients[patientIndex], 'proses', klinikData.value.kodeKlinik, diff --git a/pages/AdminLoket/[id].vue b/pages/AdminLoket/[id].vue index d074c69..c1baa1c 100644 --- a/pages/AdminLoket/[id].vue +++ b/pages/AdminLoket/[id].vue @@ -164,6 +164,15 @@ }} + +
+ No. Reference (Loket) + + mdi-link + {{ currentProcessingPatient.referencePatient }} + +
+
diff --git a/pages/Anjungan/Anjungan/[id].vue b/pages/Anjungan/Anjungan/[id].vue index 18cfdf3..d2b57d1 100644 --- a/pages/Anjungan/Anjungan/[id].vue +++ b/pages/Anjungan/Anjungan/[id].vue @@ -38,7 +38,7 @@ {{ clinic.name }} -
+
{{ doctor }}
-

- Tidak ada dokter tersedia untuk klinik ini -

+
@@ -213,9 +208,7 @@ {{ doctor }} -

- Tidak ada dokter tersedia untuk klinik ini -

+

Jadwal: {{ selectedClinic.schedule }}

@@ -375,12 +368,7 @@ -

- Tidak ada dokter tersedia untuk klinik ini -

+ @@ -439,6 +427,7 @@ density="compact" required class="mb-3" + no-data-text="Klinik Tutup" > { apiDoctors && apiDoctors.length > 0 ? apiDoctors : clinic.doctors || []; if (!doctors || doctors.length === 0) { - return "Tidak ada dokter"; + return ''; } const maxDisplay = 1; @@ -1300,6 +1289,20 @@ const submitBooking = async () => { return; } + // Validasi: pastikan ada shift yang tersedia untuk tanggal ini + const availableShifts = getAvailableShiftsForBooking(); + if (availableShifts.length === 0) { + showSnackbar("Klinik Tutup pada tanggal yang dipilih.", "error"); + return; + } + + // Validasi: pastikan shift yang dipilih masih valid untuk tanggal ini + const isShiftValid = availableShifts.some(s => s.value === bookingForm.value.shift); + if (!isShiftValid) { + showSnackbar("Pilih shift yang tersedia terlebih dahulu.", "error"); + return; + } + // Validasi dokter untuk pasien Eksekutif if (isEksekutif.value && !bookingForm.value.doctor) { showSnackbar("Mohon pilih dokter terlebih dahulu.", "error"); diff --git a/pages/Anjungan/AntrianKlinikRuang/[kodeKlinik].vue b/pages/Anjungan/AntrianKlinikRuang/[kodeKlinik].vue index 8046a7e..a1c3d54 100644 --- a/pages/Anjungan/AntrianKlinikRuang/[kodeKlinik].vue +++ b/pages/Anjungan/AntrianKlinikRuang/[kodeKlinik].vue @@ -228,6 +228,56 @@ const klinikData = computed(() => { const currentTime = ref('') const currentDate = ref('') let timeInterval = null +const broadcastedKlinikPatient = ref(null) + +// Watch for Klinik Calls (Cross-Device Sync) from queueStore +watch(() => queueStore.lastKlinikCall, (newCall) => { + if (!newCall || !newCall.noantrian) return; + + console.log('๐Ÿ“ก [Anjungan] WS lastKlinikCall changed. RAW DATA:', JSON.stringify(newCall)); + + const callKode = String(newCall.kodeKlinik || '').toUpperCase(); + const myKode = String(kodeKlinik.value || '').toUpperCase(); + const isMatch = callKode === myKode; + + console.log('๐Ÿ” [Anjungan] Klinik Code Comparison:', { + received: callKode, + local: myKode, + match: isMatch + }); + + // Only react if the call is for THIS klinik + if (isMatch) { + console.log('๐Ÿ“ข [Anjungan] SUCCESS! Code match found. Processing call:', newCall.noantrian); + + // Find the patient in the current list to get full details if available + const existingPatient = klinikPatients.value.find(p => + (p.barcode && String(p.barcode) === String(newCall.barcode)) || + (p.noAntrian && p.noAntrian.split(' |')[0] === String(newCall.noantrian)) + ); + + if (existingPatient) { + console.log('โœ… [Anjungan] Linked to existing patient in memory:', existingPatient.noAntrian); + } else { + console.warn('โš ๏ธ [Anjungan] Patient not found in local list. Using broadcast details only.'); + console.log('๐Ÿงช [Anjungan] Available patient numbers in local list:', klinikPatients.value.map(p => p.noAntrian?.split(' |')[0])); + } + + // Set broadcasted patient to trigger Hero animation + broadcastedKlinikPatient.value = { + ...(existingPatient || {}), + noAntrian: existingPatient?.noAntrian || newCall.noantrian, + barcode: newCall.barcode, + tipeLayanan: newCall.tipeLayanan, + lastCalledAt: newCall.lastCalledAt || new Date().toISOString(), + status: 'di-loket', + ruang: existingPatient?.ruang || existingPatient?.namaRuang || `Ruang ${newCall.nomorRuang || '1'}`, + _source: 'websocket' + }; + } else { + console.log(`โญ๏ธ [Anjungan] Skipping call for clinic ${callKode} (this screen is for ${myKode})`); + } +}, { deep: true }); const klinikPatients = computed(() => { // Get valid room numbers for this klinik+jenisLayanan combination @@ -317,8 +367,10 @@ const displayedRuang = computed(() => { const tindakanQueues = allQueues.filter(q => q.tipeLayanan === 'Tindakan') const getCurrentAndNext = (queues, tipeLayanan) => { + // Current: patients actively being called (di-loket) OR recently called pemeriksaan const currentCandidates = queues.filter(q => - q.status === 'di-loket' && q.tipeLayanan === tipeLayanan + (q.status === 'di-loket' || (q.status === 'pemeriksaan' && q.lastCalledAt)) && + q.tipeLayanan === tipeLayanan ).sort((a, b) => { const timeA = a.lastCalledAt ? new Date(a.lastCalledAt).getTime() : 0 const timeB = b.lastCalledAt ? new Date(b.lastCalledAt).getTime() : 0 @@ -326,8 +378,11 @@ const displayedRuang = computed(() => { }) const current = currentCandidates.length > 0 ? currentCandidates[0] : null + // Next: patients waiting โ€” 'pemeriksaan' (from API) or 'menunggu' matches real statuses const next = queues.filter(q => - q.no !== current?.no && q.status === 'waiting' + q.no !== current?.no && + ['pemeriksaan', 'menunggu', 'waiting'].includes(q.status) && + !q.lastCalledAt ).slice(0, 3) return { current, next } } @@ -362,24 +417,28 @@ const currentCalledQueue = computed(() => { const CALL_DISPLAY_DURATION = 60000 // 60 seconds const now = new Date() - // Force reactivity for time updates if needed (though now is created fresh here) - // To make it truly reactive to time passing, we might need a reactive timestamp - // But since this is a computed property, it recalculates when dependencies change. - // To auto-hide after 60s without data change, we'd need a timer. - // For now, let's just use the current time when data updates. - // If we want auto-hide, we can rely on the timeInterval updating 'currentTime' - // but 'currentTime' is a string. - // Let's use a reactive timestamp or just rely on re-renders for now. - // Better: use the 'currentTime' string change to trigger re-calc if we include it. + // Force reactivity to time passing const _ = currentTime.value - // Get most recently called patient (status di-loket) - // Sort by lastCalledAt + // 1. Prioritaskan broadcastedKlinikPatient (panggilan baru dari WebSocket) + if (broadcastedKlinikPatient.value) { + const callTime = new Date(broadcastedKlinikPatient.value.lastCalledAt) + const timeDiff = now.getTime() - callTime.getTime() + + if (timeDiff <= CALL_DISPLAY_DURATION) { + return broadcastedKlinikPatient.value + } else { + // Clear it if expired + broadcastedKlinikPatient.value = null + } + } + + // 2. Fallback ke data paling lambat dari store (status di-loket atau pemeriksaan yang baru dipanggil) const calledQueues = klinikPatients.value - .filter(p => p.status === 'di-loket') + .filter(p => (p.status === 'di-loket' || p.status === 'pemeriksaan') && p.lastCalledAt) .sort((a, b) => { - const timeA = a.lastCalledAt ? new Date(a.lastCalledAt) : new Date(a.createdAt || 0) - const timeB = b.lastCalledAt ? new Date(b.lastCalledAt) : new Date(b.createdAt || 0) + const timeA = new Date(a.lastCalledAt) + const timeB = new Date(b.lastCalledAt) return timeB - timeA }) @@ -387,7 +446,7 @@ const currentCalledQueue = computed(() => { const patient = calledQueues[0] // Check if within duration - const callTime = patient.lastCalledAt ? new Date(patient.lastCalledAt) : new Date(patient.createdAt || 0) + const callTime = new Date(patient.lastCalledAt) const timeDiff = now.getTime() - callTime.getTime() if (timeDiff <= CALL_DISPLAY_DURATION) { @@ -403,14 +462,16 @@ const currentCalledQueue = computed(() => { const statistics = computed(() => { if (!kodeKlinik.value || !klinikData.value) { - return { total: 0, waiting: 0, active: 0 } + return { total: 0, anjungan: 0, active: 0 } } const all = klinikPatients.value return { total: all.length, - anjungan: all.filter(p => p.status === 'anjungan').length, - active: all.filter(p => p.status === 'di-loket').length + // Counting 'anjungan', 'menunggu', 'waiting', 'pending', 'terlambat' as waiting + anjungan: all.filter(p => ['anjungan', 'menunggu', 'waiting', 'pending', 'terlambat'].includes(p.status)).length, + // Counting 'di-loket' and 'pemeriksaan' as active/processing + active: all.filter(p => ['di-loket', 'pemeriksaan'].includes(p.status)).length } }) @@ -471,6 +532,8 @@ const fetchAllData = async () => { await clinicStore.fetchRegulerClinics(); await ruangStore.fetchRuangFromAPI(); queueStore.ensureInitialData(); + // Fetch fresh patient data for this clinic + await queueStore.fetchPatientsForClinic(kodeKlinik.value); console.log('โœ… AntrianKlinikRuang refresh: Success'); } catch (err) { console.error('โŒ AntrianKlinikRuang refresh error:', err); @@ -482,6 +545,7 @@ const anjunganClientId = computed(() => { if (!kodeKlinik.value) return '' // Priority 1: Try to get screen number from URL query params + // Use this if you want a specific screen to only show certain rooms const screenParam = route.query.screen if (screenParam) { const clientId = `anjungan-klinik-ruang-${kodeKlinik.value}-screen-${screenParam}` @@ -489,20 +553,10 @@ const anjunganClientId = computed(() => { return clientId } - // Priority 2: Try to get screen number from ruangList (ambil nomorScreen dari ruang pertama yang ada) - if (ruangListForKlinik.value && ruangListForKlinik.value.length > 0) { - // Ambil nomorScreen dari ruang pertama yang memiliki nomorScreen - const firstRuangWithScreen = ruangListForKlinik.value.find(r => r.nomorScreen) - if (firstRuangWithScreen && firstRuangWithScreen.nomorScreen) { - const clientId = `anjungan-klinik-ruang-${kodeKlinik.value}-screen-${firstRuangWithScreen.nomorScreen}` - console.log('๐Ÿ†” Using client ID from ruangList nomorScreen:', clientId) - return clientId - } - } - - // Priority 3: Fallback to client ID without screen number (broadcast) + // NOTE: Priority 2 (guessing from ruangList) removed to prevent ID mismatch + // Default to base Klinik ID (broadcast). This makes the screen receive all calls for this clinic. const clientId = `anjungan-klinik-ruang-${kodeKlinik.value}` - console.log('๐Ÿ†” Using default client ID (broadcast):', clientId) + console.log('๐Ÿ†” Using base Klinik client ID (broadcast):', clientId) return clientId }) @@ -602,12 +656,17 @@ onMounted(async () => { // WebSocket initialization is now centralized in queueStore if (kodeKlinik.value) { queueStore.initWebSocket(anjunganClientId.value); + // Register interest so WS messages trigger data refresh for this clinic + queueStore.registerClinicInterest(kodeKlinik.value); } }) onUnmounted(() => { if (timeInterval) clearInterval(timeInterval) - // wsInstance is now global + // Unregister clinic interest when leaving + if (kodeKlinik.value) { + queueStore.unregisterClinicInterest(kodeKlinik.value); + } }) // Watch for clientId changes and reconnect if needed diff --git a/stores/queueStore.js b/stores/queueStore.js index df6f7b1..06f4d2e 100644 --- a/stores/queueStore.js +++ b/stores/queueStore.js @@ -80,18 +80,18 @@ export const useQueueStore = defineStore('queue', () => { console.log(`๐ŸŒ [queueStore] Global interest unregistered. Total: ${globalInterestCount.value}`); }; - const fetchPatientsForClinic = async (kodeKlinik) => { + const fetchPatientsForClinic = async (kodeKlinik, force = false) => { if (!kodeKlinik) return { success: false, message: 'Kode Klinik diperlukan' }; isLoadingPatients.value = true; apiPatientsError.value = null; - // THROTTLE: Check if we recently fetched (within last 2 seconds) + // THROTTLE: Check if we recently fetched (within last 2 seconds), unless forced const now = Date.now(); const lastFetch = lastFetchTime.value[`clinic-${kodeKlinik}`] || 0; const timeSinceLastFetch = now - lastFetch; - if (timeSinceLastFetch < 2000) { + if (!force && timeSinceLastFetch < 2000) { 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' }; @@ -169,11 +169,22 @@ export const useQueueStore = defineStore('queue', () => { registrationType: 'api', visitId: visit.visit_id || visit.id, visitCode: visit.visit_code, - referencePatient: healthcareServices.find(s => { - const type = (s.healthcare_type_name || '').toUpperCase(); - const code = (s.healthcare_service_code || s.healtcare_service_code || '').toUpperCase(); - return type.includes('LOKET') || type.includes('PENDAFTARAN') || type.includes('REGISTRATION') || code === 'RN'; - })?.ticket || '' + referencePatient: (() => { + // First: try to find explicit LOKET/PENDAFTARAN service by type name or code + const loketService = healthcareServices.find(s => { + const type = (s.healthcare_type_name || '').toUpperCase(); + const code = (s.healthcare_service_code || s.healtcare_service_code || '').toUpperCase(); + return type.includes('LOKET') || type.includes('PENDAFTARAN') || type.includes('REGISTRATION') || code === 'RN'; + }); + if (loketService?.ticket) return loketService.ticket; + + // Fallback: find any service that is NOT KLINIK type (likely the loket/registration ticket) + const nonKlinikService = healthcareServices.find(s => { + const type = (s.healthcare_type_name || '').toUpperCase(); + return !type.includes('KLINIK') && s.ticket; + }); + return nonKlinikService?.ticket || ''; + })() }; if (isTodayPatient(p)) mappedClinicPatients.push(p); @@ -281,65 +292,141 @@ export const useQueueStore = defineStore('queue', () => { 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) => { - const messageData = data?.data || 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; - console.log('๐Ÿ“จ [queueStore] Global WS Message received:', data); + console.log('๐Ÿ“จ [queueStore] Global WS Message received Type:', typeof messageData); + console.log('๐Ÿ“จ [queueStore] Global WS Message content:', JSON.stringify(messageData)); - // Handle Call Events (cross-device sync for "Sedang Dipanggil" popup) + // Handle Call Events (cross-device sync for "Sedang Dipanggil" popup + 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) { console.log('๐Ÿ“ž [queueStore] Call event received:', 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) { - // 1. If message specifies a loket, refresh that one specifically - console.log(`๐ŸŽฏ [queueStore] WS targeting Loket ${targetLoketId}: Refreshing...`); - fetchPatientsForLoket(targetLoketId); + // 1. If message specifies a loket, refresh that one specifically (Force bypass throttle) + console.log(`๐ŸŽฏ [queueStore] WS targeting Loket ${targetLoketId}: Refreshing (FORCED)...`); + fetchPatientsForLoket(targetLoketId, true); refreshedSomething = true; } if (targetKlinikId) { - // 2. If message specifies a klinik, refresh those clinics - // Check if it matches any of our active interests or if it's a general trigger + // 2. If message specifies a klinik, refresh those clinics (Force bypass throttle) const interestingClinics = Object.keys(activeClinicInterest.value); if (interestingClinics.includes(String(targetKlinikId)) || targetKlinikId === 'broadcast') { - console.log(`๐ŸŽฏ [queueStore] WS targeting Clinic ${targetKlinikId}: Refreshing...`); - fetchPatientsForClinic(targetKlinikId === 'broadcast' ? interestingClinics[0] : targetKlinikId); + const clinicToFetch = targetKlinikId === 'broadcast' ? interestingClinics[0] : targetKlinikId; + console.log(`๐ŸŽฏ [queueStore] WS targeting Clinic ${targetKlinikId}: Refreshing ${clinicToFetch} (FORCED)...`); + fetchPatientsForClinic(clinicToFetch, true); refreshedSomething = true; } } - // 3. If we have global interest (e.g. Check-in page open), always refresh everything on ANY trigger + // 3. Global interest refresh (Force bypass throttle) if (globalInterestCount.value > 0) { - console.log(`๐Ÿ“ก [queueStore] WS trigger: Global Interest active, triggering staggered bulk refresh...`); - fetchAllPatients(); + console.log(`๐Ÿ“ก [queueStore] WS trigger: Global Interest active, triggering bulk refresh (FORCED)...`); + fetchAllPatients(); // Note: fetchAllPatients might need force internal too, but typically calls fetchPatientsForClinic refreshedSomething = true; } - // 4. Fallback: If nothing specific was refreshed but we have generic interest, refresh all active things + // 4. Fallback: If nothing specific was refreshed but we have generic interest, refresh all active things (FORCED) if (!refreshedSomething) { const interestingLokets = Object.keys(activeLoketInterest.value); const interestingClinics = Object.keys(activeClinicInterest.value); if (interestingLokets.length > 0) { - console.log(`๐ŸŒ [queueStore] WS trigger: Refreshing ${interestingLokets.length} active lokets:`, interestingLokets); + console.log(`๐ŸŒ [queueStore] WS trigger fallback: Refreshing ${interestingLokets.length} active lokets (FORCED):`, interestingLokets); interestingLokets.forEach(loketId => { - fetchPatientsForLoket(loketId); + fetchPatientsForLoket(loketId, true); }); refreshedSomething = true; } if (interestingClinics.length > 0) { - console.log(`๐ŸŒ [queueStore] WS trigger: Refreshing ${interestingClinics.length} active clinics:`, interestingClinics); + console.log(`๐ŸŒ [queueStore] WS trigger fallback: Refreshing ${interestingClinics.length} active clinics (FORCED):`, interestingClinics); interestingClinics.forEach(kodeKlinik => { - fetchPatientsForClinic(kodeKlinik); + fetchPatientsForClinic(kodeKlinik, true); }); refreshedSomething = true; } @@ -608,25 +695,25 @@ export const useQueueStore = defineStore('queue', () => { }; /** - * Fetch patient data untuk loket tertentu dari API - */ - const fetchPatientsForLoket = async (loketId) => { - if (!loketId) { - console.error('loketId required for fetchPatientsForLoket'); - return { success: false, message: 'ID Loket diperlukan' }; - } + * Fetch patient data untuk loket tertentu dari API + */ +const fetchPatientsForLoket = async (loketId, force = false) => { + if (!loketId) { + console.error('loketId required for fetchPatientsForLoket'); + return { success: false, message: 'ID Loket diperlukan' }; + } - isLoadingPatients.value = true; - apiPatientsError.value = null; + isLoadingPatients.value = true; + apiPatientsError.value = null; - // THROTTLE: Check if we recently fetched (within last 2 seconds) - const now = Date.now(); - const lastFetch = lastFetchTime.value[loketId] || 0; - const timeSinceLastFetch = now - lastFetch; - - if (timeSinceLastFetch < 2000 && apiPatientsPerLoket.value[loketId]) { - console.log(`โญ๏ธ [queueStore] Skipping fetch for loket ${loketId} (last fetched ${Math.round(timeSinceLastFetch/1000)}s ago)`); - isLoadingPatients.value = false; + // THROTTLE: Check if we recently fetched (within last 2 seconds), unless forced + const now = Date.now(); + const lastFetch = lastFetchTime.value[loketId] || 0; + 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)`); + isLoadingPatients.value = false; return { success: true, message: 'Using cached data', @@ -914,7 +1001,8 @@ export const useQueueStore = defineStore('queue', () => { const id = loket.id || loket.no; if (id) { // We use await to wait for the fetch to finish, then wait a bit more - await fetchPatientsForLoket(id); + // Pass the 'force' flag down to bypass individual throttles + await fetchPatientsForLoket(id, force); // Wait 150ms between requests (not too long, but enough to breathe) await new Promise(resolve => setTimeout(resolve, 150)); @@ -2654,6 +2742,10 @@ export const useQueueStore = defineStore('queue', () => { switch (action) { case "proses": + allPatients.value[patientIndex] = { + ...allPatients.value[patientIndex], + status: "di-loket" + }; currentProcessingPatient.value[storageKey] = allPatients.value[patientIndex]; message = `Memproses ${pCode}`; break; @@ -3344,6 +3436,7 @@ export const useQueueStore = defineStore('queue', () => { isWsConnected, sendViaPost, lastGlobalCall, + lastKlinikCall, registerInterest, unregisterInterest, activeLoketInterest,