diff --git a/src/components/POS/PrinterConfigModal.vue b/src/components/POS/PrinterConfigModal.vue new file mode 100644 index 0000000..73647a8 --- /dev/null +++ b/src/components/POS/PrinterConfigModal.vue @@ -0,0 +1,227 @@ + + + diff --git a/src/pages/POS/Point.vue b/src/pages/POS/Point.vue index 5c71373..854ee64 100644 --- a/src/pages/POS/Point.vue +++ b/src/pages/POS/Point.vue @@ -17,6 +17,7 @@ import CheckoutModal from '@Components/POS/CheckoutModal.vue'; import ClientModal from '@Components/POS/ClientModal.vue'; import QRscan from '@Components/POS/QRscan.vue'; import SerialSelector from '@Components/POS/SerialSelector.vue'; +import PrinterConfigModal from '@Components/POS/PrinterConfigModal.vue'; /** i18n */ const { t } = useI18n(); @@ -37,6 +38,9 @@ const lastSaleData = ref(null); const showSerialSelector = ref(false); const serialSelectorProduct = ref(null); +// Estado para configuración de impresora +const showPrinterConfig = ref(false); + /** Buscador de productos */ const searcher = useSearcher({ url: apiURL('inventario'), @@ -293,14 +297,15 @@ const handleConfirmSale = async (paymentData) => { // Mostrar notificación de que se está generando el ticket window.Notify.info('Generando ticket...'); - // Generar ticket PDF con descarga automática + // Generar ticket PDF con descarga automática e impresión await ticketService.generateSaleTicket(response, { businessName: 'HIKVISION DISTRIBUIDOR', - autoDownload: true + autoDownload: true, + autoPrint: true // Activar impresión automática con QZ Tray }); // Notificación de éxito - window.Notify.success('Ticket descargado correctamente'); + window.Notify.success('Ticket generado e impreso correctamente'); } catch (ticketError) { console.error('Error generando ticket:', ticketError); window.Notify.warning('Venta registrada, pero hubo un error al generar el ticket'); @@ -394,19 +399,29 @@ onUnmounted(() => {

- +
+ + +
@@ -583,5 +598,11 @@ onUnmounted(() => { @close="closeSerialSelector" @confirm="handleSerialConfirm" /> + + + diff --git a/src/services/printService.js b/src/services/printService.js new file mode 100644 index 0000000..500d994 --- /dev/null +++ b/src/services/printService.js @@ -0,0 +1,349 @@ +/** + * Servicio de impresión usando QZ Tray + * Maneja la conexión con impresoras térmicas y de tickets + * + * Requiere que QZ Tray esté cargado globalmente (window.qz) + * Se carga automáticamente desde CDN si no está disponible + */ +class PrintService { + constructor() { + this.isConnected = false; + this.defaultPrinter = null; + this.qz = null; + this.loadQZ(); + } + + /** + * Cargar QZ Tray desde CDN si no está disponible + */ + async loadQZ() { + // Si ya está cargado globalmente, usarlo + if (window.qz) { + this.qz = window.qz; + return; + } + + // Cargar desde CDN + try { + await this.loadScript('https://cdn.jsdelivr.net/npm/qz-tray@2.2/qz-tray.min.js'); + this.qz = window.qz; + console.log('QZ Tray cargado desde CDN'); + } catch (error) { + console.error('Error cargando QZ Tray:', error); + } + } + + /** + * Cargar script dinámicamente + */ + loadScript(src) { + return new Promise((resolve, reject) => { + // Verificar si ya está cargado + if (document.querySelector(`script[src="${src}"]`)) { + resolve(); + return; + } + + const script = document.createElement('script'); + script.src = src; + script.onload = resolve; + script.onerror = reject; + document.head.appendChild(script); + }); + } + + /** + * Conectar con QZ Tray + * @returns {Promise} + */ + async connect() { + try { + // Asegurar que QZ esté cargado + if (!this.qz) { + await this.loadQZ(); + } + + if (!this.qz) { + throw new Error('QZ Tray no se pudo cargar. Verifica tu conexión a internet.'); + } + + if (this.isConnected) { + return true; + } + + // Verificar si QZ Tray está corriendo + if (!this.qz.websocket.isActive()) { + await this.qz.websocket.connect(); + } + + this.isConnected = true; + console.log('QZ Tray conectado exitosamente'); + return true; + } catch (error) { + console.error('Error conectando con QZ Tray:', error); + this.isConnected = false; + + // Mensaje de error más amigable + if (error.message?.includes('Unable to establish connection')) { + throw new Error('QZ Tray no está ejecutándose. Por favor, inicia la aplicación QZ Tray.'); + } + + throw error; + } + } + + /** + * Desconectar de QZ Tray + */ + async disconnect() { + try { + if (this.qz && this.qz.websocket.isActive()) { + await this.qz.websocket.disconnect(); + } + this.isConnected = false; + console.log('QZ Tray desconectado'); + } catch (error) { + console.error('Error desconectando QZ Tray:', error); + } + } + + /** + * Obtener lista de impresoras disponibles + * @returns {Promise} + */ + async getPrinters() { + try { + await this.connect(); + const printers = await this.qz.printers.find(); + return printers; + } catch (error) { + console.error('Error obteniendo impresoras:', error); + throw error; + } + } + + /** + * Obtener la impresora predeterminada + * @returns {Promise} + */ + async getDefaultPrinter() { + try { + await this.connect(); + const printer = await this.qz.printers.getDefault(); + this.defaultPrinter = printer; + return printer; + } catch (error) { + console.error('Error obteniendo impresora predeterminada:', error); + throw error; + } + } + + /** + * Establecer impresora predeterminada + * @param {string} printerName - Nombre de la impresora + */ + setDefaultPrinter(printerName) { + this.defaultPrinter = printerName; + // Guardar en localStorage para persistencia + localStorage.setItem('pos_default_printer', printerName); + } + + /** + * Obtener impresora guardada en localStorage + * @returns {string|null} + */ + getSavedPrinter() { + return localStorage.getItem('pos_default_printer'); + } + + /** + * Imprimir un PDF desde base64 + * @param {string} base64Data - PDF en base64 o data URI + * @param {string} printerName - Nombre de la impresora (opcional) + * @returns {Promise} + */ + async printPDF(base64Data, printerName = null) { + try { + await this.connect(); + + const printer = printerName || this.getSavedPrinter() || await this.getDefaultPrinter(); + + const config = this.qz.configs.create(printer, { + scaleContent: false, + rasterize: true + }); + + // Asegurar que tenemos el data URI completo + let pdfDataUri = base64Data; + if (!pdfDataUri.startsWith('data:')) { + pdfDataUri = `data:application/pdf;base64,${base64Data}`; + } + + const data = [ + { + type: 'pixel', + format: 'pdf', + flavor: 'base64', + data: pdfDataUri + } + ]; + + await this.qz.print(config, data); + console.log('Impresión enviada exitosamente'); + return true; + } catch (error) { + console.error('Error imprimiendo PDF:', error); + + // Si falla con PDF, mostrar error descriptivo + if (error.message?.includes('parse')) { + throw new Error('La impresora no puede procesar PDFs. Verifica la configuración de la impresora.'); + } + + throw error; + } + } + + /** + * Imprimir usando comandos ESC/POS (para impresoras térmicas) + * @param {Array} commands - Array de comandos ESC/POS + * @param {string} printerName - Nombre de la impresora (opcional) + * @returns {Promise} + */ + async printRaw(commands, printerName = null) { + try { + await this.connect(); + + const printer = printerName || this.getSavedPrinter() || await this.getDefaultPrinter(); + + const config = this.qz.configs.create(printer); + + const data = [ + { + type: 'raw', + format: 'command', + data: commands + } + ]; + + await this.qz.print(config, data); + console.log('Impresión enviada exitosamente'); + return true; + } catch (error) { + console.error('Error imprimiendo comandos raw:', error); + throw error; + } + } + + /** + * Imprimir HTML (convertido a imagen) + * @param {string} html - HTML a imprimir + * @param {string} printerName - Nombre de la impresora (opcional) + * @returns {Promise} + */ + async printHTML(html, printerName = null) { + try { + await this.connect(); + + const printer = printerName || this.getSavedPrinter() || await this.getDefaultPrinter(); + + const config = this.qz.configs.create(printer); + + const data = [ + { + type: 'pixel', + format: 'html', + flavor: 'plain', + data: html + } + ]; + + await this.qz.print(config, data); + console.log('Impresión enviada exitosamente'); + return true; + } catch (error) { + console.error('Error imprimiendo HTML:', error); + throw error; + } + } + + /** + * Verificar si QZ Tray está disponible y conectado + * @returns {boolean} + */ + isAvailable() { + return this.qz && this.qz.websocket.isActive(); + } + + /** + * Imprimir PDF usando window.print() (fallback) + * Abre el PDF en una nueva ventana e invoca el diálogo de impresión + * @param {string} pdfDataUri - PDF como data URI + * @returns {Promise} + */ + async printPDFWithDialog(pdfDataUri) { + return new Promise((resolve, reject) => { + try { + // Asegurar que tenemos el data URI completo + let dataUri = pdfDataUri; + if (!dataUri.startsWith('data:')) { + dataUri = `data:application/pdf;base64,${pdfDataUri}`; + } + + // Convertir data URI a Blob + const byteString = atob(dataUri.split(',')[1]); + const mimeString = dataUri.split(',')[0].split(':')[1].split(';')[0]; + const ab = new ArrayBuffer(byteString.length); + const ia = new Uint8Array(ab); + + for (let i = 0; i < byteString.length; i++) { + ia[i] = byteString.charCodeAt(i); + } + + const blob = new Blob([ab], { type: mimeString }); + const blobUrl = URL.createObjectURL(blob); + + // Abrir ventana nueva con el PDF + const printWindow = window.open(blobUrl, '_blank', 'width=800,height=600'); + + if (!printWindow) { + URL.revokeObjectURL(blobUrl); + reject(new Error('No se pudo abrir la ventana de impresión. Verifica que los popups estén permitidos.')); + return; + } + + // Esperar a que cargue y luego imprimir + printWindow.onload = () => { + setTimeout(() => { + try { + printWindow.print(); + + // Cerrar ventana después de imprimir (opcional) + setTimeout(() => { + printWindow.close(); + URL.revokeObjectURL(blobUrl); + resolve(); + }, 500); + } catch (error) { + printWindow.close(); + URL.revokeObjectURL(blobUrl); + reject(error); + } + }, 250); + }; + + printWindow.onerror = () => { + printWindow.close(); + URL.revokeObjectURL(blobUrl); + reject(new Error('Error cargando el PDF en la ventana')); + }; + } catch (error) { + reject(error); + } + }); + } +} + +// Exportar instancia singleton +const printService = new PrintService(); + +export default printService; diff --git a/src/services/ticketService.js b/src/services/ticketService.js index 910512c..ace083c 100644 --- a/src/services/ticketService.js +++ b/src/services/ticketService.js @@ -1,6 +1,7 @@ import jsPDF from 'jspdf'; import QRCode from 'qrcode'; import { formatMoney, PAYMENT_METHODS } from '@/utils/formatters'; +import printService from '@Services/printService'; /** * Servicio para generar tickets de venta en formato PDF @@ -22,11 +23,16 @@ const ticketService = { * Generar ticket de venta * @param {Object} saleData - Datos de la venta * @param {Object} options - Opciones de configuración + * @param {boolean} options.autoDownload - Descargar automáticamente el PDF + * @param {boolean} options.autoPrint - Imprimir automáticamente con QZ Tray + * @param {string} options.printerName - Nombre de la impresora (opcional) */ async generateSaleTicket(saleData, options = {}) { const { businessName = 'HIKVISION DISTRIBUIDOR', - autoDownload = true + autoDownload = true, + autoPrint = false, + printerName = null } = options; // Detectar ubicación del usuario @@ -356,10 +362,34 @@ const ticketService = { if (autoDownload) { const fileName = `ticket-${folio}.pdf`; doc.save(fileName); - return doc; - } else { - return doc; } + + // Imprimir automáticamente si se solicita + if (autoPrint) { + try { + // Convertir PDF a base64 data URI + const pdfBase64 = doc.output('datauristring'); + + // Intentar imprimir con QZ Tray + try { + await printService.printPDF(pdfBase64, printerName); + console.log('Ticket enviado a la impresora con QZ Tray'); + } catch (qzError) { + console.warn('QZ Tray falló, usando diálogo de impresión:', qzError.message); + + // Fallback: usar window.print() + await printService.printPDFWithDialog(pdfBase64); + window.Notify.info('Se abrió el diálogo de impresión'); + } + } catch (error) { + console.error('Error imprimiendo ticket:', error); + + // Mostrar error al usuario + window.Notify.warning('No se pudo imprimir automáticamente. Usa el PDF descargado.'); + } + } + + return doc; }, /**