update qr funtion
This commit is contained in:
@@ -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
|
||||
@@ -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
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user