perbaikan LOAD usage memperingan penggunaan

This commit is contained in:
Fanrouver
2026-05-18 09:22:47 +07:00
parent 2bf00eff3c
commit cb3310b44b
6 changed files with 88 additions and 299 deletions
+46 -187
View File
@@ -36,7 +36,6 @@ export const useQueueStore = defineStore('queue', () => {
if (!loketId) return;
const id = String(loketId);
activeLoketInterest.value[id] = (activeLoketInterest.value[id] || 0) + 1;
console.log(`🔌 [queueStore] Registered interest in Loket ${id}. Active:`, activeLoketInterest.value);
};
const unregisterInterest = (loketId) => {
@@ -48,14 +47,12 @@ export const useQueueStore = defineStore('queue', () => {
delete activeLoketInterest.value[id];
}
}
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) => {
@@ -67,17 +64,14 @@ export const useQueueStore = defineStore('queue', () => {
delete activeClinicInterest.value[code];
}
}
console.log(`🔌 [queueStore] Unregistered interest in Clinic ${code}. Active clinics:`, activeClinicInterest.value);
};
const registerGlobalInterest = () => {
globalInterestCount.value++;
console.log(`🌐 [queueStore] Global interest registered. Total: ${globalInterestCount.value}`);
};
const unregisterGlobalInterest = () => {
globalInterestCount.value = Math.max(0, globalInterestCount.value - 1);
console.log(`🌐 [queueStore] Global interest unregistered. Total: ${globalInterestCount.value}`);
};
const fetchPatientsForClinic = async (kodeKlinik, force = false) => {
@@ -304,10 +298,7 @@ export const useQueueStore = defineStore('queue', () => {
const targetLoketId = messageData?.loketId || messageData?.idloket;
const targetKlinikId = messageData?.klinikId || messageData?.idklinik;
console.log('📨 [queueStore] Global WS Message received Type:', typeof messageData);
console.log('📨 [queueStore] Global WS Message content:', JSON.stringify(messageData));
// Handle Call Events (cross-device sync for "Sedang Dipanggil" popup
// Handle Call Events and WS messages
if (messageData?.triggerRefresh) {
if (messageData.klinikId) {
console.log(`🔄 [queueStore] Received refresh trigger for clinic ${messageData.klinikId}`);
@@ -335,7 +326,6 @@ export const useQueueStore = defineStore('queue', () => {
}
}
if (messageData?.callEvent) {
console.log('📞 [queueStore] Call event received:', messageData.callEvent);
lastGlobalCall.value = messageData.callEvent;
}
@@ -381,53 +371,39 @@ export const useQueueStore = defineStore('queue', () => {
);
}
}
// TRIGGER STRATEGIC REFRESHES
let refreshedSomething = false;
if (targetLoketId) {
// 1. If message specifies a loket, refresh that one specifically (Force bypass throttle)
console.log(`🎯 [queueStore] WS targeting Loket ${targetLoketId}: Refreshing (FORCED)...`);
fetchPatientsForLoket(targetLoketId, true);
refreshedSomething = true;
}
if (targetKlinikId) {
// 2. If message specifies a klinik, refresh those clinics (Force bypass throttle)
const interestingClinics = Object.keys(activeClinicInterest.value);
if (interestingClinics.includes(String(targetKlinikId)) || targetKlinikId === 'broadcast') {
const clinicToFetch = targetKlinikId === 'broadcast' ? interestingClinics[0] : targetKlinikId;
console.log(`🎯 [queueStore] WS targeting Clinic ${targetKlinikId}: Refreshing ${clinicToFetch} (FORCED)...`);
fetchPatientsForClinic(clinicToFetch, true);
refreshedSomething = true;
}
}
// 3. Global interest refresh (Force bypass throttle)
if (globalInterestCount.value > 0) {
console.log(`📡 [queueStore] WS trigger: Global Interest active, triggering bulk refresh (FORCED)...`);
fetchAllPatients(); // Note: fetchAllPatients might need force internal too, but typically calls fetchPatientsForClinic
fetchAllPatients();
refreshedSomething = true;
}
// 4. Fallback: If nothing specific was refreshed but we have generic interest, refresh all active things (FORCED)
if (!refreshedSomething) {
const interestingLokets = Object.keys(activeLoketInterest.value);
const interestingClinics = Object.keys(activeClinicInterest.value);
if (interestingLokets.length > 0) {
console.log(`🌐 [queueStore] WS trigger fallback: Refreshing ${interestingLokets.length} active lokets (FORCED):`, interestingLokets);
interestingLokets.forEach(loketId => {
fetchPatientsForLoket(loketId, true);
});
interestingLokets.forEach(loketId => { fetchPatientsForLoket(loketId, true); });
refreshedSomething = true;
}
if (interestingClinics.length > 0) {
console.log(`🌐 [queueStore] WS trigger fallback: Refreshing ${interestingClinics.length} active clinics (FORCED):`, interestingClinics);
interestingClinics.forEach(kodeKlinik => {
fetchPatientsForClinic(kodeKlinik, true);
});
interestingClinics.forEach(kodeKlinik => { fetchPatientsForClinic(kodeKlinik, true); });
refreshedSomething = true;
}
}
@@ -1009,7 +985,34 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
}
}
console.log(`✅ [queueStore] Staggered bulk fetch completed. Total: ${allPatients.value.length} patients in memory.`);
// Trim allPatients if it grows too large (keep processed patients manageable)
trimPatients();
};
/**
* PHASE 2: Cap allPatients to prevent unbounded memory growth.
* Removes 'processed' patients first, then oldest non-critical ones if still over limit.
*/
const MAX_PATIENTS_IN_MEMORY = 3000;
const trimPatients = () => {
if (allPatients.value.length <= MAX_PATIENTS_IN_MEMORY) return;
// 1. Remove 'processed' patients first (they are done)
const activePatients = allPatients.value.filter(p => p.status !== 'processed');
if (activePatients.length <= MAX_PATIENTS_IN_MEMORY) {
allPatients.value = activePatients;
return;
}
// 2. If still over limit, keep the MAX most recent by createdAt
const sorted = [...activePatients].sort((a, b) => {
const aTime = a.createdAt ? new Date(a.createdAt).getTime() : 0;
const bTime = b.createdAt ? new Date(b.createdAt).getTime() : 0;
return bTime - aTime; // Newest first
});
allPatients.value = sorted.slice(0, MAX_PATIENTS_IN_MEMORY);
console.warn(`[queueStore] allPatients trimmed to ${MAX_PATIENTS_IN_MEMORY} entries to prevent memory leak.`);
};
/**
@@ -1585,30 +1588,24 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
* Handles complex names like "UMUM / JKMM / SPM / DLL"
*/
const isPaymentCompatible = (patientPayment, loketPayments) => {
if (!loketPayments || loketPayments.length === 0) return true; // No restriction
if (!loketPayments || loketPayments.length === 0) return true;
if (!patientPayment) return false;
const normalizedPatient = String(patientPayment).toUpperCase().trim();
// Map patient payment to category
const patientCategory =
(normalizedPatient.includes('JKN') || normalizedPatient.includes('BPJS')) ? 'JKN' :
normalizedPatient.includes('EKSEKUTIF') || normalizedPatient.includes('VIP') ? 'EKSEKUTIF' :
'UMUM'; // Default to UMUM for non-JKN, non-EKSEKUTIF
'UMUM';
// Check if loket accepts this category
return loketPayments.some(lp => {
const normalized = String(lp).toUpperCase().trim();
const result = (normalized === patientCategory) ||
return (normalized === patientCategory) ||
(patientCategory === 'UMUM' && (normalized.includes('UMUM') || normalized.includes('MANDIRI'))) ||
(patientCategory === 'JKN' && (normalized.includes('JKN') || normalized.includes('BPJS'))) ||
(patientCategory === 'EKSEKUTIF' && (normalized.includes('EKSEKUTIF') || normalized.includes('VIP'))) ||
(normalized === 'BPJS' && patientCategory === 'JKN') ||
(normalized === 'JKN' && patientCategory === 'JKN');
console.log(` - Checking loket accepts "${lp}" (normalized: "${normalized}") vs patient category "${patientCategory}": ${result}`);
return result;
});
};
@@ -1943,66 +1940,36 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
idklinikstatus2: "2"
};
console.log('📤 [TERLAMBAT] Sending API request:', payload);
// POST to external API and WAIT for response
try {
const response = await fetch('http://10.10.123.140:8089/api/v1/tiket/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
console.log('📥 [TERLAMBAT] API response status:', response.status);
// Read response body
const responseData = await response.json().catch(() => ({}));
console.log('📥 [TERLAMBAT] API response data:', responseData);
if (response.ok) {
console.log(`✅ [TERLAMBAT] API accepted request`);
// Add delay to ensure database is updated
console.log('⏳ [TERLAMBAT] Waiting 500ms for database update...');
await new Promise(resolve => setTimeout(resolve, 500));
// REFRESH data from database to get updated status
console.log(`🔄 [TERLAMBAT] Refreshing data from database...`);
await fetchPatientsForLoket(loketId);
// Verify status after refresh
const updatedPatient = allPatients.value.find(p => p.barcode === patient.barcode);
console.log('🔍 [TERLAMBAT] Patient after refresh:', updatedPatient);
if (updatedPatient?.status === 'terlambat') {
// API successfully saved status
console.log('✅ [TERLAMBAT] Status persisted in database!');
message = `Pasien ${patientCode} ditandai terlambat`;
} else {
// API didn't save status - use LocalStorage fallback
console.warn('⚠️ [TERLAMBAT] API did not persist status. Using LocalStorage fallback.');
// Save to LocalStorage
const storageKey = `patient-status-${patient.barcode}`;
localStorage.setItem(storageKey, JSON.stringify({
status: 'terlambat',
timestamp: Date.now(),
barcode: patient.barcode
}));
// Manually update local state since API didn't save
localStorage.setItem(storageKey, JSON.stringify({ status: 'terlambat', timestamp: Date.now(), barcode: patient.barcode }));
const patientIndex = allPatients.value.findIndex(p => p.barcode === patient.barcode);
if (patientIndex !== -1) {
allPatients.value[patientIndex] = {
...allPatients.value[patientIndex],
status: 'terlambat',
calledByAdmin: false
};
allPatients.value[patientIndex] = { ...allPatients.value[patientIndex], status: 'terlambat', calledByAdmin: false };
syncApiPatientStatus(allPatients.value[patientIndex], 'terlambat');
}
message = `Pasien ${patientCode} ditandai terlambat (local)`;
}
} else {
@@ -2035,66 +2002,33 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
idklinikstatus2: "2"
};
console.log('📤 [PENDING] Sending API request:', payload);
// POST to external API and WAIT for response
try {
const response = await fetch('http://10.10.123.140:8089/api/v1/tiket/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
console.log('📥 [PENDING] API response status:', response.status);
// Read response body
const responseData = await response.json().catch(() => ({}));
console.log('📥 [PENDING] API response data:', responseData);
if (response.ok) {
console.log(`✅ [PENDING] API accepted request`);
// Add delay to ensure database is updated
console.log('⏳ [PENDING] Waiting 500ms for database update...');
await new Promise(resolve => setTimeout(resolve, 500));
// REFRESH data from database to get updated status
console.log(`🔄 [PENDING] Refreshing data from database...`);
await fetchPatientsForLoket(loketId);
// Verify status after refresh
const updatedPatient = allPatients.value.find(p => p.barcode === patient.barcode);
console.log('🔍 [PENDING] Patient after refresh:', updatedPatient);
if (updatedPatient?.status === 'pending') {
// API successfully saved status
console.log('✅ [PENDING] Status persisted in database!');
message = `Pasien ${patientCode} di-pending`;
} else {
// API didn't save status - use LocalStorage fallback
console.warn('⚠️ [PENDING] API did not persist status. Using LocalStorage fallback.');
// Save to LocalStorage
const storageKey = `patient-status-${patient.barcode}`;
localStorage.setItem(storageKey, JSON.stringify({
status: 'pending',
timestamp: Date.now(),
barcode: patient.barcode
}));
// Manually update local state since API didn't save
localStorage.setItem(storageKey, JSON.stringify({ status: 'pending', timestamp: Date.now(), barcode: patient.barcode }));
const patientIndex = allPatients.value.findIndex(p => p.barcode === patient.barcode);
if (patientIndex !== -1) {
allPatients.value[patientIndex] = {
...allPatients.value[patientIndex],
status: 'pending',
calledByAdmin: false
};
allPatients.value[patientIndex] = { ...allPatients.value[patientIndex], status: 'pending', calledByAdmin: false };
syncApiPatientStatus(allPatients.value[patientIndex], 'pending');
}
message = `Pasien ${patientCode} di-pending (local)`;
}
} else {
@@ -2291,13 +2225,9 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
const targetLoket = allLokets.find(l =>
l.pelayanan && Array.isArray(l.pelayanan) && l.pelayanan.includes(newPatient.kodeKlinik)
);
if (targetLoket) {
newPatient.loketId = targetLoket.id;
newPatient.loket = targetLoket.namaLoket;
console.log(`✅ Auto-assigned Ticket ${newPatient.noAntrian} to Loket ${targetLoket.id} (${targetLoket.namaLoket})`);
} else {
console.log(`⚠️ No specific Loket found for Clinic ${newPatient.kodeKlinik}, defaulting to general pool.`);
}
}
@@ -2364,8 +2294,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
// Use API ticket number if available, otherwise generate locally
if (apiTicketNumber) {
newNoAntrian = apiTicketNumber; // Use ticket from API (e.g., "PK001")
console.log('🎫 Using API ticket number:', newNoAntrian);
newNoAntrian = apiTicketNumber;
} else {
// 1. Ambil huruf pertama dari nama klinik/poli
const firstLetter = klinikRuang.namaKlinik.charAt(0).toUpperCase();
@@ -3001,51 +2930,23 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
*/
const registerRegulerPatientViaApi = async (clinic, paymentType, visitType = 'SEKARANG', isFastTrack = false, fastTrackData = null) => {
try {
console.log('🔄 [queueStore] Generating ticket via API for REGULER patient...');
console.log('📋 [queueStore] Payment Type:', paymentType, '| Clinic:', clinic.name, '| Clinic Code:', clinic.kode);
// 1. Find appropriate idloket based on BOTH clinic code AND payment type
// IMPROVED: Check specifically for API-source lokets (id < 1000)
const apiLoketsExist = loketStore.lokets?.some(l => l.source === 'api' || l.id < 1000);
if (!apiLoketsExist) {
console.log('🔄 [queueStore] API loket data empty, fetching from API...');
await loketStore.fetchLoketFromAPI();
}
const allLokets = loketStore.lokets || [];
// Determine payment type for matching (normalize BPJS to JKN for API compatibility)
const paymentTypeForMatching = paymentType === "BPJS" ? "JKN" : paymentType;
console.log('🔍 [queueStore] Searching lokets...');
console.log(' Total lokets:', allLokets.length);
console.log(' API lokets (id < 1000):', allLokets.filter(l => l.source === 'api' && l.id < 1000).length);
console.log(' Looking for clinic:', clinic.kode, 'payment:', paymentTypeForMatching);
// Find loket that handles BOTH the clinic AND the payment type
const targetLoket = allLokets.find(l => {
// Check source: only use API lokets (id < 1000), not local eksekutif
if (l.source !== 'api' || l.id >= 1000) return false;
// Check if loket handles the clinic
const handlesClinic = l.pelayanan && Array.isArray(l.pelayanan) && l.pelayanan.includes(clinic.kode);
if (handlesClinic) {
// Debug: This loket handles the clinic, now check payment
console.log(` ✓ Loket ${l.id} (${l.namaLoket}) handles clinic ${clinic.kode}`);
console.log(` Pembayaran array:`, l.pembayaran);
// NEW: Use isPaymentCompatible helper for robust payment matching
const acceptsPayment = isPaymentCompatible(paymentTypeForMatching, l.pembayaran);
console.log(` Accepts payment ${paymentTypeForMatching}:`, acceptsPayment);
if (acceptsPayment) {
console.log(` ✅ MATCH FOUND: Loket ${l.id}`);
return true;
}
return isPaymentCompatible(paymentTypeForMatching, l.pembayaran);
}
return false;
});
@@ -3065,8 +2966,7 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
};
}
console.log('🎯 [queueStore] Target Loket:', `${targetLoket.namaLoket} (ID: ${targetLoket.id})`, '| Accepts Payment:', targetLoket.pembayaran);
// 2. Determine idpembayaran based on paymentType
const idloket = String(targetLoket.id);
// BPJS (JKN) = "2", UMUM and others = "1"
@@ -3100,7 +3000,6 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
}
const result = await response.json();
console.log('📥 [queueStore] API Response:', result);
if (result.metadata && result.metadata.code !== 200) {
return { success: false, message: result.message || 'Gagal generate barcode via API' };
@@ -3158,7 +3057,6 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
allPatients.value.push(newPatient);
// NOTE: Kita tidak perlu incrementBarcodeCounter() di sini karena barcode berasal dari API
console.log(`✅ [queueStore] Successfully registered REGULER patient via API: ${noAntrian}`);
return {
success: true,
@@ -3182,8 +3080,6 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
*/
const checkInPatientViaApi = async (barcode) => {
try {
console.log(`🔄 [queueStore] Syncing check-in via API for barcode: ${barcode}...`);
const body = {
barcode: barcode,
statuspasien: "5",
@@ -3192,13 +3088,9 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
idklinikstatus2: "2"
};
console.log('📤 [queueStore] Check-in API Body:', body);
const response = await fetch('http://10.10.123.140:8089/api/v1/tiket/checkin', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
@@ -3207,7 +3099,6 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
}
const result = await response.json();
console.log('📥 [queueStore] Check-in API Response:', result);
return {
success: true,
@@ -3229,53 +3120,21 @@ const fetchPatientsForLoket = async (loketId, force = false) => {
// Format barcode: YYMMDD + 5 digit (contoh: 26011500001)
// Jangan gunakan fallback ke noAntrian atau no karena bisa menyebabkan false positive
const checkInPatient = async (patientIdOrBarcode) => {
console.log('🔍 checkInPatient called with:', patientIdOrBarcode);
console.log('📊 Total patients in store:', allPatients.value.length);
// Clean input - remove whitespace and normalize
const cleanInput = String(patientIdOrBarcode).trim();
// PRIORITAS: Hanya cari dengan EXACT barcode match (case-insensitive, whitespace-insensitive)
// Format barcode: YYMMDD + 5 digit (contoh: 26011500001)
// 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();
// EXACT barcode match (case-insensitive, whitespace-insensitive)
// Ini adalah satu-satunya cara yang aman untuk match pasien
if (patientBarcode === cleanInput ||
patientBarcode.toLowerCase() === cleanInput.toLowerCase()) {
console.log('✅ Found by exact barcode match:', patientBarcode, '===', cleanInput);
return true;
}
return false;
return patientBarcode === cleanInput || patientBarcode.toLowerCase() === cleanInput.toLowerCase();
});
if (patientIndex === -1) {
console.log('❌ Patient not found. Searched for barcode:', cleanInput);
console.log('📋 Available barcodes (first 10):', allPatients.value.slice(0, 10).map(p => ({
no: p.no,
barcode: p.barcode,
noAntrian: p.noAntrian?.split(' |')[0]
})));
return { success: false, message: `Pasien dengan barcode ${cleanInput} tidak ditemukan. Pastikan barcode benar (format: YYMMDD + 5 digit, contoh: 26011500001).` };
}
// IMPORTANT: Get fresh patient data from array to avoid stale data
// Use the patientIndex we found earlier, but get fresh data from array
const patient = allPatients.value[patientIndex];
console.log('✅ Patient found (fresh):', {
no: patient.no,
barcode: patient.barcode,
noAntrian: patient.noAntrian,
status: patient.status,
processStage: patient.processStage
});
// Only allow check-in if status is anjungan (sudah dipanggil) or pending
// Pasien dengan status "menunggu" belum bisa check-in (belum dipanggil)
if (patient.status === 'menunggu') {
console.log('⚠️ Patient status is menunggu (belum dipanggil):', patient.status);
return {