update layout klinik dan websocket layar klinik
This commit is contained in:
@@ -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<WebSocket | null>(null)
|
||||
const isConnected = ref(false)
|
||||
const reconnectAttempts = ref(0)
|
||||
const reconnectTimer = ref<NodeJS.Timeout | null>(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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -88,11 +88,17 @@
|
||||
</v-chip>
|
||||
</div>
|
||||
<div class="patient-queue-list">
|
||||
<div
|
||||
<v-tooltip
|
||||
v-for="patient in paginatedAllPatients(ruang)"
|
||||
:key="patient.no"
|
||||
:class="getPatientCardClass(patient)"
|
||||
:text="patient.status === 'pending' ? 'Pasien Pending' : ''"
|
||||
:disabled="patient.status !== 'pending'"
|
||||
>
|
||||
<template v-slot:activator="{ props }">
|
||||
<div
|
||||
v-bind="props"
|
||||
:class="getPatientCardClass(patient)"
|
||||
>
|
||||
<!-- Left Section: Number and Status -->
|
||||
<div class="patient-card-left">
|
||||
<div class="patient-queue-number">
|
||||
@@ -166,7 +172,9 @@
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
<!-- Pagination -->
|
||||
<v-pagination
|
||||
@@ -353,6 +361,7 @@ import { useMasterStore } from '@/stores/masterStore';
|
||||
import PageHeader from '@/components/common/PageHeader.vue';
|
||||
import PatientCard from '@/components/features/queue/PatientCard.vue';
|
||||
import AppSnackbar from '@/components/common/AppSnackbar.vue';
|
||||
import { useWebSocket } from '@/composables/useWebSocket';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -409,6 +418,19 @@ const showPindahRuangDialog = ref(false);
|
||||
const selectedPatientForPindah = ref(null);
|
||||
const selectedRuangBaru = ref(null);
|
||||
|
||||
// WebSocket configuration
|
||||
const config = useRuntimeConfig();
|
||||
const wsBaseUrl = config.public?.wsBaseUrl || 'ws://10.10.150.100:8084/api/v1/ws';
|
||||
|
||||
// WebSocket client ID for admin (you can make this dynamic)
|
||||
const adminClientId = `admin-klinik-ruang-${kodeKlinik.value}`;
|
||||
|
||||
// Initialize WebSocket for sending messages
|
||||
const { sendViaPost } = useWebSocket({
|
||||
url: wsBaseUrl,
|
||||
clientId: adminClientId,
|
||||
});
|
||||
|
||||
const showSnackbar = (message, color = 'success') => {
|
||||
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 */
|
||||
|
||||
@@ -81,12 +81,6 @@
|
||||
<p class="empty-text-small">Tidak Ada Antrian</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State for entire room -->
|
||||
<div v-if="ruang.totalQueues === 0" class="empty-state">
|
||||
<v-icon size="48" color="grey-lighten-3">mdi-clock-outline</v-icon>
|
||||
<p class="empty-text">Tidak Ada Antrian</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -136,10 +130,11 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { useQueueStore } from '@/stores/queueStore'
|
||||
import { useMasterStore } from '@/stores/masterStore'
|
||||
import { useRoute } from '#app'
|
||||
import { useWebSocket } from '@/composables/useWebSocket'
|
||||
|
||||
definePageMeta({
|
||||
layout: false,
|
||||
@@ -369,6 +364,184 @@ const updateTime = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// WebSocket configuration for Anjungan
|
||||
const config = useRuntimeConfig()
|
||||
const wsBaseUrl = config.public?.wsBaseUrl || 'ws://10.10.150.100:8084/api/v1/ws'
|
||||
|
||||
// WebSocket client ID for anjungan (you can make this dynamic based on screen number)
|
||||
const anjunganClientId = computed(() => {
|
||||
if (!kodeKlinik.value) return ''
|
||||
|
||||
// Priority 1: Try to get screen number from URL query params
|
||||
const screenParam = route.query.screen
|
||||
if (screenParam) {
|
||||
const clientId = `anjungan-klinik-ruang-${kodeKlinik.value}-screen-${screenParam}`
|
||||
console.log('🆔 Using client ID from URL query screen param:', clientId)
|
||||
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)
|
||||
const clientId = `anjungan-klinik-ruang-${kodeKlinik.value}`
|
||||
console.log('🆔 Using default client ID (no screen number):', clientId)
|
||||
return clientId
|
||||
})
|
||||
|
||||
// WebSocket instance
|
||||
let wsInstance = null
|
||||
const isConnected = ref(false)
|
||||
|
||||
// Initialize WebSocket connection
|
||||
const initWebSocket = () => {
|
||||
if (!kodeKlinik.value || !anjunganClientId.value) {
|
||||
console.warn('Cannot initialize WebSocket: missing kodeKlinik or clientId')
|
||||
return null
|
||||
}
|
||||
|
||||
console.log('🔌 Initializing WebSocket connection...')
|
||||
console.log('📍 URL:', wsBaseUrl)
|
||||
console.log('🆔 Client ID:', anjunganClientId.value)
|
||||
|
||||
wsInstance = useWebSocket({
|
||||
url: wsBaseUrl,
|
||||
clientId: anjunganClientId.value,
|
||||
onMessage: (data) => {
|
||||
console.log('📨 WebSocket raw message received:', data)
|
||||
console.log('📨 Message type:', typeof data)
|
||||
|
||||
// Backend mengirim data langsung atau dalam wrapper
|
||||
// Handle both cases
|
||||
let messageData = data
|
||||
|
||||
// Jika data ada dalam property 'data', ambil itu
|
||||
if (data && typeof data === 'object' && data.data) {
|
||||
messageData = data.data
|
||||
console.log('📦 Extracted data from wrapper:', messageData)
|
||||
}
|
||||
|
||||
// Handle patient called message - format: { noantrian: "AA001", tipeLayanan: "Pemeriksaan Awal" }
|
||||
if (messageData && messageData.noantrian) {
|
||||
const nomorAntrian = messageData.noantrian
|
||||
const tipeLayanan = messageData.tipeLayanan || null // "Pemeriksaan Awal" atau "Tindakan"
|
||||
console.log('✅ Processing patient call for nomor antrian:', nomorAntrian, 'tipeLayanan:', tipeLayanan)
|
||||
|
||||
// Find patient by nomor antrian (check both noAntrian and barcode)
|
||||
const patientIndex = queueStore.allPatients.findIndex(p => {
|
||||
const pNoAntrian = p.noAntrian?.split(" |")[0] || p.noAntrian
|
||||
return (pNoAntrian === nomorAntrian || p.barcode === nomorAntrian) &&
|
||||
p.kodeKlinik === kodeKlinik.value &&
|
||||
p.processStage === 'klinik-ruang'
|
||||
})
|
||||
|
||||
if (patientIndex !== -1) {
|
||||
// Update patient status to 'di-loket' (called) dan update tipeLayanan serta tracking flags
|
||||
const oldPatient = { ...queueStore.allPatients[patientIndex] }
|
||||
const updateData = {
|
||||
...queueStore.allPatients[patientIndex],
|
||||
status: 'di-loket',
|
||||
lastCalledAt: new Date().toISOString(),
|
||||
lastCalledTipeLayanan: tipeLayanan,
|
||||
}
|
||||
|
||||
// Update tipeLayanan agar pasien muncul di kolom yang sesuai
|
||||
if (tipeLayanan) {
|
||||
updateData.tipeLayanan = tipeLayanan
|
||||
}
|
||||
|
||||
// Update tracking flags berdasarkan tipeLayanan
|
||||
if (tipeLayanan === 'Pemeriksaan Awal') {
|
||||
updateData.calledPemeriksaanAwal = true
|
||||
} else if (tipeLayanan === 'Tindakan') {
|
||||
updateData.calledTindakan = true
|
||||
}
|
||||
|
||||
queueStore.allPatients[patientIndex] = updateData
|
||||
|
||||
console.log('✅ Patient data updated in store based on nomor antrian:', nomorAntrian)
|
||||
console.log('📊 Old:', { status: oldPatient.status, tipeLayanan: oldPatient.tipeLayanan })
|
||||
console.log('📊 New:', {
|
||||
status: updateData.status,
|
||||
tipeLayanan: updateData.tipeLayanan,
|
||||
calledPemeriksaanAwal: updateData.calledPemeriksaanAwal,
|
||||
calledTindakan: updateData.calledTindakan
|
||||
})
|
||||
} else {
|
||||
// If patient doesn't exist, log warning
|
||||
console.warn('⚠️ Patient not found in store with nomor antrian:', nomorAntrian)
|
||||
console.log('📋 Available patients:', klinikPatients.value.map(p => ({
|
||||
noAntrian: p.noAntrian?.split(" |")[0],
|
||||
barcode: p.barcode,
|
||||
kodeKlinik: p.kodeKlinik
|
||||
})))
|
||||
}
|
||||
} else {
|
||||
console.log('ℹ️ Message format is invalid or missing noantrian:', messageData)
|
||||
}
|
||||
},
|
||||
onOpen: () => {
|
||||
console.log('✅ Anjungan WebSocket connected successfully!')
|
||||
console.log('🆔 Connected as:', anjunganClientId.value)
|
||||
isConnected.value = true
|
||||
},
|
||||
onClose: () => {
|
||||
console.log('⚠️ Anjungan WebSocket closed')
|
||||
isConnected.value = false
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('❌ Anjungan WebSocket error:', error)
|
||||
isConnected.value = false
|
||||
},
|
||||
reconnectInterval: 3000,
|
||||
maxReconnectAttempts: 10,
|
||||
})
|
||||
|
||||
// Watch connection status
|
||||
if (wsInstance.isConnected) {
|
||||
watch(wsInstance.isConnected, (connected) => {
|
||||
isConnected.value = connected
|
||||
})
|
||||
}
|
||||
|
||||
return wsInstance
|
||||
}
|
||||
|
||||
// Responsive scaling function
|
||||
const updateScale = () => {
|
||||
const container = document.querySelector('.antrian-display-container')
|
||||
if (!container) return
|
||||
|
||||
const viewportWidth = window.innerWidth
|
||||
const viewportHeight = window.innerHeight
|
||||
const baseWidth = 1920
|
||||
const baseHeight = 1080
|
||||
|
||||
// Calculate scale factors
|
||||
const scaleX = viewportWidth / baseWidth
|
||||
const scaleY = viewportHeight / baseHeight
|
||||
|
||||
// Use smaller scale to fit both dimensions
|
||||
const scale = Math.min(scaleX, scaleY)
|
||||
|
||||
// Apply scale using CSS variable
|
||||
container.style.setProperty('--scale-factor', scale.toString())
|
||||
|
||||
// Adjust container dimensions for smaller screens
|
||||
if (viewportWidth < baseWidth || viewportHeight < baseHeight) {
|
||||
container.style.width = `${baseWidth}px`
|
||||
container.style.height = `${baseHeight}px`
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Redirect to index if klinik not found
|
||||
if (!klinikData.value) {
|
||||
@@ -378,27 +551,60 @@ onMounted(() => {
|
||||
|
||||
updateTime()
|
||||
timeInterval = setInterval(updateTime, 1000)
|
||||
|
||||
// Initialize and connect WebSocket
|
||||
if (kodeKlinik.value) {
|
||||
wsInstance = initWebSocket()
|
||||
if (wsInstance) {
|
||||
wsInstance.connect()
|
||||
console.log('🚀 WebSocket connection initiated')
|
||||
}
|
||||
}
|
||||
|
||||
// Setup responsive scaling
|
||||
updateScale()
|
||||
window.addEventListener('resize', updateScale)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (timeInterval) clearInterval(timeInterval)
|
||||
if (wsInstance) {
|
||||
wsInstance.disconnect()
|
||||
console.log('🔌 WebSocket disconnected')
|
||||
}
|
||||
window.removeEventListener('resize', updateScale)
|
||||
})
|
||||
|
||||
// Watch for clientId changes and reconnect if needed
|
||||
watch(anjunganClientId, (newClientId, oldClientId) => {
|
||||
if (newClientId && newClientId !== oldClientId && wsInstance) {
|
||||
console.log('🔄 Client ID changed, reconnecting...')
|
||||
wsInstance.disconnect()
|
||||
wsInstance = initWebSocket()
|
||||
if (wsInstance) {
|
||||
wsInstance.connect()
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.antrian-display-container {
|
||||
background: var(--color-neutral-300);
|
||||
min-height: 100vh;
|
||||
max-width: 1920px;
|
||||
max-height: 1080px;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
padding: 24px;
|
||||
font-family: 'Inter', 'Roboto', sans-serif;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 0 auto;
|
||||
box-sizing: border-box;
|
||||
|
||||
// Responsive scaling menggunakan CSS variables dan JavaScript
|
||||
transform-origin: top center;
|
||||
--scale-factor: 1;
|
||||
transform: scale(var(--scale-factor));
|
||||
}
|
||||
|
||||
/* ========== HEADER ========== */
|
||||
@@ -660,17 +866,7 @@ onUnmounted(() => {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 32px 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 16px;
|
||||
color: var(--color-neutral-600);
|
||||
margin-top: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
// Empty state removed - only show in individual columns
|
||||
|
||||
/* ========== FOOTER STATS ========== */
|
||||
.footer-stats-bar {
|
||||
@@ -745,12 +941,112 @@ onUnmounted(() => {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.ws-status-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.ws-status-indicator.ws-connected {
|
||||
background: rgba(76, 175, 80, 0.1);
|
||||
color: #4caf50;
|
||||
}
|
||||
|
||||
.ws-status-indicator.ws-disconnected {
|
||||
background: rgba(244, 67, 54, 0.1);
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.ws-status-text {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* ========== RESPONSIVE ========== */
|
||||
@media (min-width: 1920px) and (min-height: 1080px) {
|
||||
.antrian-display-container {
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
padding: 24px;
|
||||
// Base responsive rules untuk komponen individual
|
||||
@media (max-width: 1919px) or (max-height: 1079px) {
|
||||
// Adjust font sizes and spacing secara proporsional
|
||||
.display-header {
|
||||
padding: clamp(12px, 1.25vw, 24px) clamp(20px, 2.08vw, 40px);
|
||||
margin-bottom: clamp(12px, 1.85vw, 20px);
|
||||
}
|
||||
|
||||
.logo-circle {
|
||||
width: clamp(48px, 4.17vw, 80px);
|
||||
height: clamp(48px, 4.17vw, 80px);
|
||||
}
|
||||
|
||||
.hospital-name {
|
||||
font-size: clamp(28px, 2.5vw, 48px);
|
||||
}
|
||||
|
||||
.display-subtitle {
|
||||
font-size: clamp(14px, 1.04vw, 20px);
|
||||
}
|
||||
|
||||
.time-large {
|
||||
font-size: clamp(36px, 2.92vw, 56px);
|
||||
}
|
||||
|
||||
.date-small {
|
||||
font-size: clamp(12px, 0.94vw, 18px);
|
||||
}
|
||||
|
||||
.hero-call-section {
|
||||
padding: clamp(16px, 1.67vw, 32px);
|
||||
margin-bottom: clamp(12px, 1.85vw, 20px);
|
||||
}
|
||||
|
||||
.call-label {
|
||||
font-size: clamp(20px, 1.46vw, 28px);
|
||||
}
|
||||
|
||||
.call-number {
|
||||
font-size: clamp(80px, 7.29vw, 140px);
|
||||
}
|
||||
|
||||
.call-clinic {
|
||||
font-size: clamp(24px, 1.88vw, 36px);
|
||||
}
|
||||
|
||||
.clinics-grid {
|
||||
gap: clamp(8px, 0.83vw, 16px);
|
||||
margin-bottom: clamp(12px, 1.85vw, 20px);
|
||||
}
|
||||
|
||||
.clinic-header-bar {
|
||||
padding: clamp(10px, 1.04vw, 16px) clamp(12px, 1.04vw, 20px);
|
||||
}
|
||||
|
||||
.clinic-title {
|
||||
font-size: clamp(16px, 1.15vw, 22px);
|
||||
}
|
||||
|
||||
.clinic-content {
|
||||
padding: clamp(12px, 1.04vw, 20px);
|
||||
}
|
||||
|
||||
.current-number {
|
||||
font-size: clamp(56px, 4.17vw, 80px);
|
||||
}
|
||||
|
||||
.footer-stats-bar {
|
||||
padding: clamp(12px, 1.04vw, 20px) clamp(24px, 2.08vw, 40px);
|
||||
gap: clamp(16px, 1.67vw, 32px);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: clamp(28px, 2.08vw, 40px);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: clamp(11px, 0.73vw, 14px);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user