push error missing function
This commit is contained in:
@@ -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
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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'
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user