diff --git a/components/features/queue/CurrentPatientCard.vue b/components/features/queue/CurrentPatientCard.vue
index 1e70028..7386991 100644
--- a/components/features/queue/CurrentPatientCard.vue
+++ b/components/features/queue/CurrentPatientCard.vue
@@ -84,6 +84,11 @@
{{ patient.klinik }} | {{ patient.pembayaran }}
+
+ mdi-link
+ Ref. Loket:
+ {{ patient.referencePatient }}
+
@@ -342,6 +347,22 @@ defineEmits(['action', 'change-klinik', 'process-next', 'call', 'open-klinik-rua
font-weight: 600;
}
+.patient-reference {
+ font-size: 12px;
+ line-height: 18px;
+ color: var(--color-neutral-700);
+}
+
+.reference-label {
+ font-weight: 600;
+ color: var(--color-neutral-700);
+}
+
+.reference-value {
+ font-weight: 700;
+ font-size: 13px;
+}
+
.create-queue-buttons {
margin-top: 0;
}
diff --git a/nuxt.config.ts b/nuxt.config.ts
index 89a2cdf..9aaf281 100644
--- a/nuxt.config.ts
+++ b/nuxt.config.ts
@@ -80,7 +80,7 @@ export default defineNuxtConfig({
authUrl: process.env.AUTH_ORIGIN,
// authUrl: process.env.AUTH_ORIGIN || "http://10.10.150.175:3001",
// authUrl: process.env.AUTH_ORIGIN || "http://localhost:3001",
- wsBaseUrl: process.env.WS_BASE_URL || 'ws://10.10.150.100:8084/api/v1/ws',
+ wsBaseUrl: process.env.WS_BASE_URL || 'ws://10.10.123.135:8084/api/v1/ws',
ekstrakExpertiseUrl: process.env.EKSTRAK_EXPERTISE_URL || 'http://10.10.123.218/ekstrakexpertise',
verificationApiBaseUrl: process.env.VERIFICATION_API_BASE_URL || 'http://10.10.123.140:8089/api/v1',
},
diff --git a/pages/AdminKlinikRuang/[kodeKlinik].vue b/pages/AdminKlinikRuang/[kodeKlinik].vue
index f05077e..114ccc1 100644
--- a/pages/AdminKlinikRuang/[kodeKlinik].vue
+++ b/pages/AdminKlinikRuang/[kodeKlinik].vue
@@ -1471,29 +1471,25 @@ const handleCallPatientFromList = async (ruang, patient, tipeLayanan) => {
// Send via WebSocket to Anjungan clients
try {
- // Get all possible client IDs for this klinik (anjungan screens)
+ // Robust Klinik Code: Use patient's code first, then clinic data, then route
+ const patientKlinikKode = updateData.kodeKlinik || updateData.idKlinik;
+ const dataKlinikKode = klinikData.value?.kodeKlinik;
+ const targetKlinikKode = String(patientKlinikKode || dataKlinikKode || kodeKlinik.value);
+
+ console.log('๐ฅ [AdminKlinikRuang] Final Target Klinik Kode:', targetKlinikKode);
+
const anjunganClientIds = [];
- // Always send to client ID without screen number (broadcast to all screens for this klinik)
- anjunganClientIds.push(`anjungan-klinik-ruang-${klinikData.value.kodeKlinik}`);
+ // 1. Always send to base client ID (broadcast to all screens for this klinik)
+ anjunganClientIds.push(`anjungan-klinik-ruang-${targetKlinikKode}`);
- // Also send to specific screen if ruang has nomorScreen
+ // 2. Also send to specific screen if this room has a nomorScreen
if (ruang.nomorScreen) {
- const specificScreenId = `anjungan-klinik-ruang-${klinikData.value.kodeKlinik}-screen-${ruang.nomorScreen}`;
- if (!anjunganClientIds.includes(specificScreenId)) {
- anjunganClientIds.push(specificScreenId);
- }
+ anjunganClientIds.push(`anjungan-klinik-ruang-${targetKlinikKode}-screen-${ruang.nomorScreen}`);
}
- // Also send to all other screens for this klinik (from ruangList)
- ruangList.value.forEach(r => {
- if (r.nomorScreen) {
- const screenId = `anjungan-klinik-ruang-${klinikData.value.kodeKlinik}-screen-${r.nomorScreen}`;
- if (!anjunganClientIds.includes(screenId)) {
- anjunganClientIds.push(screenId);
- }
- }
- });
+ // NOTE: Removed redundant loop to all other screens.
+ // Screens should either be Klinik-wide (using base ID) or Room-specific (using screen param in URL).
console.log('๐ All target client IDs:', anjunganClientIds);
@@ -1504,6 +1500,10 @@ const handleCallPatientFromList = async (ruang, patient, tipeLayanan) => {
console.log('๐ Target clients:', anjunganClientIds);
console.log('๐ฆ Nomor antrian:', nomorAntrian);
console.log('๐ฆ Tipe layanan:', tipeLayanan);
+ console.log('๐ Call timestamp:', updateData.lastCalledAt);
+ console.log('๐ฅ [AdminKlinikRuang] debug - Route kodeKlinik:', kodeKlinik.value);
+ console.log('๐ฅ [AdminKlinikRuang] debug - Data kodeKlinik:', klinikData.value?.kodeKlinik);
+ console.log('๐ฅ [AdminKlinikRuang] debug - Data id:', klinikData.value?.id);
// Send to all relevant clients in parallel for speed
await Promise.all(anjunganClientIds.map(async (clientId) => {
@@ -1511,18 +1511,27 @@ const handleCallPatientFromList = async (ruang, patient, tipeLayanan) => {
to_client: clientId,
data: {
noantrian: nomorAntrian,
- klinikId: klinikData.value?.kodeKlinik,
+ klinikId: kodeKlinik.value, // DONT USE Data kodeKlinik, use Route param
tipeLayanan: tipeLayanan,
- triggerRefresh: true
+ triggerRefresh: true,
+ // Include full call event so Anjungan can patch patient state directly
+ callKlinikEvent: {
+ noantrian: nomorAntrian,
+ barcode: updateData.barcode,
+ kodeKlinik: kodeKlinik.value, // DONT USE Data kodeKlinik, use Route param
+ tipeLayanan: tipeLayanan,
+ lastCalledAt: updateData.lastCalledAt,
+ nomorRuang: String(ruang.nomorRuang)
+ }
},
};
- console.log(`๐จ Sending to ${clientId}:`, message);
+ console.log(`๐จ [AdminKlinikRuang] Triggering broadcast to ${clientId}:`, JSON.stringify(message.data.callKlinikEvent));
const result = await sendViaPost(message);
- console.log(`โ
Response from ${clientId}:`, result);
+ console.log(`โ
[AdminKlinikRuang] Response from ${clientId}:`, result);
}));
- console.log('โ
All WebSocket messages sent successfully');
+ console.log('โ
[AdminKlinikRuang] All WebSocket messages sent successfully');
} catch (error) {
console.error('โ Error sending WebSocket message:', error);
// Don't block the UI if WebSocket fails
@@ -1698,6 +1707,31 @@ const handleCallPatientByTipe = async (ruang, tipeLayanan) => {
queueStore.allPatients[patientIndex].barcode ||
'-';
+ // --- API call: update visit status to idvisit 16 (PG PEMERIKSAAN AWAL) or 17 (PG PEMERIKSAAN) ---
+ try {
+ const visitStatusId = tipeLayanan === 'Pemeriksaan Awal' ? 16 : 17;
+ const patient = queueStore.allPatients[patientIndex];
+ const apiPayload = {
+ patient_visit_healthcare_service_id: patient.healthcareServiceId,
+ visit_code: patient.barcode || patient.visitCode,
+ visit_status_id: [visitStatusId]
+ };
+ console.log(`๐ค [AdminKlinikRuang] Sending call status (idvisit ${visitStatusId}) to API:`, apiPayload);
+ const apiResponse = await fetch('http://10.10.123.135:8084/api/v1/visit/status/finish', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(apiPayload)
+ });
+ if (apiResponse.ok) {
+ console.log(`โ
[AdminKlinikRuang] Call status ${visitStatusId} saved to API`);
+ } else {
+ console.warn(`โ ๏ธ [AdminKlinikRuang] API returned error for call status:`, apiResponse.status);
+ }
+ } catch (apiError) {
+ console.error('โ [AdminKlinikRuang] Error sending call status to API:', apiError);
+ // Continue even if API fails โ local UI still updates
+ }
+
try {
const anjunganClientIds = [];
anjunganClientIds.push(`anjungan-klinik-ruang-${klinikData.value.kodeKlinik}`);
@@ -1771,7 +1805,7 @@ const handlePatientAction = async (ruang, action) => {
};
// Handle proses pasien (untuk pasien yang belum diproses)
-const handleProcessPatient = (ruang, patient) => {
+const handleProcessPatient = async (ruang, patient) => {
const patientIndex = queueStore.allPatients.findIndex(p => p.no === patient.no);
if (patientIndex === -1) {
showSnackbar('Pasien tidak ditemukan', 'error');
@@ -1779,7 +1813,7 @@ const handleProcessPatient = (ruang, patient) => {
}
// Set sebagai current processing
- const result = queueStore.processPatientKlinikRuang(
+ const result = await queueStore.processPatientKlinikRuang(
queueStore.allPatients[patientIndex],
'proses',
klinikData.value.kodeKlinik,
@@ -1792,7 +1826,7 @@ const handleProcessPatient = (ruang, patient) => {
}
};
-const handleProcessPendingPatient = (ruang, patient) => {
+const handleProcessPendingPatient = async (ruang, patient) => {
// Proses kembali pasien yang dipending
const patientIndex = queueStore.allPatients.findIndex(p => p.no === patient.no);
if (patientIndex === -1) {
@@ -1807,7 +1841,7 @@ const handleProcessPendingPatient = (ruang, patient) => {
};
// Set sebagai current processing
- const result = queueStore.processPatientKlinikRuang(
+ const result = await queueStore.processPatientKlinikRuang(
queueStore.allPatients[patientIndex],
'proses',
klinikData.value.kodeKlinik,
diff --git a/pages/AdminLoket/[id].vue b/pages/AdminLoket/[id].vue
index d074c69..c1baa1c 100644
--- a/pages/AdminLoket/[id].vue
+++ b/pages/AdminLoket/[id].vue
@@ -164,6 +164,15 @@
}}
+
+
+ No. Reference (Loket)
+
+ mdi-link
+ {{ currentProcessingPatient.referencePatient }}
+
+
+
diff --git a/pages/Anjungan/Anjungan/[id].vue b/pages/Anjungan/Anjungan/[id].vue
index 18cfdf3..d2b57d1 100644
--- a/pages/Anjungan/Anjungan/[id].vue
+++ b/pages/Anjungan/Anjungan/[id].vue
@@ -38,7 +38,7 @@
{{ clinic.name }}
-
+
{{ doctor }}
-
- Tidak ada dokter tersedia untuk klinik ini
-
+
@@ -213,9 +208,7 @@
{{ doctor }}
-
- Tidak ada dokter tersedia untuk klinik ini
-
+
Jadwal: {{ selectedClinic.schedule }}
@@ -375,12 +368,7 @@
-
- Tidak ada dokter tersedia untuk klinik ini
-
+
@@ -439,6 +427,7 @@
density="compact"
required
class="mb-3"
+ no-data-text="Klinik Tutup"
>
{
apiDoctors && apiDoctors.length > 0 ? apiDoctors : clinic.doctors || [];
if (!doctors || doctors.length === 0) {
- return "Tidak ada dokter";
+ return '';
}
const maxDisplay = 1;
@@ -1300,6 +1289,20 @@ const submitBooking = async () => {
return;
}
+ // Validasi: pastikan ada shift yang tersedia untuk tanggal ini
+ const availableShifts = getAvailableShiftsForBooking();
+ if (availableShifts.length === 0) {
+ showSnackbar("Klinik Tutup pada tanggal yang dipilih.", "error");
+ return;
+ }
+
+ // Validasi: pastikan shift yang dipilih masih valid untuk tanggal ini
+ const isShiftValid = availableShifts.some(s => s.value === bookingForm.value.shift);
+ if (!isShiftValid) {
+ showSnackbar("Pilih shift yang tersedia terlebih dahulu.", "error");
+ return;
+ }
+
// Validasi dokter untuk pasien Eksekutif
if (isEksekutif.value && !bookingForm.value.doctor) {
showSnackbar("Mohon pilih dokter terlebih dahulu.", "error");
diff --git a/pages/Anjungan/AntrianKlinikRuang/[kodeKlinik].vue b/pages/Anjungan/AntrianKlinikRuang/[kodeKlinik].vue
index 8046a7e..a1c3d54 100644
--- a/pages/Anjungan/AntrianKlinikRuang/[kodeKlinik].vue
+++ b/pages/Anjungan/AntrianKlinikRuang/[kodeKlinik].vue
@@ -228,6 +228,56 @@ const klinikData = computed(() => {
const currentTime = ref('')
const currentDate = ref('')
let timeInterval = null
+const broadcastedKlinikPatient = ref(null)
+
+// Watch for Klinik Calls (Cross-Device Sync) from queueStore
+watch(() => queueStore.lastKlinikCall, (newCall) => {
+ if (!newCall || !newCall.noantrian) return;
+
+ console.log('๐ก [Anjungan] WS lastKlinikCall changed. RAW DATA:', JSON.stringify(newCall));
+
+ const callKode = String(newCall.kodeKlinik || '').toUpperCase();
+ const myKode = String(kodeKlinik.value || '').toUpperCase();
+ const isMatch = callKode === myKode;
+
+ console.log('๐ [Anjungan] Klinik Code Comparison:', {
+ received: callKode,
+ local: myKode,
+ match: isMatch
+ });
+
+ // Only react if the call is for THIS klinik
+ if (isMatch) {
+ console.log('๐ข [Anjungan] SUCCESS! Code match found. Processing call:', newCall.noantrian);
+
+ // Find the patient in the current list to get full details if available
+ const existingPatient = klinikPatients.value.find(p =>
+ (p.barcode && String(p.barcode) === String(newCall.barcode)) ||
+ (p.noAntrian && p.noAntrian.split(' |')[0] === String(newCall.noantrian))
+ );
+
+ if (existingPatient) {
+ console.log('โ
[Anjungan] Linked to existing patient in memory:', existingPatient.noAntrian);
+ } else {
+ console.warn('โ ๏ธ [Anjungan] Patient not found in local list. Using broadcast details only.');
+ console.log('๐งช [Anjungan] Available patient numbers in local list:', klinikPatients.value.map(p => p.noAntrian?.split(' |')[0]));
+ }
+
+ // Set broadcasted patient to trigger Hero animation
+ broadcastedKlinikPatient.value = {
+ ...(existingPatient || {}),
+ noAntrian: existingPatient?.noAntrian || newCall.noantrian,
+ barcode: newCall.barcode,
+ tipeLayanan: newCall.tipeLayanan,
+ lastCalledAt: newCall.lastCalledAt || new Date().toISOString(),
+ status: 'di-loket',
+ ruang: existingPatient?.ruang || existingPatient?.namaRuang || `Ruang ${newCall.nomorRuang || '1'}`,
+ _source: 'websocket'
+ };
+ } else {
+ console.log(`โญ๏ธ [Anjungan] Skipping call for clinic ${callKode} (this screen is for ${myKode})`);
+ }
+}, { deep: true });
const klinikPatients = computed(() => {
// Get valid room numbers for this klinik+jenisLayanan combination
@@ -317,8 +367,10 @@ const displayedRuang = computed(() => {
const tindakanQueues = allQueues.filter(q => q.tipeLayanan === 'Tindakan')
const getCurrentAndNext = (queues, tipeLayanan) => {
+ // Current: patients actively being called (di-loket) OR recently called pemeriksaan
const currentCandidates = queues.filter(q =>
- q.status === 'di-loket' && q.tipeLayanan === tipeLayanan
+ (q.status === 'di-loket' || (q.status === 'pemeriksaan' && q.lastCalledAt)) &&
+ q.tipeLayanan === tipeLayanan
).sort((a, b) => {
const timeA = a.lastCalledAt ? new Date(a.lastCalledAt).getTime() : 0
const timeB = b.lastCalledAt ? new Date(b.lastCalledAt).getTime() : 0
@@ -326,8 +378,11 @@ const displayedRuang = computed(() => {
})
const current = currentCandidates.length > 0 ? currentCandidates[0] : null
+ // Next: patients waiting โ 'pemeriksaan' (from API) or 'menunggu' matches real statuses
const next = queues.filter(q =>
- q.no !== current?.no && q.status === 'waiting'
+ q.no !== current?.no &&
+ ['pemeriksaan', 'menunggu', 'waiting'].includes(q.status) &&
+ !q.lastCalledAt
).slice(0, 3)
return { current, next }
}
@@ -362,24 +417,28 @@ const currentCalledQueue = computed(() => {
const CALL_DISPLAY_DURATION = 60000 // 60 seconds
const now = new Date()
- // Force reactivity for time updates if needed (though now is created fresh here)
- // To make it truly reactive to time passing, we might need a reactive timestamp
- // But since this is a computed property, it recalculates when dependencies change.
- // To auto-hide after 60s without data change, we'd need a timer.
- // For now, let's just use the current time when data updates.
- // If we want auto-hide, we can rely on the timeInterval updating 'currentTime'
- // but 'currentTime' is a string.
- // Let's use a reactive timestamp or just rely on re-renders for now.
- // Better: use the 'currentTime' string change to trigger re-calc if we include it.
+ // Force reactivity to time passing
const _ = currentTime.value
- // Get most recently called patient (status di-loket)
- // Sort by lastCalledAt
+ // 1. Prioritaskan broadcastedKlinikPatient (panggilan baru dari WebSocket)
+ if (broadcastedKlinikPatient.value) {
+ const callTime = new Date(broadcastedKlinikPatient.value.lastCalledAt)
+ const timeDiff = now.getTime() - callTime.getTime()
+
+ if (timeDiff <= CALL_DISPLAY_DURATION) {
+ return broadcastedKlinikPatient.value
+ } else {
+ // Clear it if expired
+ broadcastedKlinikPatient.value = null
+ }
+ }
+
+ // 2. Fallback ke data paling lambat dari store (status di-loket atau pemeriksaan yang baru dipanggil)
const calledQueues = klinikPatients.value
- .filter(p => p.status === 'di-loket')
+ .filter(p => (p.status === 'di-loket' || p.status === 'pemeriksaan') && p.lastCalledAt)
.sort((a, b) => {
- const timeA = a.lastCalledAt ? new Date(a.lastCalledAt) : new Date(a.createdAt || 0)
- const timeB = b.lastCalledAt ? new Date(b.lastCalledAt) : new Date(b.createdAt || 0)
+ const timeA = new Date(a.lastCalledAt)
+ const timeB = new Date(b.lastCalledAt)
return timeB - timeA
})
@@ -387,7 +446,7 @@ const currentCalledQueue = computed(() => {
const patient = calledQueues[0]
// Check if within duration
- const callTime = patient.lastCalledAt ? new Date(patient.lastCalledAt) : new Date(patient.createdAt || 0)
+ const callTime = new Date(patient.lastCalledAt)
const timeDiff = now.getTime() - callTime.getTime()
if (timeDiff <= CALL_DISPLAY_DURATION) {
@@ -403,14 +462,16 @@ const currentCalledQueue = computed(() => {
const statistics = computed(() => {
if (!kodeKlinik.value || !klinikData.value) {
- return { total: 0, waiting: 0, active: 0 }
+ return { total: 0, anjungan: 0, active: 0 }
}
const all = klinikPatients.value
return {
total: all.length,
- anjungan: all.filter(p => p.status === 'anjungan').length,
- active: all.filter(p => p.status === 'di-loket').length
+ // Counting 'anjungan', 'menunggu', 'waiting', 'pending', 'terlambat' as waiting
+ anjungan: all.filter(p => ['anjungan', 'menunggu', 'waiting', 'pending', 'terlambat'].includes(p.status)).length,
+ // Counting 'di-loket' and 'pemeriksaan' as active/processing
+ active: all.filter(p => ['di-loket', 'pemeriksaan'].includes(p.status)).length
}
})
@@ -471,6 +532,8 @@ const fetchAllData = async () => {
await clinicStore.fetchRegulerClinics();
await ruangStore.fetchRuangFromAPI();
queueStore.ensureInitialData();
+ // Fetch fresh patient data for this clinic
+ await queueStore.fetchPatientsForClinic(kodeKlinik.value);
console.log('โ
AntrianKlinikRuang refresh: Success');
} catch (err) {
console.error('โ AntrianKlinikRuang refresh error:', err);
@@ -482,6 +545,7 @@ const anjunganClientId = computed(() => {
if (!kodeKlinik.value) return ''
// Priority 1: Try to get screen number from URL query params
+ // Use this if you want a specific screen to only show certain rooms
const screenParam = route.query.screen
if (screenParam) {
const clientId = `anjungan-klinik-ruang-${kodeKlinik.value}-screen-${screenParam}`
@@ -489,20 +553,10 @@ const anjunganClientId = computed(() => {
return clientId
}
- // Priority 2: Try to get screen number from ruangList (ambil nomorScreen dari ruang pertama yang ada)
- if (ruangListForKlinik.value && ruangListForKlinik.value.length > 0) {
- // Ambil nomorScreen dari ruang pertama yang memiliki nomorScreen
- const firstRuangWithScreen = ruangListForKlinik.value.find(r => r.nomorScreen)
- if (firstRuangWithScreen && firstRuangWithScreen.nomorScreen) {
- const clientId = `anjungan-klinik-ruang-${kodeKlinik.value}-screen-${firstRuangWithScreen.nomorScreen}`
- console.log('๐ Using client ID from ruangList nomorScreen:', clientId)
- return clientId
- }
- }
-
- // Priority 3: Fallback to client ID without screen number (broadcast)
+ // NOTE: Priority 2 (guessing from ruangList) removed to prevent ID mismatch
+ // Default to base Klinik ID (broadcast). This makes the screen receive all calls for this clinic.
const clientId = `anjungan-klinik-ruang-${kodeKlinik.value}`
- console.log('๐ Using default client ID (broadcast):', clientId)
+ console.log('๐ Using base Klinik client ID (broadcast):', clientId)
return clientId
})
@@ -602,12 +656,17 @@ onMounted(async () => {
// WebSocket initialization is now centralized in queueStore
if (kodeKlinik.value) {
queueStore.initWebSocket(anjunganClientId.value);
+ // Register interest so WS messages trigger data refresh for this clinic
+ queueStore.registerClinicInterest(kodeKlinik.value);
}
})
onUnmounted(() => {
if (timeInterval) clearInterval(timeInterval)
- // wsInstance is now global
+ // Unregister clinic interest when leaving
+ if (kodeKlinik.value) {
+ queueStore.unregisterClinicInterest(kodeKlinik.value);
+ }
})
// Watch for clientId changes and reconnect if needed
diff --git a/stores/queueStore.js b/stores/queueStore.js
index df6f7b1..06f4d2e 100644
--- a/stores/queueStore.js
+++ b/stores/queueStore.js
@@ -80,18 +80,18 @@ export const useQueueStore = defineStore('queue', () => {
console.log(`๐ [queueStore] Global interest unregistered. Total: ${globalInterestCount.value}`);
};
- const fetchPatientsForClinic = async (kodeKlinik) => {
+ const fetchPatientsForClinic = async (kodeKlinik, force = false) => {
if (!kodeKlinik) return { success: false, message: 'Kode Klinik diperlukan' };
isLoadingPatients.value = true;
apiPatientsError.value = null;
- // THROTTLE: Check if we recently fetched (within last 2 seconds)
+ // THROTTLE: Check if we recently fetched (within last 2 seconds), unless forced
const now = Date.now();
const lastFetch = lastFetchTime.value[`clinic-${kodeKlinik}`] || 0;
const timeSinceLastFetch = now - lastFetch;
- if (timeSinceLastFetch < 2000) {
+ if (!force && timeSinceLastFetch < 2000) {
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' };
@@ -169,11 +169,22 @@ export const useQueueStore = defineStore('queue', () => {
registrationType: 'api',
visitId: visit.visit_id || visit.id,
visitCode: visit.visit_code,
- referencePatient: healthcareServices.find(s => {
- const type = (s.healthcare_type_name || '').toUpperCase();
- const code = (s.healthcare_service_code || s.healtcare_service_code || '').toUpperCase();
- return type.includes('LOKET') || type.includes('PENDAFTARAN') || type.includes('REGISTRATION') || code === 'RN';
- })?.ticket || ''
+ referencePatient: (() => {
+ // First: try to find explicit LOKET/PENDAFTARAN service by type name or code
+ const loketService = healthcareServices.find(s => {
+ const type = (s.healthcare_type_name || '').toUpperCase();
+ const code = (s.healthcare_service_code || s.healtcare_service_code || '').toUpperCase();
+ return type.includes('LOKET') || type.includes('PENDAFTARAN') || type.includes('REGISTRATION') || code === 'RN';
+ });
+ if (loketService?.ticket) return loketService.ticket;
+
+ // Fallback: find any service that is NOT KLINIK type (likely the loket/registration ticket)
+ const nonKlinikService = healthcareServices.find(s => {
+ const type = (s.healthcare_type_name || '').toUpperCase();
+ return !type.includes('KLINIK') && s.ticket;
+ });
+ return nonKlinikService?.ticket || '';
+ })()
};
if (isTodayPatient(p)) mappedClinicPatients.push(p);
@@ -281,65 +292,141 @@ export const useQueueStore = defineStore('queue', () => {
const isWsConnected = ref(false);
const wsClientId = ref(`client-${Math.random().toString(36).substring(7)}`);
const lastGlobalCall = ref(null);
+ const lastKlinikCall = ref(null);
const onWsMessage = (data) => {
- const messageData = data?.data || data;
+ // Robust data extraction: some relays wrap data in another 'data' property
+ let messageData = data?.data || data;
+ if (messageData?.data && !messageData.callKlinikEvent && !messageData.callEvent) {
+ messageData = messageData.data; // Double wrap check
+ }
+
const targetLoketId = messageData?.loketId || messageData?.idloket;
const targetKlinikId = messageData?.klinikId || messageData?.idklinik;
- console.log('๐จ [queueStore] Global WS Message received:', data);
+ 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 (cross-device sync for "Sedang Dipanggil" popup
+ if (messageData?.triggerRefresh) {
+ if (messageData.klinikId) {
+ console.log(`๐ [queueStore] Received refresh trigger for clinic ${messageData.klinikId}`);
+
+ // Handle current processing update if provided
+ if (messageData.currentProcessingUpdate) {
+ console.log(`๐ฏ [queueStore] Applying current processing update:`, messageData.currentProcessingUpdate);
+
+ // Merge specifically for the keys that exist in the update
+ Object.keys(messageData.currentProcessingUpdate).forEach(key => {
+ currentProcessingPatient.value[key] = messageData.currentProcessingUpdate[key];
+
+ // Also patch the status in allPatients if possible
+ const processingPatient = messageData.currentProcessingUpdate[key];
+ if (processingPatient && processingPatient.no) {
+ const idx = allPatients.value.findIndex(p => p.no === processingPatient.no);
+ if (idx !== -1) {
+ allPatients.value[idx] = { ...allPatients.value[idx], status: 'di-loket' };
+ }
+ }
+ });
+ }
+
+ fetchPatientsForClinic(messageData.klinikId, true);
+ }
+ }
if (messageData?.callEvent) {
console.log('๐ [queueStore] Call event received:', messageData.callEvent);
lastGlobalCall.value = messageData.callEvent;
}
+
+ // Handle Klinik Call Events (cross-device sync for AntrianKlinikRuang display)
+ if (messageData?.callKlinikEvent) {
+ const ev = messageData.callKlinikEvent;
+ console.log('๐ฅ [queueStore] Klinik call event received:', ev);
+
+ // PERSISTENCE FIX: Save to lastKlinikCall for displays to watch
+ lastKlinikCall.value = ev;
+
+ // Find the patient in allPatients and patch directly for immediate UI update
+ // Match by barcode (string) or target antrian number (part before |)
+ const idx = allPatients.value.findIndex(p =>
+ p.processStage === 'klinik-ruang' &&
+ p.kodeKlinik === ev.kodeKlinik &&
+ (
+ (p.barcode && String(p.barcode) === String(ev.barcode)) ||
+ (p.noAntrian && p.noAntrian.split(' |')[0] === ev.noantrian)
+ )
+ );
+
+ if (idx !== -1) {
+ // Create a patched object to ensure reactivity
+ const updatedPatient = {
+ ...allPatients.value[idx],
+ tipeLayanan: ev.tipeLayanan,
+ lastCalledAt: ev.lastCalledAt || new Date().toISOString(),
+ lastCalledTipeLayanan: ev.tipeLayanan,
+ status: 'di-loket',
+ calledPemeriksaanAwal: ev.tipeLayanan === 'Pemeriksaan Awal' ? true : allPatients.value[idx].calledPemeriksaanAwal,
+ calledTindakan: ev.tipeLayanan === 'Tindakan' ? true : allPatients.value[idx].calledTindakan
+ };
+
+ allPatients.value[idx] = updatedPatient;
+ console.log(`โ
[queueStore] Successfully patched patient ${ev.noantrian} status to di-loket (lastCalledAt: ${updatedPatient.lastCalledAt})`);
+ } else {
+ console.warn(`โ ๏ธ [queueStore] Patient ${ev.noantrian} not found in store for clinic ${ev.kodeKlinik}.`);
+ console.log('๐งช [queueStore] Available klinik-ruang patients in store:',
+ allPatients.value
+ .filter(p => p.processStage === 'klinik-ruang')
+ .map(p => `[${p.kodeKlinik}] ${p.noAntrian?.split(' |')[0]} / ${p.barcode}`)
+ );
+ }
+ }
// TRIGGER STRATEGIC REFRESHES
let refreshedSomething = false;
if (targetLoketId) {
- // 1. If message specifies a loket, refresh that one specifically
- console.log(`๐ฏ [queueStore] WS targeting Loket ${targetLoketId}: Refreshing...`);
- fetchPatientsForLoket(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
- // Check if it matches any of our active interests or if it's a general trigger
+ // 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') {
- console.log(`๐ฏ [queueStore] WS targeting Clinic ${targetKlinikId}: Refreshing...`);
- fetchPatientsForClinic(targetKlinikId === 'broadcast' ? interestingClinics[0] : targetKlinikId);
+ const clinicToFetch = targetKlinikId === 'broadcast' ? interestingClinics[0] : targetKlinikId;
+ console.log(`๐ฏ [queueStore] WS targeting Clinic ${targetKlinikId}: Refreshing ${clinicToFetch} (FORCED)...`);
+ fetchPatientsForClinic(clinicToFetch, true);
refreshedSomething = true;
}
}
- // 3. If we have global interest (e.g. Check-in page open), always refresh everything on ANY trigger
+ // 3. Global interest refresh (Force bypass throttle)
if (globalInterestCount.value > 0) {
- console.log(`๐ก [queueStore] WS trigger: Global Interest active, triggering staggered bulk refresh...`);
- fetchAllPatients();
+ console.log(`๐ก [queueStore] WS trigger: Global Interest active, triggering bulk refresh (FORCED)...`);
+ fetchAllPatients(); // Note: fetchAllPatients might need force internal too, but typically calls fetchPatientsForClinic
refreshedSomething = true;
}
- // 4. Fallback: If nothing specific was refreshed but we have generic interest, refresh all active things
+ // 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: Refreshing ${interestingLokets.length} active lokets:`, interestingLokets);
+ console.log(`๐ [queueStore] WS trigger fallback: Refreshing ${interestingLokets.length} active lokets (FORCED):`, interestingLokets);
interestingLokets.forEach(loketId => {
- fetchPatientsForLoket(loketId);
+ fetchPatientsForLoket(loketId, true);
});
refreshedSomething = true;
}
if (interestingClinics.length > 0) {
- console.log(`๐ [queueStore] WS trigger: Refreshing ${interestingClinics.length} active clinics:`, interestingClinics);
+ console.log(`๐ [queueStore] WS trigger fallback: Refreshing ${interestingClinics.length} active clinics (FORCED):`, interestingClinics);
interestingClinics.forEach(kodeKlinik => {
- fetchPatientsForClinic(kodeKlinik);
+ fetchPatientsForClinic(kodeKlinik, true);
});
refreshedSomething = true;
}
@@ -608,25 +695,25 @@ export const useQueueStore = defineStore('queue', () => {
};
/**
- * Fetch patient data untuk loket tertentu dari API
- */
- const fetchPatientsForLoket = async (loketId) => {
- if (!loketId) {
- console.error('loketId required for fetchPatientsForLoket');
- return { success: false, message: 'ID Loket diperlukan' };
- }
+ * Fetch patient data untuk loket tertentu dari API
+ */
+const fetchPatientsForLoket = async (loketId, force = false) => {
+ if (!loketId) {
+ console.error('loketId required for fetchPatientsForLoket');
+ return { success: false, message: 'ID Loket diperlukan' };
+ }
- isLoadingPatients.value = true;
- apiPatientsError.value = null;
+ isLoadingPatients.value = true;
+ apiPatientsError.value = null;
- // THROTTLE: Check if we recently fetched (within last 2 seconds)
- const now = Date.now();
- const lastFetch = lastFetchTime.value[loketId] || 0;
- const timeSinceLastFetch = now - lastFetch;
-
- if (timeSinceLastFetch < 2000 && apiPatientsPerLoket.value[loketId]) {
- console.log(`โญ๏ธ [queueStore] Skipping fetch for loket ${loketId} (last fetched ${Math.round(timeSinceLastFetch/1000)}s ago)`);
- isLoadingPatients.value = false;
+ // THROTTLE: Check if we recently fetched (within last 2 seconds), unless forced
+ const now = Date.now();
+ const lastFetch = lastFetchTime.value[loketId] || 0;
+ const timeSinceLastFetch = now - lastFetch;
+
+ if (!force && timeSinceLastFetch < 2000 && apiPatientsPerLoket.value[loketId]) {
+ console.log(`โญ๏ธ [queueStore] Skipping fetch for loket ${loketId} (last fetched ${Math.round(timeSinceLastFetch/1000)}s ago)`);
+ isLoadingPatients.value = false;
return {
success: true,
message: 'Using cached data',
@@ -914,7 +1001,8 @@ export const useQueueStore = defineStore('queue', () => {
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);
+ // Pass the 'force' flag down to bypass individual throttles
+ await fetchPatientsForLoket(id, force);
// Wait 150ms between requests (not too long, but enough to breathe)
await new Promise(resolve => setTimeout(resolve, 150));
@@ -2654,6 +2742,10 @@ export const useQueueStore = defineStore('queue', () => {
switch (action) {
case "proses":
+ allPatients.value[patientIndex] = {
+ ...allPatients.value[patientIndex],
+ status: "di-loket"
+ };
currentProcessingPatient.value[storageKey] = allPatients.value[patientIndex];
message = `Memproses ${pCode}`;
break;
@@ -3344,6 +3436,7 @@ export const useQueueStore = defineStore('queue', () => {
isWsConnected,
sendViaPost,
lastGlobalCall,
+ lastKlinikCall,
registerInterest,
unregisterInterest,
activeLoketInterest,