import { ref } from 'vue'; import QRCode from 'qrcode'; export interface ThermalPrintData { noAntrian: string; barcode: string; klinik: string; ruang?: string; nomorRuang?: string; shift: string; pembayaran: string; tanggal: string; waktu: string; namaDokter?: string; status?: 'ALLOWED' | 'NOT_ALLOWED'; } export const useThermalPrint = () => { const isPrinting = ref(false); // NOTE: generateBarcode telah dihapus dari useThermalPrint // Gunakan generateBarcode dari queueStore.js untuk konsistensi // Semua barcode generation harus melalui queueStore.generateBarcode() /** * Generate QR Code data URL */ const generateQRCode = async (data: string): Promise => { try { const qrDataUrl = await QRCode.toDataURL(data, { errorCorrectionLevel: 'H', // High error correction for better scanning reliability type: 'image/png', quality: 1, 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' } }); return qrDataUrl; } catch (error) { console.error('Error generating QR Code:', error); throw error; } }; /** * Format date untuk tampilan * Format: senin, 12 Jan 2026 */ const formatDate = (dateString: string): string => { try { let date = dateString ? new Date(dateString) : new Date(); if (isNaN(date.getTime())) { date = new Date(); } // Array nama hari dalam bahasa Indonesia (lowercase) 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']; const dayName = days[date.getDay()]; const day = date.getDate(); const month = months[date.getMonth()]; const year = date.getFullYear(); // Tahun 4 digit return `${dayName}, ${day} ${month} ${year}`; } catch (error) { // Fallback jika error const date = new Date(); const days = ['minggu', 'senin', 'selasa', 'rabu', 'kamis', 'jumat', 'sabtu']; const months = ['Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des']; const dayName = days[date.getDay()]; const day = date.getDate(); const month = months[date.getMonth()]; const year = date.getFullYear(); return `${dayName}, ${day} ${month} ${year}`; } }; /** * Format time untuk tampilan */ const formatTime = (dateString: string): string => { try { let date = dateString ? new Date(dateString) : new Date(); if (isNaN(date.getTime())) { date = new Date(); } const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); return `${hours}:${minutes}`; } catch (error) { const date = new Date(); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); return `${hours}:${minutes}`; } }; /** * Generate HTML untuk thermal print */ const generatePrintHTML = async (data: ThermalPrintData): Promise => { // QR Code data: hanya barcode saja (status akan dicek real-time saat check-in dari queueStore) // Format: BARCODE (contoh: 250811100163) // Alasan: Status bisa berubah setelah tiket di-print (dari 'menunggu' jadi 'waiting' setelah dipanggil) // Status akan dicek langsung dari queueStore saat scan QR untuk akurasi real-time // Normalize barcode - pastikan tidak ada whitespace dan valid const qrData = String(data.barcode || '').trim(); if (!qrData) { throw new Error('Barcode tidak valid untuk QR code'); } console.log('📱 Generating QR code for ticket with barcode:', qrData); const qrCodeImage = await generateQRCode(qrData); console.log('✅ QR code generated successfully'); // Format nomor antrian (hilangkan bagian "| Onsite - barcode") const noAntrianDisplay = data.noAntrian.split(' |')[0]; // Format informasi ruang: "Poli Anak - Ruang A" let ruangInfo = ''; if (data.klinik && data.nomorRuang) { // Konversi nomor ruang ke abjad (1 = A, 2 = B, 3 = C, dst) const ruangNumber = parseInt(data.nomorRuang) || 1; const ruangLetter = String.fromCharCode(64 + ruangNumber); // 64 = '@', 65 = 'A', 66 = 'B', dst ruangInfo = `${data.klinik} - Ruang ${ruangLetter}`; } else if (data.klinik && data.ruang) { // Fallback jika hanya ada nama ruang ruangInfo = `${data.klinik} - ${data.ruang}`; } else if (data.klinik) { // Hanya klinik jika tidak ada info ruang ruangInfo = data.klinik; } const html = ` Nomor Antrian - ${noAntrianDisplay}
RSUD DR. SAIFUL ANWAR
Jl. Jaksa Agung Suprapto No.2, Malang
Tiket Antrean
${noAntrianDisplay}
${ruangInfo ? `
${ruangInfo}
` : `
${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()); } // Status tidak lagi disertakan dalam QR code // Status akan dicek real-time dari queueStore saat scan QR // Format QR code: hanya BARCODE saja untuk fleksibilitas status yang bisa berubah // Normalize barcode - pastikan format konsisten (trim whitespace) const barcode = String(patient.barcode || '').trim(); if (!barcode) { console.error('❌ Barcode pasien tidak valid untuk print ticket:', patient); throw new Error('Barcode pasien tidak valid'); } console.log('🖨️ Printing ticket for patient:', { noAntrian: patient.noAntrian, barcode: barcode, status: patient.status, klinik: patient.klinik }); const printData: ThermalPrintData = { noAntrian: patient.noAntrian || '', barcode: barcode, klinik: patient.klinik || '', ruang: patient.ruang || undefined, nomorRuang: patient.nomorRuang || undefined, shift: patient.shift || 'Shift 1', pembayaran: patient.pembayaran || '', tanggal: tanggalKunjungan, waktu: waktu, namaDokter: patient.namaDokter || undefined, // Status tidak digunakan lagi, hanya untuk kompatibilitas interface status: undefined }; return await printTicket(printData); }; return { isPrinting, printTicket, printTicketFromPatient, generateQRCode }; };