diff --git a/composables/useWebSocket.ts b/composables/useWebSocket.ts index e59d7d3..7a37e72 100644 --- a/composables/useWebSocket.ts +++ b/composables/useWebSocket.ts @@ -1,9 +1,9 @@ // composables/useWebSocket.ts -import { ref, computed, onUnmounted, getCurrentInstance } from 'vue' +import { ref, computed, onUnmounted, getCurrentInstance, toValue } from 'vue' export interface WebSocketConfig { - url: string - clientId: string + url: string | any + clientId: string | any onMessage?: (data: any) => void onOpen?: () => void onClose?: () => void @@ -24,8 +24,11 @@ export const useWebSocket = (config: WebSocketConfig) => { const reconnectAttempts = ref(0) const reconnectTimer = ref(null) + const currentUrl = computed(() => toValue(config.url)) + const currentClientId = computed(() => toValue(config.clientId)) + const wsUrl = computed(() => { - let baseUrl = config.url + let baseUrl = currentUrl.value // Automatically upgrade to secure protocols if caller uses HTTPS if (typeof window !== 'undefined' && window.location.protocol === 'https:') { @@ -37,21 +40,44 @@ export const useWebSocket = (config: WebSocketConfig) => { } const url = new URL(baseUrl) - url.searchParams.set('client_id', config.clientId) + url.searchParams.set('client_id', currentClientId.value) return url.toString() }) + const clearHandlers = (wsInstance: WebSocket | null) => { + if (wsInstance) { + wsInstance.onopen = null + wsInstance.onmessage = null + wsInstance.onclose = null + wsInstance.onerror = null + } + } + const connect = () => { try { - // Close existing connection if any - if (ws.value && ws.value.readyState !== WebSocket.CLOSED) { - ws.value.close() + // Clear any pending reconnect timer first + if (reconnectTimer.value) { + clearTimeout(reconnectTimer.value) + reconnectTimer.value = null } - ws.value = new WebSocket(wsUrl.value) + // Close existing connection if any, and CLEAN HANDLERS to prevent recursion + if (ws.value) { + if (ws.value.readyState !== WebSocket.CLOSED) { + console.log('🔌 Closing existing WebSocket before new connection...') + clearHandlers(ws.value) + ws.value.close() + } + ws.value = null + } + + const connectionUrl = wsUrl.value + console.log('🔌 Connecting to WebSocket:', connectionUrl) + + ws.value = new WebSocket(connectionUrl) ws.value.onopen = () => { - console.log('WebSocket connected:', config.clientId) + console.log('✅ WebSocket connected:', currentClientId.value) isConnected.value = true reconnectAttempts.value = 0 config.onOpen?.() @@ -67,16 +93,17 @@ export const useWebSocket = (config: WebSocketConfig) => { } ws.value.onclose = () => { - console.log('WebSocket closed:', config.clientId) + console.log('❌ WebSocket closed:', currentClientId.value) isConnected.value = false config.onClose?.() - // Attempt to reconnect + // Attempt to reconnect only if we didn't just reach max attempts + // The reconnectTimer is cleared in disconnect() and at start of connect() if (reconnectAttempts.value < (config.maxReconnectAttempts || 5)) { reconnectAttempts.value++ const interval = config.reconnectInterval || 3000 + console.log(`⏳ Reconnecting in ${interval}ms... Attempt ${reconnectAttempts.value}`) reconnectTimer.value = setTimeout(() => { - console.log(`Reconnecting... Attempt ${reconnectAttempts.value}`) connect() }, interval) } else { @@ -85,11 +112,11 @@ export const useWebSocket = (config: WebSocketConfig) => { } ws.value.onerror = (error) => { - console.error('WebSocket error:', error) + console.error('⚠️ WebSocket error:', error) config.onError?.(error) } } catch (error) { - console.error('Error creating WebSocket:', error) + console.error('❌ Error creating WebSocket:', error) config.onError?.(error as Event) } } @@ -100,10 +127,13 @@ export const useWebSocket = (config: WebSocketConfig) => { reconnectTimer.value = null } if (ws.value) { + console.log('🔌 Manual disconnect: Cleaning handlers and closing...') + clearHandlers(ws.value) ws.value.close() ws.value = null } isConnected.value = false + reconnectAttempts.value = 0 } const sendMessage = (message: any) => { @@ -125,7 +155,7 @@ export const useWebSocket = (config: WebSocketConfig) => { postUrl = config.fallbackPostUrl } else { // Derive post URL from WebSocket URL (legacy behavior) - let baseUrl = config.url + let baseUrl = currentUrl.value if (baseUrl.startsWith('ws://')) { baseUrl = baseUrl.replace('ws://', 'http://') } else if (baseUrl.startsWith('wss://')) { diff --git a/pages/Anjungan/Anjungan/[id].vue b/pages/Anjungan/Anjungan/[id].vue index 0d8a2db..b926e0b 100644 --- a/pages/Anjungan/Anjungan/[id].vue +++ b/pages/Anjungan/Anjungan/[id].vue @@ -1798,8 +1798,9 @@ watch( background-color: var(--color-neutral-100); border: 1px solid var(--color-neutral-300); border-radius: 8px; - overflow: hidden; + overflow-y: auto; margin-bottom: 20px; + max-height: 420px; } .doctor-info-unit { @@ -1814,9 +1815,8 @@ watch( font-size: 13px; line-height: 1.4; color: var(--color-neutral-800); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + white-space: normal; + word-wrap: break-word; flex: 1; } @@ -1852,6 +1852,10 @@ watch( grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 8px; + max-height: 540px; + overflow-y: auto; + padding-right: 4px; + align-content: start; } .doctor-button-wrapper { @@ -1924,10 +1928,6 @@ watch( white-space: normal; word-wrap: break-word; line-height: 1.3; - display: -webkit-box; - -webkit-box-orient: vertical; - overflow: hidden; - text-overflow: ellipsis; } /* Responsive untuk layar lebih kecil */ diff --git a/pages/CheckInPasien/checkIn.vue b/pages/CheckInPasien/checkIn.vue index 2e6b181..95d1e49 100644 --- a/pages/CheckInPasien/checkIn.vue +++ b/pages/CheckInPasien/checkIn.vue @@ -111,23 +111,42 @@ Preview kamera sedang berjalan + + + mdi-camera-flip +
-
- -

Memuat kamera...

-
+ >
+ + + +
+ +

Memuat kamera...

("environment"); +const isFrontCamera = computed(() => currentFacingMode.value === "user"); +const fileInput = ref(null); +const isProcessingFile = ref(false); // Daftar QR code yang sudah berhasil di-scan (untuk mencegah double antrian) const SUCCESSFUL_SCANS_KEY = "successful_qr_scans"; @@ -2124,23 +2147,15 @@ const startScanning = async () => { if (html5QrCode) { cameraReady.value = false; - // Konfigurasi kamera untuk mobile dan desktop - const cameraConfig = isMobile.value - ? { facingMode: "environment" } // Mobile: gunakan kamera belakang - : { facingMode: "user" }; // Desktop: gunakan kamera depan (webcam) + // Konfigurasi kamera dinamis + const cameraConfig = { facingMode: currentFacingMode.value }; - // Video constraints yang dioptimalkan untuk kualitas tinggi - const baseVideoConstraints = isMobile.value - ? { - facingMode: "environment", - width: { ideal: 1280, min: 640, max: 1920 }, - height: { ideal: 720, min: 480, max: 1080 }, - } - : { - facingMode: "user", - width: { ideal: 1920, min: 1280, max: 1920 }, - height: { ideal: 1080, min: 720, max: 1080 }, - }; + // Video constraints yang dioptimalkan + const baseVideoConstraints = { + facingMode: currentFacingMode.value, + width: { ideal: 1920, min: 1280 }, + height: { ideal: 1080, min: 720 }, + }; // Tambahkan advanced constraints hanya jika didukung const videoConstraints: any = { ...baseVideoConstraints }; @@ -2155,17 +2170,14 @@ const startScanning = async () => { await html5QrCode.start( cameraConfig, { - fps: 15, // Reduced FPS improved stability on mobile - qrbox: function ( - viewfinderWidth: number, - viewfinderHeight: number, - ) { + fps: 20, // Increased for better responsiveness + qrbox: (viewfinderWidth: number, viewfinderHeight: number) => { const minEdgeSize = Math.min(viewfinderWidth, viewfinderHeight); - // Use 70% of the viewfinder for the qrbox to ensure it's comfortably inside - const qrboxSize = Math.floor(minEdgeSize * 0.7); + // Set to 72% for optimal balance between target size and quiet zone + const qrboxSize = Math.floor(minEdgeSize * 0.72); return { - width: Math.max(qrboxSize, 200), - height: Math.max(qrboxSize, 200), + width: Math.max(qrboxSize, 250), + height: Math.max(qrboxSize, 250), }; }, aspectRatio: 1.0, @@ -2173,8 +2185,8 @@ const startScanning = async () => { videoConstraints: videoConstraints, rememberLastUsedCamera: true, showTorchButtonIfSupported: true, - // Tambahkan opsi untuk meningkatkan deteksi - verbose: false, // Set true untuk debugging + useBarCodeDetectorIfSupported: true, // Native scanning if available + verbose: false, // Set true for debugging }, (decodedText: string, decodedResult: any) => { // QR Code berhasil di-scan @@ -2230,23 +2242,14 @@ const startScanning = async () => { await html5QrCode.start( { facingMode: "user" }, { - fps: 30, - qrbox: function ( - viewfinderWidth: number, - viewfinderHeight: number, - ) { - const minEdgeSize = Math.min( - viewfinderWidth, - viewfinderHeight, - ); - const percentageSize = Math.floor(minEdgeSize * 0.85); - const fixedSize = 300; - const qrboxSize = Math.max(percentageSize, fixedSize); - const finalSize = Math.min(qrboxSize, minEdgeSize * 0.95); - + fps: 20, + qrbox: (viewfinderWidth: number, viewfinderHeight: number) => { + const minEdgeSize = Math.min(viewfinderWidth, viewfinderHeight); + // Optimized 72% + const qrboxSize = Math.floor(minEdgeSize * 0.72); return { - width: Math.max(finalSize, 250), - height: Math.max(finalSize, 250), + width: Math.max(qrboxSize, 250), + height: Math.max(qrboxSize, 250), }; }, aspectRatio: 1.0, @@ -2257,6 +2260,7 @@ const startScanning = async () => { height: { ideal: 720, min: 480, max: 1080 }, }, rememberLastUsedCamera: true, + useBarCodeDetectorIfSupported: true, verbose: false, }, (decodedText: string, decodedResult: any) => { @@ -2311,41 +2315,25 @@ const startScanning = async () => { await html5QrCode.start( { deviceId: { exact: deviceId } }, { - fps: 30, - qrbox: function ( - viewfinderWidth: number, - viewfinderHeight: number, - ) { - const minEdgeSize = Math.min( - viewfinderWidth, - viewfinderHeight, - ); - const percentageSize = Math.floor(minEdgeSize * 0.8); - const fixedSize = isMobile.value ? 300 : 400; - const qrboxSize = Math.max(percentageSize, fixedSize); - const finalSize = Math.min(qrboxSize, minEdgeSize * 0.8); - + fps: 20, + qrbox: (viewfinderWidth: number, viewfinderHeight: number) => { + const minEdgeSize = Math.min(viewfinderWidth, viewfinderHeight); + // Optimized 72% + const qrboxSize = Math.floor(minEdgeSize * 0.85); return { - width: Math.max(finalSize, 250), - height: Math.max(finalSize, 250), + width: Math.max(qrboxSize, 250), + height: Math.max(qrboxSize, 250), }; }, aspectRatio: 1.0, disableFlip: false, videoConstraints: { deviceId: { exact: deviceId }, - width: { - ideal: isMobile.value ? 1280 : 1920, - min: isMobile.value ? 640 : 1280, - max: isMobile.value ? 1920 : 1920, - }, - height: { - ideal: isMobile.value ? 720 : 1080, - min: isMobile.value ? 480 : 720, - max: isMobile.value ? 1080 : 1080, - }, + width: { ideal: isMobile.value ? 1280 : 1920 }, + height: { ideal: isMobile.value ? 720 : 1080 }, }, rememberLastUsedCamera: true, + useBarCodeDetectorIfSupported: true, verbose: false, }, (decodedText: string, decodedResult: any) => { @@ -2424,23 +2412,28 @@ const startScanning = async () => { const stopScanning = async () => { try { if (html5QrCode) { - // Stop scanner - if (isScanning.value) { - await html5QrCode.stop(); - await html5QrCode.clear(); - } - - // Note: Media tracks are automatically stopped when html5QrCode.stop() is called - + // Immediate state update + const scanner = html5QrCode; + html5QrCode = null; isScanning.value = false; cameraReady.value = false; + + // Stop and clear + try { + if (scanner.getState && scanner.getState() > 1) { // 1 is stopped, 2 is scanning, 3 is paused + await scanner.stop(); + } + await scanner.clear(); + } catch (err) { + console.warn("Scanner shutdown error (non-critical):", err); + } + showSnackbar("Info", "Scanner dihentikan", "info", "mdi-camera-off"); } } catch (err: any) { - console.error("Error stopping scanner:", err); + console.error("Error in stopScanning:", err); isScanning.value = false; cameraReady.value = false; - // Force cleanup html5QrCode = null; } }; @@ -2913,6 +2906,80 @@ const matchNoAntrian = (input: string, noAntrian: string): boolean => { return false; }; +// --- FUNGSI TAMBAHAN SCANNER --- + +/** + * Berpindah antara kamera depan dan belakang + */ +const switchCamera = async () => { + if (!isScanning.value) return; + + const newMode = currentFacingMode.value === "user" ? "environment" : "user"; + console.log(`Switching camera to: ${newMode}`); + + // Stop scanner dulu + if (html5QrCode) { + try { + await html5QrCode.stop(); + } catch (err) { + console.warn("Error stopping for camera switch:", err); + } + } + + // Update mode dan restart + currentFacingMode.value = newMode; + cameraReady.value = false; + await startScanning(); +}; + +/** + * Trigger pemilih file (Photo Mode) + */ +const triggerFileSelect = () => { + if (fileInput.value) { + fileInput.value.click(); + } +}; + +/** + * Handle scan dari file gambar (Photo Mode) + */ +const handleFileSelect = async (event: Event) => { + const input = event.target as HTMLInputElement; + if (!input.files || input.files.length === 0) return; + + const file = input.files[0]; + isProcessingFile.value = true; + + try { + // Pastikan scanner ter-inisialisasi + if (!html5QrCode) { + const { Html5Qrcode: Html5QrcodeClass } = await import("html5-qrcode"); + html5QrCode = new Html5QrcodeClass(qrCodeId); + } + + console.log("Processing file scan..."); + const decodedText = await html5QrCode.scanFile(file, true); + + if (decodedText) { + console.log("✅ QR Code detected from file:", decodedText); + handleQRScanSuccess(decodedText); + } + } catch (err) { + console.error("Error scanning file:", err); + showSnackbar( + "Error", + "Gagal mendeteksi QR code dari foto. Pastikan foto jelas dan terang.", + "error", + "mdi-alert-circle", + ); + } finally { + isProcessingFile.value = false; + // Reset input agar bisa pilih file yang sama lagi jika perlu + if (input) input.value = ""; + } +}; + const onDetect = async (decodedText: string) => { // Format QR Code yang didukung: // 1. Format thermal print: hanya BARCODE saja (contoh: 250811100163) @@ -5158,14 +5225,14 @@ onUnmounted(() => { height: 100% !important; border-radius: 16px; display: block !important; - object-fit: cover; + object-fit: contain; background: #000; aspect-ratio: 1 / 1; - transform: scaleX(-1); /* Mirror effect */ - -webkit-transform: scaleX(-1); /* Safari support */ - /* Reduce brightness to prevent overexposure/bloom */ - filter: brightness(0.75) contrast(1.1); - -webkit-filter: brightness(0.75) contrast(1.1); +} + +.qr-reader-wrapper.is-front :deep(video) { + transform: scaleX(-1); /* Mirror effect only for front camera */ + -webkit-transform: scaleX(-1); } .qr-reader-wrapper :deep(canvas) { @@ -5212,12 +5279,12 @@ onUnmounted(() => { width: 100% !important; height: 100% !important; aspect-ratio: 1 / 1; - object-fit: cover; - transform: scaleX(-1); /* Mirror effect */ - -webkit-transform: scaleX(-1); /* Safari support */ - /* Reduce brightness to prevent overexposure/bloom */ - filter: brightness(0.75) contrast(1.1); - -webkit-filter: brightness(0.75) contrast(1.1); + object-fit: contain; +} + +.qr-reader-wrapper.is-front :deep(#qr-reader__scan_region video) { + transform: scaleX(-1); /* Mirror effect only for front camera */ + -webkit-transform: scaleX(-1); } .scanner-status { diff --git a/stores/queueStore.js b/stores/queueStore.js index 6f1fcd6..d64b761 100644 --- a/stores/queueStore.js +++ b/stores/queueStore.js @@ -326,7 +326,7 @@ export const useQueueStore = defineStore('queue', () => { const { connect, disconnect, sendViaPost, isConnected } = useWebSocket({ url: wsBaseUrl, - clientId: wsClientId.value, + clientId: wsClientId, onOpen: () => { console.log('✅ [queueStore] WebSocket connected'); isWsConnected.value = true;