QRcode update

This commit is contained in:
Fanrouver
2026-01-14 11:04:47 +07:00
parent f3e90adea3
commit 6c80c08a83
5 changed files with 589 additions and 162 deletions
+263
View File
@@ -0,0 +1,263 @@
<template>
<table class="ticket-table" cellpadding="0" cellspacing="0">
<tr>
<td class="ticket-content">
<div class="header">
<div class="hospital-name">RSUD DR. SAIFUL ANWAR</div>
<div class="hospital-address">Jl. Jaksa Agung Suprapto No.2, Malang</div>
<div class="ticket-title">Tiket Antrean</div>
</div>
<div class="no-antrian">{{ noAntrianDisplay }}</div>
<div class="klinik-name">{{ ruangInfo || klinik }}</div>
<div class="qr-code-section">
<div class="qr-code">
<img :src="qrCodeImage" alt="QR Code" />
</div>
<div class="barcode-number">{{ barcode }}</div>
</div>
<div class="info-section">
<div class="info-row">
<div class="info-label-wrapper">
<span class="info-label-text">Tanggal</span><span class="info-colon">:</span>
</div>
<span class="info-value">{{ tanggal }}</span>
</div>
<div class="info-row">
<div class="info-label-wrapper">
<span class="info-label-text">Waktu</span><span class="info-colon">:</span>
</div>
<span class="info-value">{{ waktu }}</span>
</div>
<div class="info-row">
<div class="info-label-wrapper">
<span class="info-label-text">Shift</span><span class="info-colon">:</span>
</div>
<span class="info-value">{{ shift }}</span>
</div>
<div class="info-row">
<div class="info-label-wrapper">
<span class="info-label-text">Pembayaran</span><span class="info-colon">:</span>
</div>
<span class="info-value">{{ pembayaran }}</span>
</div>
<div v-if="namaDokter" class="info-row">
<div class="info-label-wrapper">
<span class="info-label-text">Dokter</span><span class="info-colon">:</span>
</div>
<span class="info-value">{{ namaDokter }}</span>
</div>
</div>
<div class="footer">
<div class="footer-text">Tunjukkan tiket ini saat check-in</div>
<div class="footer-text">QR Code digunakan untuk check-in pasien</div>
<div class="footer-text">Terima kasih</div>
</div>
</td>
</tr>
</table>
</template>
<script setup lang="ts">
import type { ThermalPrintData } from '@/composables/useThermalPrint';
interface Props {
data: ThermalPrintData;
qrCodeImage: string;
noAntrianDisplay: string;
ruangInfo: string;
}
const props = defineProps<Props>();
const {
barcode,
klinik,
shift,
pembayaran,
tanggal,
waktu,
namaDokter
} = props.data;
</script>
<style scoped>
.ticket-table {
width: 100%;
border-collapse: collapse;
border: 2px dashed #000;
margin: 0;
background: white;
}
.ticket-table td {
padding: 0;
border: none;
}
.ticket-content {
padding: 2mm 4mm 4mm 4mm;
text-align: center;
}
.header {
margin-bottom: 2mm;
border-bottom: 2px solid #000;
padding-bottom: 1mm;
margin-top: 0;
}
.hospital-name {
font-size: 14px;
font-weight: bold;
margin-bottom: 0.5mm;
text-transform: uppercase;
letter-spacing: 1px;
line-height: 1.0;
}
.hospital-address {
font-size: 10px;
font-weight: bold;
margin-bottom: 0.5mm;
line-height: 1.0;
}
.ticket-title {
font-size: 12px;
font-weight: bold;
margin-top: 1mm;
text-transform: uppercase;
line-height: 1.0;
}
.qr-code-section {
margin-top: 0;
margin-bottom: 0.5mm;
text-align: center;
}
.qr-code {
width: 30mm;
height: 30mm;
margin: 0 auto;
border: none;
padding: 0.5mm;
background: white;
}
.qr-code img {
width: 100%;
height: 100%;
display: block;
}
.barcode-number {
font-size: 10px;
font-weight: bold;
margin-top: -0.5mm;
word-break: break-all;
line-height: 1.0;
}
.info-section {
margin-top: 2mm;
text-align: left;
border-top: 1px dashed #000;
padding-top: 1mm;
padding-left: 3mm;
padding-right: 3mm;
}
.info-row {
margin-bottom: 0.8mm;
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 0;
line-height: 1.0;
}
.info-label-text {
font-weight: bold;
font-size: 10px;
text-align: left;
text-transform: uppercase;
word-break: keep-all;
line-height: 1.0;
}
.info-colon {
font-weight: bold;
font-size: 10px;
margin-left: auto;
padding-right: 2mm;
line-height: 1.0;
}
.info-label-wrapper {
display: flex;
width: 22mm;
flex-shrink: 0;
align-items: flex-start;
gap: 1mm;
}
.info-value {
font-size: 10px;
font-weight: bold;
text-align: right;
word-break: break-word;
line-height: 1.0;
}
.no-antrian {
font-size: 28px;
font-weight: bold;
margin: 0.3mm 0;
letter-spacing: 2px;
text-transform: uppercase;
line-height: 1.0;
}
.klinik-name {
font-size: 12px;
font-weight: bold;
margin-top: 0.3mm;
margin-bottom: 2mm;
padding-bottom: 0;
text-transform: uppercase;
overflow: visible;
line-height: 1.0;
}
.footer {
margin-top: 2mm;
padding-top: 1mm;
padding-bottom: 0;
margin-bottom: 6mm;
border-top: 2px solid #000;
font-size: 9px;
font-weight: bold;
text-align: center;
line-height: 1.0;
}
.footer-text {
margin-bottom: 0.5mm;
font-weight: bold;
line-height: 1.0;
}
.footer-text:last-child {
margin-bottom: 0;
}
</style>
+192 -104
View File
@@ -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<string> => {
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;
}
}
</style>
</head>
<body>
<div class="ticket-container">
<div class="header">
<div class="hospital-name">RSUD DR. SAIFUL ANWAR</div>
<div class="hospital-address">Jl. Jaksa Agung Suprapto No.2, Malang</div>
<div class="ticket-title">Tiket Antrian</div>
</div>
<div class="no-antrian">${noAntrianDisplay}</div>
${ruangInfo ? `<div class="klinik-name">${ruangInfo}</div>` : `<div class="klinik-name">${data.klinik}</div>`}
<div class="qr-code-section">
<div class="qr-code">
<img src="${qrCodeImage}" alt="QR Code" />
</div>
<div class="barcode-number">${data.barcode}</div>
</div>
<div class="info-section">
<div class="info-row">
<div class="info-label-wrapper">
<span class="info-label-text">Tanggal</span><span class="info-colon">:</span>
<table class="ticket-table" cellpadding="0" cellspacing="0">
<tr>
<td class="ticket-content">
<div class="header">
<div class="hospital-name">RSUD DR. SAIFUL ANWAR</div>
<div class="hospital-address">Jl. Jaksa Agung Suprapto No.2, Malang</div>
<div class="ticket-title">Tiket Antrean</div>
</div>
<span class="info-value">${data.tanggal}</span>
</div>
<div class="info-row">
<div class="info-label-wrapper">
<span class="info-label-text">Waktu</span><span class="info-colon">:</span>
<div class="no-antrian">${noAntrianDisplay}</div>
${ruangInfo ? `<div class="klinik-name">${ruangInfo}</div>` : `<div class="klinik-name">${data.klinik}</div>`}
<div class="qr-code-section">
<div class="qr-code">
<img src="${qrCodeImage}" alt="QR Code" />
</div>
<div class="barcode-number">${data.barcode}</div>
</div>
<span class="info-value">${data.waktu}</span>
</div>
<div class="info-row">
<div class="info-label-wrapper">
<span class="info-label-text">Shift</span><span class="info-colon">:</span>
<div class="info-section">
<div class="info-row">
<div class="info-label-wrapper">
<span class="info-label-text">Tanggal</span><span class="info-colon">:</span>
</div>
<span class="info-value">${data.tanggal}</span>
</div>
<div class="info-row">
<div class="info-label-wrapper">
<span class="info-label-text">Waktu</span><span class="info-colon">:</span>
</div>
<span class="info-value">${data.waktu}</span>
</div>
<div class="info-row">
<div class="info-label-wrapper">
<span class="info-label-text">Shift</span><span class="info-colon">:</span>
</div>
<span class="info-value">${data.shift}</span>
</div>
<div class="info-row">
<div class="info-label-wrapper">
<span class="info-label-text">Pembayaran</span><span class="info-colon">:</span>
</div>
<span class="info-value">${data.pembayaran}</span>
</div>
${data.namaDokter ? `
<div class="info-row">
<div class="info-label-wrapper">
<span class="info-label-text">Dokter</span><span class="info-colon">:</span>
</div>
<span class="info-value">${data.namaDokter}</span>
</div>
` : ''}
</div>
<span class="info-value">${data.shift}</span>
</div>
<div class="info-row">
<div class="info-label-wrapper">
<span class="info-label-text">Pembayaran</span><span class="info-colon">:</span>
<div class="footer">
<div class="footer-text">Tunjukkan tiket ini saat check-in</div>
<div class="footer-text">QR Code digunakan untuk check-in pasien</div>
<div class="footer-text">Terima kasih</div>
</div>
<span class="info-value">${data.pembayaran}</span>
</div>
${data.namaDokter ? `
<div class="info-row">
<div class="info-label-wrapper">
<span class="info-label-text">Dokter</span><span class="info-colon">:</span>
</div>
<span class="info-value">${data.namaDokter}</span>
</div>
` : ''}
</div>
<div class="footer">
<div class="footer-text">Tunjukkan tiket ini saat check-in</div>
<div class="footer-text">QR Code digunakan untuk check-in pasien</div>
<div class="footer-text">Terima kasih</div>
</div>
<!-- Feed lines untuk auto cutter (space setelah footer untuk cutter) -->
<div class="cut-section"></div>
<!-- ESC/POS Auto Cut Command Comment (untuk printer driver yang support) -->
<!-- ESC/POS Commands for EPSON M352A: -->
<!-- ESC i = Full Cut (0x1B 0x69) -->
<!-- ESC m = Partial Cut (0x1B 0x6D) -->
<!-- GS V 0 = Full Cut (0x1D 0x56 0x00) -->
<!-- GS V 1 = Partial Cut (0x1D 0x56 0x01) -->
</div>
<!-- Feed lines untuk auto cutter (space setelah footer untuk cutter) -->
<div class="cut-section"></div>
<!-- ESC/POS Auto Cut Command Comment (untuk printer driver yang support) -->
<!-- ESC/POS Commands for EPSON M352A: -->
<!-- ESC i = Full Cut (0x1B 0x69) -->
<!-- ESC m = Partial Cut (0x1B 0x6D) -->
<!-- GS V 0 = Full Cut (0x1D 0x56 0x00) -->
<!-- GS V 1 = Partial Cut (0x1D 0x56 0x01) -->
</td>
</tr>
</table>
</body>
</html>
`;
@@ -612,6 +699,7 @@ export const useThermalPrint = () => {
isPrinting,
printTicket,
printTicketFromPatient,
generateQRCode
generateQRCode,
generateBarcode
};
};
+57 -13
View File
@@ -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
+12 -3
View File
@@ -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
+65 -42
View File
@@ -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)