From 083fe3e364a87f5388d0fc0ddfd4e05c9cd9183c Mon Sep 17 00:00:00 2001 From: Fanrouver Date: Mon, 26 Jan 2026 14:37:05 +0700 Subject: [PATCH] update api loket admin --- pages/{AdminLoket.vue => AdminLoket/[id].vue} | 511 +++++++++--------- pages/AdminLoket/index.vue | 126 +++++ pages/Anjungan/AntrianLoket/[id].vue | 67 +++ stores/loketStore.js | 104 ++-- 4 files changed, 488 insertions(+), 320 deletions(-) rename pages/{AdminLoket.vue => AdminLoket/[id].vue} (56%) create mode 100644 pages/AdminLoket/index.vue diff --git a/pages/AdminLoket.vue b/pages/AdminLoket/[id].vue similarity index 56% rename from pages/AdminLoket.vue rename to pages/AdminLoket/[id].vue index 16acf70..f66ace7 100644 --- a/pages/AdminLoket.vue +++ b/pages/AdminLoket/[id].vue @@ -2,209 +2,222 @@
+ > + +
- - -
- - + + +
+ + - - -
-
+ + +
+
- - - - - -
- - -
- - + + + + + +
+ + +
+ + +
+ + +
+
+
+ + + + + + + + + + + + + + + + Pilih Klinik Ruang + + mdi-close + + + + + + +
+

Informasi Pasien

+ + +
+ Barcode + {{ currentProcessingPatient.barcode }} +
+
+ +
+ No. Antrian + {{ currentProcessingPatient.noAntrian }} +
+
+
- + + + + +
+ + {{ klinikRuang.kodeKlinik }} + + {{ klinikRuang.namaKlinik }} +
+ +
+ + +
+
+ mdi-door +
Ruang {{ ruang.nomorRuang }}
+
+
+
+
+
- - +
- - - - - - - - - - - - - - Pilih Klinik Ruang - - mdi-close - - - - - - -
-

Informasi Pasien

- - -
- Barcode - {{ currentProcessingPatient.barcode }} -
-
- -
- No. Antrian - {{ currentProcessingPatient.noAntrian }} -
-
-
-
- - - - - - -
- - {{ klinikRuang.kodeKlinik }} - - {{ klinikRuang.namaKlinik }} -
- -
- - -
-
- mdi-door -
Ruang {{ ruang.nomorRuang }}
-
-
-
-
-
-
-
-
- - - + +
+ + diff --git a/pages/Anjungan/AntrianLoket/[id].vue b/pages/Anjungan/AntrianLoket/[id].vue index ba7e6f7..bd67211 100644 --- a/pages/Anjungan/AntrianLoket/[id].vue +++ b/pages/Anjungan/AntrianLoket/[id].vue @@ -201,6 +201,8 @@ const clinicStore = useClinicStore() const currentTime = ref('') const currentDate = ref('') let timeInterval = null +let broadcastChannel = null +const broadcastedPatient = ref(null) // Get loket ID from route const loketId = computed(() => { @@ -665,6 +667,11 @@ const isInTTSWindow = (queue) => { // Current called queue - tiket yang sedang diproses di admin loket // Nomor antrian menjadi "dipanggil" jika diproses pada AdminLoket DAN sudah dipanggil oleh admin const currentCalledQueue = computed(() => { + // Prioritas 0: Broadcasted patient (Real-time from BroadcastChannel) + if (broadcastedPatient.value) { + return broadcastedPatient.value + } + // Prioritas 1: Pasien yang sedang diproses di admin loket (currentProcessingPatient) // Hanya tampilkan jika sudah dipanggil eksplisit oleh admin (calledByAdmin === true) if (currentProcessingPatient.value) { @@ -926,11 +933,71 @@ onMounted(() => { // Langsung start timer tanpa menunggu fetch updateTime() + // Init BroadcastChannel + try { + broadcastChannel = new BroadcastChannel('antrian-loket-channel') + broadcastChannel.onmessage = (event) => { + console.log('Anjungan received broadcast:', event.data) + if (event.data && event.data.type === 'CALL_PATIENT') { + const { patient, loketId: senderLoketId } = event.data + + // Cek apakah event ini untuk loket ini + // Gunakan loose equality untuk handle string vs number + if (loketId.value) { + if (String(loketId.value) != String(senderLoketId)) { + console.log('Skipping broadcast for different loket:', senderLoketId) + return + } + } + + // Update local state untuk prioritas tampilan hero + broadcastedPatient.value = patient + + // PENTING: Update Store agar list antrian juga berubah + // 1. Update currentProcessingPatient + if (!queueStore.currentProcessingPatient) { + queueStore.currentProcessingPatient = {} + } + queueStore.currentProcessingPatient.loket = patient + + // 2. Update status patient di allPatients list jika ada + if (queueStore.allPatients && Array.isArray(queueStore.allPatients)) { + const idx = queueStore.allPatients.findIndex(p => + p.no === patient.no || + p.noAntrian === patient.noAntrian + ) + + if (idx !== -1) { + // Update existing patient data + // Kita update status menjadi 'di-loket' dan lastCalledAt + const updatedPatient = { + ...queueStore.allPatients[idx], + ...patient, + status: 'di-loket', // Pastikan status sinkron + lastCalledAt: patient.lastCalledAt || new Date().toISOString() + } + queueStore.allPatients[idx] = updatedPatient + } else { + // Opsional: Jika pasien tidak ditemukan di list (misal data awal kosong), tambahkan? + // Sebaiknya jangan sembarang nambah, tapi untuk robustnes UI boleh saja + // queueStore.allPatients.push(patient) + } + } + + console.log('Store updated from broadcast') + } + } + } catch (e) { + console.error('BroadcastChannel error:', e) + } + + // Start TTS checker timeInterval = setInterval(updateTime, 1000) }) onUnmounted(() => { if (timeInterval) clearInterval(timeInterval) + if (broadcastChannel) broadcastChannel.close() }) diff --git a/stores/loketStore.js b/stores/loketStore.js index 17ada66..0ef36b9 100644 --- a/stores/loketStore.js +++ b/stores/loketStore.js @@ -262,37 +262,31 @@ export const useLoketStore = defineStore('loket', () => { /** * Fetch loket data dari API backend - * Mengikuti pattern dari clinicStore.fetchRegulerClinics() - * Filter hanya loket dengan pembayaran JKN (REGULER) - * Sort berdasarkan nama loket (urut dari 1) + * MENGGUNAKAN endpoint /api/v1/klinik/reguler untuk memetakan Loket -> Clinics */ const fetchLoketFromAPI = async (force = false, retryCount = 0) => { - // Guard: singleton pattern - prevent duplicate fetch if (activeFetchPromise && retryCount === 0) { console.log('⏳ Loket fetch in progress, reusing existing request...'); return activeFetchPromise; } - // Guard: Skip if data already exists and not forced - const hasData = loketData.value && loketData.value.length > 0; + const hasData = apiLoketData.value && apiLoketData.value.length > 0; if (hasData && !force && retryCount === 0) { return { success: true, message: 'Data sudah tersedia' }; } - // Define the actual fetch operation const performFetch = async () => { isLoadingAPI.value = true; apiError.value = null; try { - console.log(`🔄 Fetching loket from API (Try ${retryCount + 1})...`); - const response = await fetch('http://10.10.150.131:8089/api/v1/klinik/loket'); + console.log(`🔄 Fetching clinics for loket config (Try ${retryCount + 1})...`); + // Endpoint ini memberikan list klinik, setiap klinik punya list loket + const response = await fetch('http://10.10.150.131:8089/api/v1/klinik/reguler'); if (!response.ok) { - // Handle Rate Limiting with exponential backoff if (response.status === 429 && retryCount < 3) { const delay = (retryCount + 1) * 1500; - console.warn(`⚠️ Rate limit hit (429). Retrying in ${delay}ms...`); await new Promise(resolve => setTimeout(resolve, delay)); activeFetchPromise = null; return fetchLoketFromAPI(force, retryCount + 1); @@ -301,81 +295,73 @@ export const useLoketStore = defineStore('loket', () => { } const rawData = await response.json(); - console.log('📦 Raw API Response received'); - console.log('📦 Raw data:', rawData); + const clinics = rawData.data || []; - // Extract data array - const data = rawData.data || rawData; - if (!Array.isArray(data)) { - throw new Error('Data format tidak valid. Expected array.'); - } + // AGGREGATE: Build Loket Map from Clinics + const loketMap = new Map(); - console.log('📊 Total items from API:', data.length); + clinics.forEach(clinic => { + if (clinic.loket && Array.isArray(clinic.loket)) { + clinic.loket.forEach(l => { + const id = parseInt(l.idloket); + if (!loketMap.has(id)) { + loketMap.set(id, { + id: id, + namaLoket: l.namaloket, + kodeLoket: l.kodeloket, + kuota: clinic.kuota || 100, // Fallback + pelayanan: [], + _spesialisDetail: [], + pembayaran: 'JKN', + source: 'api', + loketAktif: true + }); + } + + const loketObj = loketMap.get(id); + // Add clinic code to pelayanan if not already there + if (!loketObj.pelayanan.includes(clinic.code)) { + loketObj.pelayanan.push(clinic.code); + loketObj._spesialisDetail.push({ + idklinik: clinic.idklinik, + namaklinik: clinic.namaklinik, + code: clinic.code + }); + } + }); + } + }); - // Filter hanya JKN (REGULER) dan transform ke format internal - const lokets = data - .filter(item => { - const hasJKN = item.pembayaran?.some(p => p.pembayaran === 'JKN'); - if (!hasJKN) { - console.warn('⚠️ Loket skipped (no JKN):', item.namaloket, 'pembayaran:', item.pembayaran); - } - return hasJKN; - }) - .map((item) => ({ - id: parseInt(item.idloket), - no: 0, // Will be set after sort - namaLoket: item.namaloket, - kodeLoket: item.kodeloket, - kuota: parseInt(item.kuotaloket), - pelayanan: item.spesialis?.map(s => s.idklinik) || [], - pembayaran: 'JKN', - keterangan: item.tipevisit?.map(t => t.tipevisit).join(', ') || 'ONLINE', - statusPelayanan: item.tipeloket || 'REGULER', - loketAktif: item.loketaktif, - jenisLoket: item.jenisloket, - - // Simpan detail lengkap spesialis untuk display nama klinik - _spesialisDetail: item.spesialis || [], - })); + const lokets = Array.from(loketMap.values()); - // Sort by nama loket (extract number and sort: LOKET 1, 2, 3,... 14) + // Sort by nama loket (LOKET 1, 2, ...) lokets.sort((a, b) => { const numA = parseInt(a.namaLoket.match(/\d+/)?.[0] || 0); const numB = parseInt(b.namaLoket.match(/\d+/)?.[0] || 0); return numA - numB; }); - // Update `no` field to sequential after sort + // Set No sequential lokets.forEach((loket, idx) => { loket.no = idx + 1; - loket.source = 'api'; // Mark as API data }); - console.log(`✅ ${lokets.length} loket JKN berhasil di-filter dan dimuat dari API`); - console.log('✅ Loket data:', lokets.map(l => ({ id: l.id, namaLoket: l.namaLoket, pembayaran: l.pembayaran }))); - - // Update API store - TIDAK di-persist apiLoketData.value = lokets; lastSyncTimestamp.value = new Date().toISOString(); - console.log(`✅ ${lokets.length} loket JKN berhasil dimuat dari API`); return { success: true, - message: `${lokets.length} loket berhasil dimuat`, + message: `${lokets.length} loket berhasil dipetakan dari klinik`, data: lokets }; } catch (error) { - console.error('❌ Error fetching loket:', error); + console.error('❌ Error fetching loket config:', error); apiError.value = error.message; - return { - success: false, - message: `Gagal memuat data: ${error.message}` - }; + return { success: false, message: `Gagal memuat: ${error.message}` }; } finally { isLoadingAPI.value = false; activeFetchPromise = null; - console.log('✅ API fetch flow completed'); } };