From 22d7205271d7b387ebf4cf3d75b56c639f17958a Mon Sep 17 00:00:00 2001 From: Fanrouver Date: Fri, 9 Jan 2026 08:44:27 +0700 Subject: [PATCH] update qr funtion --- env. example => .env example | 19 +- composables/useThermalPrint.ts | 44 ++- nuxt.config.ts | 25 +- pages/Anjungan/AntreanMasuk/[id].vue | 19 +- pages/Anjungan/AntrianLoket/[id].vue | 19 +- pages/CheckInPasien/checkIn.vue | 442 +++++++++++++++++---------- stores/queueStore.js | 19 +- 7 files changed, 395 insertions(+), 192 deletions(-) rename env. example => .env example (81%) diff --git a/env. example b/.env example similarity index 81% rename from env. example rename to .env example index e29da83..c39cd55 100644 --- a/env. example +++ b/.env example @@ -20,8 +20,7 @@ # You can generate one here: https://generate-secret.vercel.app/32 NUXT_AUTH_SECRET="your-super-secret-string-of-at-least-32-characters" -# The base URL of your application -# AUTH_ORIGIN="http://localhost:3000" + # Keycloak Credentials # Get these from your Keycloak client configuration @@ -32,7 +31,21 @@ NUXT_AUTH_SECRET="your-super-secret-string-of-at-least-32-characters" KEYCLOAK_CLIENT_ID="akbar-test" KEYCLOAK_CLIENT_SECRET="FDyv3UYMgJOYPnvzXVVv6diRtcgEevKg" KEYCLOAK_ISSUER="https://auth.rssa.top/realms/sandbox" +# The base URL of your application +# AUTH_ORIGIN="http://localhost:3000" # AUTH_ORIGIN="http://10.10.150.175:3001" # AUTH_ORIGIN="http://0.0.0.0:3000" # AUTH_ORIGIN="https://antrean.dev.rssa.id/ -# AUTH_ORIGIN="https://antrean.rssa.id/ \ No newline at end of file +# AUTH_ORIGIN="https://antrean.rssa.id/ + +#nuxt config.ts dev server host and port +# HOST="localhost" +# PORT=3000 +# HOST="http://10.10.150.175:3000" +# PORT=3000 +# HOST="http://0.0.0.0:3000" +# PORT=3000 +# HOST="https://antrean.dev.rssa.id/" +# PORT=3000 +# HOST="https://antrean.rssa.id/" +# PORT=3000 \ No newline at end of file diff --git a/composables/useThermalPrint.ts b/composables/useThermalPrint.ts index d177832..ee83667 100644 --- a/composables/useThermalPrint.ts +++ b/composables/useThermalPrint.ts @@ -97,9 +97,21 @@ export const useThermalPrint = () => { * Generate HTML untuk thermal print */ const generatePrintHTML = async (data: ThermalPrintData): Promise => { - // QR Code data: barcode|status (ALLOWED atau NOT_ALLOWED) - const qrData = `${data.barcode}|${data.status || 'NOT_ALLOWED'}`; + // QR Code data: hanya barcode saja (status akan dicek real-time saat check-in dari queueStore) + // Format: BARCODE (contoh: 250811100163) + // Alasan: Status bisa berubah setelah tiket di-print (dari 'menunggu' jadi 'waiting' setelah dipanggil) + // Status akan dicek langsung dari queueStore saat scan QR untuk akurasi real-time + + // Normalize barcode - pastikan tidak ada whitespace dan valid + const qrData = String(data.barcode || '').trim(); + + if (!qrData) { + throw new Error('Barcode tidak valid untuk QR code'); + } + + console.log('📱 Generating QR code for ticket with barcode:', qrData); const qrCodeImage = await generateQRCode(qrData); + console.log('✅ QR code generated successfully'); // Format nomor antrian (hilangkan bagian "| Onsite - barcode") const noAntrianDisplay = data.noAntrian.split(' |')[0]; @@ -494,20 +506,36 @@ export const useThermalPrint = () => { waktu = formatTime(new Date().toISOString()); } - // Determine status - pasien baru dari anjungan biasanya status 'menunggu' (NOT_ALLOWED) - // Hanya setelah dipanggil oleh admin loket, status menjadi 'waiting' (ALLOWED) - const status: 'ALLOWED' | 'NOT_ALLOWED' = patient.status === 'waiting' ? 'ALLOWED' : 'NOT_ALLOWED'; - + // Status tidak lagi disertakan dalam QR code + // Status akan dicek real-time dari queueStore saat scan QR + // Format QR code: hanya BARCODE saja untuk fleksibilitas status yang bisa berubah + + // Normalize barcode - pastikan format konsisten (trim whitespace) + const barcode = String(patient.barcode || '').trim(); + + if (!barcode) { + console.error('❌ Barcode pasien tidak valid untuk print ticket:', patient); + throw new Error('Barcode pasien tidak valid'); + } + + console.log('🖨️ Printing ticket for patient:', { + noAntrian: patient.noAntrian, + barcode: barcode, + status: patient.status, + klinik: patient.klinik + }); + const printData: ThermalPrintData = { noAntrian: patient.noAntrian || '', - barcode: patient.barcode || '', + barcode: barcode, klinik: patient.klinik || '', shift: patient.shift || 'Shift 1', pembayaran: patient.pembayaran || '', tanggal: tanggalKunjungan, waktu: waktu, namaDokter: patient.namaDokter || undefined, - status: status + // Status tidak digunakan lagi, hanya untuk kompatibilitas interface + status: undefined }; return await printTicket(printData); diff --git a/nuxt.config.ts b/nuxt.config.ts index 6047e62..057f3c1 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -94,10 +94,27 @@ export default defineNuxtConfig({ "~/assets/scss/main.scss", ], - devServer: { - port: 3000, - host: 'localhost' - }, + devServer: (() => { + const hostEnv = process.env.HOST || 'localhost'; + + // Parse HOST if it's a full URL (e.g., "http://10.10.150.175:3000") + let host = hostEnv; + let port = 3000; + + try { + const url = new URL(hostEnv); + host = url.hostname; + port = url.port ? parseInt(url.port, 10) : 3000; + } catch { + // If HOST is not a URL, use it as-is (e.g., "10.10.150.175" or "localhost") + host = hostEnv; + } + + return { + port: port, + host: host + }; + })(), vite: { css: { diff --git a/pages/Anjungan/AntreanMasuk/[id].vue b/pages/Anjungan/AntreanMasuk/[id].vue index 6da9a20..7f7acbf 100644 --- a/pages/Anjungan/AntreanMasuk/[id].vue +++ b/pages/Anjungan/AntreanMasuk/[id].vue @@ -179,15 +179,14 @@ const calledForCheckIn = computed(() => { // and not yet checked in (processStage still 'loket') const isCalled = p.status === 'waiting' && p.processStage === 'loket' - // Must be from anjungan (onsite registration) - const isFromAnjungan = p.registrationType === 'onsite' || - (p.noAntrian && p.noAntrian.includes('Onsite')) + // Tampilkan semua pasien dengan status 'waiting', tidak hanya dari anjungan + // Semua pasien yang sudah dipanggil oleh admin loket harus ditampilkan // Filter berdasarkan loketId (untuk saat ini hanya loket 1) // Nanti bisa ditambahkan field loketId di data pasien untuk filter yang lebih spesifik const isForThisLoket = !p.loketId || p.loketId === currentLoketId - return isCalled && isFromAnjungan && isForThisLoket + return isCalled && isForThisLoket }) // Tidak perlu sorting - tampilkan semua sesuai urutan dari store }) @@ -216,28 +215,26 @@ const statistics = computed(() => { // Ensure loketPatients is an array const patients = Array.isArray(loketPatients.value) ? loketPatients.value : [] - // Filter tickets from anjungan untuk loket ini + // Filter semua pasien untuk loket ini (tidak hanya dari anjungan) // Untuk saat ini, hanya loket 1 yang aktif - const allAnjunganTickets = patients.filter(p => { + const allLoketTickets = patients.filter(p => { if (!p) return false - const isFromAnjungan = p.registrationType === 'onsite' || - (p.noAntrian && p.noAntrian.includes('Onsite')) // Filter berdasarkan loketId (untuk saat ini hanya loket 1) const isForThisLoket = !p.loketId || p.loketId === currentLoketId - return isFromAnjungan && isForThisLoket + return isForThisLoket }) // Menunggu = belum dipanggil (status 'menunggu') - const menungguCount = allAnjunganTickets.filter(p => p && p.status === 'menunggu').length + const menungguCount = allLoketTickets.filter(p => p && p.status === 'menunggu').length // Dipanggil untuk check-in = sudah dipanggil (status 'waiting') const calledForCheckInArray = Array.isArray(calledForCheckIn.value) ? calledForCheckIn.value : [] const activeCount = calledForCheckInArray.length return { - total: allAnjunganTickets.length || 0, + total: allLoketTickets.length || 0, waiting: menungguCount || 0, // Jumlah yang masih menunggu untuk dipanggil active: activeCount || 0 // Jumlah yang sudah dipanggil dan ditampilkan di layar } diff --git a/pages/Anjungan/AntrianLoket/[id].vue b/pages/Anjungan/AntrianLoket/[id].vue index 4174401..3c8d245 100644 --- a/pages/Anjungan/AntrianLoket/[id].vue +++ b/pages/Anjungan/AntrianLoket/[id].vue @@ -139,11 +139,15 @@ const currentProcessingPatient = computed(() => { return queueStore.currentProcessingPatient?.loket || null }) -// Get all patients with processStage "loket" and filter status "di-loket" dan "pending" +// Get all patients with processStage "loket" and filter status "waiting" (sudah dipanggil), "di-loket" (sudah check-in), dan "pending" const loketPatients = computed(() => { const allPatients = queueStore.getPatientsByStage('loket').value.all - // Tampilkan antrian dengan status "di-loket" dan "pending" - return allPatients.filter(p => p.status === 'di-loket' || p.status === 'pending') + // Tampilkan antrian dengan status: + // - "waiting" (sudah dipanggil oleh admin loket, sudah muncul di AntrianLoket, bisa check-in) + // - "di-loket" (sudah check-in) + // - "pending" (pending) + // Jangan tampilkan status "menunggu" (belum dipanggil) + return allPatients.filter(p => p.status === 'waiting' || p.status === 'di-loket' || p.status === 'pending') }) // Get all lokets from loketStore @@ -164,11 +168,11 @@ const displayedLokets = computed(() => { return [] } - // Include currentProcessingPatient dalam list jika ada dan status "di-loket" atau "pending" + // Include currentProcessingPatient dalam list jika ada dan status "waiting", "di-loket", atau "pending" let allPatientsForDistribution = [...loketPatients.value] // Jika ada antrian yang sedang diproses, tambahkan ke list distribusi - if (currentProcessingPatient.value && (currentProcessingPatient.value.status === 'di-loket' || currentProcessingPatient.value.status === 'pending')) { + if (currentProcessingPatient.value && (currentProcessingPatient.value.status === 'waiting' || currentProcessingPatient.value.status === 'di-loket' || currentProcessingPatient.value.status === 'pending')) { // Pastikan currentProcessingPatient ada di list (jika belum ada) const existsInList = allPatientsForDistribution.find(p => p.no === currentProcessingPatient.value.no) if (!existsInList) { @@ -195,7 +199,7 @@ const displayedLokets = computed(() => { // Prioritas 1: Assign antrian yang sedang diproses (currentProcessingPatient) ke loket yang sesuai // Antrian yang sedang diproses HARUS muncul di loket yang dituju - if (currentProcessingPatient.value && (currentProcessingPatient.value.status === 'di-loket' || currentProcessingPatient.value.status === 'pending')) { + if (currentProcessingPatient.value && (currentProcessingPatient.value.status === 'waiting' || currentProcessingPatient.value.status === 'di-loket' || currentProcessingPatient.value.status === 'pending')) { const processingPatient = currentProcessingPatient.value const targetLoketId = processingPatient.loketId || 1 const targetLoketName = processingPatient.loket || getDefaultLoket() @@ -274,7 +278,8 @@ const displayedLokets = computed(() => { // Current called queue - antrian yang sedang diproses atau paling baru dipanggil const currentCalledQueue = computed(() => { // Prioritas 1: Antrian yang sedang diproses di AdminLoket (currentProcessingPatient) - if (currentProcessingPatient.value && (currentProcessingPatient.value.status === 'di-loket' || currentProcessingPatient.value.status === 'pending')) { + // Status "waiting", "di-loket", atau "pending" (semuanya sudah dipanggil dan muncul di AntrianLoket) + if (currentProcessingPatient.value && (currentProcessingPatient.value.status === 'waiting' || currentProcessingPatient.value.status === 'di-loket' || currentProcessingPatient.value.status === 'pending')) { // Cari loket yang dituju dari assignment antrian const loketInfo = displayedLokets.value.find(l => l.currentQueue && l.currentQueue.no === currentProcessingPatient.value.no diff --git a/pages/CheckInPasien/checkIn.vue b/pages/CheckInPasien/checkIn.vue index 4cb0871..6d05cc7 100644 --- a/pages/CheckInPasien/checkIn.vue +++ b/pages/CheckInPasien/checkIn.vue @@ -1770,10 +1770,31 @@ const handleQRScanSuccess = (decodedText: string) => { } // Cek apakah QR code sudah pernah berhasil di-scan (mencegah double antrian) + // Hanya abaikan jika benar-benar sudah berhasil check-in sebelumnya (status di-loket) if (isQRCodeAlreadyScanned(decodedText)) { - console.log('⏭️ Scan diabaikan: QR code sudah pernah berhasil di-scan'); - showSnackbar('Peringatan', 'QR Code ini sudah pernah berhasil di-scan. Tidak dapat digunakan lagi untuk mencegah double antrian.', 'warning', 'mdi-alert-circle'); - return; + // Verifikasi ulang: cek apakah pasien benar-benar sudah check-in di queueStore + const cleanInput = String(decodedText).trim(); + const patientInStore = queueStore.allPatients.find(p => { + const patientBarcode = String(p.barcode || '').trim(); + return patientBarcode === cleanInput || patientBarcode.toUpperCase() === cleanInput.toUpperCase(); + }); + + if (patientInStore && patientInStore.status === 'di-loket') { + // Pasien benar-benar sudah check-in, abaikan scan + console.log('⏭️ Scan diabaikan: QR code sudah pernah berhasil di-scan dan pasien sudah di-loket'); + showSnackbar('Peringatan', 'QR Code ini sudah pernah berhasil di-scan. Tidak dapat digunakan lagi untuk mencegah double antrian.', 'warning', 'mdi-alert-circle'); + return; + } else { + // Pasien belum benar-benar check-in, mungkin ada masalah di data sebelumnya + // Biarkan scan berlanjut untuk verifikasi ulang + console.warn('⚠️ QR code marked as scanned but patient not in di-loket status, allowing re-scan for verification'); + // Hapus dari successful scans untuk memungkinkan scan ulang + if (typeof window !== 'undefined' && successfulScans.value.has(decodedText)) { + successfulScans.value.delete(decodedText); + const scansArray = Array.from(successfulScans.value); + localStorage.setItem(SUCCESSFUL_SCANS_KEY, JSON.stringify(scansArray)); + } + } } // Debounce: cegah scan berulang dalam waktu singkat @@ -1794,13 +1815,43 @@ const handleQRScanSuccess = (decodedText: string) => { // Proses data QR try { - onDetect(decodedText); + // onDetect is async, so we need to handle it properly + onDetect(decodedText).catch((error) => { + console.error('Error processing QR data in onDetect:', error); + // Jangan simpan sebagai successful scan jika ada error + // Hanya tampilkan error message + showSnackbar('Error', `Gagal memproses data QR Code: ${error.message || 'Error tidak diketahui'}`, 'error', 'mdi-alert'); + + // Simpan ke history sebagai failed + saveToHistory({ + patientId: decodedText, + queueNumber: null, + klinikQueueNumber: null, + pembayaran: 'N/A', + status: 'failed', + checkInTime: new Date().toISOString(), + checkInDate: new Date().toISOString(), + method: 'QR Scan' + }); + }); } catch (error) { console.error('Error processing QR data:', error); - showSnackbar('Error', 'Gagal memproses data QR Code', 'error', 'mdi-alert'); + showSnackbar('Error', `Gagal memproses data QR Code: ${error instanceof Error ? error.message : 'Error tidak diketahui'}`, 'error', 'mdi-alert'); + + // Simpan ke history sebagai failed + saveToHistory({ + patientId: decodedText, + queueNumber: null, + klinikQueueNumber: null, + pembayaran: 'N/A', + status: 'failed', + checkInTime: new Date().toISOString(), + checkInDate: new Date().toISOString(), + method: 'QR Scan' + }); } - // Simpan ke localStorage untuk riwayat + // Simpan ke localStorage untuk riwayat (selalu simpan, baik berhasil atau gagal) saveScannedQRData(decodedText); }; @@ -1980,176 +2031,237 @@ onUnmounted(async () => { }); const onDetect = async (decodedText: string) => { - // Try to parse as QR code format: ID|STATUS - const parts = decodedText.split('|'); + // Format QR Code: hanya BARCODE saja (contoh: 250811100163) + // Status akan dicek real-time dari queueStore saat scan QR - if (parts.length === 2) { - // QR Code format - const [patientId, status] = parts; + console.log('🔍 onDetect called with QR data:', decodedText); + + // Clean input - remove whitespace and normalize + const cleanInput = String(decodedText).trim(); + const cleanInputUpper = cleanInput.toUpperCase(); + + console.log('🔍 Cleaned input:', cleanInput); + console.log('📊 Total patients in store:', queueStore.allPatients.length); + + // Cari pasien di queueStore berdasarkan berbagai kriteria + // Prioritas: exact barcode match (case-insensitive, whitespace-insensitive) + const foundPatient = queueStore.allPatients.find(p => { + if (!p) return false; - // Validasi format QR code - if (!patientId || !status) { - showSnackbar('Error', 'QR Code tidak valid. Format harus: ID_PASIEN|STATUS', 'error', 'mdi-close-circle'); - return; + // Normalize barcode untuk comparison (remove whitespace, case-insensitive) + const patientBarcode = String(p.barcode || '').trim(); + const patientBarcodeUpper = patientBarcode.toUpperCase(); + const patientBarcodeNormalized = patientBarcodeUpper.replace(/\s+/g, ''); + const cleanInputNormalized = cleanInputUpper.replace(/\s+/g, ''); + + // 1. Exact barcode match (case-insensitive, whitespace-insensitive) + if (patientBarcode === cleanInput || + patientBarcodeNormalized === cleanInputNormalized || + patientBarcodeUpper === cleanInputUpper) { + console.log('✅ Found by exact barcode match:', patientBarcode, '===', cleanInput); + return true; } - // Cek apakah pasien diperbolehkan check-in - const isAllowed = status === 'ALLOWED'; - - if (isAllowed) { - // Cari pasien berdasarkan barcode atau ID - const checkInResult = queueStore.checkInPatient(patientId); - - if (checkInResult.success && checkInResult.patient) { - // Simpan hasil check-in - lastCheckInResult.value = { - success: true, - patientId: checkInResult.patient.barcode, - status: 'ALLOWED' - }; - - // Simpan ke history check-in - saveToHistory({ - patientId: checkInResult.patient.barcode, - queueNumber: checkInResult.patient.noAntrian, - klinikQueueNumber: checkInResult.patient.noAntrian?.split(" |")[0] || checkInResult.patient.noAntrian, - pembayaran: checkInResult.patient.pembayaran || 'N/A', - status: 'success', - checkInTime: new Date().toISOString(), - checkInDate: new Date().toISOString(), - method: 'QR Scan' - }); - - // Simpan QR code yang berhasil di-scan - saveSuccessfulScan(decodedText); - infoMessage.value = `✅ Check-in Berhasil!\n\nPasien ${checkInResult.patient.noAntrian.split(" |")[0]} berhasil melakukan check-in.`; - infoAction.value = 'checkin'; - infoDialog.value = true; - } else { - // Simpan ke history dengan status failed jika check-in gagal - const cleanInput = String(patientId).trim().toUpperCase(); - const foundPatient = queueStore.allPatients.find(p => { - if (p.barcode === cleanInput || p.barcode === patientId) return true; - const parsedNo = parseInt(cleanInput.replace(/[^0-9]/g, '')) || parseInt(patientId); - if (!isNaN(parsedNo) && p.no === parsedNo) return true; - const noAntrianUpper = (p.noAntrian || '').toUpperCase(); - if (noAntrianUpper.includes(cleanInput) || noAntrianUpper.includes(patientId)) return true; - return false; - }); - - saveToHistory({ - patientId: patientId, - queueNumber: foundPatient?.noAntrian || null, - klinikQueueNumber: foundPatient?.noAntrian?.split(" |")[0] || null, - pembayaran: foundPatient?.pembayaran || 'N/A', - status: 'failed', - checkInTime: new Date().toISOString(), - checkInDate: new Date().toISOString(), - method: 'QR Scan' - }); - - infoMessage.value = `❌ Check-in Gagal!\n\n${checkInResult.message}`; - infoAction.value = 'checkin'; - infoDialog.value = true; - } - } else { - // Jika belum diperbolehkan, cari data pasien untuk disimpan ke history - // Cari pasien dari queueStore berdasarkan patientId/barcode - const cleanInput = String(patientId).trim().toUpperCase(); - const foundPatient = queueStore.allPatients.find(p => { - // Exact barcode match - if (p.barcode === cleanInput || p.barcode === patientId) { - return true; - } - // Try parsing as number - const parsedNo = parseInt(cleanInput.replace(/[^0-9]/g, '')) || parseInt(patientId); - if (!isNaN(parsedNo) && p.no === parsedNo) { - return true; - } - // Check if noAntrian includes the input - const noAntrianUpper = (p.noAntrian || '').toUpperCase(); - if (noAntrianUpper.includes(cleanInput) || noAntrianUpper.includes(patientId)) { - return true; - } - return false; - }); - - // Simpan ke history dengan status NOT_ALLOWED - saveToHistory({ - patientId: patientId, - queueNumber: foundPatient?.noAntrian || null, - klinikQueueNumber: foundPatient?.noAntrian?.split(" |")[0] || null, - pembayaran: foundPatient?.pembayaran || 'N/A', - status: 'NOT_ALLOWED', - checkInTime: new Date().toISOString(), - checkInDate: new Date().toISOString(), - method: 'QR Scan' - }); - - // Tampilkan pesan - lastCheckInResult.value = { - success: false, - patientId: patientId, - status: status - }; - - infoMessage.value = `⏳ Belum Diizinkan Check-in\n\nAntrean Pasien ${patientId} belum diperbolehkan check-in. Mohon menunggu hingga antrean Anda dipanggil.`; - infoAction.value = 'kembali'; - infoDialog.value = true; + // 2. Try parsing as number and match with no + const parsedNo = parseInt(cleanInput.replace(/[^0-9]/g, '')) || parseInt(decodedText); + if (!isNaN(parsedNo) && p.no === parsedNo) { + console.log('✅ Found by no:', p.no); + return true; } - } else { - // Try as barcode directly + + // 3. Check if noAntrian includes the input (case-insensitive) + const noAntrianUpper = (p.noAntrian || '').toUpperCase(); + const cleanInputUpperForSearch = cleanInputUpper.replace(/[^A-Z0-9]/g, ''); + if (noAntrianUpper.includes(cleanInputUpperForSearch) || + noAntrianUpper.includes(cleanInput) || + noAntrianUpper.includes(cleanInputNormalized)) { + console.log('✅ Found by noAntrian:', p.noAntrian); + return true; + } + + // 4. Try to extract number from noAntrian (e.g., "UM0014 | Onsite - ..." -> "0014") + const noAntrianNumber = noAntrianUpper.match(/([A-Z]+)(\d+)/); + if (noAntrianNumber) { + const extractedNumber = noAntrianNumber[2]; + const inputNumber = cleanInput.replace(/[^0-9]/g, ''); + if (inputNumber && (extractedNumber.includes(inputNumber) || inputNumber.includes(extractedNumber))) { + console.log('✅ Found by extracted number from noAntrian'); + return true; + } + } + + // 5. Check if barcode is substring or contains input (untuk handle partial match) + if (patientBarcode && cleanInput && + (patientBarcode.includes(cleanInput) || cleanInput.includes(patientBarcode) || + patientBarcodeNormalized.includes(cleanInputNormalized) || cleanInputNormalized.includes(patientBarcodeNormalized))) { + console.log('✅ Found by barcode substring match'); + return true; + } + + return false; + }); + + if (!foundPatient) { + // Pasien tidak ditemukan dengan pencarian manual + // Coba langsung menggunakan checkInPatient sebagai fallback (memiliki logika pencarian lebih lengkap) + console.warn('⚠️ Patient not found with manual search, trying checkInPatient as fallback'); const checkInResult = queueStore.checkInPatient(decodedText); if (checkInResult.success && checkInResult.patient) { + // Berhasil ditemukan via checkInPatient + console.log('✅ Patient found via checkInPatient:', checkInResult.patient); + + // Proses check-in berhasil lastCheckInResult.value = { - success: true, - patientId: checkInResult.patient.barcode, - status: 'ALLOWED' + success: true, + patientId: checkInResult.patient.barcode, + status: 'ALLOWED' }; saveToHistory({ - patientId: checkInResult.patient.barcode, - queueNumber: checkInResult.patient.noAntrian, - klinikQueueNumber: checkInResult.patient.noAntrian?.split(" |")[0] || checkInResult.patient.noAntrian, - pembayaran: checkInResult.patient.pembayaran || 'N/A', - status: 'success', - checkInTime: new Date().toISOString(), - checkInDate: new Date().toISOString(), - method: 'QR Scan' + patientId: checkInResult.patient.barcode, + queueNumber: checkInResult.patient.noAntrian, + klinikQueueNumber: checkInResult.patient.noAntrian?.split(" |")[0] || checkInResult.patient.noAntrian, + pembayaran: checkInResult.patient.pembayaran || 'N/A', + status: 'success', + checkInTime: new Date().toISOString(), + checkInDate: new Date().toISOString(), + method: 'QR Scan' }); - infoMessage.value = `✅ Check-in Berhasil!\n\nPasien ${checkInResult.patient.noAntrian.split(" |")[0]} berhasil melakukan check-in.`; + saveSuccessfulScan(decodedText); + infoMessage.value = `✅ Check-in Berhasil!\n\nPasien ${checkInResult.patient.noAntrian.split(" |")[0]} berhasil melakukan check-in dan status berubah menjadi di loket.`; infoAction.value = 'checkin'; infoDialog.value = true; + return; } else { - // Simpan ke history dengan status failed jika check-in gagal - const cleanInput = String(decodedText).trim().toUpperCase(); - const foundPatient = queueStore.allPatients.find(p => { - if (p.barcode === cleanInput || p.barcode === decodedText) return true; - const parsedNo = parseInt(cleanInput.replace(/[^0-9]/g, '')) || parseInt(decodedText); - if (!isNaN(parsedNo) && p.no === parsedNo) return true; - const noAntrianUpper = (p.noAntrian || '').toUpperCase(); - if (noAntrianUpper.includes(cleanInput) || noAntrianUpper.includes(decodedText)) return true; - return false; - }); + // Tetap tidak ditemukan + console.error('❌ Patient not found. QR data:', decodedText); + console.error('📋 Available barcodes (first 10):', queueStore.allPatients.slice(0, 10).map(p => ({ + no: p.no, + barcode: p.barcode, + noAntrian: p.noAntrian?.split(' |')[0] + }))); saveToHistory({ patientId: decodedText, - queueNumber: foundPatient?.noAntrian || null, - klinikQueueNumber: foundPatient?.noAntrian?.split(" |")[0] || null, - pembayaran: foundPatient?.pembayaran || 'N/A', + queueNumber: null, + klinikQueueNumber: null, + pembayaran: 'N/A', status: 'failed', checkInTime: new Date().toISOString(), checkInDate: new Date().toISOString(), method: 'QR Scan' }); - infoMessage.value = `❌ Check-in Gagal!\n\n${checkInResult.message}`; + infoMessage.value = `❌ Check-in Gagal!\n\nPasien tidak ditemukan di sistem.\n\nQR Code: ${decodedText}\n\nPesan: ${checkInResult.message || 'Pastikan QR code valid dan pasien sudah terdaftar di sistem.'}`; infoAction.value = 'checkin'; infoDialog.value = true; + return; } } + + // Cek status pasien REAL-TIME dari queueStore + // Validasi check-in berdasarkan status: + // - "waiting" (sudah dipanggil dan muncul di AntrianLoket) → BOLEH check-in → ubah ke "di-loket" + // - "menunggu" (belum dipanggil, belum muncul di AntrianLoket) → TIDAK BOLEH check-in + // - "di-loket" (sudah check-in) → sudah check-in sebelumnya + // - "pending" → BOLEH check-in + + const patientStatus = foundPatient.status; + + if (patientStatus === 'menunggu') { + // Status "menunggu" = belum dipanggil oleh admin loket, belum muncul di AntrianLoket + // TIDAK BOLEH check-in + saveToHistory({ + patientId: foundPatient.barcode, + queueNumber: foundPatient.noAntrian, + klinikQueueNumber: foundPatient.noAntrian?.split(" |")[0] || foundPatient.noAntrian, + pembayaran: foundPatient.pembayaran || 'N/A', + status: 'NOT_ALLOWED', + checkInTime: new Date().toISOString(), + checkInDate: new Date().toISOString(), + method: 'QR Scan' + }); + + lastCheckInResult.value = { + success: false, + patientId: foundPatient.barcode, + status: 'NOT_ALLOWED' + }; + + infoMessage.value = `⏳ Belum Diizinkan Check-in\n\nNomor Antrean ${foundPatient.noAntrian.split(" |")[0]} belum dipanggil oleh admin loket. Mohon menunggu hingga nomor antrean Anda dipanggil dan muncul di layar Antrian Loket.`; + infoAction.value = 'kembali'; + infoDialog.value = true; + return; + } + + if (patientStatus === 'di-loket') { + // Sudah check-in sebelumnya + saveToHistory({ + patientId: foundPatient.barcode, + queueNumber: foundPatient.noAntrian, + klinikQueueNumber: foundPatient.noAntrian?.split(" |")[0] || foundPatient.noAntrian, + pembayaran: foundPatient.pembayaran || 'N/A', + status: 'failed', + checkInTime: new Date().toISOString(), + checkInDate: new Date().toISOString(), + method: 'QR Scan' + }); + + infoMessage.value = `⚠️ Sudah Check-in\n\nNomor Antrean ${foundPatient.noAntrian.split(" |")[0]} sudah melakukan check-in sebelumnya.`; + infoAction.value = 'checkin'; + infoDialog.value = true; + return; + } + + // Status "waiting" atau "pending" → BOLEH check-in + // Panggil checkInPatient untuk validasi dan update status ke "di-loket" + // Gunakan barcode pasien yang ditemukan, bukan decodedText (untuk memastikan format benar) + const patientBarcodeForCheckIn = foundPatient.barcode || decodedText; + console.log('🔍 Calling checkInPatient with barcode:', patientBarcodeForCheckIn); + const checkInResult = queueStore.checkInPatient(patientBarcodeForCheckIn); + + if (checkInResult.success && checkInResult.patient) { + // Check-in berhasil + lastCheckInResult.value = { + success: true, + patientId: checkInResult.patient.barcode, + status: 'ALLOWED' + }; + + saveToHistory({ + patientId: checkInResult.patient.barcode, + queueNumber: checkInResult.patient.noAntrian, + klinikQueueNumber: checkInResult.patient.noAntrian?.split(" |")[0] || checkInResult.patient.noAntrian, + pembayaran: checkInResult.patient.pembayaran || 'N/A', + status: 'success', + checkInTime: new Date().toISOString(), + checkInDate: new Date().toISOString(), + method: 'QR Scan' + }); + + saveSuccessfulScan(decodedText); + infoMessage.value = `✅ Check-in Berhasil!\n\nPasien ${checkInResult.patient.noAntrian.split(" |")[0]} berhasil melakukan check-in dan status berubah menjadi di loket.`; + infoAction.value = 'checkin'; + infoDialog.value = true; + } else { + // Check-in gagal (misalnya validasi di checkInPatient gagal) + saveToHistory({ + patientId: foundPatient.barcode, + queueNumber: foundPatient.noAntrian, + klinikQueueNumber: foundPatient.noAntrian?.split(" |")[0] || foundPatient.noAntrian, + pembayaran: foundPatient.pembayaran || 'N/A', + status: 'failed', + checkInTime: new Date().toISOString(), + checkInDate: new Date().toISOString(), + method: 'QR Scan' + }); + + infoMessage.value = `❌ Check-in Gagal!\n\n${checkInResult.message || 'Status pasien tidak memungkinkan untuk check-in.'}`; + infoAction.value = 'checkin'; + infoDialog.value = true; + } }; const handleInfoAction = async () => { @@ -2521,7 +2633,11 @@ const clearHistory = () => { }; const openHistoryDialog = () => { + // Pastikan history dimuat ulang saat dialog dibuka loadHistory(); + // Reset filter saat membuka dialog + historySearch.value = ''; + historyStatusFilter.value = ''; historyDialog.value = true; }; @@ -2583,14 +2699,28 @@ const filteredQRHistory = computed(() => { }); const filteredHistory = computed(() => { + // Pastikan history sudah dimuat + if (checkInHistory.value.length === 0 && typeof window !== 'undefined') { + loadHistory(); + } + let filtered = [...checkInHistory.value]; + // Sort by checkInTime (terbaru di atas) + filtered.sort((a, b) => { + const timeA = new Date(a.checkInTime || a.checkInDate || 0).getTime(); + const timeB = new Date(b.checkInTime || b.checkInDate || 0).getTime(); + return timeB - timeA; // Descending order (newest first) + }); + // Filter by search if (historySearch.value) { const search = historySearch.value.toLowerCase(); filtered = filtered.filter(item => item.patientId.toLowerCase().includes(search) || - (item.queueNumber && item.queueNumber.toLowerCase().includes(search)) + (item.queueNumber && item.queueNumber.toLowerCase().includes(search)) || + (item.klinikQueueNumber && item.klinikQueueNumber.toLowerCase().includes(search)) || + (item.method && item.method.toLowerCase().includes(search)) ); } @@ -2721,12 +2851,18 @@ const statsCompleted = computed(() => { const statsWaiting = computed(() => { // Ambil data menunggu dari queueStore untuk stage 'loket' // Menghitung pasien dengan status 'menunggu' (belum dipanggil) atau 'waiting' (sudah dipanggil tapi belum check-in) - const loketPatients = queueStore.getPatientsByStage('loket'); - const menungguPatients = loketPatients.value.menunggu || []; - const waitingPatients = loketPatients.value.waiting || []; - - // Total pasien yang masih menunggu check-in (belum dipanggil + sudah dipanggil tapi belum check-in) - return menungguPatients.length + waitingPatients.length; + // Di halaman CheckInPasien, kita menghitung total yang masih menunggu check-in (belum + sudah dipanggil) + try { + const loketPatients = queueStore.getPatientsByStage('loket'); + const menungguPatients = loketPatients.value.menunggu || []; + const waitingPatients = loketPatients.value.waiting || []; + + // Total pasien yang masih menunggu check-in (belum dipanggil + sudah dipanggil tapi belum check-in) + return menungguPatients.length + waitingPatients.length; + } catch (error) { + console.error('Error calculating statsWaiting:', error); + return 0; + } }); // Load history on mount diff --git a/stores/queueStore.js b/stores/queueStore.js index 88676e1..614b597 100644 --- a/stores/queueStore.js +++ b/stores/queueStore.js @@ -935,18 +935,25 @@ export const useQueueStore = defineStore('queue', () => { status: p.status }))); - // Clean input - remove whitespace and convert to uppercase for comparison - const cleanInput = String(patientIdOrBarcode).trim().toUpperCase(); + // Clean input - remove whitespace and normalize + const cleanInput = String(patientIdOrBarcode).trim(); + const cleanInputUpper = cleanInput.toUpperCase(); // Try to find by multiple criteria: - // 1. Exact barcode match + // 1. Exact barcode match (case-insensitive, whitespace-insensitive) // 2. Parse as number and match with no // 3. Extract number from input (e.g., "UM0014" -> "0014" or "14") and match with noAntrian // 4. Check if noAntrian includes the input const patientIndex = allPatients.value.findIndex(p => { - // Exact barcode match - if (p.barcode === cleanInput || p.barcode === patientIdOrBarcode) { - console.log('✅ Found by barcode:', p.barcode); + // Normalize barcode untuk comparison + const patientBarcode = String(p.barcode || '').trim(); + const patientBarcodeUpper = patientBarcode.toUpperCase(); + + // Exact barcode match (case-insensitive, whitespace-insensitive) + if (patientBarcode === cleanInput || + patientBarcodeUpper === cleanInputUpper || + patientBarcode === patientIdOrBarcode) { + console.log('✅ Found by barcode:', patientBarcode, '===', cleanInput); return true; }