From 23164bcf2d4839ce0614b8daafc7be1682662302 Mon Sep 17 00:00:00 2001 From: Fanrouver Date: Wed, 11 Feb 2026 10:29:19 +0700 Subject: [PATCH] push error missing function --- pages/AdminKlinikRuang/[kodeKlinik].vue | 502 ++---------------------- stores/queueStore.js | 257 ++++++++++-- stores/ruangStore.js | 17 +- 3 files changed, 268 insertions(+), 508 deletions(-) diff --git a/pages/AdminKlinikRuang/[kodeKlinik].vue b/pages/AdminKlinikRuang/[kodeKlinik].vue index e5ddc68..6abceaa 100644 --- a/pages/AdminKlinikRuang/[kodeKlinik].vue +++ b/pages/AdminKlinikRuang/[kodeKlinik].vue @@ -469,10 +469,11 @@ clearable /> - + @@ -890,10 +891,8 @@ const showFilterDialog = ref({}); const filterOptions = ref({}); const klinikRuangSearch = ref(''); -// API Patient Data -const apiPatients = ref([]); -const isLoadingPatients = ref(false); const apiError = ref(null); +const activePanel = ref(null); // Tracks the open Klinik Ruang panel in dialog // Helper: Check if a patient is from today const isTodayPatient = (patient) => { @@ -945,10 +944,10 @@ const adminClientId = computed(() => { }) const fetchAllData = async () => { - if (!klinikData.value) return; + if (!kodeKlinik.value) return; console.log('πŸ”„ AdminKlinikRuang refresh: Syncing data...'); try { - await fetchPatientsFromAPI(); + await queueStore.fetchPatientsForClinic(kodeKlinik.value); queueStore.ensureInitialData(); console.log('βœ… AdminKlinikRuang refresh: Success'); } catch (err) { @@ -983,451 +982,7 @@ const showSnackbar = (message, color = 'success') => { * Fetch patients from API for the clinic * Uses filters: klinik_id, klinik_ruang_id, active, limit */ -const fetchPatientsFromAPI = async () => { - if (!klinikData.value) { - console.warn('⚠️ Klinik data not available'); - return; - } - - isLoadingPatients.value = true; - apiError.value = null; - - try { - // Get klinik_id from clinicStore - find clinic by kodeKlinik - const clinic = clinicStore.clinics.find(c => - c.kode === klinikData.value.kodeKlinik && - (!jenisLayanan.value || c.jenisLayanan === jenisLayanan.value) - ); - - if (!clinic || !clinic.id) { - console.warn('⚠️ Klinik ID not found for', klinikData.value.kodeKlinik); - console.log('Available clinics:', clinicStore.clinics.map(c => ({ kode: c.kode, id: c.id, jenisLayanan: c.jenisLayanan }))); - isLoadingPatients.value = false; - return; - } - - const klinikId = clinic.id; - - // Build API URL with filters - using Nuxt proxy to avoid CORS - // CRITICAL: Do NOT use active filter at top level - we need to see all healthcare_services - // to filter by healthcare_type_name="KLINIK" with active=true - const baseUrl = '/visit-api/visit'; - const params = new URLSearchParams({ - klinik_id: klinikId.toString(), - limit: '500' - }); - - // If there are rooms, fetch data for each room - // For now, we'll fetch all patients for the clinic and filter by room on the client side - const url = `${baseUrl}?${params.toString()}`; - - console.log('πŸ”„ Fetching patients from:', url); - console.log('πŸ“‹ Using clinic ID:', klinikId, 'for clinic code:', klinikData.value.kodeKlinik); - - const response = await fetch(url); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const rawResponse = await response.json(); - console.log('πŸ“¦ Raw API Response:', rawResponse); - - // Extract data array from response wrapper - const data = rawResponse?.data || []; - const message = rawResponse?.message || ''; - - console.log('πŸ“‹ API Message:', message); - console.log('πŸ“Š Data array length:', data.length); - - // Process the response data - if (Array.isArray(data) && data.length > 0) { - // Map API data to our internal format - apiPatients.value = []; - - data.forEach((visit, index) => { - // Each visit can have multiple healthcare_services (sub-services/rooms) - const allHealthcareServices = visit.healthcare_services || []; - - // CRITICAL FIX: Filter for KLINIK healthcare_type_name with active=true - // This identifies patients who have been transferred to klinik ruang - const healthcareServices = allHealthcareServices.filter(service => - service.healthcare_type_name === 'KLINIK' && service.active === true - ); - - if (healthcareServices.length > 0) { - healthcareServices.forEach(service => { - // Convert room ID to string for nomorRuang matching - const roomId = service.fk_ms_sub_healthcare_service_id; - const roomIdString = roomId ? String(roomId) : null; - - // Extract status from visit_statuses array (use latest status) - const visitStatuses = visit.visit_statuses || []; - const latestStatus = visitStatuses.length > 0 - ? visitStatuses[visitStatuses.length - 1] - : null; - - // Map status from API or default to 'di-loket' (patients just arrived from loket) - // Status 'pemeriksaan' will be set explicitly when patient is transferred from loket - let patientStatus = 'di-loket'; // Default for klinik-ruang patients - if (latestStatus && latestStatus.desc) { - const desc = latestStatus.desc.toLowerCase(); - if (desc.includes('pemeriksaan') || desc.includes('sedang diproses')) { - patientStatus = 'pemeriksaan'; - } else if (desc.includes('check-in') || desc.includes('loket')) { - patientStatus = 'di-loket'; - } - } - - const mappedPatient = { - no: visit.id || (10000 + index), - barcode: visit.visit_code || service.ticket || '', // Prioritize visit_code - noAntrian: service.ticket || visit.visit_code || '', - jamPanggil: service.check_in_datetime || visit.registration_datetime || '', - klinik: service.healthcare_service_name || klinikData.value.namaKlinik, - kodeKlinik: klinikData.value.kodeKlinik, - klinikId: service.fk_ms_healthcare_service_id || klinikId, - ruang: service.sub_healthcare_service_name || '', - nomorRuang: roomIdString, // Use string version of room ID - pembayaran: service.payment_type_name || visit.payment_type_name || '', // Payment type from service level - status: patientStatus, // Status from API or default 'di-loket' - processStage: 'klinik-ruang', - createdAt: visit.registration_datetime || new Date().toISOString(), - visitType: visit.visit_type_name || 'ONSITE', - noRM: visit.norm || '', - fastTrack: 'TIDAK', - registrationType: 'api', - visitId: visit.id, - visitCode: visit.visit_code, - // Store original API data for reference - _apiData: { visit, service } - }; - - // Only add if patient is from today - if (isTodayPatient(mappedPatient)) { - apiPatients.value.push(mappedPatient); - } - }); - } else { - // If no healthcare_services, create one entry for the visit - // Extract status from visit_statuses - const visitStatuses = visit.visit_statuses || []; - const latestStatus = visitStatuses.length > 0 - ? visitStatuses[visitStatuses.length - 1] - : null; - - let patientStatus = 'di-loket'; - if (latestStatus && latestStatus.desc) { - const desc = latestStatus.desc.toLowerCase(); - if (desc.includes('pemeriksaan') || desc.includes('sedang diproses')) { - patientStatus = 'pemeriksaan'; - } else if (desc.includes('check-in') || desc.includes('loket')) { - patientStatus = 'di-loket'; - } - } - - const mappedPatient = { - no: visit.id || (10000 + index), - barcode: visit.visit_code || '', // Use visit_code for barcode - noAntrian: visit.visit_code || '', - jamPanggil: visit.registration_datetime || '', - klinik: klinikData.value.namaKlinik, - kodeKlinik: klinikData.value.kodeKlinik, - klinikId: klinikId, - ruang: '', - nomorRuang: null, - pembayaran: visit.payment_type_name || '', // Payment type from visit level - status: patientStatus, // Status from API or default 'di-loket' - processStage: 'klinik-ruang', - createdAt: visit.registration_datetime || new Date().toISOString(), - visitType: visit.visit_type_name || 'ONSITE', - noRM: visit.norm || '', - fastTrack: 'TIDAK', - registrationType: 'api', - visitId: visit.id, - visitCode: visit.visit_code, - _apiData: { visit } - }; - - // Only add if patient is from today - if (isTodayPatient(mappedPatient)) { - apiPatients.value.push(mappedPatient); - } - } - }); - - const totalVisits = data.length; - const totalPatientEntries = apiPatients.value.length; - - console.log(`βœ… Loaded ${totalPatientEntries} patient entries from ${totalVisits} visits (today only)`); - console.log('πŸ“‹ Sample patient data:', apiPatients.value.slice(0, 2).map(p => ({ - ticket: p.noAntrian, - ruang: p.ruang, - nomorRuang: p.nomorRuang, - pembayaran: p.pembayaran, - kodeKlinik: p.kodeKlinik, - createdAt: p.createdAt - }))); - - // Merge with queueStore.allPatients to maintain consistency - mergeApiPatientsToStore(); - } else { - console.warn('⚠️ Unexpected API response format:', data); - apiPatients.value = []; - } - } catch (error) { - console.error('❌ Error fetching patients:', error); - apiError.value = error.message; - showSnackbar(`Gagal memuat data pasien: ${error.message}`, 'error'); - } finally { - isLoadingPatients.value = false; - } -}; - -/** - * Fetch patients for a specific room - */ -const fetchPatientsForRoom = async (ruang) => { - if (!klinikData.value || !ruang) { - console.warn('⚠️ Klinik data or room not available'); - return; - } - - isLoadingPatients.value = true; - apiError.value = null; - - try { - // Get klinik_id from masterStore ruangData - const ruangData = masterStore.ruangData || []; - const currentKlinik = ruangData.find(r => - r.kodeKlinik === klinikData.value.kodeKlinik && - (!jenisLayanan.value || r.jenisLayanan === jenisLayanan.value) - ); - - if (!currentKlinik || !currentKlinik.idKlinik) { - console.warn('⚠️ Klinik ID not found'); - isLoadingPatients.value = false; - return; - } - - const klinikId = currentKlinik.idKlinik; - - // Build API URL with filters including room ID - using Nuxt proxy to avoid CORS - const baseUrl = '/visit-api/visit'; - const params = new URLSearchParams({ - active: '1', - klinik_id: klinikId.toString(), - limit: '500' - }); - - // Add room filter if room has an ID - if (ruang.idRuang) { - params.append('klinik_ruang_id', ruang.idRuang.toString()); - } - - const url = `${baseUrl}?${params.toString()}`; - - console.log('πŸ”„ Fetching patients for room from:', url); - - const response = await fetch(url); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const rawResponse = await response.json(); - - // Extract data array from response wrapper - const data = rawResponse?.data || []; - - // Process and merge the response data - if (Array.isArray(data) && data.length > 0) { - const roomPatients = []; - - data.forEach((visit, index) => { - const healthcareServices = visit.healthcare_services || []; - - if (healthcareServices.length > 0) { - healthcareServices.forEach(service => { - // Convert room ID to string for nomorRuang matching - const roomId = service.fk_ms_sub_healthcare_service_id; - const roomIdString = roomId ? String(roomId) : null; - - // Extract status from visit_statuses - const visitStatuses = visit.visit_statuses || []; - const latestStatus = visitStatuses.length > 0 - ? visitStatuses[visitStatuses.length - 1] - : null; - - let patientStatus = 'di-loket'; - if (latestStatus && latestStatus.desc) { - const desc = latestStatus.desc.toLowerCase(); - if (desc.includes('pemeriksaan') || desc.includes('sedang diproses')) { - patientStatus = 'pemeriksaan'; - } else if (desc.includes('check-in') || desc.includes('loket')) { - patientStatus = 'di-loket'; - } - } - - const mappedPatient = { - no: visit.id || (10000 + index), - barcode: visit.visit_code || service.ticket || '', // Prioritize visit_code - noAntrian: service.ticket || visit.visit_code || '', - jamPanggil: service.check_in_datetime || visit.registration_datetime || '', - klinik: service.healthcare_service_name || klinikData.value.namaKlinik, - kodeKlinik: klinikData.value.kodeKlinik, - klinikId: service.fk_ms_healthcare_service_id || klinikId, - ruang: ruang.namaRuang, - nomorRuang: roomIdString, // Use string version of room ID - nomorScreen: ruang.nomorScreen, - pembayaran: service.payment_type_name || visit.payment_type_name || '', // Payment type from service level - status: patientStatus, // Status from API or default 'di-loket' - processStage: 'klinik-ruang', - createdAt: visit.registration_datetime || new Date().toISOString(), - visitType: visit.visit_type_name || 'ONSITE', - noRM: visit.norm || '', - fastTrack: 'TIDAK', - registrationType: 'api', - visitId: visit.id, - visitCode: visit.visit_code, - _apiData: { visit, service } - }; - - // Only add if patient is from today - if (isTodayPatient(mappedPatient)) { - roomPatients.push(mappedPatient); - } - }); - } else { - // Extract status from visit_statuses - const visitStatuses = visit.visit_statuses || []; - const latestStatus = visitStatuses.length > 0 - ? visitStatuses[visitStatuses.length - 1] - : null; - - let patientStatus = 'di-loket'; - if (latestStatus && latestStatus.desc) { - const desc = latestStatus.desc.toLowerCase(); - if (desc.includes('pemeriksaan') || desc.includes('sedang diproses')) { - patientStatus = 'pemeriksaan'; - } else if (desc.includes('check-in') || desc.includes('loket')) { - patientStatus = 'di-loket'; - } - } - - const mappedPatient = { - no: visit.id || (10000 + index), - barcode: visit.visit_code || '', // Use visit_code for barcode - noAntrian: visit.visit_code || '', - jamPanggil: visit.registration_datetime || '', - klinik: klinikData.value.namaKlinik, - kodeKlinik: klinikData.value.kodeKlinik, - klinikId: klinikId, - ruang: ruang.namaRuang, - nomorRuang: ruang.nomorRuang, - nomorScreen: ruang.nomorScreen, - pembayaran: visit.payment_type_name || '', // Payment type from visit level - status: patientStatus, // Status from API or default 'di-loket' - processStage: 'klinik-ruang', - createdAt: visit.registration_datetime || new Date().toISOString(), - visitType: visit.visit_type_name || 'ONSITE', - noRM: visit.norm || '', - fastTrack: 'TIDAK', - registrationType: 'api', - visitId: visit.id, - visitCode: visit.visit_code, - _apiData: { visit } - }; - - // Only add if patient is from today - if (isTodayPatient(mappedPatient)) { - roomPatients.push(mappedPatient); - } - } - }); - - // Update apiPatients for this specific room - // Remove old patients for this room and add new ones - apiPatients.value = [ - ...apiPatients.value.filter(p => p.nomorRuang !== ruang.nomorRuang), - ...roomPatients - ]; - - console.log(`βœ… Loaded ${roomPatients.length} patients for room ${ruang.namaRuang}`); - - mergeApiPatientsToStore(); - } - } catch (error) { - console.error('❌ Error fetching patients for room:', error); - apiError.value = error.message; - } finally { - isLoadingPatients.value = false; - } -}; - -/** - * Merge API patients into queueStore.allPatients - * SMART MERGE: Preserves local updates (status, call indicators) while refreshing API data - */ -const mergeApiPatientsToStore = () => { - if (apiPatients.value.length === 0) return; - - // Build a Map of API patients by identifier for fast lookup - const apiPatientMap = new Map(); - apiPatients.value.forEach(apiP => { - const id = apiP.visitId || apiP.visitCode || apiP.barcode || apiP.noAntrian; - if (id) apiPatientMap.set(id, apiP); - }); - - // Update existing patients OR preserve local changes - const updatedPatients = queueStore.allPatients.map(localP => { - const id = localP.visitId || localP.visitCode || localP.barcode || localP.noAntrian; - const apiP = apiPatientMap.get(id); - - if (!apiP) return localP; // No API match, keep local patient - - // Found API match - MERGE data intelligently - // Check if patient has local UI updates that should be preserved - const hasLocalUpdates = ( - localP.status !== 'pemeriksaan' || // Status changed from default - localP.calledPemeriksaanAwal || // Called for pemeriksaan awal - localP.calledTindakan || // Called for tindakan - localP.lastCalledAt // Has call timestamp - ); - - if (hasLocalUpdates) { - console.log('βœ… Preserving local updates for:', localP.noAntrian); - // Keep local patient data but refresh certain API fields - return { - ...apiP, // Base API data (fresh barcode, noAntrian, etc) - status: localP.status, // Preserve local status - calledPemeriksaanAwal: localP.calledPemeriksaanAwal, - calledTindakan: localP.calledTindakan, - tipeLayanan: localP.tipeLayanan, - lastCalledAt: localP.lastCalledAt, - lastCalledTipeLayanan: localP.lastCalledTipeLayanan - }; - } else { - // No local updates, use fresh API data - return apiP; - } - }); - - // Find NEW API patients (not in allPatients) - const existingIds = new Set( - queueStore.allPatients.map(p => p.visitId || p.visitCode || p.barcode || p.noAntrian).filter(Boolean) - ); - - const newApiPatients = apiPatients.value.filter(apiP => { - const id = apiP.visitId || apiP.visitCode || apiP.barcode || apiP.noAntrian; - return !existingIds.has(id); - }); - - // Replace with updated patients + add new ones - queueStore.allPatients = [...updatedPatients, ...newApiPatients]; - - console.log(`πŸ“Š Total patients in store: ${queueStore.allPatients.length}`); -}; +// Local merging and fetching logic removed, now handled by queueStore.fetchPatientsForClinic // Get all patients for room (menggunakan data dari API) const getAllPatientsForRoom = (ruang) => { @@ -2307,43 +1862,38 @@ watch(ruangList, (newList) => { }, { immediate: true }); onMounted(async () => { - console.log('πŸš€ AdminKlinikRuang mounted, провСряСм data...'); + console.log('πŸš€ AdminKlinikRuang mounted, syncing data...'); - // 1. Sync data if needed + // 1. Sync master data if needed try { - // Fetch clinics first await clinicStore.fetchRegulerClinics(); - // Then sync rooms await ruangStore.fetchRuangFromAPI(); - - // 2. Fetch patient data from API - if (klinikData.value) { - console.log('πŸ“‹ Fetching patient data for clinic:', klinikData.value.kodeKlinik); - await fetchPatientsFromAPI(); - } } catch (error) { console.error('❌ Error syncing data in AdminKlinikRuang:', error); } - if (!klinikData.value) { - console.warn('⚠️ Klinik data not found after sync'); - } - - // 3. Centralized WebSocket & polling - await nextTick(); - - // Initial fetch/sync + // 2. Initial fetch and periodic polling await fetchAllData(); - // Polling every 10 seconds to keep data synchronized as fallback - const pollingInterval = setInterval(fetchAllData, 10000); - - // Use centralized WebSocket + // 3. Centralized WebSocket & interest registration queueStore.initWebSocket(adminClientId.value); - onUnmounted(() => { - clearInterval(pollingInterval); - }); + if (kodeKlinik.value) { + queueStore.registerClinicInterest(kodeKlinik.value); + } +}); + +// 4. Lifecycle cleanup (Keep outside async block to preserve context) +let pollInterval; +onMounted(() => { + pollInterval = setInterval(fetchAllData, 30000); +}); + +onUnmounted(() => { + if (pollInterval) clearInterval(pollInterval); + if (kodeKlinik.value) { + queueStore.unregisterClinicInterest(kodeKlinik.value); + } }); diff --git a/stores/queueStore.js b/stores/queueStore.js index 77d9915..b6c50a2 100644 --- a/stores/queueStore.js +++ b/stores/queueStore.js @@ -16,12 +16,20 @@ export const useQueueStore = defineStore('queue', () => { // ============================================ // 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 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 registerInterest = (loketId) => { @@ -40,7 +48,26 @@ export const useQueueStore = defineStore('queue', () => { delete activeLoketInterest.value[id]; } } - console.log(`πŸ”Œ [queueStore] Unregistered interest in Loket ${id}. Active:`, activeLoketInterest.value); + console.log(`πŸ”Œ [queueStore] Unregistered interest in Loket ${id}. Active lokets:`, activeLoketInterest.value); + }; + + const registerClinicInterest = (kodeKlinik) => { + if (!kodeKlinik) return; + const code = String(kodeKlinik); + activeClinicInterest.value[code] = (activeClinicInterest.value[code] || 0) + 1; + console.log(`πŸ”Œ [queueStore] Registered interest in Clinic ${code}. Active clinics:`, activeClinicInterest.value); + }; + + const unregisterClinicInterest = (kodeKlinik) => { + if (!kodeKlinik) return; + const code = String(kodeKlinik); + if (activeClinicInterest.value[code]) { + activeClinicInterest.value[code] = Math.max(0, activeClinicInterest.value[code] - 1); + if (activeClinicInterest.value[code] === 0) { + delete activeClinicInterest.value[code]; + } + } + console.log(`πŸ”Œ [queueStore] Unregistered interest in Clinic ${code}. Active clinics:`, activeClinicInterest.value); }; const registerGlobalInterest = () => { @@ -53,13 +80,151 @@ export const useQueueStore = defineStore('queue', () => { 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()); - // synchronization guard (moved lower) + const fetchPatientsForClinic = async (kodeKlinik) => { + if (!kodeKlinik) return { success: false, message: 'Kode Klinik diperlukan' }; + + isLoadingPatients.value = true; + apiPatientsError.value = null; + + // THROTTLE: Check if we recently fetched (within last 5 seconds) + const now = Date.now(); + const lastFetch = lastFetchTime.value[`clinic-${kodeKlinik}`] || 0; + const timeSinceLastFetch = now - lastFetch; + + if (timeSinceLastFetch < 5000) { + 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' }; + } + + lastFetchTime.value[`clinic-${kodeKlinik}`] = now; + + try { + // Find clinic ID from clinicStore + const clinic = clinicStore.clinics.find(c => c.kode === kodeKlinik); + if (!clinic || !clinic.id) { + throw new Error(`Klinik ID tidak ditemukan untuk kode: ${kodeKlinik}`); + } + + const url = `/visit-api/visit?klinik_id=${clinic.id}&limit=500`; + 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 mappedClinicPatients = []; + data.forEach((visit, index) => { + const healthcareServices = visit.healthcare_services || []; + + // Filter for KLINIK healthcare_type_name with active=true + const services = healthcareServices.filter(s => + s.healthcare_type_name === 'KLINIK' && s.active === true + ); + + if (services.length > 0) { + services.forEach(service => { + const roomId = service.fk_ms_sub_healthcare_service_id; + const visitStatuses = visit.visit_statuses || []; + const latestStatus = visitStatuses[visitStatuses.length - 1]; + + let patientStatus = 'di-loket'; + if (latestStatus?.desc) { + const desc = latestStatus.desc.toLowerCase(); + if (desc.includes('pemeriksaan') || desc.includes('sedang diproses')) { + patientStatus = 'pemeriksaan'; + } + } + + const p = { + no: visit.id || (20000 + index), + barcode: visit.visit_code || service.ticket || '', + noAntrian: service.ticket || visit.visit_code || '', + jamPanggil: service.check_in_datetime || visit.registration_datetime || '', + klinik: service.healthcare_service_name || clinic.name, + kodeKlinik: kodeKlinik, + klinikId: clinic.id, + ruang: service.sub_healthcare_service_name || '', + nomorRuang: roomId ? String(roomId) : String(clinic.id), + pembayaran: service.payment_type_name || visit.payment_type_name || '', + status: patientStatus, + processStage: 'klinik-ruang', + createdAt: visit.registration_datetime || new Date().toISOString(), + visitType: visit.visit_type_name || 'ONSITE', + noRM: visit.norm || '', + fastTrack: 'TIDAK', + registrationType: 'api', + visitId: visit.visit_id || visit.id, + visitCode: visit.visit_code + }; + + if (isTodayPatient(p)) mappedClinicPatients.push(p); + }); + } + }); + + // MERGE LOGIC + const newPatientMap = new Map(); + mappedClinicPatients.forEach(p => { + const key = p.visitId ? `vid-${p.visitId}` : (p.barcode ? `bc-${p.barcode}` : `no-${p.no}`); + newPatientMap.set(key, p); + }); + + // Update existing allPatients while preserving local UI state + allPatients.value = allPatients.value.map(p => { + if (p.processStage !== 'klinik-ruang' || p.kodeKlinik !== kodeKlinik) return p; + + const key = p.visitId ? `vid-${p.visitId}` : (p.barcode ? `bc-${p.barcode}` : `no-${p.no}`); + const apiP = newPatientMap.get(key); + + if (apiP) { + // If we have local updates, preserve them + const hasLocalUpdates = ( + p.status !== 'di-loket' || + p.calledPemeriksaanAwal || + p.calledTindakan || + p.lastCalledAt + ); + + if (hasLocalUpdates) { + newPatientMap.delete(key); // Mark as handled + return { + ...apiP, + status: p.status, + calledPemeriksaanAwal: p.calledPemeriksaanAwal, + calledTindakan: p.calledTindakan, + tipeLayanan: p.tipeLayanan, + lastCalledAt: p.lastCalledAt, + lastCalledTipeLayanan: p.lastCalledTipeLayanan + }; + } else { + newPatientMap.delete(key); // Handled + return apiP; + } + } + + // If it was an API patient that disappeared from the list, it's either stale or from another room + if (p.registrationType === 'api') return null; + + return p; + }).filter(Boolean); + + // Add remaining new patients + allPatients.value.push(...newPatientMap.values()); + + console.log(`βœ… [queueStore] Successfully fetched ${mappedClinicPatients.length} patients for clinic ${kodeKlinik}`); + return { success: true, message: `${mappedClinicPatients.length} pasien dimuat` }; + + } catch (error) { + console.error(`❌ [queueStore] Error fetching clinic patients (${kodeKlinik}):`, error); + apiPatientsError.value = error.message; + return { success: false, message: error.message }; + } finally { + isLoadingPatients.value = false; + } + }; // ============================================ // WEBSOCKET INTEGRATION (CENTRALIZED) @@ -107,26 +272,47 @@ export const useQueueStore = defineStore('queue', () => { // TRIGGER STRATEGIC REFRESHES const messageData = data?.data || data; const targetLoketId = messageData?.loketId || messageData?.idloket; + const targetKlinikId = messageData?.klinikId || messageData?.idklinik; 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) { + // 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) { - // 2. If it's a generic message and we have global interest (Check-in page open) + // 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...`); fetchAllPatients(); } else { - // 3. Otherwise, only refresh lokets that currently have active interest (Admin tabs open) + // 4. Otherwise, only refresh lokets/clinics that currently have active interest 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); interestingLokets.forEach(loketId => { fetchPatientsForLoket(loketId); }); - } else { - console.log(`πŸ”• [queueStore] WS generic message: No active interest, skipping bulk refresh.`); + } + + if (interestingClinics.length > 0) { + console.log(`🌐 [queueStore] WS generic message: 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.`); } } @@ -454,17 +640,35 @@ export const useQueueStore = defineStore('queue', () => { allPatients.value = allPatients.value.filter(p => { const key = p.idtiket ? `id-${p.idtiket}` : `bc-${p.barcode}`; - // Remove if it's being replaced by new API batch (matches by barcode or idtiket) + // CRITICAL FIX: If patient has already moved to a later stage (e.g. klinik-ruang), + // do NOT remove it or overwrite it with a 'loket' stage API patient. + if (p.processStage && p.processStage !== 'loket') { + // Check if this advanced patient matches the incoming batch + if (newPatientMap.has(key)) { + // Signal to skip adding the 'loket' version from API + newPatientMap.delete(key); + } + return true; // Keep the advanced 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)) { + // Skip replacing it with 'menunggu'/'anjungan' status + newPatientMap.delete(key); + return true; + } + + // Remove if it's being replaced by new API batch if (newPatientMap.has(key)) return false; - // Also check if barcode matches any new API patient (for cases where local has no idtiket) - // This prevents duplicates like barcode "2602050017" appearing as both BPJS (local) and JKN (API) + // Also check if barcode matches any new API patient if (p.barcode) { const barcodeMatches = patientsWithLoketId.some(newP => newP.barcode === p.barcode); if (barcodeMatches) return false; } - // Remove if it's a stale 'api' patient for this loket (that wasn't in the new batch) + // Remove if it's a stale 'api' patient for this loket if (p.registrationType === 'api' && String(p.loketId) === String(loketId)) return false; return true; @@ -499,6 +703,7 @@ export const useQueueStore = defineStore('queue', () => { } }; + /** * Global fetcher for all patients across all available lokets */ @@ -879,11 +1084,7 @@ export const useQueueStore = defineStore('queue', () => { }; // Initial state - START EMPTY to avoid clashing with hydration - const allPatients = ref([]); - const quotaUsed = ref(5); - // State - currentProcessingPatient is now an object mapping 'stage-id' to patient - // For example: { 'loket-1': patient, 'loket-2': patient, 'klinik-3': patient } - const currentProcessingPatient = ref({}); + // Placeholder lines for moved state references (already handled at top) // Daftar klinik untuk dropdown diambil 1 pintu dari clinicStore (Computed for stability) const kliniks = computed(() => clinicStore.clinics || []); @@ -1721,12 +1922,15 @@ export const useQueueStore = defineStore('queue', () => { nomorScreen: ruang.nomorScreen, fastTrack: patient ? (patient.fastTrack || "TIDAK") : "TIDAK", pembayaran: patient ? patient.pembayaran : "UMUM", - noRM: patient ? patient.noRM : null, + noRM: patient ? (patient.noRM || patient.norm) : null, status: "pemeriksaan", // Patients from loket to klinik ruang start as "pemeriksaan" processStage: "klinik-ruang", // Set ke klinik-ruang langsung createdAt: timestamp.toISOString(), referencePatient: patient ? patient.noAntrian : null, sourcePatientNo: patient ? patient.no : null, + visitId: patient ? (patient.visitId || patient.id) : null, + visitCode: patient ? (patient.visitCode || patient.visit_code) : null, + registrationType: patient ? (patient.registrationType || 'onsite') : 'onsite', // Tracking panggilan calledPemeriksaanAwal: false, calledTindakan: false, @@ -2459,7 +2663,7 @@ export const useQueueStore = defineStore('queue', () => { // PRIORITAS: Hanya cari dengan EXACT barcode match (case-insensitive, whitespace-insensitive) // Format barcode: YYMMDD + 5 digit (contoh: 26011500001) - // Jangan gunakan fallback ke noAntrian atau no karena bisa menyebabkan false positive + // Ini adalah satu-satunya cara yang aman untuk match pasien const patientIndex = allPatients.value.findIndex(p => { // Normalize barcode untuk comparison const patientBarcode = String(p.barcode || '').trim(); @@ -2605,6 +2809,9 @@ export const useQueueStore = defineStore('queue', () => { ensureInitialData, quotaUsed, currentProcessingPatient, + lastUpdated, + lastFetchTime, + lastGlobalFetchTime, kliniks, penunjangs, @@ -2645,6 +2852,7 @@ export const useQueueStore = defineStore('queue', () => { // API Patient Actions fetchPatientsForLoket, + fetchPatientsForClinic, fetchAllPatients, getPatientsForLoket, mapStatusFromDeskripsi, @@ -2660,6 +2868,9 @@ export const useQueueStore = defineStore('queue', () => { registerInterest, unregisterInterest, activeLoketInterest, + registerClinicInterest, + unregisterClinicInterest, + activeClinicInterest, registerGlobalInterest, unregisterGlobalInterest, }; diff --git a/stores/ruangStore.js b/stores/ruangStore.js index c7b40d6..bbcb570 100644 --- a/stores/ruangStore.js +++ b/stores/ruangStore.js @@ -408,11 +408,8 @@ export const useRuangStore = defineStore('ruang', () => { const isDuplicate = group.apiRooms.some(existing => { const existingId = existing.idruangan || existing.idruang; - const existingName = (existing.namaruangan || existing.nama_ruang || existing.nama || '').trim().toUpperCase(); - - // Duplicate if ID matches OR Name matches (more aggressive de-duplication) + // Only de-duplicate if IDs match. If IDs are different but names same, they might be valid distinct rooms. if (roomId && existingId && String(roomId) === String(existingId)) return true; - if (roomName && existingName && roomName === existingName) return true; return false; }); @@ -439,15 +436,16 @@ export const useRuangStore = defineStore('ruang', () => { if (apiRooms.length > 0 && !(isGenericSingleRoom && hasManySpecialists)) { apiRooms.forEach((apiRoom, index) => { const fallbackName = `Ruang ${apiRoom.idruangan || apiRoom.nomor_ruang || index + 1}`; + const finalRoomId = apiRoom.idruangan || apiRoom.idruang || (index + 1).toString(); mergedRooms.push({ id: roomIdCounter++, kodeKlinik: clinic.kode, namaKlinik: clinic.name, - kodeRuang: apiRoom.idruangan || apiRoom.idruang || `${clinic.kode}-${roomIdCounter}`, + kodeRuang: finalRoomId, namaRuang: apiRoom.namaruangan || apiRoom.nama_ruang || apiRoom.nama || fallbackName, - nomorRuang: apiRoom.idruangan || apiRoom.nomor_ruang || (index + 1).toString(), - nomorScreen: apiRoom.nomor_screen || `${clinic.kode}${apiRoom.idruangan || index + 1}`, + nomorRuang: finalRoomId, + nomorScreen: apiRoom.nomor_screen || `${clinic.kode}${finalRoomId}`, jenisLayanan: clinic.jenisLayanan || 'Reguler' }); }); @@ -455,13 +453,14 @@ export const useRuangStore = defineStore('ruang', () => { // Priority 2: Fallback to clinic identity if no rooms found else { console.log(`ℹ️ Using clinic fallback for ${clinic.name} (${apiRooms.length === 0 ? 'Empty API' : 'Generic API'})`); + const fallbackRoomId = rawClinicData.idruangan || rawClinicData.idruang || clinic.id || clinic.kode; mergedRooms.push({ id: roomIdCounter++, kodeKlinik: clinic.kode, namaKlinik: clinic.name, - kodeRuang: rawClinicData.idruangan || rawClinicData.idruang || clinic.id || clinic.kode, + kodeRuang: fallbackRoomId, namaRuang: clinic.name || rawClinicData.namaklinik || 'Ruang 1', - nomorRuang: rawClinicData.nomor_ruang || '1', + nomorRuang: String(fallbackRoomId), nomorScreen: rawClinicData.nomor_screen || `${clinic.kode}1`, jenisLayanan: clinic.jenisLayanan || 'Reguler' });