push error missing function

This commit is contained in:
Fanrouver
2026-02-11 10:29:19 +07:00
parent a7a654b72a
commit 23164bcf2d
3 changed files with 268 additions and 508 deletions

No files matched your search

+234 -23
View File
@@ -16,12 +16,20 @@ export const useQueueStore = defineStore('queue', () => {
// ============================================
// State untuk API patient data per loket
const allPatients = ref([]);
const apiPatientsPerLoket = ref({});
const isLoadingPatients = ref(false);
const apiPatientsError = ref(null);
const quotaUsed = ref(5);
const currentProcessingPatient = ref({});
const lastUpdated = ref(Date.now());
const lastFetchTime = ref({});
const lastGlobalFetchTime = ref(0); // Cooldown for bulk refreshes
// Scoped Refresh Logic: track which lokets are currently being viewed
const activeLoketInterest = ref({}); // { [loketId]: count }
const activeClinicInterest = ref({}); // { [kodeKlinik]: count }
const globalInterestCount = ref(0); // Tracks pages that need ALL loket data (e.g. CheckInPasien)
const registerInterest = (loketId) => {
@@ -40,7 +48,26 @@ export const useQueueStore = defineStore('queue', () => {
delete activeLoketInterest.value[id];
}
}
console.log(`🔌 [queueStore] Unregistered interest in Loket ${id}. Active:`, activeLoketInterest.value);
console.log(`🔌 [queueStore] Unregistered interest in Loket ${id}. Active lokets:`, activeLoketInterest.value);
};
const registerClinicInterest = (kodeKlinik) => {
if (!kodeKlinik) return;
const code = String(kodeKlinik);
activeClinicInterest.value[code] = (activeClinicInterest.value[code] || 0) + 1;
console.log(`🔌 [queueStore] Registered interest in Clinic ${code}. Active clinics:`, activeClinicInterest.value);
};
const unregisterClinicInterest = (kodeKlinik) => {
if (!kodeKlinik) return;
const code = String(kodeKlinik);
if (activeClinicInterest.value[code]) {
activeClinicInterest.value[code] = Math.max(0, activeClinicInterest.value[code] - 1);
if (activeClinicInterest.value[code] === 0) {
delete activeClinicInterest.value[code];
}
}
console.log(`🔌 [queueStore] Unregistered interest in Clinic ${code}. Active clinics:`, activeClinicInterest.value);
};
const registerGlobalInterest = () => {
@@ -53,13 +80,151 @@ export const useQueueStore = defineStore('queue', () => {
console.log(`🌐 [queueStore] Global interest unregistered. Total: ${globalInterestCount.value}`);
};
// Throttle mechanism: track last fetch time per loket
const lastFetchTime = ref({});
const lastGlobalFetchTime = ref(0); // Cooldown for bulk refreshes
// Synchronization Guard: track last update time to break loops across tabs
const lastUpdated = ref(Date.now());
// synchronization guard (moved lower)
const fetchPatientsForClinic = async (kodeKlinik) => {
if (!kodeKlinik) return { success: false, message: 'Kode Klinik diperlukan' };
isLoadingPatients.value = true;
apiPatientsError.value = null;
// THROTTLE: Check if we recently fetched (within last 5 seconds)
const now = Date.now();
const lastFetch = lastFetchTime.value[`clinic-${kodeKlinik}`] || 0;
const timeSinceLastFetch = now - lastFetch;
if (timeSinceLastFetch < 5000) {
console.log(`⏭️ [queueStore] Skipping fetch for clinic ${kodeKlinik} (last fetched ${Math.round(timeSinceLastFetch/1000)}s ago)`);
isLoadingPatients.value = false;
return { success: true, message: 'Using cache' };
}
lastFetchTime.value[`clinic-${kodeKlinik}`] = now;
try {
// Find clinic ID from clinicStore
const clinic = clinicStore.clinics.find(c => c.kode === kodeKlinik);
if (!clinic || !clinic.id) {
throw new Error(`Klinik ID tidak ditemukan untuk kode: ${kodeKlinik}`);
}
const url = `/visit-api/visit?klinik_id=${clinic.id}&limit=500`;
console.log(`🔄 [queueStore] Fetching patients for clinic ${kodeKlinik} (ID: ${clinic.id})...`);
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const rawResponse = await response.json();
const data = rawResponse?.data || [];
const mappedClinicPatients = [];
data.forEach((visit, index) => {
const healthcareServices = visit.healthcare_services || [];
// Filter for KLINIK healthcare_type_name with active=true
const services = healthcareServices.filter(s =>
s.healthcare_type_name === 'KLINIK' && s.active === true
);
if (services.length > 0) {
services.forEach(service => {
const roomId = service.fk_ms_sub_healthcare_service_id;
const visitStatuses = visit.visit_statuses || [];
const latestStatus = visitStatuses[visitStatuses.length - 1];
let patientStatus = 'di-loket';
if (latestStatus?.desc) {
const desc = latestStatus.desc.toLowerCase();
if (desc.includes('pemeriksaan') || desc.includes('sedang diproses')) {
patientStatus = 'pemeriksaan';
}
}
const p = {
no: visit.id || (20000 + index),
barcode: visit.visit_code || service.ticket || '',
noAntrian: service.ticket || visit.visit_code || '',
jamPanggil: service.check_in_datetime || visit.registration_datetime || '',
klinik: service.healthcare_service_name || clinic.name,
kodeKlinik: kodeKlinik,
klinikId: clinic.id,
ruang: service.sub_healthcare_service_name || '',
nomorRuang: roomId ? String(roomId) : String(clinic.id),
pembayaran: service.payment_type_name || visit.payment_type_name || '',
status: patientStatus,
processStage: 'klinik-ruang',
createdAt: visit.registration_datetime || new Date().toISOString(),
visitType: visit.visit_type_name || 'ONSITE',
noRM: visit.norm || '',
fastTrack: 'TIDAK',
registrationType: 'api',
visitId: visit.visit_id || visit.id,
visitCode: visit.visit_code
};
if (isTodayPatient(p)) mappedClinicPatients.push(p);
});
}
});
// MERGE LOGIC
const newPatientMap = new Map();
mappedClinicPatients.forEach(p => {
const key = p.visitId ? `vid-${p.visitId}` : (p.barcode ? `bc-${p.barcode}` : `no-${p.no}`);
newPatientMap.set(key, p);
});
// Update existing allPatients while preserving local UI state
allPatients.value = allPatients.value.map(p => {
if (p.processStage !== 'klinik-ruang' || p.kodeKlinik !== kodeKlinik) return p;
const key = p.visitId ? `vid-${p.visitId}` : (p.barcode ? `bc-${p.barcode}` : `no-${p.no}`);
const apiP = newPatientMap.get(key);
if (apiP) {
// If we have local updates, preserve them
const hasLocalUpdates = (
p.status !== 'di-loket' ||
p.calledPemeriksaanAwal ||
p.calledTindakan ||
p.lastCalledAt
);
if (hasLocalUpdates) {
newPatientMap.delete(key); // Mark as handled
return {
...apiP,
status: p.status,
calledPemeriksaanAwal: p.calledPemeriksaanAwal,
calledTindakan: p.calledTindakan,
tipeLayanan: p.tipeLayanan,
lastCalledAt: p.lastCalledAt,
lastCalledTipeLayanan: p.lastCalledTipeLayanan
};
} else {
newPatientMap.delete(key); // Handled
return apiP;
}
}
// If it was an API patient that disappeared from the list, it's either stale or from another room
if (p.registrationType === 'api') return null;
return p;
}).filter(Boolean);
// Add remaining new patients
allPatients.value.push(...newPatientMap.values());
console.log(`✅ [queueStore] Successfully fetched ${mappedClinicPatients.length} patients for clinic ${kodeKlinik}`);
return { success: true, message: `${mappedClinicPatients.length} pasien dimuat` };
} catch (error) {
console.error(`❌ [queueStore] Error fetching clinic patients (${kodeKlinik}):`, error);
apiPatientsError.value = error.message;
return { success: false, message: error.message };
} finally {
isLoadingPatients.value = false;
}
};
// ============================================
// WEBSOCKET INTEGRATION (CENTRALIZED)
@@ -107,26 +272,47 @@ export const useQueueStore = defineStore('queue', () => {
// TRIGGER STRATEGIC REFRESHES
const messageData = data?.data || data;
const targetLoketId = messageData?.loketId || messageData?.idloket;
const targetKlinikId = messageData?.klinikId || messageData?.idklinik;
if (targetLoketId) {
// 1. If message specifies a loket, refresh that one specifically
console.log(`🎯 [queueStore] WS targeting Loket ${targetLoketId}: Refreshing...`);
fetchPatientsForLoket(targetLoketId);
} else if (targetKlinikId) {
// 2. If message specifies a klinik, refresh those clinics
// Since the WS might only send numeric ID, we check active interests
// that might correspond to this ID or just refresh all active clinics
console.log(`🎯 [queueStore] WS targeting Clinic ${targetKlinikId}: Refreshing active clinic interests...`);
const interestingClinics = Object.keys(activeClinicInterest.value);
interestingClinics.forEach(kodeKlinik => {
fetchPatientsForClinic(kodeKlinik);
});
} else if (globalInterestCount.value > 0) {
// 2. If it's a generic message and we have global interest (Check-in page open)
// 3. If it's a generic message and we have global interest (Check-in page open)
// Trigger bulk staggered refresh
console.log(`📡 [queueStore] WS generic message: Global Interest active, triggering staggered bulk refresh...`);
fetchAllPatients();
} else {
// 3. Otherwise, only refresh lokets that currently have active interest (Admin tabs open)
// 4. Otherwise, only refresh lokets/clinics that currently have active interest
const interestingLokets = Object.keys(activeLoketInterest.value);
const interestingClinics = Object.keys(activeClinicInterest.value);
if (interestingLokets.length > 0) {
console.log(`🌐 [queueStore] WS generic message: Refreshing ${interestingLokets.length} active lokets:`, interestingLokets);
interestingLokets.forEach(loketId => {
fetchPatientsForLoket(loketId);
});
} else {
console.log(`🔕 [queueStore] WS generic message: No active interest, skipping bulk refresh.`);
}
if (interestingClinics.length > 0) {
console.log(`🌐 [queueStore] WS generic message: Refreshing ${interestingClinics.length} active clinics:`, interestingClinics);
interestingClinics.forEach(kodeKlinik => {
fetchPatientsForClinic(kodeKlinik);
});
}
if (interestingLokets.length === 0 && interestingClinics.length === 0) {
console.log(`🔕 [queueStore] WS generic message: No active interest, skipping refresh.`);
}
}
@@ -454,17 +640,35 @@ export const useQueueStore = defineStore('queue', () => {
allPatients.value = allPatients.value.filter(p => {
const key = p.idtiket ? `id-${p.idtiket}` : `bc-${p.barcode}`;
// Remove if it's being replaced by new API batch (matches by barcode or idtiket)
// CRITICAL FIX: If patient has already moved to a later stage (e.g. klinik-ruang),
// do NOT remove it or overwrite it with a 'loket' stage API patient.
if (p.processStage && p.processStage !== 'loket') {
// Check if this advanced patient matches the incoming batch
if (newPatientMap.has(key)) {
// Signal to skip adding the 'loket' version from API
newPatientMap.delete(key);
}
return true; // Keep the advanced patient
}
// If it's still in loket stage but marked as processed, and it's in the API batch,
// we might be experiencing API lag. Keep it as processed.
if (p.status === 'processed' && newPatientMap.has(key)) {
// Skip replacing it with 'menunggu'/'anjungan' status
newPatientMap.delete(key);
return true;
}
// Remove if it's being replaced by new API batch
if (newPatientMap.has(key)) return false;
// Also check if barcode matches any new API patient (for cases where local has no idtiket)
// This prevents duplicates like barcode "2602050017" appearing as both BPJS (local) and JKN (API)
// Also check if barcode matches any new API patient
if (p.barcode) {
const barcodeMatches = patientsWithLoketId.some(newP => newP.barcode === p.barcode);
if (barcodeMatches) return false;
}
// Remove if it's a stale 'api' patient for this loket (that wasn't in the new batch)
// Remove if it's a stale 'api' patient for this loket
if (p.registrationType === 'api' && String(p.loketId) === String(loketId)) return false;
return true;
@@ -499,6 +703,7 @@ export const useQueueStore = defineStore('queue', () => {
}
};
/**
* Global fetcher for all patients across all available lokets
*/
@@ -879,11 +1084,7 @@ export const useQueueStore = defineStore('queue', () => {
};
// Initial state - START EMPTY to avoid clashing with hydration
const allPatients = ref([]);
const quotaUsed = ref(5);
// State - currentProcessingPatient is now an object mapping 'stage-id' to patient
// For example: { 'loket-1': patient, 'loket-2': patient, 'klinik-3': patient }
const currentProcessingPatient = ref({});
// Placeholder lines for moved state references (already handled at top)
// Daftar klinik untuk dropdown diambil 1 pintu dari clinicStore (Computed for stability)
const kliniks = computed(() => clinicStore.clinics || []);
@@ -1721,12 +1922,15 @@ export const useQueueStore = defineStore('queue', () => {
nomorScreen: ruang.nomorScreen,
fastTrack: patient ? (patient.fastTrack || "TIDAK") : "TIDAK",
pembayaran: patient ? patient.pembayaran : "UMUM",
noRM: patient ? patient.noRM : null,
noRM: patient ? (patient.noRM || patient.norm) : null,
status: "pemeriksaan", // Patients from loket to klinik ruang start as "pemeriksaan"
processStage: "klinik-ruang", // Set ke klinik-ruang langsung
createdAt: timestamp.toISOString(),
referencePatient: patient ? patient.noAntrian : null,
sourcePatientNo: patient ? patient.no : null,
visitId: patient ? (patient.visitId || patient.id) : null,
visitCode: patient ? (patient.visitCode || patient.visit_code) : null,
registrationType: patient ? (patient.registrationType || 'onsite') : 'onsite',
// Tracking panggilan
calledPemeriksaanAwal: false,
calledTindakan: false,
@@ -2459,7 +2663,7 @@ export const useQueueStore = defineStore('queue', () => {
// PRIORITAS: Hanya cari dengan EXACT barcode match (case-insensitive, whitespace-insensitive)
// Format barcode: YYMMDD + 5 digit (contoh: 26011500001)
// Jangan gunakan fallback ke noAntrian atau no karena bisa menyebabkan false positive
// Ini adalah satu-satunya cara yang aman untuk match pasien
const patientIndex = allPatients.value.findIndex(p => {
// Normalize barcode untuk comparison
const patientBarcode = String(p.barcode || '').trim();
@@ -2605,6 +2809,9 @@ export const useQueueStore = defineStore('queue', () => {
ensureInitialData,
quotaUsed,
currentProcessingPatient,
lastUpdated,
lastFetchTime,
lastGlobalFetchTime,
kliniks,
penunjangs,
@@ -2645,6 +2852,7 @@ export const useQueueStore = defineStore('queue', () => {
// API Patient Actions
fetchPatientsForLoket,
fetchPatientsForClinic,
fetchAllPatients,
getPatientsForLoket,
mapStatusFromDeskripsi,
@@ -2660,6 +2868,9 @@ export const useQueueStore = defineStore('queue', () => {
registerInterest,
unregisterInterest,
activeLoketInterest,
registerClinicInterest,
unregisterClinicInterest,
activeClinicInterest,
registerGlobalInterest,
unregisterGlobalInterest,
};