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
+1 -13
View File
@@ -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)
+1 -45
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
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 {