From 6e7160c935d3e5c37140f67b9869cbf6a49da5fd Mon Sep 17 00:00:00 2001 From: bagus-arie05 Date: Mon, 12 Jan 2026 11:21:54 +0700 Subject: [PATCH] update layout klinik dan websocket layar klinik --- composables/useWebSocket.ts | 167 +++++++++ nuxt.config.ts | 1 + pages/AdminKlinikRuang/[kodeKlinik].vue | 182 +++++++-- .../AntrianKlinikRuang/[kodeKlinik].vue | 352 ++++++++++++++++-- 4 files changed, 642 insertions(+), 60 deletions(-) create mode 100644 composables/useWebSocket.ts diff --git a/composables/useWebSocket.ts b/composables/useWebSocket.ts new file mode 100644 index 0000000..12bdeb9 --- /dev/null +++ b/composables/useWebSocket.ts @@ -0,0 +1,167 @@ +// composables/useWebSocket.ts +import { ref, computed, onUnmounted } from 'vue' + +export interface WebSocketConfig { + url: string + clientId: string + onMessage?: (data: any) => void + onOpen?: () => void + onClose?: () => void + onError?: (error: Event) => void + reconnectInterval?: number + maxReconnectAttempts?: number +} + +export interface WebSocketMessage { + to_client: string + data: any +} + +export const useWebSocket = (config: WebSocketConfig) => { + const ws = ref(null) + const isConnected = ref(false) + const reconnectAttempts = ref(0) + const reconnectTimer = ref(null) + + const wsUrl = computed(() => { + const url = new URL(config.url) + url.searchParams.set('client_id', config.clientId) + return url.toString() + }) + + const connect = () => { + try { + // Close existing connection if any + if (ws.value && ws.value.readyState !== WebSocket.CLOSED) { + ws.value.close() + } + + ws.value = new WebSocket(wsUrl.value) + + ws.value.onopen = () => { + console.log('WebSocket connected:', config.clientId) + isConnected.value = true + reconnectAttempts.value = 0 + config.onOpen?.() + } + + ws.value.onmessage = (event) => { + try { + const data = JSON.parse(event.data) + config.onMessage?.(data) + } catch (error) { + console.error('Error parsing WebSocket message:', error) + } + } + + ws.value.onclose = () => { + console.log('WebSocket closed:', config.clientId) + isConnected.value = false + config.onClose?.() + + // Attempt to reconnect + if (reconnectAttempts.value < (config.maxReconnectAttempts || 5)) { + reconnectAttempts.value++ + const interval = config.reconnectInterval || 3000 + reconnectTimer.value = setTimeout(() => { + console.log(`Reconnecting... Attempt ${reconnectAttempts.value}`) + connect() + }, interval) + } else { + console.error('Max reconnection attempts reached') + } + } + + ws.value.onerror = (error) => { + console.error('WebSocket error:', error) + config.onError?.(error) + } + } catch (error) { + console.error('Error creating WebSocket:', error) + config.onError?.(error as Event) + } + } + + const disconnect = () => { + if (reconnectTimer.value) { + clearTimeout(reconnectTimer.value) + reconnectTimer.value = null + } + if (ws.value) { + ws.value.close() + ws.value = null + } + isConnected.value = false + } + + const sendMessage = (message: any) => { + if (ws.value && ws.value.readyState === WebSocket.OPEN) { + ws.value.send(JSON.stringify(message)) + return true + } + console.warn('WebSocket is not connected') + return false + } + + // Send data via POST to WebSocket server + const sendViaPost = async (message: WebSocketMessage) => { + try { + // Extract base URL from WebSocket URL (convert ws:// to http:// or wss:// to https://) + let baseUrl = config.url + if (baseUrl.startsWith('ws://')) { + baseUrl = baseUrl.replace('ws://', 'http://') + } else if (baseUrl.startsWith('wss://')) { + baseUrl = baseUrl.replace('wss://', 'https://') + } + + // Remove query parameters and ensure we have the correct POST endpoint + const urlObj = new URL(baseUrl) + const 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: { + 'Content-Type': 'application/json', + }, + 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) + return result + } catch (error) { + console.error('❌ Error sending message via POST:', error) + throw error + } + } + + onUnmounted(() => { + disconnect() + }) + + return { + ws: computed(() => ws.value), + isConnected: computed(() => isConnected.value), + connect, + disconnect, + sendMessage, + sendViaPost, + reconnectAttempts: computed(() => reconnectAttempts.value), + } +} + diff --git a/nuxt.config.ts b/nuxt.config.ts index 057f3c1..8124e0e 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -81,6 +81,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', }, }, diff --git a/pages/AdminKlinikRuang/[kodeKlinik].vue b/pages/AdminKlinikRuang/[kodeKlinik].vue index 93470a8..768cbe2 100644 --- a/pages/AdminKlinikRuang/[kodeKlinik].vue +++ b/pages/AdminKlinikRuang/[kodeKlinik].vue @@ -88,11 +88,17 @@
-
+ +
{ snackbarText.value = message; snackbarColor.value = color; @@ -600,29 +622,14 @@ const confirmPindahRuang = () => { }; // Handle call patient from list (dari card pasien di daftar) -const handleCallPatientFromList = (ruang, patient, tipeLayanan) => { +const handleCallPatientFromList = async (ruang, patient, tipeLayanan) => { if (!patient || !patient.no) { showSnackbar('Pasien tidak valid', 'error'); return; } - // Pastikan pasien sudah diproses terlebih dahulu - const current = getCurrentProcessingForRoom(ruang); - if (!current || current.no !== patient.no) { - // Jika pasien belum diproses, proses dulu - const processResult = queueStore.processPatientKlinikRuang( - patient, - 'proses', - klinikData.value.kodeKlinik, - ruang.nomorRuang - ); - if (!processResult.success) { - showSnackbar(processResult.message, 'error'); - return; - } - } - // Update tipeLayanan pasien untuk tracking panggilan terakhir + // Hanya memanggil, tidak memproses pasien (tidak naik ke current processing) const patientIndex = queueStore.allPatients.findIndex(p => p.no === patient.no); if (patientIndex === -1) { showSnackbar('Pasien tidak ditemukan', 'error'); @@ -633,7 +640,7 @@ const handleCallPatientFromList = (ruang, patient, tipeLayanan) => { // Update tipeLayanan pasien agar muncul di kolom yang sesuai di Anjungan const updateData = { ...queueStore.allPatients[patientIndex], - status: 'di-loket', + status: queueStore.allPatients[patientIndex].status, // Tetap status yang sama, tidak diubah ke 'di-loket' tipeLayanan: tipeLayanan, // Update tipeLayanan untuk display di Anjungan lastCalledAt: new Date().toISOString(), lastCalledTipeLayanan: tipeLayanan @@ -648,13 +655,70 @@ const handleCallPatientFromList = (ruang, patient, tipeLayanan) => { queueStore.allPatients[patientIndex] = updateData; - // Update current processing (tetap 1 pasien, tidak dipisah per tipe) - const key = `klinik-ruang-${klinikData.value.kodeKlinik}-${ruang.nomorRuang}`; - queueStore.currentProcessingPatient[key] = queueStore.allPatients[patientIndex]; + // TIDAK mengupdate current processing - pasien tetap di daftar pasien const patientCode = queueStore.allPatients[patientIndex].noAntrian?.split(" |")[0] || queueStore.allPatients[patientIndex].barcode || '-'; + + // Send via WebSocket to Anjungan clients + try { + // Get all possible client IDs for this klinik (anjungan screens) + 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}`); + + // Also send to specific screen if ruang has nomorScreen + if (ruang.nomorScreen) { + const specificScreenId = `anjungan-klinik-ruang-${klinikData.value.kodeKlinik}-screen-${ruang.nomorScreen}`; + if (!anjunganClientIds.includes(specificScreenId)) { + anjunganClientIds.push(specificScreenId); + } + } + + // 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); + } + } + }); + + console.log('📋 All target client IDs:', anjunganClientIds); + + // Extract nomor antrian (ambil bagian sebelum " |") + const nomorAntrian = updateData.noAntrian?.split(" |")[0] || updateData.barcode || '-'; + + console.log('📤 Sending WebSocket message via POST...'); + console.log('📍 Target clients:', anjunganClientIds); + console.log('đŸ“Ļ Nomor antrian:', nomorAntrian); + console.log('đŸ“Ļ Tipe layanan:', tipeLayanan); + + // Send to all relevant clients + for (const clientId of anjunganClientIds) { + // Kirim nomor antrian + tipe layanan agar Anjungan tahu harus tampilkan di kolom mana + const message = { + to_client: clientId, + data: { + noantrian: nomorAntrian, + tipeLayanan: tipeLayanan // "Pemeriksaan Awal" atau "Tindakan" + }, + }; + + console.log(`📨 Sending to ${clientId}:`, message); + const result = await sendViaPost(message); + console.log(`✅ Response from ${clientId}:`, result); + } + + console.log('✅ All WebSocket messages sent successfully'); + } catch (error) { + console.error('❌ Error sending WebSocket message:', error); + // Don't block the UI if WebSocket fails + } + showSnackbar(`Memanggil pasien ${patientCode} untuk ${tipeLayanan}`, 'success'); }; @@ -751,7 +815,7 @@ const getStatusLabel = (status) => { // Handle call patient by tipe (untuk pasien yang sedang diproses) -const handleCallPatientByTipe = (ruang, tipeLayanan) => { +const handleCallPatientByTipe = async (ruang, tipeLayanan) => { const current = getCurrentProcessingForRoom(ruang); if (!current || !current.no) { showSnackbar('Tidak ada pasien yang sedang diproses', 'error'); @@ -791,6 +855,65 @@ const handleCallPatientByTipe = (ruang, tipeLayanan) => { const patientCode = queueStore.allPatients[patientIndex].noAntrian?.split(" |")[0] || queueStore.allPatients[patientIndex].barcode || '-'; + + // Send via WebSocket to Anjungan clients + try { + // Get all possible client IDs for this klinik (anjungan screens) + 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}`); + + // Also send to specific screen if ruang has nomorScreen + if (ruang.nomorScreen) { + const specificScreenId = `anjungan-klinik-ruang-${klinikData.value.kodeKlinik}-screen-${ruang.nomorScreen}`; + if (!anjunganClientIds.includes(specificScreenId)) { + anjunganClientIds.push(specificScreenId); + } + } + + // 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); + } + } + }); + + console.log('📋 All target client IDs:', anjunganClientIds); + + // Extract nomor antrian (ambil bagian sebelum " |") + const nomorAntrian = updateData.noAntrian?.split(" |")[0] || updateData.barcode || '-'; + + console.log('📤 Sending WebSocket message via POST...'); + console.log('📍 Target clients:', anjunganClientIds); + console.log('đŸ“Ļ Nomor antrian:', nomorAntrian); + console.log('đŸ“Ļ Tipe layanan:', tipeLayanan); + + // Send to all relevant clients + for (const clientId of anjunganClientIds) { + // Kirim nomor antrian + tipe layanan agar Anjungan tahu harus tampilkan di kolom mana + const message = { + to_client: clientId, + data: { + noantrian: nomorAntrian, + tipeLayanan: tipeLayanan // "Pemeriksaan Awal" atau "Tindakan" + }, + }; + + console.log(`📨 Sending to ${clientId}:`, message); + const result = await sendViaPost(message); + console.log(`✅ Response from ${clientId}:`, result); + } + + console.log('✅ All WebSocket messages sent successfully'); + } catch (error) { + console.error('❌ Error sending WebSocket message:', error); + // Don't block the UI if WebSocket fails + } + showSnackbar(`Memanggil pasien ${patientCode} untuk ${tipeLayanan}`, 'success'); }; @@ -1241,8 +1364,6 @@ onMounted(() => { display: flex; flex-direction: column; gap: 6px; - max-height: 200px; - overflow-y: auto; } .patient-queue-item { @@ -1260,11 +1381,8 @@ onMounted(() => { .patient-queue-item-pending-status { background: linear-gradient(135deg, #ffebee 0%, #ffcdd2 100%); border: 2px solid #ef5350; -} - -.patient-queue-item-pending-status:hover { - background: linear-gradient(135deg, #ffcdd2 0%, #ef9a9a 100%); - border-color: #e53935; + cursor: default; + transition: none !important; } /* Pasien yang sudah digenerate/diproses */ diff --git a/pages/Anjungan/AntrianKlinikRuang/[kodeKlinik].vue b/pages/Anjungan/AntrianKlinikRuang/[kodeKlinik].vue index 288d74d..87a0f98 100644 --- a/pages/Anjungan/AntrianKlinikRuang/[kodeKlinik].vue +++ b/pages/Anjungan/AntrianKlinikRuang/[kodeKlinik].vue @@ -81,12 +81,6 @@

Tidak Ada Antrian

- - -
- mdi-clock-outline -

Tidak Ada Antrian

-
@@ -136,10 +130,11 @@