perbaikan LOAD usage memperingan penggunaan
This commit is contained in:
@@ -72,8 +72,6 @@ export const useWebSocket = (config: WebSocketConfig) => {
|
||||
}
|
||||
|
||||
const connectionUrl = wsUrl.value
|
||||
console.log('🔌 Connecting to WebSocket:', connectionUrl)
|
||||
|
||||
ws.value = new WebSocket(connectionUrl)
|
||||
|
||||
ws.value.onopen = () => {
|
||||
@@ -166,9 +164,6 @@ export const useWebSocket = (config: WebSocketConfig) => {
|
||||
postUrl = `${urlObj.protocol}//${urlObj.host}${urlObj.pathname}`
|
||||
}
|
||||
|
||||
console.log('📡 POST URL:', postUrl)
|
||||
console.log('📦 POST Body:', JSON.stringify(message, null, 2))
|
||||
|
||||
const response = await fetch(postUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -177,20 +172,13 @@ export const useWebSocket = (config: WebSocketConfig) => {
|
||||
body: JSON.stringify(message),
|
||||
})
|
||||
|
||||
console.log('📥 POST Response status:', response.status, response.statusText)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text().catch(() => 'Unknown error')
|
||||
console.error('❌ POST Error response:', errorText)
|
||||
throw new Error(`HTTP error! status: ${response.status}, message: ${errorText}`)
|
||||
}
|
||||
|
||||
const result = await response.json().catch(() => {
|
||||
console.log('ℹ️ Response is not JSON, assuming success')
|
||||
return { success: true }
|
||||
})
|
||||
|
||||
console.log('✅ POST Response data:', result)
|
||||
const result = await response.json().catch(() => ({ success: true }))
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('❌ Error sending message via POST:', error)
|
||||
|
||||
@@ -2439,11 +2439,8 @@ const stopScanning = async () => {
|
||||
};
|
||||
|
||||
const handleQRScanSuccess = (decodedText: string) => {
|
||||
console.log("🎯 QR Scan Success! Data:", decodedText);
|
||||
|
||||
// Guard: Jika sedang proses atau dialog sedang terbuka, abaikan scan
|
||||
if (isProcessingScan.value || infoDialog.value) {
|
||||
console.log("⏭️ Scan diabaikan: Sedang memproses atau dialog terbuka");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3015,20 +3012,14 @@ const onDetect = async (decodedText: string) => {
|
||||
// 2. Format testing: "patientId|status" (contoh: P-00001|ALLOWED)
|
||||
// Status akan dicek real-time dari queueStore saat scan QR
|
||||
|
||||
console.log("🔍 onDetect called with QR data:", decodedText);
|
||||
|
||||
// Extract barcode/patientId dari QR data (handle format testing dengan pipe)
|
||||
const qrData = extractQRData(decodedText);
|
||||
const searchBarcode = qrData.barcode; // Barcode/patientId untuk dicari di queueStore
|
||||
const searchBarcode = qrData.barcode;
|
||||
|
||||
// Clean input - remove whitespace and normalize
|
||||
const cleanInput = String(searchBarcode).trim();
|
||||
const cleanInputUpper = cleanInput.toUpperCase();
|
||||
|
||||
console.log("🔍 Extracted barcode/patientId:", searchBarcode);
|
||||
console.log("🔍 Cleaned input:", cleanInput);
|
||||
console.log("📊 Total patients in store:", queueStore.allPatients.length);
|
||||
|
||||
// IMPORTANT: Hanya cari dengan EXACT barcode match untuk menghindari false positive
|
||||
// Format barcode: YYMMDD + 5 digit (contoh: 26011500001)
|
||||
// Jangan gunakan fallback ke noAntrian atau no karena bisa menyebabkan false positive
|
||||
@@ -3045,12 +3036,6 @@ const onDetect = async (decodedText: string) => {
|
||||
patientBarcode === cleanInput ||
|
||||
patientBarcode.toLowerCase() === cleanInput.toLowerCase()
|
||||
) {
|
||||
console.log(
|
||||
"✅ Found by exact barcode match:",
|
||||
patientBarcode,
|
||||
"===",
|
||||
cleanInput,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3148,13 +3133,6 @@ const onDetect = async (decodedText: string) => {
|
||||
const klinikQueueNumber =
|
||||
freshPatient.noAntrian?.split(" |")[0] || freshPatient.noAntrian || "N/A";
|
||||
|
||||
console.log("🔍 Fresh patient status:", {
|
||||
barcode: freshPatient.barcode,
|
||||
noAntrian: freshPatient.noAntrian,
|
||||
status: patientStatus,
|
||||
processStage: freshPatient.processStage,
|
||||
});
|
||||
|
||||
// Cek apakah sudah check-in (status === 'di-loket') - gunakan data fresh
|
||||
if (patientStatus === "di-loket") {
|
||||
// Sudah check-in sebelumnya
|
||||
@@ -3230,16 +3208,6 @@ const onDetect = async (decodedText: string) => {
|
||||
// Gunakan barcode pasien yang fresh, bukan decodedText (untuk memastikan format benar)
|
||||
const patientBarcodeForCheckIn =
|
||||
freshPatient.barcode || searchBarcode || decodedText;
|
||||
console.log(
|
||||
"🔍 Calling checkInPatient with barcode (fresh):",
|
||||
patientBarcodeForCheckIn,
|
||||
);
|
||||
console.log(
|
||||
"🔍 Original QR data:",
|
||||
decodedText,
|
||||
"| Extracted barcode:",
|
||||
searchBarcode,
|
||||
);
|
||||
const checkInResult = await queueStore.checkInPatient(
|
||||
patientBarcodeForCheckIn,
|
||||
);
|
||||
@@ -3381,12 +3349,6 @@ const checkInManual = async () => {
|
||||
const cleanInputUpper = cleanInput.toUpperCase();
|
||||
const originalInput = inputValue.toUpperCase().trim();
|
||||
|
||||
console.log("🔍 checkInManual called with:", inputValue);
|
||||
console.log("🔍 Extracted barcode/patientId:", searchBarcode);
|
||||
console.log("🔍 Cleaned input:", cleanInput);
|
||||
console.log("🔍 Original input:", originalInput);
|
||||
console.log("📊 Total patients in store:", queueStore.allPatients.length);
|
||||
|
||||
let foundPatient: any = null;
|
||||
let freshPatient: any = null;
|
||||
|
||||
@@ -3405,12 +3367,6 @@ const checkInManual = async () => {
|
||||
patientBarcode === cleanInput ||
|
||||
patientBarcode.toLowerCase() === cleanInput.toLowerCase()
|
||||
) {
|
||||
console.log(
|
||||
"✅ Found by exact barcode match:",
|
||||
patientBarcode,
|
||||
"===",
|
||||
cleanInput,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+5
-38
@@ -3,7 +3,6 @@ import { defineStore } from 'pinia';
|
||||
import { ref, computed } from 'vue';
|
||||
|
||||
export const useClinicStore = defineStore('clinic', () => {
|
||||
console.log('🏪 Initializing clinicStore...');
|
||||
|
||||
// Data clinics - Single source of truth untuk semua data klinik
|
||||
// Includes basic info (name, kode, icon, doctors, shifts) + master config (totalQuota, jamShiftPerHari, jadwalKlinik, tanggalTutup)
|
||||
@@ -588,8 +587,6 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
},
|
||||
]);
|
||||
|
||||
console.log('📊 Initial clinics count:', clinics.value.length);
|
||||
console.log('📋 Initial clinics:', clinics.value.map(c => ({ id: c.id, name: c.name, jenisLayanan: c.jenisLayanan })));
|
||||
|
||||
const isLoadingAPI = ref(false);
|
||||
const apiError = ref(null);
|
||||
@@ -638,14 +635,11 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
|
||||
// Helper: Parse jam_operasional to create jamShiftPerHari structure
|
||||
const parseJamOperasional = (jamOperasional) => {
|
||||
console.log('🔄 Parsing jam operasional:', jamOperasional);
|
||||
|
||||
const jamShiftPerHari = {}; // { 'Senin': [{ dari, sampai, kuota }], 'Selasa': [...] }
|
||||
const jadwalKlinikSet = new Set();
|
||||
let totalQuota = 0;
|
||||
|
||||
if (!jamOperasional) {
|
||||
console.log('⚠️ No jamOperasional provided, using defaults');
|
||||
return {
|
||||
jamShiftPerHari: {
|
||||
'Senin': [{ dari: '07:00', sampai: '11:00', kuota: 100 }]
|
||||
@@ -657,7 +651,6 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
}
|
||||
|
||||
if (!Array.isArray(jamOperasional)) {
|
||||
console.log('⚠️ jamOperasional is not an array:', typeof jamOperasional);
|
||||
return {
|
||||
jamShiftPerHari: {
|
||||
'Senin': [{ dari: '07:00', sampai: '11:00', kuota: 100 }]
|
||||
@@ -668,21 +661,10 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
};
|
||||
}
|
||||
|
||||
console.log(`📋 Processing ${jamOperasional.length} schedule items`);
|
||||
|
||||
// Group by day and create shift per day structure
|
||||
jamOperasional.forEach((item, index) => {
|
||||
console.log(` 📍 Schedule ${index + 1}:`, {
|
||||
hari: item.hari,
|
||||
jam_operasional: item.jam_operasional,
|
||||
kuota: item.kuota
|
||||
});
|
||||
|
||||
jamOperasional.forEach((item) => {
|
||||
const dayIndo = mapDayToIndonesian(item.hari);
|
||||
if (!dayIndo) {
|
||||
console.log(` ⚠️ Could not map day: ${item.hari}`);
|
||||
return;
|
||||
}
|
||||
if (!dayIndo) return;
|
||||
|
||||
jadwalKlinikSet.add(dayIndo);
|
||||
|
||||
@@ -707,28 +689,18 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
}
|
||||
}
|
||||
|
||||
console.log(` ✅ Shift added for ${dayIndo}: ${dari} - ${sampai}, kuota: ${kuota}`);
|
||||
|
||||
// Initialize array if not exists
|
||||
if (!jamShiftPerHari[dayIndo]) {
|
||||
jamShiftPerHari[dayIndo] = [];
|
||||
}
|
||||
|
||||
jamShiftPerHari[dayIndo].push({
|
||||
dari,
|
||||
sampai,
|
||||
kuota
|
||||
});
|
||||
|
||||
jamShiftPerHari[dayIndo].push({ dari, sampai, kuota });
|
||||
totalQuota += kuota;
|
||||
} else {
|
||||
console.log(` ⚠️ Invalid jam_operasional format: "${jamOp}"`);
|
||||
}
|
||||
});
|
||||
|
||||
// If no valid shift found, add default
|
||||
if (Object.keys(jamShiftPerHari).length === 0) {
|
||||
console.log('⚠️ No valid shifts found, using default');
|
||||
jamShiftPerHari['Senin'] = [{ dari: '07:00', sampai: '11:00', kuota: 100 }];
|
||||
jadwalKlinikSet.add('Senin');
|
||||
totalQuota = 100;
|
||||
@@ -737,16 +709,12 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
// Count total unique shifts across all days
|
||||
const totalShifts = Object.values(jamShiftPerHari).reduce((sum, shifts) => sum + shifts.length, 0);
|
||||
|
||||
const result = {
|
||||
return {
|
||||
jamShiftPerHari,
|
||||
jadwalKlinik: Array.from(jadwalKlinikSet),
|
||||
totalQuota,
|
||||
shift: totalShifts
|
||||
};
|
||||
|
||||
console.log('✅ Parse result:', result);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// State for singleton fetch promise
|
||||
@@ -772,7 +740,7 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
apiError.value = null;
|
||||
|
||||
try {
|
||||
console.log(`🔄 Fetching clinics from API (Try ${retryCount + 1})...`);
|
||||
console.log(`🔄 [clinicStore] Fetching clinics from API (Try ${retryCount + 1})...`);
|
||||
|
||||
let rawData;
|
||||
try {
|
||||
@@ -879,7 +847,6 @@ export const useClinicStore = defineStore('clinic', () => {
|
||||
// Finally sort by ID if both are same
|
||||
return a.id - b.id;
|
||||
});
|
||||
console.log('📊 getAllClinics computed:', sorted.length, 'items');
|
||||
return sorted;
|
||||
});
|
||||
|
||||
|
||||
+32
-3
@@ -6,6 +6,12 @@ export const useDoctorStore = defineStore('doctor', () => {
|
||||
const doctorsByKlinikId = ref({});
|
||||
const lastSyncTimestamp = ref(null);
|
||||
const loadingDoctors = ref({});
|
||||
|
||||
// Track clinics that consistently return 500 errors to avoid hammering the server
|
||||
// Format: { [idklinik]: { failCount: number, lastFailAt: number } }
|
||||
const failedClinics = ref({});
|
||||
const MAX_FAIL_COUNT = 3; // Blacklist after 3 consecutive failures
|
||||
const BLACKLIST_DURATION_MS = 30 * 60 * 1000; // 30 minutes
|
||||
|
||||
/**
|
||||
* Check if sync is needed based on 2 AM schedule
|
||||
@@ -38,11 +44,24 @@ export const useDoctorStore = defineStore('doctor', () => {
|
||||
/**
|
||||
* Fetch doctors for a clinic from API with retry logic
|
||||
*/
|
||||
const fetchDoctorsForClinic = async (clinic, force = false, retries = 2) => {
|
||||
const fetchDoctorsForClinic = async (clinic, force = false, retries = 1) => {
|
||||
if (!clinic || !clinic.id) return;
|
||||
|
||||
const idklinik = clinic.id;
|
||||
|
||||
// Check blacklist: skip if this clinic has failed too many times recently
|
||||
const failInfo = failedClinics.value[idklinik];
|
||||
if (failInfo && failInfo.failCount >= MAX_FAIL_COUNT) {
|
||||
const timeSinceLastFail = Date.now() - failInfo.lastFailAt;
|
||||
if (timeSinceLastFail < BLACKLIST_DURATION_MS) {
|
||||
// Still in blacklist window — skip silently
|
||||
return doctorsByKlinikId.value[idklinik] || [];
|
||||
} else {
|
||||
// Blacklist expired — reset and try again
|
||||
delete failedClinics.value[idklinik];
|
||||
}
|
||||
}
|
||||
|
||||
// Skip if already in cache and not forced, and sync not needed
|
||||
if (!force && doctorsByKlinikId.value[idklinik] && !isSyncNeeded()) {
|
||||
return doctorsByKlinikId.value[idklinik];
|
||||
@@ -50,8 +69,6 @@ export const useDoctorStore = defineStore('doctor', () => {
|
||||
|
||||
// Skip if already loading
|
||||
if (loadingDoctors.value[idklinik]) {
|
||||
// Wait a bit if already loading? No, just return existing promise if we had one,
|
||||
// but for simplicity we just return.
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -111,6 +128,17 @@ export const useDoctorStore = defineStore('doctor', () => {
|
||||
} catch (error) {
|
||||
const status = error.response ? error.response.status : (error.status || 'unknown');
|
||||
console.error(`❌ [doctorStore] Gagal mengambil dokter untuk klinik ID ${idklinik} (${clinic.name}) - Status: ${status}`);
|
||||
|
||||
// Track failure for blacklisting
|
||||
if (!failedClinics.value[idklinik]) {
|
||||
failedClinics.value[idklinik] = { failCount: 0, lastFailAt: 0 };
|
||||
}
|
||||
failedClinics.value[idklinik].failCount++;
|
||||
failedClinics.value[idklinik].lastFailAt = Date.now();
|
||||
|
||||
if (failedClinics.value[idklinik].failCount >= MAX_FAIL_COUNT) {
|
||||
console.warn(`⚠️ [doctorStore] Klinik ${clinic.name} (ID: ${idklinik}) diblacklist selama 30 menit karena ${MAX_FAIL_COUNT}x gagal berturut-turut.`);
|
||||
}
|
||||
|
||||
// Don't overwrite existing data on failure unless it's empty
|
||||
if (!doctorsByKlinikId.value[idklinik]) {
|
||||
@@ -145,6 +173,7 @@ export const useDoctorStore = defineStore('doctor', () => {
|
||||
doctorsByKlinikId,
|
||||
lastSyncTimestamp,
|
||||
loadingDoctors,
|
||||
failedClinics,
|
||||
fetchDoctorsForClinic,
|
||||
syncAllDoctors,
|
||||
isSyncNeeded
|
||||
|
||||
+3
-13
@@ -110,22 +110,12 @@ export const useLoketStore = defineStore('loket', () => {
|
||||
|
||||
// Merged data (computed) - Gabungan API + Local
|
||||
const loketData = computed(() => {
|
||||
// Debug log
|
||||
if (apiLoketData.value.length > 0) {
|
||||
console.log('📊 [loketData computed] API data found:', apiLoketData.value.length, 'items');
|
||||
}
|
||||
if (localLoketData.value.length > 0) {
|
||||
console.log('📊 [loketData computed] Local data found:', localLoketData.value.length, 'items');
|
||||
}
|
||||
|
||||
// Create new objects to prevent state mutation in computed
|
||||
const merged = [
|
||||
...apiLoketData.value.map(i => ({...i})),
|
||||
...localLoketData.value.map(i => ({...i}))
|
||||
]
|
||||
|
||||
console.log('📊 [loketData computed] Merged total:', merged.length, 'items');
|
||||
|
||||
// Sort by ID
|
||||
merged.sort((a, b) => a.id - b.id)
|
||||
|
||||
@@ -544,9 +534,9 @@ export const useLoketStore = defineStore('loket', () => {
|
||||
key: 'loket-store-state',
|
||||
storage: typeof window !== 'undefined' ? localStorage : undefined,
|
||||
paths: [
|
||||
'localLoketData', // Persist EKSEKUTIF data (ID 1000+)
|
||||
'apiLoketData', // Persist API data (REGULER) to minimize data loss
|
||||
'lastSyncTimestamp' // Track when API data was last fetched
|
||||
'localLoketData', // Persist EKSEKUTIF data (ID 1000+) only
|
||||
// NOTE: apiLoketData & lastSyncTimestamp are intentionally NOT persisted
|
||||
// API data is always fetched fresh on load to save localStorage writes
|
||||
],
|
||||
serializer: {
|
||||
deserialize: JSON.parse,
|
||||
|
||||
+46
-187
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user