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

No files matched your search

+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
};
};