diff --git a/pages/AdminKlinikRuang/[kodeKlinik].vue b/pages/AdminKlinikRuang/[kodeKlinik].vue index 6abceaa..cd27871 100644 --- a/pages/AdminKlinikRuang/[kodeKlinik].vue +++ b/pages/AdminKlinikRuang/[kodeKlinik].vue @@ -986,40 +986,38 @@ const showSnackbar = (message, color = 'success') => { // Get all patients for room (menggunakan data dari API) const getAllPatientsForRoom = (ruang) => { - // Debug logging - console.log('🔍 Filtering patients for room:', { - ruangName: ruang.namaRuang, - ruangNomor: ruang.nomorRuang, - klinikCode: klinikData.value?.kodeKlinik - }); - // Prioritize API patients, fallback to queueStore patients const patients = queueStore.allPatients .filter(p => { const matches = p.kodeKlinik === klinikData.value?.kodeKlinik && p.nomorRuang === ruang.nomorRuang && - p.processStage === 'klinik-ruang' && - // CRITICAL: Only show patients with status "pemeriksaan" - // This prevents duplicate patients with different statuses - p.status === 'pemeriksaan'; - - if (!matches && p.kodeKlinik === klinikData.value?.kodeKlinik) { - console.log('❌ Patient did not match room:', { - ticket: p.noAntrian, - patientRoom: p.nomorRuang, - expectedRoom: ruang.nomorRuang, - stage: p.processStage, - status: p.status - }); - } + p.processStage === 'klinik-ruang'; return matches; }); - console.log(` ✅ Found ${patients.length} patients for room ${ruang.namaRuang}`); + // Deduplicate by barcode/visitId (keep the one with most advanced status) + const deduplicatedMap = new Map(); + const statusPriority = { 'processed': 4, 'diproses': 3, 'pemeriksaan': 2, 'di-loket': 1 }; - return patients; + patients.forEach(p => { + const key = p.visitId || p.barcode || p.no; + const existing = deduplicatedMap.get(key); + + if (!existing) { + deduplicatedMap.set(key, p); + } else { + const pPrio = statusPriority[p.status] || 0; + const ePrio = statusPriority[existing.status] || 0; + + if (pPrio > ePrio) { + deduplicatedMap.set(key, p); + } + } + }); + + return Array.from(deduplicatedMap.values()); }; // Get filtered and sorted patients for room @@ -1350,6 +1348,9 @@ const confirmPindahRuang = () => { 'success' ); + // Trigger real-time update + broadcastUpdate(); + closeKelolaPasienDialog(); }; @@ -1374,6 +1375,7 @@ const confirmPindahKlinik = async () => { if (result.success) { showSnackbar(result.message, 'success'); + broadcastUpdate(); closeKelolaPasienDialog(); } else { showSnackbar(result.message, 'error'); @@ -1414,6 +1416,7 @@ const confirmKonsultasi = async () => { } closeKelolaPasienDialog(); + broadcastUpdate(); } else { showSnackbar(result.message, 'error'); } @@ -1589,6 +1592,42 @@ const toggleLayananStatus = (tipeLayanan) => { break; } } + + // Trigger real-time update for Anjungan + broadcastUpdate(); +}; + +/** + * Broadcast update to all Anjungan screens for this clinic + */ +const broadcastUpdate = async () => { + try { + const anjunganClientIds = []; + + // Base broadcast ID + anjunganClientIds.push(`anjungan-klinik-ruang-${klinikData.value.kodeKlinik}`); + + // Screen-specific IDs + ruangList.value.forEach(r => { + if (r.nomorScreen) { + anjunganClientIds.push(`anjungan-klinik-ruang-${klinikData.value.kodeKlinik}-screen-${r.nomorScreen}`); + } + }); + + console.log('📡 [AdminKlinikRuang] Broadcasting update trigger to:', anjunganClientIds); + + for (const clientId of anjunganClientIds) { + await sendViaPost({ + to_client: clientId, + data: { + klinikId: klinikData.value.kodeKlinik, + triggerRefresh: true + } + }); + } + } catch (error) { + console.error('❌ [AdminKlinikRuang] Error broadcasting update:', error); + } }; const getStatusColor = (status) => { @@ -1743,6 +1782,9 @@ const handlePatientAction = (ruang, action) => { } showSnackbar(result.message, result.success ? 'success' : 'error'); + if (result.success) { + broadcastUpdate(); + } }; // Handle proses pasien (untuk pasien yang belum diproses) @@ -1762,6 +1804,9 @@ const handleProcessPatient = (ruang, patient) => { ); showSnackbar(result.message, result.success ? 'success' : 'error'); + if (result.success) { + broadcastUpdate(); + } }; const handleProcessPendingPatient = (ruang, patient) => { @@ -1787,6 +1832,9 @@ const handleProcessPendingPatient = (ruang, patient) => { ); showSnackbar(result.message, result.success ? 'success' : 'error'); + if (result.success) { + broadcastUpdate(); + } }; const paginatedAllPatients = (ruang) => { diff --git a/pages/AdminLoket/[id].vue b/pages/AdminLoket/[id].vue index 90a4093..7d0c155 100644 --- a/pages/AdminLoket/[id].vue +++ b/pages/AdminLoket/[id].vue @@ -378,9 +378,6 @@ onMounted(async () => { } }, 60000); // Check every minute - // Polling every 10 seconds to keep data synchronized as fallback - const pollingInterval = setInterval(fetchAllData, 10000); - // Initialize and connect WebSocket (Centralized) queueStore.initWebSocket(anjunganClientId.value); @@ -388,7 +385,6 @@ onMounted(async () => { queueStore.registerInterest(loketId.value); onUnmounted(() => { - clearInterval(pollingInterval); // Unregister interest when leaving the page queueStore.unregisterInterest(loketId.value); }); @@ -785,16 +781,50 @@ const handleCall = async (count) => { } else { callMultiplePatients(count, loketId.value); } + + // Real-time broadcast + broadcastUpdate(); + // Force Vue to process reactivity updates await nextTick(); }; const handleTableAction = (item, action) => { processPatient(item, action); + // Real-time broadcast after process + broadcastUpdate(); }; const handleProcessNext = () => { processNextQueue(); + // Real-time broadcast after process next + broadcastUpdate(); +}; + +const { sendViaPost } = useWebSocket(); + +const broadcastUpdate = async () => { + try { + const displayClientIds = []; + + // Base broadcast IDs for this loket + displayClientIds.push(`anjungan-loket-${loketId.value}`); + displayClientIds.push(`anjungan-masuk-${loketId.value}`); + + console.log('📡 [AdminLoket] Broadcasting update trigger to:', displayClientIds); + + for (const clientId of displayClientIds) { + await sendViaPost({ + to_client: clientId, + data: { + loketId: loketId.value, + triggerRefresh: true + } + }); + } + } catch (error) { + console.error('❌ [AdminLoket] Error broadcasting update:', error); + } }; const handleCallPatient = () => { @@ -805,7 +835,10 @@ const handleCallPatient = () => { snackbarColor.value = "success"; snackbar.value = true; - // BROADCAST CALL EVENT + // WebSocket Broadcast for remote displays + broadcastUpdate(); + + // local BROADCAST CALL EVENT for same-browser tabs if (broadcastChannel) { broadcastChannel.postMessage({ type: "CALL_PATIENT", @@ -1096,20 +1129,11 @@ const buatAntreanKlinikRuang = async (klinikRuang, ruang) => { ); if (result.success && result.patient) { - // CRITICAL: Update source patient status to "processed" after klinik ruang creation - // This prevents duplicate patients with different statuses in Admin Klinik Ruang - const sourcePatientIndex = queueStore.allPatients.findIndex( - (p) => p.no === patient.no, + // Patient remains in current status until admin clicks "Selesai" + // This ensures the workflow can be completed properly + console.log( + `✅ Room queue created for patient ${patient.noAntrian}. Patient remains in current status.`, ); - if (sourcePatientIndex !== -1) { - queueStore.allPatients[sourcePatientIndex] = { - ...queueStore.allPatients[sourcePatientIndex], - status: "processed", - }; - console.log( - `✅ Updated source patient ${patient.noAntrian} status to "processed"`, - ); - } try { await printTicketFromPatient(result.patient); diff --git a/pages/Anjungan/Anjungan/[id].vue b/pages/Anjungan/Anjungan/[id].vue index 08534e3..0d8a2db 100644 --- a/pages/Anjungan/Anjungan/[id].vue +++ b/pages/Anjungan/Anjungan/[id].vue @@ -784,14 +784,11 @@ onMounted(async () => { // Initial fetch await fetchAllData(); - // Polling every 10 seconds to keep data synchronized as fallback - const pollingInterval = setInterval(fetchAllData, 10000); - // Use centralized WebSocket queueStore.initWebSocket(anjunganClientId.value); onUnmounted(() => { - clearInterval(pollingInterval); + // wsInstance is now global }); }); diff --git a/pages/Anjungan/AntreanMasuk/[id].vue b/pages/Anjungan/AntreanMasuk/[id].vue index 7a56f28..7957f89 100644 --- a/pages/Anjungan/AntreanMasuk/[id].vue +++ b/pages/Anjungan/AntreanMasuk/[id].vue @@ -588,7 +588,7 @@ onMounted(() => { // Wait for stores to be hydrated from persisted state // Use nextTick to ensure stores are ready - nextTick(() => { + nextTick(async () => { // Redirect to index if screen not found if (!screenData.value) { navigateTo('/anjungan/antreanmasuk') @@ -596,21 +596,21 @@ onMounted(() => { } // Initial fetch - fetchAllData(); - - // Polling every 10 seconds to keep data synchronized as fallback - const pollingInterval = setInterval(fetchAllData, 10000); + await fetchAllData(); // Initialize and connect WebSocket (Centralized) queueStore.initWebSocket(anjunganClientId.value); - onUnmounted(() => { - clearInterval(pollingInterval); - // wsInstance is now global - }); + // Register global interest to receive staggered bulk refreshes on generic WS messages + queueStore.registerGlobalInterest(); - updateTime() - timeInterval = setInterval(updateTime, 1000) + onUnmounted(() => { + // Unregister global interest when leaving the page + queueStore.unregisterGlobalInterest(); + }); + + updateTime(); + timeInterval = setInterval(updateTime, 1000); }) }) diff --git a/pages/Anjungan/AntrianKlinik/[id].vue b/pages/Anjungan/AntrianKlinik/[id].vue index 689b9cb..fc956fb 100644 --- a/pages/Anjungan/AntrianKlinik/[id].vue +++ b/pages/Anjungan/AntrianKlinik/[id].vue @@ -353,12 +353,23 @@ onMounted(() => { updateTime(); timeInterval = setInterval(updateTime, 1000); - refreshInterval = setInterval(() => {}, 5000); -}); -onUnmounted(() => { - if (timeInterval) clearInterval(timeInterval); - if (refreshInterval) clearInterval(refreshInterval); + // Initialize and connect WebSocket (Centralized) + const anjunganClientId = `anjungan-klinik-${screenId.value}`; + queueStore.initWebSocket(anjunganClientId); + + // Register interest in each clinic on this screen for scoped WebSocket refreshes + screenKliniks.value.forEach(k => { + queueStore.registerClinicInterest(k.kode); + }); + + onUnmounted(() => { + if (timeInterval) clearInterval(timeInterval); + // Unregister clinic interests + screenKliniks.value.forEach(k => { + queueStore.unregisterClinicInterest(k.kode); + }); + }); }); diff --git a/pages/Anjungan/AntrianKlinikRuang/[kodeKlinik].vue b/pages/Anjungan/AntrianKlinikRuang/[kodeKlinik].vue index 02be5af..9c4aebf 100644 --- a/pages/Anjungan/AntrianKlinikRuang/[kodeKlinik].vue +++ b/pages/Anjungan/AntrianKlinikRuang/[kodeKlinik].vue @@ -579,17 +579,10 @@ onMounted(async () => { updateTime() timeInterval = setInterval(updateTime, 1000) - // Polling every 10 seconds to keep data synchronized as fallback - const pollingInterval = setInterval(fetchAllData, 10000); - - // Initialize and connect WebSocket (Centralized) + // WebSocket initialization is now centralized in queueStore if (kodeKlinik.value) { queueStore.initWebSocket(anjunganClientId.value); } - - onUnmounted(() => { - clearInterval(pollingInterval); - }); }) onUnmounted(() => { diff --git a/pages/Anjungan/AntrianLoket/[id].vue b/pages/Anjungan/AntrianLoket/[id].vue index 280b4f1..fa116c0 100644 --- a/pages/Anjungan/AntrianLoket/[id].vue +++ b/pages/Anjungan/AntrianLoket/[id].vue @@ -995,9 +995,6 @@ onMounted(async () => { // Initial fetch await fetchAllData(); - // Polling every 10 seconds to keep data synchronized as fallback - const pollingInterval = setInterval(fetchAllData, 10000); - // Initialize and connect WebSocket (Centralized) queueStore.initWebSocket(anjunganClientId.value); @@ -1005,7 +1002,6 @@ onMounted(async () => { queueStore.registerInterest(loketId.value); onUnmounted(() => { - clearInterval(pollingInterval); // Unregister interest when leaving the page queueStore.unregisterInterest(loketId.value); }); diff --git a/pages/CheckInPasien/checkIn.vue b/pages/CheckInPasien/checkIn.vue index d1c9fbf..2e6b181 100644 --- a/pages/CheckInPasien/checkIn.vue +++ b/pages/CheckInPasien/checkIn.vue @@ -2724,9 +2724,6 @@ onMounted(async () => { // Initial fetch/sync await fetchAllData(); - // Polling every 10 seconds to keep data synchronized as fallback - const pollingInterval = setInterval(fetchAllData, 10000); - // Initialize WebSocket (Centralized) queueStore.initWebSocket(checkInClientId.value); @@ -2739,7 +2736,6 @@ onMounted(async () => { }, 60000); // Check every minute onUnmounted(() => { - clearInterval(pollingInterval); clearInterval(resetCheckInterval); // Unregister global interest queueStore.unregisterGlobalInterest(); diff --git a/stores/queueStore.js b/stores/queueStore.js index b6c50a2..a00e55a 100644 --- a/stores/queueStore.js +++ b/stores/queueStore.js @@ -172,10 +172,28 @@ export const useQueueStore = defineStore('queue', () => { newPatientMap.set(key, p); }); + // CRITICAL FIX: Collect currently processing patient numbers to protect them + const processingPatientNos = new Set( + Object.values(currentProcessingPatient.value || {}) + .filter(p => p) + .map(p => p.no) + ); + // Update existing allPatients while preserving local UI state allPatients.value = allPatients.value.map(p => { if (p.processStage !== 'klinik-ruang' || p.kodeKlinik !== kodeKlinik) return p; + // CRITICAL FIX: Protect currently processing patients from being removed + if (processingPatientNos.has(p.no)) { + // console.log(`🛡️ [queueStore] Protecting currently processing patient ${p.noAntrian} from clinic refresh`); + const key = p.visitId ? `vid-${p.visitId}` : (p.barcode ? `bc-${p.barcode}` : `no-${p.no}`); + // Remove from newPatientMap to prevent duplicate + if (newPatientMap.has(key)) { + newPatientMap.delete(key); + } + return p; // Keep the currently processing patient as-is + } + const key = p.visitId ? `vid-${p.visitId}` : (p.barcode ? `bc-${p.barcode}` : `no-${p.no}`); const apiP = newPatientMap.get(key); @@ -214,7 +232,7 @@ export const useQueueStore = defineStore('queue', () => { // Add remaining new patients allPatients.value.push(...newPatientMap.values()); - console.log(`✅ [queueStore] Successfully fetched ${mappedClinicPatients.length} patients for clinic ${kodeKlinik}`); + // console.log(`✅ [queueStore] Successfully fetched ${mappedClinicPatients.length} patients for clinic ${kodeKlinik}`); return { success: true, message: `${mappedClinicPatients.length} pasien dimuat` }; } catch (error) { @@ -267,58 +285,70 @@ export const useQueueStore = defineStore('queue', () => { isWsConnected.value = false; }, onMessage: (data) => { - console.log('📨 [queueStore] Global WS Message received:', data); - - // TRIGGER STRATEGIC REFRESHES const messageData = data?.data || data; const targetLoketId = messageData?.loketId || messageData?.idloket; const targetKlinikId = messageData?.klinikId || messageData?.idklinik; + const isTrigger = messageData?.triggerRefresh || targetLoketId || targetKlinikId; + + if (!isTrigger) { + console.log('📨 [queueStore] Global WS Message received (No trigger, skipping refresh)'); + return; + } + + console.log('📨 [queueStore] Global WS Trigger Message received:', data); + + // 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); - } else if (targetKlinikId) { + refreshedSomething = true; + } + + if (targetKlinikId) { // 2. If message specifies a klinik, refresh those clinics - // Since the WS might only send numeric ID, we check active interests - // that might correspond to this ID or just refresh all active clinics console.log(`🎯 [queueStore] WS targeting Clinic ${targetKlinikId}: Refreshing active clinic interests...`); const interestingClinics = Object.keys(activeClinicInterest.value); interestingClinics.forEach(kodeKlinik => { fetchPatientsForClinic(kodeKlinik); }); - } else if (globalInterestCount.value > 0) { - // 3. If it's a generic message and we have global interest (Check-in page open) - // Trigger bulk staggered refresh - console.log(`📡 [queueStore] WS generic message: Global Interest active, triggering staggered bulk refresh...`); + refreshedSomething = true; + } + + // 3. If we have global interest (e.g. Check-in page open), always refresh everything on ANY trigger + if (globalInterestCount.value > 0) { + console.log(`📡 [queueStore] WS trigger: Global Interest active, triggering staggered bulk refresh...`); fetchAllPatients(); - } else { - // 4. Otherwise, only refresh lokets/clinics that currently have active interest + refreshedSomething = true; + } + + // 4. Fallback: If nothing specific was refreshed but we have generic interest, refresh all active things + if (!refreshedSomething) { const interestingLokets = Object.keys(activeLoketInterest.value); const interestingClinics = Object.keys(activeClinicInterest.value); if (interestingLokets.length > 0) { - console.log(`🌐 [queueStore] WS generic message: Refreshing ${interestingLokets.length} active lokets:`, interestingLokets); + console.log(`🌐 [queueStore] WS trigger: Refreshing ${interestingLokets.length} active lokets:`, interestingLokets); interestingLokets.forEach(loketId => { fetchPatientsForLoket(loketId); }); + refreshedSomething = true; } if (interestingClinics.length > 0) { - console.log(`🌐 [queueStore] WS generic message: Refreshing ${interestingClinics.length} active clinics:`, interestingClinics); + console.log(`🌐 [queueStore] WS trigger: Refreshing ${interestingClinics.length} active clinics:`, interestingClinics); interestingClinics.forEach(kodeKlinik => { fetchPatientsForClinic(kodeKlinik); }); - } - - if (interestingLokets.length === 0 && interestingClinics.length === 0) { - console.log(`🔕 [queueStore] WS generic message: No active interest, skipping refresh.`); + refreshedSomething = true; } } - // 4. Base data sync - clinicStore.fetchRegulerClinics(); - ensureInitialData(); + if (!refreshedSomething) { + console.log(`🔕 [queueStore] WS trigger received but no active interest matched. Skipping.`); + } } }); @@ -379,12 +409,16 @@ export const useQueueStore = defineStore('queue', () => { 4: 'anjungan', 5: 'di-loket', 6: 'di-loket', + 28: 'pending', // PE PENDAFTARAN + 29: 'terlambat', // TR PENDAFTARAN "1": 'menunggu', "2": 'menunggu', "3": 'anjungan', "4": 'anjungan', "5": 'di-loket', - "6": 'di-loket' + "6": 'di-loket', + "28": 'pending', // PE PENDAFTARAN + "29": 'terlambat' // TR PENDAFTARAN }; /** @@ -441,8 +475,8 @@ export const useQueueStore = defineStore('queue', () => { if (apiPatient.posisi && Array.isArray(apiPatient.posisi) && apiPatient.posisi.length > 0) { // Find the "best" status among all positions - // Priority: di-loket > anjungan > menunggu - const statusPriority = { 'di-loket': 3, 'anjungan': 2, 'menunggu': 1 }; + // Priority: di-loket > anjungan > menunggu > pending > terlambat + const statusPriority = { 'di-loket': 5, 'anjungan': 4, 'menunggu': 3, 'pending': 2, 'terlambat': 1 }; let bestPriority = 0; apiPatient.posisi.forEach(pos => { @@ -573,8 +607,8 @@ export const useQueueStore = defineStore('queue', () => { if (!existing) { deduplicatedMap.set(id, p); } else { - // Priority: di-loket > anjungan > menunggu - const statusPriority = { 'di-loket': 3, 'anjungan': 2, 'menunggu': 1 }; + // Priority: di-loket > anjungan > menunggu > pending > terlambat + const statusPriority = { 'di-loket': 5, 'anjungan': 4, 'menunggu': 3, 'pending': 2, 'terlambat': 1 }; const pPrio = statusPriority[p.status] || 0; const ePrio = statusPriority[existing.status] || 0; @@ -612,7 +646,7 @@ export const useQueueStore = defineStore('queue', () => { if (newPatientMap.has(key)) { // If existing patient has more advanced status (anjungan/di-loket), preserve it! // This handles cases where we updated status locally ('onsite' or 'api') but API is lagging - const statusPriority = { 'di-loket': 3, 'anjungan': 2, 'menunggu': 1 }; + const statusPriority = { 'di-loket': 5, 'anjungan': 4, 'menunggu': 3, 'pending': 2, 'terlambat': 1 }; const existingPrio = statusPriority[p.status] || 0; const newPatient = newPatientMap.get(key); const newPrio = statusPriority[newPatient.status] || 0; @@ -651,6 +685,20 @@ export const useQueueStore = defineStore('queue', () => { return true; // Keep the advanced patient } + // CRITICAL FIX: Protect currently processing patients from being removed + // Check if this patient is currently being processed in any admin interface + const isCurrentlyProcessing = Object.values(currentProcessingPatient.value || {}).some( + processingPatient => processingPatient && processingPatient.no === p.no + ); + if (isCurrentlyProcessing) { + console.log(`🛡️ [queueStore] Protecting currently processing patient ${p.noAntrian} from WebSocket refresh`); + // Remove from newPatientMap to prevent duplicate + if (newPatientMap.has(key)) { + newPatientMap.delete(key); + } + return true; // Keep the currently processing patient + } + // If it's still in loket stage but marked as processed, and it's in the API batch, // we might be experiencing API lag. Keep it as processed. if (p.status === 'processed' && newPatientMap.has(key)) { @@ -658,6 +706,14 @@ export const useQueueStore = defineStore('queue', () => { newPatientMap.delete(key); return true; } + + // CRITICAL FIX: Protect terlambat and pending status from being overwritten + // API might have lag in updating these statuses, so keep local version + if ((p.status === 'terlambat' || p.status === 'pending') && newPatientMap.has(key)) { + console.log(`🛡️ [queueStore] Protecting ${p.status} patient ${p.noAntrian} from API overwrite`); + newPatientMap.delete(key); + return true; + } // Remove if it's being replaced by new API batch if (newPatientMap.has(key)) return false; @@ -674,11 +730,75 @@ export const useQueueStore = defineStore('queue', () => { return true; }); - // 5. Add merged patients - allPatients.value.push(...patientsWithLoketId); + + // 5. Add merged patients (with deduplication check) + // Don't add if patient already exists in allPatients (prevents duplication with LocalStorage fallback) + patientsWithLoketId.forEach(newPatient => { + const existingIndex = allPatients.value.findIndex(p => + (p.barcode && newPatient.barcode && p.barcode === newPatient.barcode) || + (p.idtiket && newPatient.idtiket && p.idtiket === newPatient.idtiket) + ); + + if (existingIndex === -1) { + // Patient doesn't exist, add it + allPatients.value.push(newPatient); + } else { + // Patient exists - only update if new status has higher priority + const existing = allPatients.value[existingIndex]; + const statusPriority = { 'di-loket': 5, 'anjungan': 4, 'menunggu': 3, 'pending': 2, 'terlambat': 1 }; + const existingPrio = statusPriority[existing.status] || 0; + const newPrio = statusPriority[newPatient.status] || 0; + + // Only update if new status has higher priority (e.g., di-loket > terlambat) + // This prevents API data from overwriting LocalStorage terlambat/pending status + if (newPrio > existingPrio) { + console.log(`🔄 [queueStore] Updating patient ${newPatient.barcode} from ${existing.status} to ${newPatient.status}`); + allPatients.value[existingIndex] = { + ...newPatient, + // Preserve some local state + calledByAdmin: existing.calledByAdmin, + lastCalledAt: existing.lastCalledAt + }; + } else { + console.log(`🛡️ [queueStore] Protecting patient ${existing.barcode} with status ${existing.status} from API overwrite (${newPatient.status})`); + } + } + }); - console.log(`✅ [queueStore] Successfully fetched ${finalPatients.length} unique patients for loket ${loketId} (Original: ${mappedPatients.length})`); - console.log(`📊 [queueStore] Total patients in allPatients: ${allPatients.value.length}`); + // 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) => { + if (patient.barcode) { + const storageKey = `patient-status-${patient.barcode}`; + const savedData = localStorage.getItem(storageKey); + + if (savedData) { + try { + const { status, timestamp } = JSON.parse(savedData); + + // Only apply if status is terlambat or pending and not too old (24 hours) + const isRecent = Date.now() - timestamp < 24 * 60 * 60 * 1000; + + if ((status === 'terlambat' || status === 'pending') && isRecent) { + console.log(`🔄 [LocalStorage] Restoring ${status} status for patient ${patient.barcode}`); + allPatients.value[index] = { + ...patient, + status: status + }; + } else if (!isRecent) { + // Clean up old data + localStorage.removeItem(storageKey); + } + } catch (e) { + console.error('Error parsing LocalStorage data:', e); + localStorage.removeItem(storageKey); + } + } + } + }); + + // console.log(`✅ [queueStore] Successfully fetched ${finalPatients.length} unique patients for loket ${loketId} (Original: ${mappedPatients.length})`); + // console.log(`📊 [queueStore] Total patients in allPatients: ${allPatients.value.length}`); return { success: true, @@ -1214,7 +1334,7 @@ export const useQueueStore = defineStore('queue', () => { return; } - console.log(`🔄 Syncing state to version: ${incomingVersion}`); + // console.log(`🔄 Syncing state to version: ${incomingVersion}`); if (newState) { isSyncing = true; // Block local watch from updating timestamp @@ -1583,7 +1703,7 @@ export const useQueueStore = defineStore('queue', () => { }; }; - const processPatient = (patient, action, adminType = 'loket', specificId = null) => { + const processPatient = async (patient, action, adminType = 'loket', specificId = null) => { const storageKey = specificId ? `${adminType}-${specificId}` : adminType; const patientCode = patient.noAntrian.split(" |")[0]; let message = ""; @@ -1659,29 +1779,189 @@ export const useQueueStore = defineStore('queue', () => { } break; - case "terlambat": - allPatients.value[patientIndex] = { - ...allPatients.value[patientIndex], - status: "terlambat", - calledByAdmin: false // Reset flag - }; + case "terlambat": { + // DON'T update local state immediately - wait for API success then refresh from DB + const loketId = patient.loketId; + if (currentProcessingPatient.value[storageKey]?.no === patient.no) { currentProcessingPatient.value[storageKey] = null; } - message = `Pasien ${patientCode} ditandai terlambat`; + + message = `Menandai pasien ${patientCode} sebagai terlambat...`; + + const payload = { + barcode: patient.barcode || "", + statuspasien: "29", + statuspasien2: "29", + idklinikstatus: "2", + idklinikstatus2: "2" + }; + + console.log('📤 [TERLAMBAT] Sending API request:', payload); + + // POST to external API and WAIT for response + try { + const response = await fetch('http://10.10.150.131:8089/api/v1/tiket/update', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload) + }); + + console.log('📥 [TERLAMBAT] API response status:', response.status); + + // Read response body + const responseData = await response.json().catch(() => ({})); + console.log('📥 [TERLAMBAT] API response data:', responseData); + + if (response.ok) { + console.log(`✅ [TERLAMBAT] API accepted request`); + + // Add delay to ensure database is updated + console.log('⏳ [TERLAMBAT] Waiting 500ms for database update...'); + await new Promise(resolve => setTimeout(resolve, 500)); + + // REFRESH data from database to get updated status + console.log(`🔄 [TERLAMBAT] Refreshing data from database...`); + await fetchPatientsForLoket(loketId); + + // Verify status after refresh + const updatedPatient = allPatients.value.find(p => p.barcode === patient.barcode); + console.log('🔍 [TERLAMBAT] Patient after refresh:', updatedPatient); + + if (updatedPatient?.status === 'terlambat') { + // API successfully saved status + console.log('✅ [TERLAMBAT] Status persisted in database!'); + message = `Pasien ${patientCode} ditandai terlambat`; + } else { + // API didn't save status - use LocalStorage fallback + console.warn('⚠️ [TERLAMBAT] API did not persist status. Using LocalStorage fallback.'); + + // Save to LocalStorage + const storageKey = `patient-status-${patient.barcode}`; + localStorage.setItem(storageKey, JSON.stringify({ + status: 'terlambat', + timestamp: Date.now(), + barcode: patient.barcode + })); + + // Manually update local state since API didn't save + const patientIndex = allPatients.value.findIndex(p => p.barcode === patient.barcode); + if (patientIndex !== -1) { + allPatients.value[patientIndex] = { + ...allPatients.value[patientIndex], + status: 'terlambat', + calledByAdmin: false + }; + syncApiPatientStatus(allPatients.value[patientIndex], 'terlambat'); + } + + message = `Pasien ${patientCode} ditandai terlambat (local)`; + } + } else { + console.error(`❌ [TERLAMBAT] API rejected request:`, responseData); + message = `Gagal: ${responseData.message || 'API error'}`; + } + } catch (error) { + console.error(`❌ [TERLAMBAT] Error:`, error); + message = `Error: ${error.message}`; + } + break; + } - case "pending": - allPatients.value[patientIndex] = { - ...allPatients.value[patientIndex], - status: "pending", - calledByAdmin: false // Reset flag - }; + case "pending": { + // DON'T update local state immediately - wait for API success then refresh from DB + const loketId = patient.loketId; + if (currentProcessingPatient.value[storageKey]?.no === patient.no) { currentProcessingPatient.value[storageKey] = null; } - message = `Pasien ${patientCode} di-pending`; + + message = `Menandai pasien ${patientCode} sebagai pending...`; + + const payload = { + barcode: patient.barcode || "", + statuspasien: "28", + statuspasien2: "28", + idklinikstatus: "2", + idklinikstatus2: "2" + }; + + console.log('📤 [PENDING] Sending API request:', payload); + + // POST to external API and WAIT for response + try { + const response = await fetch('http://10.10.150.131:8089/api/v1/tiket/update', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(payload) + }); + + console.log('📥 [PENDING] API response status:', response.status); + + // Read response body + const responseData = await response.json().catch(() => ({})); + console.log('📥 [PENDING] API response data:', responseData); + + if (response.ok) { + console.log(`✅ [PENDING] API accepted request`); + + // Add delay to ensure database is updated + console.log('⏳ [PENDING] Waiting 500ms for database update...'); + await new Promise(resolve => setTimeout(resolve, 500)); + + // REFRESH data from database to get updated status + console.log(`🔄 [PENDING] Refreshing data from database...`); + await fetchPatientsForLoket(loketId); + + // Verify status after refresh + const updatedPatient = allPatients.value.find(p => p.barcode === patient.barcode); + console.log('🔍 [PENDING] Patient after refresh:', updatedPatient); + + if (updatedPatient?.status === 'pending') { + // API successfully saved status + console.log('✅ [PENDING] Status persisted in database!'); + message = `Pasien ${patientCode} di-pending`; + } else { + // API didn't save status - use LocalStorage fallback + console.warn('⚠️ [PENDING] API did not persist status. Using LocalStorage fallback.'); + + // Save to LocalStorage + const storageKey = `patient-status-${patient.barcode}`; + localStorage.setItem(storageKey, JSON.stringify({ + status: 'pending', + timestamp: Date.now(), + barcode: patient.barcode + })); + + // Manually update local state since API didn't save + const patientIndex = allPatients.value.findIndex(p => p.barcode === patient.barcode); + if (patientIndex !== -1) { + allPatients.value[patientIndex] = { + ...allPatients.value[patientIndex], + status: 'pending', + calledByAdmin: false + }; + syncApiPatientStatus(allPatients.value[patientIndex], 'pending'); + } + + message = `Pasien ${patientCode} di-pending (local)`; + } + } else { + console.error(`❌ [PENDING] API rejected request:`, responseData); + message = `Gagal: ${responseData.message || 'API error'}`; + } + } catch (error) { + console.error(`❌ [PENDING] Error:`, error); + message = `Error: ${error.message}`; + } + break; + } case "aktifkan": { const currentStatus = allPatients.value[patientIndex].status; @@ -1691,7 +1971,35 @@ export const useQueueStore = defineStore('queue', () => { ...allPatients.value[patientIndex], status: "di-loket" }; + syncApiPatientStatus(allPatients.value[patientIndex], "di-loket"); message = `Pasien ${patientCode} diaktifkan kembali dan masuk ke tabel Di Loket`; + + // POST to external API to revert status to di-loket + try { + fetch('http://10.10.150.131:8089/api/v1/tiket/update', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + barcode: patient.barcode || "", + statuspasien: "2", // Status code for "di-loket" + statuspasien2: "2", + idklinikstatus: "1", + idklinikstatus2: "1" + }) + }).then(response => { + if (response.ok) { + console.log(`✅ [queueStore] Successfully activated patient ${patient.barcode}`); + } else { + console.error(`⚠️ [queueStore] Failed to activate patient ${patient.barcode}:`, response.status); + } + }).catch(error => { + console.error(`❌ [queueStore] Error activating patient ${patient.barcode}:`, error); + }); + } catch (error) { + console.error(`❌ [queueStore] Error initiating activation for patient ${patient.barcode}:`, error); + } } else { message = `Pasien ${patientCode} tidak dapat diaktifkan (status: ${currentStatus})`; }