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