From a7a654b72a3c7ebd1ad423f58e858c569f5d7d19 Mon Sep 17 00:00:00 2001 From: Fanrouver Date: Wed, 11 Feb 2026 09:34:42 +0700 Subject: [PATCH] update WS admin loket dan checkin --- composables/useQueue.js | 36 ++++++--- pages/AdminLoket/[id].vue | 24 +++++- pages/Anjungan/AntrianLoket/[id].vue | 5 ++ pages/CheckInPasien/checkIn.vue | 5 ++ stores/queueStore.js | 114 +++++++++++++++++++++++---- 5 files changed, 153 insertions(+), 31 deletions(-) diff --git a/composables/useQueue.js b/composables/useQueue.js index 1fe7900..b85a67e 100644 --- a/composables/useQueue.js +++ b/composables/useQueue.js @@ -135,24 +135,36 @@ export const useQueue = (adminType = "loket", specificId = null) => { // Filtered lists const filteredKliniks = computed(() => { - if (!klinikSearch.value) return queueStore.kliniks; - return queueStore.kliniks.filter((k) => - k.name.toLowerCase().includes(klinikSearch.value.toLowerCase()) - ); + let result = queueStore.kliniks; + if (klinikSearch.value) { + result = result.filter((k) => + k.name.toLowerCase().includes(klinikSearch.value.toLowerCase()) + ); + } + // Sort alphabetically by name + return [...result].sort((a, b) => (a.name || "").localeCompare(b.name || "")); }); const filteredPenunjangs = computed(() => { - if (!penunjangSearch.value) return queueStore.penunjangs; - return queueStore.penunjangs.filter((p) => - p.name.toLowerCase().includes(penunjangSearch.value.toLowerCase()) - ); + let result = queueStore.penunjangs; + if (penunjangSearch.value) { + result = result.filter((p) => + p.name.toLowerCase().includes(penunjangSearch.value.toLowerCase()) + ); + } + // Sort alphabetically by name + return [...result].sort((a, b) => (a.name || "").localeCompare(b.name || "")); }); const filteredChangeKliniks = computed(() => { - if (!changeKlinikSearch.value) return queueStore.kliniks; - return queueStore.kliniks.filter((k) => - k.name.toLowerCase().includes(changeKlinikSearch.value.toLowerCase()) - ); + let result = queueStore.kliniks; + if (changeKlinikSearch.value) { + result = result.filter((k) => + k.name.toLowerCase().includes(changeKlinikSearch.value.toLowerCase()) + ); + } + // Sort alphabetically by name + return [...result].sort((a, b) => (a.name || "").localeCompare(b.name || "")); }); // Methods diff --git a/pages/AdminLoket/[id].vue b/pages/AdminLoket/[id].vue index 0cf009b..90a4093 100644 --- a/pages/AdminLoket/[id].vue +++ b/pages/AdminLoket/[id].vue @@ -177,10 +177,11 @@ class="mb-4" /> - + @@ -383,8 +384,13 @@ onMounted(async () => { // Initialize and connect WebSocket (Centralized) queueStore.initWebSocket(anjunganClientId.value); + // Register interest in this specific loket for scoped WebSocket refreshes + queueStore.registerInterest(loketId.value); + onUnmounted(() => { clearInterval(pollingInterval); + // Unregister interest when leaving the page + queueStore.unregisterInterest(loketId.value); }); }); @@ -464,6 +470,7 @@ const selectedFastTrack = ref(null); // Dialog Klinik Ruang const showKlinikRuangDialog = ref(false); const klinikRuangSearch = ref(""); +const activePanel = ref(null); // Tracks the open Klinik Ruang panel // Watch dialog state to reset search when closed watch(showKlinikRuangDialog, (newValue) => { @@ -483,9 +490,22 @@ const klinikRuangList = computed(() => { const targetLayanan = isLoketEksekutif ? "Eksekutif" : "Reguler"; // Filter rooms by service type to match Loket's service level - return (ruangStore.ruangData || []).filter( + const baseList = (ruangStore.ruangData || []).filter( (k) => k.jenisLayanan === targetLayanan, ); + + // Sort clinics alphabetically by namaKlinik + const sortedClinics = [...baseList].sort((a, b) => + (a.namaKlinik || "").localeCompare(b.namaKlinik || ""), + ); + + // Sort each clinical room list alphabetically by namaRuang + return sortedClinics.map((clinic) => ({ + ...clinic, + ruangList: [...(clinic.ruangList || [])].sort((a, b) => + (a.namaRuang || "").localeCompare(b.namaRuang || ""), + ), + })); }); const filteredKlinikRuang = computed(() => { diff --git a/pages/Anjungan/AntrianLoket/[id].vue b/pages/Anjungan/AntrianLoket/[id].vue index dca9b6c..280b4f1 100644 --- a/pages/Anjungan/AntrianLoket/[id].vue +++ b/pages/Anjungan/AntrianLoket/[id].vue @@ -1001,8 +1001,13 @@ onMounted(async () => { // Initialize and connect WebSocket (Centralized) queueStore.initWebSocket(anjunganClientId.value); + // Register interest in this specific loket for scoped WebSocket refreshes + 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 9f1959e..d1c9fbf 100644 --- a/pages/CheckInPasien/checkIn.vue +++ b/pages/CheckInPasien/checkIn.vue @@ -2729,6 +2729,9 @@ onMounted(async () => { // Initialize WebSocket (Centralized) queueStore.initWebSocket(checkInClientId.value); + + // Register global interest to receive staggered bulk refreshes on generic WS messages + queueStore.registerGlobalInterest(); // Set interval to check every minute for reset time const resetCheckInterval = setInterval(() => { @@ -2738,6 +2741,8 @@ onMounted(async () => { onUnmounted(() => { clearInterval(pollingInterval); clearInterval(resetCheckInterval); + // Unregister global interest + queueStore.unregisterGlobalInterest(); }); } else { hasCamera.value = false; diff --git a/stores/queueStore.js b/stores/queueStore.js index 7ee2f49..77d9915 100644 --- a/stores/queueStore.js +++ b/stores/queueStore.js @@ -20,8 +20,42 @@ export const useQueueStore = defineStore('queue', () => { const isLoadingPatients = ref(false); const apiPatientsError = ref(null); + // Scoped Refresh Logic: track which lokets are currently being viewed + const activeLoketInterest = ref({}); // { [loketId]: count } + const globalInterestCount = ref(0); // Tracks pages that need ALL loket data (e.g. CheckInPasien) + + const registerInterest = (loketId) => { + if (!loketId) return; + const id = String(loketId); + activeLoketInterest.value[id] = (activeLoketInterest.value[id] || 0) + 1; + console.log(`🔌 [queueStore] Registered interest in Loket ${id}. Active:`, activeLoketInterest.value); + }; + + const unregisterInterest = (loketId) => { + if (!loketId) return; + const id = String(loketId); + if (activeLoketInterest.value[id]) { + activeLoketInterest.value[id] = Math.max(0, activeLoketInterest.value[id] - 1); + if (activeLoketInterest.value[id] === 0) { + delete activeLoketInterest.value[id]; + } + } + console.log(`🔌 [queueStore] Unregistered interest in Loket ${id}. Active:`, activeLoketInterest.value); + }; + + const registerGlobalInterest = () => { + globalInterestCount.value++; + console.log(`🌐 [queueStore] Global interest registered. Total: ${globalInterestCount.value}`); + }; + + const unregisterGlobalInterest = () => { + globalInterestCount.value = Math.max(0, globalInterestCount.value - 1); + console.log(`🌐 [queueStore] Global interest unregistered. Total: ${globalInterestCount.value}`); + }; + // Throttle mechanism: track last fetch time per loket const lastFetchTime = ref({}); + const lastGlobalFetchTime = ref(0); // Cooldown for bulk refreshes // Synchronization Guard: track last update time to break loops across tabs const lastUpdated = ref(Date.now()); @@ -68,18 +102,36 @@ export const useQueueStore = defineStore('queue', () => { isWsConnected.value = false; }, onMessage: (data) => { - console.log('📨 [queueStore] Global WS Message:', data); + console.log('📨 [queueStore] Global WS Message received:', data); // TRIGGER STRATEGIC REFRESHES - // 1. Refetch patients for all active lokets in the store - Object.keys(apiPatientsPerLoket.value).forEach(loketId => { - fetchPatientsForLoket(loketId); - }); + const messageData = data?.data || data; + const targetLoketId = messageData?.loketId || messageData?.idloket; - // 2. Refetch clinics to update quotas/availability + if (targetLoketId) { + // 1. If message specifies a loket, refresh that one specifically + console.log(`🎯 [queueStore] WS targeting Loket ${targetLoketId}: Refreshing...`); + fetchPatientsForLoket(targetLoketId); + } else if (globalInterestCount.value > 0) { + // 2. 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...`); + fetchAllPatients(); + } else { + // 3. Otherwise, only refresh lokets that currently have active interest (Admin tabs open) + const interestingLokets = Object.keys(activeLoketInterest.value); + if (interestingLokets.length > 0) { + console.log(`🌐 [queueStore] WS generic message: Refreshing ${interestingLokets.length} active lokets:`, interestingLokets); + interestingLokets.forEach(loketId => { + fetchPatientsForLoket(loketId); + }); + } else { + console.log(`🔕 [queueStore] WS generic message: No active interest, skipping bulk refresh.`); + } + } + + // 4. Base data sync clinicStore.fetchRegulerClinics(); - - // 3. Ensure base data is synced ensureInitialData(); } }); @@ -450,17 +502,40 @@ export const useQueueStore = defineStore('queue', () => { /** * Global fetcher for all patients across all available lokets */ - const fetchAllPatients = async () => { - console.log('🔄 [queueStore] Fetching all patients for all lokets...'); + /** + * Global fetcher for all patients across all available lokets + * Uses staggered fetching to prevent 429 Too Many Requests errors. + */ + const fetchAllPatients = async (force = false) => { + // 1. Cooldown Check: Prevent global refresh spam (max once every 5 seconds) + const now = Date.now(); + if (!force && now - lastGlobalFetchTime.value < 5000) { + console.log(`⏭️ [queueStore] fetchAllPatients: Global refresh on cooldown (${Math.round((now - lastGlobalFetchTime.value)/1000)}s ago)`); + return; + } + lastGlobalFetchTime.value = now; + + 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'); return; } - // Use Promise.all for faster fetching - await Promise.all(allLokets.map(l => fetchPatientsForLoket(l.id))); - console.log(`✅ [queueStore] All patients fetched. Total: ${allPatients.value.length}`); + // 2. Staggered Fetch: Instead of Promise.all, we fetch one by one with a small delay + // This spreads the load and stays under the 429 threshold + for (const loket of allLokets) { + 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); + + // Wait 150ms between requests (not too long, but enough to breathe) + await new Promise(resolve => setTimeout(resolve, 150)); + } + } + + console.log(`✅ [queueStore] Staggered bulk fetch completed. Total: ${allPatients.value.length} patients in memory.`); }; /** @@ -810,11 +885,11 @@ export const useQueueStore = defineStore('queue', () => { // For example: { 'loket-1': patient, 'loket-2': patient, 'klinik-3': patient } const currentProcessingPatient = ref({}); - // Daftar klinik untuk dropdown diambil 1 pintu dari clinicStore - const kliniks = ref(clinicStore.clinics || []); + // Daftar klinik untuk dropdown diambil 1 pintu dari clinicStore (Computed for stability) + const kliniks = computed(() => clinicStore.clinics || []); - // Penunjang data - reference dari penunjangStore - const penunjangs = ref(penunjangStore.penunjangs || []); + // Penunjang data - reference dari penunjangStore (Computed for stability) + const penunjangs = computed(() => penunjangStore.penunjangList || []); const RESET_HOUR = 2; // 2 AM reset threshold @@ -2582,6 +2657,11 @@ export const useQueueStore = defineStore('queue', () => { initWebSocket, disconnectWebSocket, isWsConnected, + registerInterest, + unregisterInterest, + activeLoketInterest, + registerGlobalInterest, + unregisterGlobalInterest, }; }, {