update WS admin loket dan checkin

This commit is contained in:
Fanrouver
2026-02-11 09:34:42 +07:00
parent c02905e954
commit a7a654b72a
5 changed files with 153 additions and 31 deletions
+24 -12
View File
@@ -135,24 +135,36 @@ export const useQueue = (adminType = "loket", specificId = null) => {
// Filtered lists
const filteredKliniks = computed(() => {
if (!klinikSearch.value) return queueStore.kliniks;
return queueStore.kliniks.filter((k) =>
k.name.toLowerCase().includes(klinikSearch.value.toLowerCase())
);
let result = queueStore.kliniks;
if (klinikSearch.value) {
result = result.filter((k) =>
k.name.toLowerCase().includes(klinikSearch.value.toLowerCase())
);
}
// Sort alphabetically by name
return [...result].sort((a, b) => (a.name || "").localeCompare(b.name || ""));
});
const filteredPenunjangs = computed(() => {
if (!penunjangSearch.value) return queueStore.penunjangs;
return queueStore.penunjangs.filter((p) =>
p.name.toLowerCase().includes(penunjangSearch.value.toLowerCase())
);
let result = queueStore.penunjangs;
if (penunjangSearch.value) {
result = result.filter((p) =>
p.name.toLowerCase().includes(penunjangSearch.value.toLowerCase())
);
}
// Sort alphabetically by name
return [...result].sort((a, b) => (a.name || "").localeCompare(b.name || ""));
});
const filteredChangeKliniks = computed(() => {
if (!changeKlinikSearch.value) return queueStore.kliniks;
return queueStore.kliniks.filter((k) =>
k.name.toLowerCase().includes(changeKlinikSearch.value.toLowerCase())
);
let result = queueStore.kliniks;
if (changeKlinikSearch.value) {
result = result.filter((k) =>
k.name.toLowerCase().includes(changeKlinikSearch.value.toLowerCase())
);
}
// Sort alphabetically by name
return [...result].sort((a, b) => (a.name || "").localeCompare(b.name || ""));
});
// Methods
+22 -2
View File
@@ -177,10 +177,11 @@
class="mb-4"
/>
<v-expansion-panels class="expansion-panels">
<v-expansion-panels v-model="activePanel" class="expansion-panels">
<v-expansion-panel
v-for="klinikRuang in filteredKlinikRuang"
:key="klinikRuang.id"
:value="klinikRuang.id"
class="expansion-panel"
>
<v-expansion-panel-title class="expansion-title">
@@ -383,8 +384,13 @@ onMounted(async () => {
// Initialize and connect WebSocket (Centralized)
queueStore.initWebSocket(anjunganClientId.value);
// Register interest in this specific loket for scoped WebSocket refreshes
queueStore.registerInterest(loketId.value);
onUnmounted(() => {
clearInterval(pollingInterval);
// Unregister interest when leaving the page
queueStore.unregisterInterest(loketId.value);
});
});
@@ -464,6 +470,7 @@ const selectedFastTrack = ref(null);
// Dialog Klinik Ruang
const showKlinikRuangDialog = ref(false);
const klinikRuangSearch = ref("");
const activePanel = ref(null); // Tracks the open Klinik Ruang panel
// Watch dialog state to reset search when closed
watch(showKlinikRuangDialog, (newValue) => {
@@ -483,9 +490,22 @@ const klinikRuangList = computed(() => {
const targetLayanan = isLoketEksekutif ? "Eksekutif" : "Reguler";
// Filter rooms by service type to match Loket's service level
return (ruangStore.ruangData || []).filter(
const baseList = (ruangStore.ruangData || []).filter(
(k) => k.jenisLayanan === targetLayanan,
);
// Sort clinics alphabetically by namaKlinik
const sortedClinics = [...baseList].sort((a, b) =>
(a.namaKlinik || "").localeCompare(b.namaKlinik || ""),
);
// Sort each clinical room list alphabetically by namaRuang
return sortedClinics.map((clinic) => ({
...clinic,
ruangList: [...(clinic.ruangList || [])].sort((a, b) =>
(a.namaRuang || "").localeCompare(b.namaRuang || ""),
),
}));
});
const filteredKlinikRuang = computed(() => {
+5
View File
@@ -1001,8 +1001,13 @@ onMounted(async () => {
// Initialize and connect WebSocket (Centralized)
queueStore.initWebSocket(anjunganClientId.value);
// Register interest in this specific loket for scoped WebSocket refreshes
queueStore.registerInterest(loketId.value);
onUnmounted(() => {
clearInterval(pollingInterval);
// Unregister interest when leaving the page
queueStore.unregisterInterest(loketId.value);
});
});
+5
View File
@@ -2729,6 +2729,9 @@ onMounted(async () => {
// Initialize WebSocket (Centralized)
queueStore.initWebSocket(checkInClientId.value);
// Register global interest to receive staggered bulk refreshes on generic WS messages
queueStore.registerGlobalInterest();
// Set interval to check every minute for reset time
const resetCheckInterval = setInterval(() => {
@@ -2738,6 +2741,8 @@ onMounted(async () => {
onUnmounted(() => {
clearInterval(pollingInterval);
clearInterval(resetCheckInterval);
// Unregister global interest
queueStore.unregisterGlobalInterest();
});
} else {
hasCamera.value = false;
+97 -17
View File
@@ -20,8 +20,42 @@ export const useQueueStore = defineStore('queue', () => {
const isLoadingPatients = ref(false);
const apiPatientsError = ref(null);
// Scoped Refresh Logic: track which lokets are currently being viewed
const activeLoketInterest = ref({}); // { [loketId]: count }
const globalInterestCount = ref(0); // Tracks pages that need ALL loket data (e.g. CheckInPasien)
const registerInterest = (loketId) => {
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) => {
if (!loketId) return;
const id = String(loketId);
if (activeLoketInterest.value[id]) {
activeLoketInterest.value[id] = Math.max(0, activeLoketInterest.value[id] - 1);
if (activeLoketInterest.value[id] === 0) {
delete activeLoketInterest.value[id];
}
}
console.log(`🔌 [queueStore] Unregistered interest in Loket ${id}. Active:`, activeLoketInterest.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}`);
};
// 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());
@@ -68,18 +102,36 @@ export const useQueueStore = defineStore('queue', () => {
isWsConnected.value = false;
},
onMessage: (data) => {
console.log('📨 [queueStore] Global WS Message:', data);
console.log('📨 [queueStore] Global WS Message received:', data);
// TRIGGER STRATEGIC REFRESHES
// 1. Refetch patients for all active lokets in the store
Object.keys(apiPatientsPerLoket.value).forEach(loketId => {
fetchPatientsForLoket(loketId);
});
const messageData = data?.data || data;
const targetLoketId = messageData?.loketId || messageData?.idloket;
// 2. Refetch clinics to update quotas/availability
if (targetLoketId) {
// 1. If message specifies a loket, refresh that one specifically
console.log(`🎯 [queueStore] WS targeting Loket ${targetLoketId}: Refreshing...`);
fetchPatientsForLoket(targetLoketId);
} else if (globalInterestCount.value > 0) {
// 2. 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)
const interestingLokets = Object.keys(activeLoketInterest.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.`);
}
}
// 4. Base data sync
clinicStore.fetchRegulerClinics();
// 3. Ensure base data is synced
ensureInitialData();
}
});
@@ -450,17 +502,40 @@ export const useQueueStore = defineStore('queue', () => {
/**
* Global fetcher for all patients across all available lokets
*/
const fetchAllPatients = async () => {
console.log('🔄 [queueStore] Fetching all patients for all lokets...');
/**
* Global fetcher for all patients across all available lokets
* Uses staggered fetching to prevent 429 Too Many Requests errors.
*/
const fetchAllPatients = async (force = false) => {
// 1. Cooldown Check: Prevent global refresh spam (max once every 5 seconds)
const now = Date.now();
if (!force && now - lastGlobalFetchTime.value < 5000) {
console.log(`⏭️ [queueStore] fetchAllPatients: Global refresh on cooldown (${Math.round((now - lastGlobalFetchTime.value)/1000)}s ago)`);
return;
}
lastGlobalFetchTime.value = now;
console.log('🔄 [queueStore] Fetching all patients for all lokets (Staggered)...');
const allLokets = loketStore.lokets || [];
if (allLokets.length === 0) {
console.warn('⚠️ [queueStore] No lokets available for fetchAllPatients');
return;
}
// Use Promise.all for faster fetching
await Promise.all(allLokets.map(l => fetchPatientsForLoket(l.id)));
console.log(`✅ [queueStore] All patients fetched. Total: ${allPatients.value.length}`);
// 2. Staggered Fetch: Instead of Promise.all, we fetch one by one with a small delay
// This spreads the load and stays under the 429 threshold
for (const loket of allLokets) {
const id = loket.id || loket.no;
if (id) {
// We use await to wait for the fetch to finish, then wait a bit more
await fetchPatientsForLoket(id);
// Wait 150ms between requests (not too long, but enough to breathe)
await new Promise(resolve => setTimeout(resolve, 150));
}
}
console.log(`✅ [queueStore] Staggered bulk fetch completed. Total: ${allPatients.value.length} patients in memory.`);
};
/**
@@ -810,11 +885,11 @@ export const useQueueStore = defineStore('queue', () => {
// For example: { 'loket-1': patient, 'loket-2': patient, 'klinik-3': patient }
const currentProcessingPatient = ref({});
// Daftar klinik untuk dropdown diambil 1 pintu dari clinicStore
const kliniks = ref(clinicStore.clinics || []);
// Daftar klinik untuk dropdown diambil 1 pintu dari clinicStore (Computed for stability)
const kliniks = computed(() => clinicStore.clinics || []);
// Penunjang data - reference dari penunjangStore
const penunjangs = ref(penunjangStore.penunjangs || []);
// Penunjang data - reference dari penunjangStore (Computed for stability)
const penunjangs = computed(() => penunjangStore.penunjangList || []);
const RESET_HOUR = 2; // 2 AM reset threshold
@@ -2582,6 +2657,11 @@ export const useQueueStore = defineStore('queue', () => {
initWebSocket,
disconnectWebSocket,
isWsConnected,
registerInterest,
unregisterInterest,
activeLoketInterest,
registerGlobalInterest,
unregisterGlobalInterest,
};
}, {