diff --git a/.env.example b/.env.example index 2e4ec3a..adcc430 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,12 @@ VITE_APP_API_SECURE=false VITE_MICROSERVICE_STOCK=http://localhost:3000/api VITE_APP_NOTIFICATIONS=false +# Información del Negocio (para tickets) +VITE_BUSINESS_CITY=Ciudad +VITE_BUSINESS_STATE=Estado +VITE_BUSINESS_COUNTRY=México +VITE_BUSINESS_PHONE=Tel: (52) 0000-0000 + VITE_REVERB_APP_ID= VITE_REVERB_APP_KEY= VITE_REVERB_APP_SECRET= diff --git a/src/components/POS/PrinterConfigModal.vue b/src/components/POS/PrinterConfigModal.vue deleted file mode 100644 index 73647a8..0000000 --- a/src/components/POS/PrinterConfigModal.vue +++ /dev/null @@ -1,227 +0,0 @@ - - - diff --git a/src/components/POS/UploadFiles.vue b/src/components/POS/UploadFiles.vue new file mode 100644 index 0000000..301738c --- /dev/null +++ b/src/components/POS/UploadFiles.vue @@ -0,0 +1,346 @@ + + + + + diff --git a/src/pages/POS/Clients/BillingRequestDetailModal.vue b/src/pages/POS/Clients/BillingRequestDetailModal.vue index 2849442..5dc81e0 100644 --- a/src/pages/POS/Clients/BillingRequestDetailModal.vue +++ b/src/pages/POS/Clients/BillingRequestDetailModal.vue @@ -6,6 +6,7 @@ import { formatCurrency, formatDate } from '@/utils/formatters'; import Modal from '@Holos/Modal.vue'; import GoogleIcon from '@Shared/GoogleIcon.vue'; import Input from '@Holos/Form/Input.vue'; +import UploadFiles from '@Components/POS/UploadFiles.vue'; /** Props */ const props = defineProps({ @@ -22,6 +23,7 @@ const emit = defineEmits(['close', 'refresh']); const processing = ref(false); const showProcessModal = ref(false); const showRejectModal = ref(false); +const showUploadModal = ref(false); const processForm = useForm({ notes: '' @@ -90,6 +92,14 @@ const openRejectModal = () => { showRejectModal.value = true; }; +const openUploadModal = () => { + showUploadModal.value = true; +}; + +const closeUploadModal = () => { + showUploadModal.value = false; +}; + const submitProcess = () => { processing.value = true; processForm.put(apiURL(`invoice-requests/${props.request.id}/process`), { @@ -209,6 +219,87 @@ const submitReject = () => { + +
+
+ +

+ Información del CFDI +

+
+ + +
+ +
+

+ {{ request.cfdi_uuid }} +

+ +
+
+ + +
+ + +
+
+
@@ -389,6 +480,15 @@ const submitReject = () => { Rechazar +
+ + diff --git a/src/pages/POS/Clients/BillingRequests.vue b/src/pages/POS/Clients/BillingRequests.vue index 23cf2f3..8eeeabf 100644 --- a/src/pages/POS/Clients/BillingRequests.vue +++ b/src/pages/POS/Clients/BillingRequests.vue @@ -198,6 +198,8 @@ onMounted(() => { RFC TOTAL ESTADO + UUID CFDI + ARCHIVOS FECHA SOLICITUD ACCIONES @@ -246,6 +248,61 @@ onMounted(() => { + + +
+

+ {{ request.cfdi_uuid }} +

+
+ + Sin UUID + + + + + +
+ + + + +
+ +
+ + + + + +
+ +
+
+ +

{{ formatDate(request.requested_at) }} @@ -268,7 +325,7 @@ onMounted(() => { diff --git a/src/services/printService.js b/src/services/printService.js index 46c7b22..bc38ebb 100644 --- a/src/services/printService.js +++ b/src/services/printService.js @@ -1,286 +1,15 @@ /** - * 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 + * Servicio de impresión simplificado + * Usa el diálogo de impresión nativo del navegador (window.print) */ 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) + * Imprimir PDF usando window.print() + * Abre el PDF en una nueva ventana e invoca el diálogo de impresión del navegador + * @param {string} pdfDataUri - PDF como data URI o base64 * @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) { + async printPDF(pdfDataUri) { return new Promise((resolve, reject) => { try { // Asegurar que tenemos el data URI completo @@ -317,7 +46,7 @@ class PrintService { try { printWindow.print(); - // Cerrar ventana después de imprimir (opcional) + // Cerrar ventana después de imprimir setTimeout(() => { printWindow.close(); URL.revokeObjectURL(blobUrl); @@ -341,46 +70,6 @@ class PrintService { } }); } - - /** - * Imprimir imagen base64 - * @param {string} base64Image - Imagen en base64 (png, jpg) - * @param {string} printerName - Nombre de la impresora (opcional) - * @returns {Promise} - */ - async printImage(base64Image, printerName = null) { - try { - await this.connect(); - - const printer = printerName || this.getSavedPrinter() || await this.getDefaultPrinter(); - - const config = this.qz.configs.create(printer, { - scaleContent: true - }); - - // Asegurar data URI - let imgData = base64Image; - if (!imgData.startsWith('data:image')) { - imgData = `data:image/png;base64,${base64Image}`; - } - - const data = [ - { - type: 'pixel', - format: 'image', - flavor: 'base64', - data: imgData - } - ]; - - await this.qz.print(config, data); - console.log('Imagen impresa exitosamente'); - return true; - } catch (error) { - console.error('Error imprimiendo imagen:', error); - throw error; - } - } } // Exportar instancia singleton diff --git a/src/services/ticketService.js b/src/services/ticketService.js index e18a399..80db1e6 100644 --- a/src/services/ticketService.js +++ b/src/services/ticketService.js @@ -3,30 +3,6 @@ import QRCode from 'qrcode'; import { formatMoney, PAYMENT_METHODS } from '@/utils/formatters'; import printService from '@Services/printService'; -/** - * Cargar PDF.js dinámicamente desde CDN - */ -async function loadPdfJs() { - if (window.pdfjsLib) return window.pdfjsLib; - - console.log('Cargando PDF.js desde CDN...'); - - // Cargar script principal - await new Promise((resolve, reject) => { - const script = document.createElement('script'); - script.src = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js'; - script.integrity = 'sha512-q+GRHzvH4z5fXN5WjM8oKGTf4PxcX1QzGjZfrFqVBqAdgEecT0kFvvn7uZ2+GL3LMDM9M79cfsREi+T17J9wMA=='; - script.crossOrigin = 'anonymous'; - script.onload = resolve; - script.onerror = reject; - document.head.appendChild(script); - }); - - // Configurar worker - window.pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; - return window.pdfjsLib; -} - /** * Servicio para generar tickets de venta en formato PDF */ @@ -37,9 +13,10 @@ const ticketService = { */ async getUserLocation() { return { - city: import.meta.env.VITE_BUSINESS_CITY, - state: import.meta.env.VITE_BUSINESS_STATE, - country: import.meta.env.VITE_BUSINESS_COUNTRY + city: import.meta.env.VITE_BUSINESS_CITY || 'Ciudad', + state: import.meta.env.VITE_BUSINESS_STATE || 'Estado', + country: import.meta.env.VITE_BUSINESS_COUNTRY || 'México', + phone: import.meta.env.VITE_BUSINESS_PHONE || 'Tel: (52) 0000-0000' }; }, @@ -48,21 +25,19 @@ const ticketService = { * @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) + * @param {boolean} options.autoPrint - Imprimir automáticamente (abre diálogo del navegador) */ async generateSaleTicket(saleData, options = {}) { const { businessName = 'HIKVISION DISTRIBUIDOR', autoDownload = true, - autoPrint = false, - printerName = null + autoPrint = false } = options; // Detectar ubicación del usuario const location = await this.getUserLocation(); const businessAddress = `${location.city}, ${location.state}, ${location.country}`; - const businessPhone = 'Tel: (52) 0000-0000'; + const businessPhone = location.phone; // Crear documento PDF - Ticket térmico 80mm de ancho const doc = new jsPDF({ @@ -391,70 +366,16 @@ const ticketService = { // Imprimir automáticamente si se solicita if (autoPrint) { try { - // Intentar convertir PDF a Imagen y usar QZ Tray (Mejor compatibilidad con térmicas) - try { - // Obtener Blob del PDF - const pdfBlob = doc.output('blob'); - const pdfUrl = URL.createObjectURL(pdfBlob); - - // Cargar librería PDF.js - const pdfjs = await loadPdfJs(); - - // Cargar documento - const pdf = await pdfjs.getDocument(pdfUrl).promise; - const page = await pdf.getPage(1); // Página 1 - - // Configurar escala (2.0 da buena nitidez para tickets) - const scale = 2.0; - const viewport = page.getViewport({ scale }); - - // Crear canvas temporal - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d'); - canvas.height = viewport.height; - canvas.width = viewport.width; - - // Renderizar PDF en Canvas - await page.render({ - canvasContext: context, - viewport: viewport - }).promise; - - // Convertir Canvas a Imagen Base64 - const imgData = canvas.toDataURL('image/png'); - - // Enviar imagen a QZ Tray - await printService.printImage(imgData, printerName); - - console.log('Ticket impreso como imagen con QZ Tray'); - - // Limpieza - URL.revokeObjectURL(pdfUrl); - canvas.remove(); - - } catch (imgError) { - console.warn('Falló impresión como imagen, intentando método nativo PDF:', imgError); - - // Fallback: Intentar mandar el PDF directo (puede fallar en térmicas) o usar diálogo - // Convertir PDF a base64 data URI - const pdfBase64 = doc.output('datauristring'); - - // Intentar QZ Tray Direct PDF - try { - await printService.printPDF(pdfBase64, printerName); - } catch (qzError) { - console.warn('QZ Tray PDF falló, abriendo diálogo:', qzError); - await printService.printPDFWithDialog(pdfBase64); - } - } - } catch (error) { - console.error('Error general imprimiendo ticket:', error); - - // Fallback final + // Convertir PDF a base64 data URI const pdfBase64 = doc.output('datauristring'); - await printService.printPDFWithDialog(pdfBase64); - - window.Notify.warning('Hubo un problema con la impresión automática. Se abrió el diálogo del sistema.'); + + // Abrir diálogo de impresión del navegador + await printService.printPDF(pdfBase64); + + console.log('Ticket enviado a impresión'); + } catch (error) { + console.error('Error imprimiendo ticket:', error); + window.Notify.warning('No se pudo abrir el diálogo de impresión. Usa el PDF descargado para imprimir.'); } } @@ -501,7 +422,9 @@ const ticketService = { doc.setFontSize(8); doc.setFont('helvetica', 'normal'); doc.setTextColor(...darkGrayColor); - doc.text(`${location.city}, ${location.state}`, centerX, yPosition, { align: 'center' }); + doc.text(`${location.city}, ${location.state}, ${location.country}`, centerX, yPosition, { align: 'center' }); + yPosition += 4; + doc.text(location.phone, centerX, yPosition, { align: 'center' }); yPosition += 6; // Línea