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 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Configuración de Impresora
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ isConnected ? 'QZ Tray conectado' : 'QZ Tray no está activo' }}
-
-
- {{ isConnected ? 'Listo para imprimir' : 'Por favor, inicia la aplicación QZ Tray' }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- No se encontraron impresoras
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ¿Cómo activar QZ Tray?
-
-
- - Descarga QZ Tray desde qz.io/download
- - Instala la aplicación en tu computadora
- - Inicia QZ Tray (debería aparecer en la bandeja del sistema)
- - Haz clic en el botón de actualizar arriba
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Subir Archivos de Factura
+
+
+ Solicitud #{{ request.id }} - {{ request.client?.name }}
+
+
+
+
+
+
+
+
+
+
+ Folio: {{ request.sale?.invoice_number || 'N/A' }}
+
+
+
+ RFC: {{ request.client?.rfc || 'N/A' }}
+
+
+
+
+
+
+
+
+
+
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 = () => {
+
+
@@ -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(() => {
- |
+ |
{
await ticketService.generateSaleTicket(response, {
businessName: 'HIKVISION DISTRIBUIDOR',
autoDownload: true,
- autoPrint: true // Activar impresión automática con QZ Tray
+ autoPrint: true // Abre diálogo de impresión automáticamente
});
// Notificación de éxito
@@ -399,29 +395,19 @@ onUnmounted(() => {
-
-
-
-
+
@@ -598,11 +584,5 @@ onUnmounted(() => {
@close="closeSerialSelector"
@confirm="handleSerialConfirm"
/>
-
-
-
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
| |