push error missing function

This commit is contained in:
Fanrouver
2026-02-11 10:29:19 +07:00
parent a7a654b72a
commit 23164bcf2d
3 changed files with 268 additions and 508 deletions
+26 -476
View File
@@ -469,10 +469,11 @@
clearable
/>
<v-expansion-panels class="expansion-panels">
<v-expansion-panels v-model="activePanel" class="expansion-panels">
<v-expansion-panel
v-for="klinikRuang in filteredKlinikRuang"
:key="klinikRuang.kodeKlinik"
:value="klinikRuang.kodeKlinik"
class="expansion-panel"
>
<v-expansion-panel-title class="expansion-title">
@@ -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);
}
});
</script>
+234 -23
View File
@@ -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,
};
+8 -9
View File
@@ -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'
});