import { ref } from 'vue'; import QRCode from 'qrcode'; export interface ThermalPrintData { noAntrian: string; barcode: string; klinik: string; shift: string; pembayaran: string; tanggal: string; waktu: string; namaDokter?: string; status?: 'ALLOWED' | 'NOT_ALLOWED'; } export const useThermalPrint = () => { const isPrinting = ref(false); /** * Generate QR Code data URL */ const generateQRCode = async (data: string): Promise => { try { const qrDataUrl = await QRCode.toDataURL(data, { errorCorrectionLevel: 'M', type: 'image/png', quality: 1, margin: 2, width: 200, // Ukuran QR Code untuk thermal printer 80mm color: { dark: '#000000', light: '#FFFFFF' } }); return qrDataUrl; } catch (error) { console.error('Error generating QR Code:', error); throw error; } }; /** * Format date untuk tampilan */ const formatDate = (dateString: string): string => { try { const date = dateString ? new Date(dateString) : new Date(); if (isNaN(date.getTime())) { return new Date().toLocaleDateString('id-ID', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); } return date.toLocaleDateString('id-ID', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); } catch (error) { return new Date().toLocaleDateString('id-ID', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); } }; /** * Format time untuk tampilan */ const formatTime = (dateString: string): string => { try { const date = dateString ? new Date(dateString) : new Date(); if (isNaN(date.getTime())) { return new Date().toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' }); } return date.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' }); } catch (error) { return new Date().toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit' }); } }; /** * Generate HTML untuk thermal print */ const generatePrintHTML = async (data: ThermalPrintData): Promise => { // QR Code data: barcode|status (ALLOWED atau NOT_ALLOWED) const qrData = `${data.barcode}|${data.status || 'NOT_ALLOWED'}`; const qrCodeImage = await generateQRCode(qrData); // Format nomor antrian (hilangkan bagian "| Onsite - barcode") const noAntrianDisplay = data.noAntrian.split(' |')[0]; const html = ` Nomor Antrian - ${noAntrianDisplay}
RSUD DR. SAIFUL ANWAR
Jl. Jaksa Agung Suprapto No.2, Malang
Tiket Antrian
${noAntrianDisplay}
${data.klinik}
QR Code
${data.barcode}
Tanggal: ${data.tanggal}
Waktu: ${data.waktu}
Shift: ${data.shift}
Pembayaran: ${data.pembayaran}
${data.namaDokter ? `
Dokter: ${data.namaDokter}
` : ''}
`; return html; }; /** * Print thermal ticket */ const printTicket = async (data: ThermalPrintData): Promise => { try { isPrinting.value = true; // Generate HTML const html = await generatePrintHTML(data); // Create print window const printWindow = window.open('', '_blank', 'width=300,height=400'); if (!printWindow) { throw new Error('Tidak dapat membuka window print. Pastikan pop-up diizinkan.'); } printWindow.document.write(html); printWindow.document.close(); // Wait for images to load await new Promise((resolve) => { printWindow.onload = () => { setTimeout(() => { printWindow.focus(); // Add event listener untuk afterprint (auto cut trigger) const handleAfterPrint = () => { console.log('Print completed. Auto cutter should activate.'); console.log('Note: For EPSON M352A, ensure printer settings have auto cut enabled.'); console.log('ESC/POS Cut Command: ESC i (0x1B 0x69) for full cut'); }; printWindow.addEventListener('afterprint', handleAfterPrint); // Print ke printer default // Catatan: Window.print() akan menggunakan printer default yang sudah di-set di sistem // Jika user ingin mengubah printer, mereka perlu mengubah default printer di Windows/OS Settings terlebih dahulu printWindow.print(); // Note: Untuk printer thermal EPSON M352A, auto cutter akan otomatis aktif jika: // 1. Driver printer sudah dikonfigurasi untuk auto cut // 2. Printer settings di Windows/OS sudah enable auto cut // 3. CSS feed lines di atas sudah cukup untuk trigger auto cut // // ESC/POS Commands yang didukung: // - ESC i (0x1B 0x69): Full Cut // - ESC m (0x1B 0x6D): Partial Cut // - GS V 0 (0x1D 0x56 0x00): Full Cut // - GS V 1 (0x1D 0x56 0x01): Partial Cut // Close window after print (optional) // Delay lebih lama untuk memastikan print dan cut selesai setTimeout(() => { printWindow.removeEventListener('afterprint', handleAfterPrint); printWindow.close(); }, 1500); resolve(true); }, 500); }; }); return true; } catch (error) { console.error('Error printing ticket:', error); throw error; } finally { isPrinting.value = false; } }; /** * Print ticket dengan data dari queueStore patient object */ const printTicketFromPatient = async (patient: any): Promise => { // Determine tanggal kunjungan let tanggalKunjungan: string; if (patient.visitDate) { // Jika ada visitDate (untuk jadwal lain), gunakan itu tanggalKunjungan = formatDate(patient.visitDate); } else if (patient.visitType === 'SEKARANG' || !patient.visitType) { // Jika kunjungan hari ini tanggalKunjungan = formatDate(new Date().toISOString()); } else { // Fallback ke tanggal sekarang tanggalKunjungan = formatDate(new Date().toISOString()); } // Determine waktu let waktu: string; if (patient.createdAt) { waktu = formatTime(patient.createdAt); } else if (patient.jamPanggil) { // Gunakan jamPanggil jika ada waktu = patient.jamPanggil; } else { waktu = formatTime(new Date().toISOString()); } // Determine status - pasien baru dari anjungan biasanya status 'menunggu' (NOT_ALLOWED) // Hanya setelah dipanggil oleh admin loket, status menjadi 'waiting' (ALLOWED) const status: 'ALLOWED' | 'NOT_ALLOWED' = patient.status === 'waiting' ? 'ALLOWED' : 'NOT_ALLOWED'; const printData: ThermalPrintData = { noAntrian: patient.noAntrian || '', barcode: patient.barcode || '', klinik: patient.klinik || '', shift: patient.shift || 'Shift 1', pembayaran: patient.pembayaran || '', tanggal: tanggalKunjungan, waktu: waktu, namaDokter: patient.namaDokter || undefined, status: status }; return await printTicket(printData); }; return { isPrinting, printTicket, printTicketFromPatient, generateQRCode }; };