diff --git a/components/checkin/ThermalTicket.vue b/components/checkin/ThermalTicket.vue new file mode 100644 index 0000000..e72a23b --- /dev/null +++ b/components/checkin/ThermalTicket.vue @@ -0,0 +1,263 @@ + + + + + diff --git a/composables/useThermalPrint.ts b/composables/useThermalPrint.ts index b6d9dc9..2181fbc 100644 --- a/composables/useThermalPrint.ts +++ b/composables/useThermalPrint.ts @@ -18,17 +18,95 @@ export interface ThermalPrintData { export const useThermalPrint = () => { const isPrinting = ref(false); + /** + * Generate barcode dengan format: YYMMDD + 5 digit sequential + * Format: YY (tahun 2 digit terakhir) + MM (bulan 2 digit) + DD (tanggal 2 digit) + XXXXX (5 digit sequential) + * Contoh: 26011400001, 26011400002, dst + * Counter akan reset setiap ganti tanggal (mulai dari 00001 lagi) + * + * @param existingBarcodes - Array barcode yang sudah ada (optional, untuk validasi uniqueness) + * @returns Barcode string dengan format YYMMDDXXXXX + */ + const generateBarcode = (existingBarcodes: string[] = []): string => { + if (typeof window === 'undefined') { + // Fallback untuk SSR: gunakan counter berdasarkan existingBarcodes + const now = new Date(); + const year = String(now.getFullYear()).slice(-2); // 2 digit tahun terakhir + const month = String(now.getMonth() + 1).padStart(2, '0'); + const day = String(now.getDate()).padStart(2, '0'); + const datePrefix = `${year}${month}${day}`; // YYMMDD + + // Gunakan counter berdasarkan existingBarcodes untuk sequential + const existingCount = existingBarcodes.filter(b => b && b.startsWith(datePrefix)).length; + const counter = existingCount + 1; + const counterCode = String(counter).padStart(5, '0'); + return `${datePrefix}${counterCode}`; + } + + const now = new Date(); + const year = String(now.getFullYear()).slice(-2); // 2 digit tahun terakhir + const month = String(now.getMonth() + 1).padStart(2, '0'); // 2 digit bulan + const day = String(now.getDate()).padStart(2, '0'); // 2 digit tanggal + const datePrefix = `${year}${month}${day}`; // YYMMDD + + // Key untuk localStorage berdasarkan tanggal + const STORAGE_KEY = `barcode_counter_${datePrefix}`; + const LAST_DATE_KEY = 'barcode_last_date'; + + // Cek apakah tanggal sudah berubah (reset counter) + const lastDate = localStorage.getItem(LAST_DATE_KEY); + const currentDate = datePrefix; + + let counter = 1; // Default mulai dari 1 + + if (lastDate === currentDate) { + // Tanggal sama, lanjutkan counter dari localStorage + const storedCounter = localStorage.getItem(STORAGE_KEY); + if (storedCounter) { + counter = parseInt(storedCounter, 10) || 1; + } + } else { + // Tanggal berbeda, reset counter ke 1 + counter = 1; + // Hapus counter lama untuk tanggal sebelumnya (cleanup) + if (lastDate) { + localStorage.removeItem(`barcode_counter_${lastDate}`); + } + } + + // Generate barcode dengan counter saat ini + let barcode = `${datePrefix}${String(counter).padStart(5, '0')}`; + + // Cek apakah barcode sudah ada di existingBarcodes + let attempts = 0; + const maxAttempts = 1000; // Maksimal 1000 pasien per hari + + while (existingBarcodes.includes(barcode) && attempts < maxAttempts) { + counter++; + barcode = `${datePrefix}${String(counter).padStart(5, '0')}`; + attempts++; + } + + // Increment counter untuk next call dan simpan + counter++; + localStorage.setItem(STORAGE_KEY, String(counter)); + localStorage.setItem(LAST_DATE_KEY, currentDate); + + return barcode; + }; + /** * Generate QR Code data URL */ const generateQRCode = async (data: string): Promise => { try { const qrDataUrl = await QRCode.toDataURL(data, { - errorCorrectionLevel: 'M', + errorCorrectionLevel: 'H', // High error correction for better scanning reliability type: 'image/png', quality: 1, - margin: 2, - width: 200, // Ukuran QR Code untuk thermal printer 80mm + margin: 0.5, // Reduced margin untuk memperkecil Positioning Detection Markers + width: 100, // Ukuran QR Code untuk thermal printer 80mm + version: 3, // QR Code version 5 (37x37 modules) color: { dark: '#000000', light: '#FFFFFF' @@ -53,7 +131,7 @@ export const useThermalPrint = () => { } // Array nama hari dalam bahasa Indonesia (lowercase) - const days = ['minggu', 'senin', 'selasa', 'rabu', 'kamis', 'jumat', 'sabtu']; + const days = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu']; // Array nama bulan singkat (title case) const months = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des']; @@ -81,25 +159,20 @@ export const useThermalPrint = () => { */ const formatTime = (dateString: string): string => { try { - const date = dateString ? new Date(dateString) : new Date(); + let date = dateString ? new Date(dateString) : new Date(); if (isNaN(date.getTime())) { - return new Date().toLocaleTimeString('id-ID', { - hour: '2-digit', - minute: '2-digit', - second: '2-digit' - }); + date = new Date(); } - return date.toLocaleTimeString('id-ID', { - hour: '2-digit', - minute: '2-digit', - second: '2-digit' - }); + + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + + return `${hours}:${minutes}`; } catch (error) { - return new Date().toLocaleTimeString('id-ID', { - hour: '2-digit', - minute: '2-digit', - second: '2-digit' - }); + const date = new Date(); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + return `${hours}:${minutes}`; } }; @@ -174,13 +247,22 @@ export const useThermalPrint = () => { color: black; } - .ticket-container { + .ticket-table { width: 100%; - text-align: center; + border-collapse: collapse; border: 2px dashed #000; - padding: 2mm 4mm 4mm 4mm; + margin: 0; background: white; - margin-bottom: 0; + } + + .ticket-table td { + padding: 0; + border: none; + } + + .ticket-content { + padding: 2mm 4mm 4mm 4mm; + text-align: center; } .header { @@ -215,14 +297,14 @@ export const useThermalPrint = () => { } .qr-code-section { - margin-top: -1mm; + margin-top: 0; margin-bottom: 0.5mm; text-align: center; } .qr-code { - width: 50mm; - height: 50mm; + width: 40mm; + height: 40mm; margin: 0 auto; border: none; padding: 0.5mm; @@ -304,10 +386,10 @@ export const useThermalPrint = () => { } .klinik-name { - font-size: 14px; + font-size: 12px; font-weight: bold; margin-top: 0.3mm; - margin-bottom: 0; + margin-bottom: 2mm; padding-bottom: 0; text-transform: uppercase; overflow: visible; @@ -352,15 +434,16 @@ export const useThermalPrint = () => { padding: 2mm 4mm 6mm 4mm; } - .ticket-container { + .ticket-table { border: none; + } + + .ticket-content { padding: 2mm 4mm 4mm 4mm; - margin-bottom: 0; - margin-top: 0; } /* Feed setelah print untuk auto cutter */ - .ticket-container::after { + .ticket-table::after { content: ''; display: block; height: 10mm; @@ -384,86 +467,90 @@ export const useThermalPrint = () => { } /* Pastikan tidak ada page break di tengah content */ - .ticket-container > * { + .ticket-content > * { page-break-inside: avoid; } } -
-
-
RSUD DR. SAIFUL ANWAR
-
Jl. Jaksa Agung Suprapto No.2, Malang
-
Tiket Antrian
-
- -
${noAntrianDisplay}
- - ${ruangInfo ? `
${ruangInfo}
` : `
${data.klinik}
`} - -
-
- QR Code -
-
${data.barcode}
-
- -
-
-
- Tanggal: + + + + +
+
+
RSUD DR. SAIFUL ANWAR
+
Jl. Jaksa Agung Suprapto No.2, Malang
+
Tiket Antrean
- ${data.tanggal} - - -
-
- Waktu: + +
${noAntrianDisplay}
+ + ${ruangInfo ? `
${ruangInfo}
` : `
${data.klinik}
`} + +
+
+ QR Code +
+
${data.barcode}
- ${data.waktu} -
- -
-
- Shift: + +
+
+
+ Tanggal: +
+ ${data.tanggal} +
+ +
+
+ Waktu: +
+ ${data.waktu} +
+ +
+
+ Shift: +
+ ${data.shift} +
+ +
+
+ Pembayaran: +
+ ${data.pembayaran} +
+ + ${data.namaDokter ? ` +
+
+ Dokter: +
+ ${data.namaDokter} +
+ ` : ''}
- ${data.shift} -
- -
-
- Pembayaran: + + - ${data.pembayaran} -
- - ${data.namaDokter ? ` -
-
- Dokter: -
- ${data.namaDokter} -
- ` : ''} -
- - - - -
- - - - - - - -
+ + +
+ + + + + + + +
`; @@ -612,6 +699,7 @@ export const useThermalPrint = () => { isPrinting, printTicket, printTicketFromPatient, - generateQRCode + generateQRCode, + generateBarcode }; }; diff --git a/pages/CheckInPasien/checkIn.vue b/pages/CheckInPasien/checkIn.vue index 563d280..02bf8a5 100644 --- a/pages/CheckInPasien/checkIn.vue +++ b/pages/CheckInPasien/checkIn.vue @@ -2129,6 +2129,29 @@ onUnmounted(async () => { cameraReady.value = false; }); +// Helper function untuk extract barcode/patientId dari format QR code +// Handle format: "barcode" atau "patientId|status" (contoh: "P-00001|ALLOWED") +// Returns: { barcode: string, status?: string } +const extractQRData = (qrData: string): { barcode: string; status?: string } => { + const cleanData = String(qrData || '').trim(); + + // Check jika format "patientId|status" (untuk testing QR code) + if (cleanData.includes('|')) { + const parts = cleanData.split('|'); + const patientId = parts[0]?.trim() || ''; + const status = parts[1]?.trim(); + return { + barcode: patientId, + status: status + }; + } + + // Format thermal print: hanya barcode saja + return { + barcode: cleanData + }; +}; + // Helper function untuk extract kode dan angka dari noAntrian // Contoh: "UM0014 | Onsite - ..." -> { kode: "UM", angka: "0014" } const extractKodeAndAngka = (noAntrian: string): { kode: string; angka: string } | null => { @@ -2211,15 +2234,22 @@ const matchNoAntrian = (input: string, noAntrian: string): boolean => { }; const onDetect = async (decodedText: string) => { - // Format QR Code: hanya BARCODE saja (contoh: 250811100163) + // Format QR Code yang didukung: + // 1. Format thermal print: hanya BARCODE saja (contoh: 250811100163) + // 2. Format testing: "patientId|status" (contoh: P-00001|ALLOWED) // Status akan dicek real-time dari queueStore saat scan QR console.log('🔍 onDetect called with QR data:', decodedText); + // Extract barcode/patientId dari QR data (handle format testing dengan pipe) + const qrData = extractQRData(decodedText); + const searchBarcode = qrData.barcode; // Barcode/patientId untuk dicari di queueStore + // Clean input - remove whitespace and normalize - const cleanInput = String(decodedText).trim(); + const cleanInput = String(searchBarcode).trim(); const cleanInputUpper = cleanInput.toUpperCase(); + console.log('🔍 Extracted barcode/patientId:', searchBarcode); console.log('🔍 Cleaned input:', cleanInput); console.log('📊 Total patients in store:', queueStore.allPatients.length); @@ -2256,7 +2286,7 @@ const onDetect = async (decodedText: string) => { const inputHasCode = /^[A-Z]+/.test(cleanInput); if (!inputHasCode) { // Input hanya angka, boleh match dengan no (tapi tetap prioritas ke barcode dan noAntrian) - const parsedNo = parseInt(cleanInput.replace(/[^0-9]/g, '')) || parseInt(decodedText); + const parsedNo = parseInt(cleanInput.replace(/[^0-9]/g, '')) || parseInt(searchBarcode); if (!isNaN(parsedNo) && p.no === parsedNo) { console.log('✅ Found by no (input tanpa kode):', p.no); return true; @@ -2271,6 +2301,7 @@ const onDetect = async (decodedText: string) => { if (!foundPatient) { // Tiket belum di-generate (tidak ada di queueStore) console.error('❌ Patient not found. QR data:', decodedText); + console.error('🔍 Extracted barcode/patientId:', searchBarcode); console.error('📋 Available barcodes (first 10):', queueStore.allPatients.slice(0, 10).map(p => ({ no: p.no, barcode: p.barcode, @@ -2278,7 +2309,7 @@ const onDetect = async (decodedText: string) => { }))); saveToHistory({ - patientId: decodedText, + patientId: searchBarcode || decodedText, queueNumber: null, klinikQueueNumber: null, pembayaran: 'N/A', @@ -2288,7 +2319,7 @@ const onDetect = async (decodedText: string) => { method: 'QR Scan' }); - const errorMsg = `❌ Tiket Belum Di-generate!\n\nQR Code: ${decodedText}\n\nTiket belum terdaftar di sistem. Pastikan tiket sudah di-generate terlebih dahulu.`; + const errorMsg = `❌ Tiket Belum Di-generate!\n\nQR Code: ${decodedText}\nBarcode/Patient ID: ${searchBarcode}\n\nTiket belum terdaftar di sistem. Pastikan tiket sudah di-generate terlebih dahulu.`; infoMessage.value = errorMsg; infoAction.value = 'checkin'; infoDialog.value = true; @@ -2309,6 +2340,7 @@ const onDetect = async (decodedText: string) => { const freshPatient = queueStore.allPatients.find(p => { const patientBarcode = String(p.barcode || '').trim(); return patientBarcode === foundPatient.barcode || + patientBarcode === searchBarcode || patientBarcode === decodedText || p.no === foundPatient.no; }); @@ -2410,8 +2442,9 @@ const onDetect = async (decodedText: string) => { // Status "waiting" atau "pending" → BOLEH check-in // Panggil checkInPatient untuk validasi dan update status ke "di-loket" // Gunakan barcode pasien yang fresh, bukan decodedText (untuk memastikan format benar) - const patientBarcodeForCheckIn = freshPatient.barcode || decodedText; + const patientBarcodeForCheckIn = freshPatient.barcode || searchBarcode || decodedText; console.log('🔍 Calling checkInPatient with barcode (fresh):', patientBarcodeForCheckIn); + console.log('🔍 Original QR data:', decodedText, '| Extracted barcode:', searchBarcode); const checkInResult = queueStore.checkInPatient(patientBarcodeForCheckIn); if (checkInResult.success && checkInResult.patient) { @@ -2518,10 +2551,16 @@ const checkInManual = async () => { } const inputValue = manualInput.value.trim(); - const cleanInput = String(inputValue).trim(); + + // Extract barcode/patientId dari input (handle format testing dengan pipe) + const qrData = extractQRData(inputValue); + const searchBarcode = qrData.barcode; // Barcode/patientId untuk dicari di queueStore + + const cleanInput = String(searchBarcode).trim(); const cleanInputUpper = cleanInput.toUpperCase(); console.log('🔍 checkInManual called with:', inputValue); + console.log('🔍 Extracted barcode/patientId:', searchBarcode); console.log('🔍 Cleaned input:', cleanInput); console.log('📊 Total patients in store:', queueStore.allPatients.length); @@ -2559,7 +2598,7 @@ const checkInManual = async () => { const inputHasCode = /^[A-Z]+/.test(cleanInput); if (!inputHasCode) { // Input hanya angka, boleh match dengan no (tapi tetap prioritas ke barcode dan noAntrian) - const parsedNo = parseInt(cleanInput.replace(/[^0-9]/g, '')) || parseInt(inputValue); + const parsedNo = parseInt(cleanInput.replace(/[^0-9]/g, '')) || parseInt(searchBarcode); if (!isNaN(parsedNo) && p.no === parsedNo) { console.log('✅ Found by no (input tanpa kode):', p.no); return true; @@ -2574,6 +2613,7 @@ const checkInManual = async () => { if (!foundPatient) { // Tiket belum di-generate (tidak ada di queueStore) console.error('❌ Patient not found. Input:', inputValue); + console.error('🔍 Extracted barcode/patientId:', searchBarcode); console.error('📋 Available barcodes (first 10):', queueStore.allPatients.slice(0, 10).map(p => ({ no: p.no, barcode: p.barcode, @@ -2581,7 +2621,7 @@ const checkInManual = async () => { }))); saveToHistory({ - patientId: inputValue, + patientId: searchBarcode || inputValue, queueNumber: null, klinikQueueNumber: null, pembayaran: 'N/A', @@ -2593,7 +2633,7 @@ const checkInManual = async () => { kodeKlinik: null }); - const errorMsg = `❌ Tiket Belum Di-generate!\n\nInput: ${inputValue}\n\nTiket belum terdaftar di sistem. Pastikan tiket sudah di-generate terlebih dahulu.`; + const errorMsg = `❌ Tiket Belum Di-generate!\n\nInput: ${inputValue}\nBarcode/Patient ID: ${searchBarcode}\n\nTiket belum terdaftar di sistem. Pastikan tiket sudah di-generate terlebih dahulu.`; infoMessage.value = errorMsg; infoAction.value = 'checkin'; infoDialog.value = true; @@ -2614,6 +2654,7 @@ const checkInManual = async () => { const freshPatient = queueStore.allPatients.find(p => { const patientBarcode = String(p.barcode || '').trim(); return patientBarcode === foundPatient.barcode || + patientBarcode === searchBarcode || patientBarcode === inputValue || p.no === foundPatient.no; }); @@ -2715,8 +2756,9 @@ const checkInManual = async () => { // Status "waiting" atau "pending" → BOLEH check-in // Panggil checkInPatient untuk validasi dan update status ke "di-loket" // Gunakan barcode pasien yang fresh, bukan inputValue (untuk memastikan format benar) - const patientBarcodeForCheckIn = freshPatient.barcode || inputValue; + const patientBarcodeForCheckIn = freshPatient.barcode || searchBarcode || inputValue; console.log('🔍 Calling checkInPatient with barcode (fresh):', patientBarcodeForCheckIn); + console.log('🔍 Original input:', inputValue, '| Extracted barcode:', searchBarcode); const checkInResult = queueStore.checkInPatient(patientBarcodeForCheckIn); if (checkInResult.success && checkInResult.patient) { @@ -2825,12 +2867,14 @@ const generateQRCode = async () => { const QRCode = (await import('qrcode')).default; // Create QR code as data URL + // Konsisten dengan useThermalPrint.ts: version 3, error correction H, margin 0.5 const qrDataUrl = await QRCode.toDataURL(generatedQRData.value, { errorCorrectionLevel: 'H', // High error correction for better scanning type: 'image/png', quality: 1, - margin: 2, - width: 300, + margin: 0.5, // Reduced margin untuk memperkecil Positioning Detection Markers + width: 300, // Lebih besar untuk testing/preview (thermal print menggunakan 100) + version: 3, // QR Code version 3 (konsisten dengan useThermalPrint.ts) color: { dark: '#000000', // Black light: '#FFFFFF' // White diff --git a/server/api/queue/patients.ts.txt b/server/api/queue/patients.ts.txt index 3f7d5b1..4e22e2f 100644 --- a/server/api/queue/patients.ts.txt +++ b/server/api/queue/patients.ts.txt @@ -91,14 +91,23 @@ export default defineEventHandler(async (event: H3Event) => { }) } - // Helper function untuk generate barcode dengan format: YYMMDD + 5 digit random + // Helper function untuk generate barcode dengan format: YYMMDD + 5 digit sequential + // Format: YY (tahun 2 digit terakhir) + MM (bulan 2 digit) + DD (tanggal 2 digit) + XXXXX (5 digit sequential) + // Contoh: 26011400001, 26011400002, dst + // Counter akan reset setiap ganti tanggal (mulai dari 00001 lagi) const generateBarcode = () => { const now = new Date(); const year = String(now.getFullYear()).slice(-2); // 2 digit tahun terakhir const month = String(now.getMonth() + 1).padStart(2, '0'); // 2 digit bulan const day = String(now.getDate()).padStart(2, '0'); // 2 digit tanggal - const randomCode = String(Math.floor(Math.random() * 100000)).padStart(5, '0'); // 5 digit random - return `${year}${month}${day}${randomCode}`; + const datePrefix = `${year}${month}${day}`; // YYMMDD + + // Gunakan counter berdasarkan existing patients untuk sequential + // NOTE: Di production, gunakan database counter atau shared counter service + const existingCount = mockDB.filter(p => p.barcode && p.barcode.startsWith(datePrefix)).length; + const counter = existingCount + 1; + const counterCode = String(counter).padStart(5, '0'); // 5 digit sequential + return `${datePrefix}${counterCode}`; }; // Generate patient data diff --git a/stores/queueStore.js b/stores/queueStore.js index 526a83e..db76e5f 100644 --- a/stores/queueStore.js +++ b/stores/queueStore.js @@ -17,61 +17,84 @@ export const useQueueStore = defineStore('queue', () => { return loket1 ? loket1.namaLoket : 'Loket A'; }; - // Helper function untuk generate barcode dengan format: YYMMDD + 5 digit random - // Format: YY (tahun 2 digit) + MM (bulan 2 digit) + DD (tanggal 2 digit) + XXXXX (5 digit random) - // Contoh: 25011212345 (12 Januari 2025 dengan random 5 digit 12345) - // IMPORTANT: Memastikan barcode selalu UNIQUE meskipun reset harian - // Menggunakan timestamp milisecond + random untuk memastikan uniqueness + // Helper function untuk generate barcode dengan format: YYMMDD + 5 digit sequential + // Format: YY (tahun 2 digit terakhir) + MM (bulan 2 digit) + DD (tanggal 2 digit) + XXXXX (5 digit sequential) + // Contoh: 26011400001, 26011400002, dst + // Counter akan reset setiap ganti tanggal (mulai dari 00001 lagi) + // IMPORTANT: Memastikan barcode selalu UNIQUE dan sequential per tanggal // NOTE: allPatientsRef adalah optional parameter untuk menghindari TDZ error saat seed initialization const generateBarcode = (existingBarcodes = [], allPatientsRef = null) => { const now = new Date(); const year = String(now.getFullYear()).slice(-2); // 2 digit tahun terakhir const month = String(now.getMonth() + 1).padStart(2, '0'); // 2 digit bulan const day = String(now.getDate()).padStart(2, '0'); // 2 digit tanggal + const datePrefix = `${year}${month}${day}`; // YYMMDD - // Generate 5 digit code dari timestamp milisecond untuk memastikan uniqueness - // Ambil 5 digit terakhir dari timestamp + random untuk menghindari duplikasi - const timestamp = Date.now(); - const randomOffset = Math.floor(Math.random() * 1000); // 0-999 - const uniqueCode = String((timestamp % 100000) + randomOffset).slice(-5).padStart(5, '0'); - - // Pastikan barcode unique dengan mengecek di existingBarcodes atau allPatientsRef - let barcode = `${year}${month}${day}${uniqueCode}`; - let attempts = 0; - const maxAttempts = 100; - - // Cek apakah barcode sudah ada - // NOTE: allPatientsRef adalah optional untuk menghindari TDZ error saat seed initialization - const checkExists = (b) => { - if (existingBarcodes.includes(b)) return true; - // Hanya cek allPatientsRef jika diberikan (tidak null/undefined) - if (allPatientsRef && allPatientsRef.value && allPatientsRef.value.some(p => p.barcode === b)) return true; - return false; - }; - - // Generate ulang jika duplikat - while (checkExists(barcode) && attempts < maxAttempts) { - const newTimestamp = Date.now(); - const newRandomOffset = Math.floor(Math.random() * 1000); - const newUniqueCode = String((newTimestamp % 100000) + newRandomOffset).slice(-5).padStart(5, '0'); - barcode = `${year}${month}${day}${newUniqueCode}`; - attempts++; - } - - // Jika masih duplikat setelah maxAttempts, tambahkan counter berdasarkan existing barcodes - if (attempts >= maxAttempts) { - const datePrefix = `${year}${month}${day}`; + // Check if localStorage is available (browser environment) + if (typeof window !== 'undefined' && typeof localStorage !== 'undefined') { + // Key untuk localStorage berdasarkan tanggal + const STORAGE_KEY = `barcode_counter_${datePrefix}`; + const LAST_DATE_KEY = 'barcode_last_date'; + + // Cek apakah tanggal sudah berubah (reset counter) + const lastDate = localStorage.getItem(LAST_DATE_KEY); + const currentDate = datePrefix; + + let counter = 1; // Default mulai dari 1 + + if (lastDate === currentDate) { + // Tanggal sama, lanjutkan counter dari localStorage + const storedCounter = localStorage.getItem(STORAGE_KEY); + if (storedCounter) { + counter = parseInt(storedCounter, 10) || 1; + } + } else { + // Tanggal berbeda, reset counter ke 1 + counter = 1; + // Hapus counter lama untuk tanggal sebelumnya (cleanup) + if (lastDate) { + localStorage.removeItem(`barcode_counter_${lastDate}`); + } + } + + // Generate barcode dengan counter saat ini + let barcode = `${datePrefix}${String(counter).padStart(5, '0')}`; + + // Cek apakah barcode sudah ada di existingBarcodes atau allPatientsRef + const checkExists = (b) => { + if (existingBarcodes.includes(b)) return true; + // Hanya cek allPatientsRef jika diberikan (tidak null/undefined) + if (allPatientsRef && allPatientsRef.value && allPatientsRef.value.some(p => p.barcode === b)) return true; + return false; + }; + + let attempts = 0; + const maxAttempts = 1000; // Maksimal 1000 pasien per hari + + // Cek uniqueness dan increment jika duplikat + while (checkExists(barcode) && attempts < maxAttempts) { + counter++; + barcode = `${datePrefix}${String(counter).padStart(5, '0')}`; + attempts++; + } + + // Increment counter untuk next call dan simpan + counter++; + localStorage.setItem(STORAGE_KEY, String(counter)); + localStorage.setItem(LAST_DATE_KEY, currentDate); + + return barcode; + } else { + // Fallback untuk SSR atau environment tanpa localStorage + // Gunakan counter berdasarkan existing barcodes atau allPatientsRef const existingCount = existingBarcodes.filter(b => b && b.startsWith(datePrefix)).length; - // Hanya cek allPatientsRef jika diberikan (tidak null/undefined) const allCount = allPatientsRef && allPatientsRef.value ? allPatientsRef.value.filter(p => p.barcode && p.barcode.startsWith(datePrefix)).length : 0; const counter = Math.max(existingCount, allCount) + 1; const counterCode = String(counter).padStart(5, '0'); - barcode = `${datePrefix}${counterCode}`; + return `${datePrefix}${counterCode}`; } - - return barcode; }; // Seed data for easy reset during dev // IMPORTANT: Setiap pasien HARUS memiliki barcode UNIK untuk menghindari konflik @@ -1056,7 +1079,7 @@ export const useQueueStore = defineStore('queue', () => { ? `${String(visitDateTime.getHours()).padStart(2, "0")}:${String(visitDateTime.getMinutes()).padStart(2, "0")}` : `${String(timestamp.getHours()).padStart(2, "0")}:${String(timestamp.getMinutes()).padStart(2, "0")}`; - // Generate barcode dengan format: YYMMDD + 5 digit random + // Generate barcode dengan format: YYMMDD + 5 digit sequential const barcode = generateBarcode([], allPatients); // Tentukan apakah pasien Eksekutif/Grand Pavilion (jika ada namaDokter, berarti Eksekutif)