update qr funtion

This commit is contained in:
Fanrouver
2026-01-09 08:44:27 +07:00
parent 8a4bb44354
commit 22d7205271
7 changed files with 395 additions and 192 deletions
+16 -3
View File
@@ -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/
# 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
+36 -8
View File
@@ -97,9 +97,21 @@ export const useThermalPrint = () => {
* Generate HTML untuk thermal print
*/
const generatePrintHTML = async (data: ThermalPrintData): Promise<string> => {
// 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);
+21 -4
View File
@@ -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: {
+8 -11
View File
@@ -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
}
+12 -7
View File
@@ -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
+289 -153
View File
@@ -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
+13 -6
View File
@@ -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;
}