WIP: agregar configuración de impresora y funcionalidad de impresión automática de tickets
This commit is contained in:
parent
a45cc247c1
commit
21b28b5bff
227
src/components/POS/PrinterConfigModal.vue
Normal file
227
src/components/POS/PrinterConfigModal.vue
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="show" class="fixed inset-0 z-50 overflow-y-auto" aria-labelledby="modal-title" role="dialog"
|
||||||
|
aria-modal="true">
|
||||||
|
<!-- Overlay -->
|
||||||
|
<div class="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity"></div>
|
||||||
|
|
||||||
|
<!-- Modal -->
|
||||||
|
<div class="flex min-h-full items-center justify-center p-4">
|
||||||
|
<div
|
||||||
|
class="relative transform overflow-hidden rounded-lg bg-white dark:bg-gray-800 text-left shadow-xl transition-all w-full max-w-lg">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="bg-indigo-600 px-6 py-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<GoogleIcon name="print" class="text-2xl text-white" />
|
||||||
|
<h3 class="text-lg font-semibold text-white">
|
||||||
|
Configuración de Impresora
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<button @click="$emit('close')" type="button"
|
||||||
|
class="text-white hover:text-gray-200 transition-colors">
|
||||||
|
<GoogleIcon name="close" class="text-xl" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Body -->
|
||||||
|
<div class="px-6 py-4 space-y-4">
|
||||||
|
<!-- Estado de QZ Tray -->
|
||||||
|
<div class="flex items-center gap-3 p-4 rounded-lg"
|
||||||
|
:class="isConnected ? 'bg-green-50 dark:bg-green-900/20' : 'bg-red-50 dark:bg-red-900/20'">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<div class="w-3 h-3 rounded-full" :class="isConnected ? 'bg-green-500' : 'bg-red-500'">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-sm font-medium"
|
||||||
|
:class="isConnected ? 'text-green-800 dark:text-green-200' : 'text-red-800 dark:text-red-200'">
|
||||||
|
{{ isConnected ? 'QZ Tray conectado' : 'QZ Tray no está activo' }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs mt-1"
|
||||||
|
:class="isConnected ? 'text-green-600 dark:text-green-300' : 'text-red-600 dark:text-red-300'">
|
||||||
|
{{ isConnected ? 'Listo para imprimir' : 'Por favor, inicia la aplicación QZ Tray' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button @click="refreshConnection" type="button"
|
||||||
|
class="p-2 rounded-lg hover:bg-white dark:hover:bg-gray-700 transition-colors">
|
||||||
|
<GoogleIcon name="refresh" class="text-lg" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Selección de impresora -->
|
||||||
|
<div v-if="isConnected">
|
||||||
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
Seleccionar Impresora
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div v-if="loading" class="flex items-center justify-center py-8">
|
||||||
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-indigo-600"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="printers.length === 0" class="text-center py-8">
|
||||||
|
<GoogleIcon name="print_disabled" class="text-4xl text-gray-400 mx-auto mb-2" />
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
No se encontraron impresoras
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="space-y-2">
|
||||||
|
<button v-for="printer in printers" :key="printer" @click="selectPrinter(printer)"
|
||||||
|
type="button" class="w-full flex items-center justify-between p-3 rounded-lg border-2 transition-all"
|
||||||
|
:class="selectedPrinter === printer
|
||||||
|
? 'border-indigo-600 bg-indigo-50 dark:bg-indigo-900/20'
|
||||||
|
: 'border-gray-200 dark:border-gray-600 hover:border-indigo-400 dark:hover:border-indigo-500'
|
||||||
|
">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<GoogleIcon name="print" class="text-xl"
|
||||||
|
:class="selectedPrinter === printer ? 'text-indigo-600' : 'text-gray-400'" />
|
||||||
|
<span class="text-sm font-medium"
|
||||||
|
:class="selectedPrinter === printer ? 'text-indigo-900 dark:text-indigo-100' : 'text-gray-700 dark:text-gray-300'">
|
||||||
|
{{ printer }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<GoogleIcon v-if="selectedPrinter === printer" name="check_circle"
|
||||||
|
class="text-xl text-indigo-600" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Botón de prueba -->
|
||||||
|
<button v-if="selectedPrinter" @click="testPrint" type="button"
|
||||||
|
class="w-full mt-4 flex items-center justify-center gap-2 px-4 py-2 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-200 rounded-lg transition-colors">
|
||||||
|
<GoogleIcon name="science" class="text-lg" />
|
||||||
|
Imprimir página de prueba
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Instrucciones si no está conectado -->
|
||||||
|
<div v-else class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
|
||||||
|
<h4 class="text-sm font-semibold text-blue-900 dark:text-blue-100 mb-2">
|
||||||
|
¿Cómo activar QZ Tray?
|
||||||
|
</h4>
|
||||||
|
<ol class="text-sm text-blue-800 dark:text-blue-200 space-y-2 list-decimal list-inside">
|
||||||
|
<li>Descarga QZ Tray desde <a href="https://qz.io/download" target="_blank"
|
||||||
|
class="underline font-medium">qz.io/download</a></li>
|
||||||
|
<li>Instala la aplicación en tu computadora</li>
|
||||||
|
<li>Inicia QZ Tray (debería aparecer en la bandeja del sistema)</li>
|
||||||
|
<li>Haz clic en el botón de actualizar arriba</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<div class="bg-gray-50 dark:bg-gray-900 px-6 py-4 flex justify-end gap-3">
|
||||||
|
<button @click="$emit('close')" type="button"
|
||||||
|
class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors">
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button @click="saveConfiguration" type="button"
|
||||||
|
:disabled="!selectedPrinter"
|
||||||
|
class="px-4 py-2 text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 disabled:bg-gray-300 disabled:cursor-not-allowed rounded-lg transition-colors">
|
||||||
|
Guardar Configuración
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
import printService from '@Services/printService';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
show: Boolean
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['close', 'saved']);
|
||||||
|
|
||||||
|
const isConnected = ref(false);
|
||||||
|
const loading = ref(false);
|
||||||
|
const printers = ref([]);
|
||||||
|
const selectedPrinter = ref(null);
|
||||||
|
|
||||||
|
const refreshConnection = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
await printService.connect();
|
||||||
|
isConnected.value = printService.isAvailable();
|
||||||
|
|
||||||
|
if (isConnected.value) {
|
||||||
|
await loadPrinters();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error conectando con QZ Tray:', error);
|
||||||
|
isConnected.value = false;
|
||||||
|
window.Notify.error('No se pudo conectar con QZ Tray. Asegúrate de que esté ejecutándose.');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadPrinters = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
const printerList = await printService.getPrinters();
|
||||||
|
printers.value = printerList;
|
||||||
|
|
||||||
|
// Cargar impresora guardada
|
||||||
|
const savedPrinter = printService.getSavedPrinter();
|
||||||
|
if (savedPrinter && printerList.includes(savedPrinter)) {
|
||||||
|
selectedPrinter.value = savedPrinter;
|
||||||
|
} else if (printerList.length > 0) {
|
||||||
|
// Seleccionar la primera impresora por defecto
|
||||||
|
const defaultPrinter = await printService.getDefaultPrinter();
|
||||||
|
selectedPrinter.value = defaultPrinter || printerList[0];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error cargando impresoras:', error);
|
||||||
|
window.Notify.error('Error al cargar la lista de impresoras');
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectPrinter = (printer) => {
|
||||||
|
selectedPrinter.value = printer;
|
||||||
|
};
|
||||||
|
|
||||||
|
const testPrint = async () => {
|
||||||
|
try {
|
||||||
|
window.Notify.info('Enviando página de prueba a la impresora...');
|
||||||
|
|
||||||
|
// Crear un PDF de prueba simple
|
||||||
|
const testHTML = `
|
||||||
|
<div style="font-family: monospace; padding: 20px; text-align: center;">
|
||||||
|
<h2>Página de Prueba</h2>
|
||||||
|
<p>Impresora: ${selectedPrinter.value}</p>
|
||||||
|
<p>Fecha: ${new Date().toLocaleString('es-MX')}</p>
|
||||||
|
<p style="margin-top: 20px;">Si puedes ver esto, la impresora está funcionando correctamente.</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
await printService.printHTML(testHTML, selectedPrinter.value);
|
||||||
|
window.Notify.success('Página de prueba enviada a la impresora');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error imprimiendo página de prueba:', error);
|
||||||
|
window.Notify.error('Error al enviar la página de prueba');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveConfiguration = () => {
|
||||||
|
if (!selectedPrinter.value) {
|
||||||
|
window.Notify.warning('Por favor selecciona una impresora');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
printService.setDefaultPrinter(selectedPrinter.value);
|
||||||
|
window.Notify.success(`Impresora configurada: ${selectedPrinter.value}`);
|
||||||
|
emit('saved', selectedPrinter.value);
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
refreshConnection();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@ -17,6 +17,7 @@ import CheckoutModal from '@Components/POS/CheckoutModal.vue';
|
|||||||
import ClientModal from '@Components/POS/ClientModal.vue';
|
import ClientModal from '@Components/POS/ClientModal.vue';
|
||||||
import QRscan from '@Components/POS/QRscan.vue';
|
import QRscan from '@Components/POS/QRscan.vue';
|
||||||
import SerialSelector from '@Components/POS/SerialSelector.vue';
|
import SerialSelector from '@Components/POS/SerialSelector.vue';
|
||||||
|
import PrinterConfigModal from '@Components/POS/PrinterConfigModal.vue';
|
||||||
|
|
||||||
/** i18n */
|
/** i18n */
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@ -37,6 +38,9 @@ const lastSaleData = ref(null);
|
|||||||
const showSerialSelector = ref(false);
|
const showSerialSelector = ref(false);
|
||||||
const serialSelectorProduct = ref(null);
|
const serialSelectorProduct = ref(null);
|
||||||
|
|
||||||
|
// Estado para configuración de impresora
|
||||||
|
const showPrinterConfig = ref(false);
|
||||||
|
|
||||||
/** Buscador de productos */
|
/** Buscador de productos */
|
||||||
const searcher = useSearcher({
|
const searcher = useSearcher({
|
||||||
url: apiURL('inventario'),
|
url: apiURL('inventario'),
|
||||||
@ -293,14 +297,15 @@ const handleConfirmSale = async (paymentData) => {
|
|||||||
// Mostrar notificación de que se está generando el ticket
|
// Mostrar notificación de que se está generando el ticket
|
||||||
window.Notify.info('Generando 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, {
|
await ticketService.generateSaleTicket(response, {
|
||||||
businessName: 'HIKVISION DISTRIBUIDOR',
|
businessName: 'HIKVISION DISTRIBUIDOR',
|
||||||
autoDownload: true
|
autoDownload: true,
|
||||||
|
autoPrint: true // Activar impresión automática con QZ Tray
|
||||||
});
|
});
|
||||||
|
|
||||||
// Notificación de éxito
|
// Notificación de éxito
|
||||||
window.Notify.success('Ticket descargado correctamente');
|
window.Notify.success('Ticket generado e impreso correctamente');
|
||||||
} catch (ticketError) {
|
} catch (ticketError) {
|
||||||
console.error('Error generando ticket:', ticketError);
|
console.error('Error generando ticket:', ticketError);
|
||||||
window.Notify.warning('Venta registrada, pero hubo un error al generar el ticket');
|
window.Notify.warning('Venta registrada, pero hubo un error al generar el ticket');
|
||||||
@ -394,19 +399,29 @@ onUnmounted(() => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div class="flex items-center gap-3">
|
||||||
@click="toggleScanMode"
|
<button
|
||||||
:class="[
|
@click="showPrinterConfig = true"
|
||||||
'flex items-center gap-2 px-4 py-3 rounded-lg font-semibold transition-all',
|
class="flex items-center gap-2 px-4 py-3 rounded-lg font-semibold transition-all bg-gray-500 hover:bg-gray-600 text-white"
|
||||||
scanMode
|
type="button"
|
||||||
? 'bg-red-500 hover:bg-red-600 text-white'
|
>
|
||||||
: 'bg-indigo-500 hover:bg-indigo-600 text-white'
|
<GoogleIcon name="print" class="text-xl" />
|
||||||
]"
|
Impresora
|
||||||
type="button"
|
</button>
|
||||||
>
|
<button
|
||||||
<GoogleIcon :name="scanMode ? 'close' : 'qr_code_scanner'" class="text-xl" />
|
@click="toggleScanMode"
|
||||||
{{ scanMode ? 'Cerrar escáner' : 'Escanear código' }}
|
:class="[
|
||||||
</button >
|
'flex items-center gap-2 px-4 py-3 rounded-lg font-semibold transition-all',
|
||||||
|
scanMode
|
||||||
|
? 'bg-red-500 hover:bg-red-600 text-white'
|
||||||
|
: 'bg-indigo-500 hover:bg-indigo-600 text-white'
|
||||||
|
]"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<GoogleIcon :name="scanMode ? 'close' : 'qr_code_scanner'" class="text-xl" />
|
||||||
|
{{ scanMode ? 'Cerrar escáner' : 'Escanear código' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -583,5 +598,11 @@ onUnmounted(() => {
|
|||||||
@close="closeSerialSelector"
|
@close="closeSerialSelector"
|
||||||
@confirm="handleSerialConfirm"
|
@confirm="handleSerialConfirm"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Modal de Configuración de Impresora -->
|
||||||
|
<PrinterConfigModal
|
||||||
|
:show="showPrinterConfig"
|
||||||
|
@close="showPrinterConfig = false"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
349
src/services/printService.js
Normal file
349
src/services/printService.js
Normal file
@ -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<boolean>}
|
||||||
|
*/
|
||||||
|
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<Array>}
|
||||||
|
*/
|
||||||
|
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<string>}
|
||||||
|
*/
|
||||||
|
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;
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import jsPDF from 'jspdf';
|
import jsPDF from 'jspdf';
|
||||||
import QRCode from 'qrcode';
|
import QRCode from 'qrcode';
|
||||||
import { formatMoney, PAYMENT_METHODS } from '@/utils/formatters';
|
import { formatMoney, PAYMENT_METHODS } from '@/utils/formatters';
|
||||||
|
import printService from '@Services/printService';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Servicio para generar tickets de venta en formato PDF
|
* Servicio para generar tickets de venta en formato PDF
|
||||||
@ -22,11 +23,16 @@ const ticketService = {
|
|||||||
* Generar ticket de venta
|
* Generar ticket de venta
|
||||||
* @param {Object} saleData - Datos de la venta
|
* @param {Object} saleData - Datos de la venta
|
||||||
* @param {Object} options - Opciones de configuración
|
* @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 = {}) {
|
async generateSaleTicket(saleData, options = {}) {
|
||||||
const {
|
const {
|
||||||
businessName = 'HIKVISION DISTRIBUIDOR',
|
businessName = 'HIKVISION DISTRIBUIDOR',
|
||||||
autoDownload = true
|
autoDownload = true,
|
||||||
|
autoPrint = false,
|
||||||
|
printerName = null
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
// Detectar ubicación del usuario
|
// Detectar ubicación del usuario
|
||||||
@ -356,10 +362,34 @@ const ticketService = {
|
|||||||
if (autoDownload) {
|
if (autoDownload) {
|
||||||
const fileName = `ticket-${folio}.pdf`;
|
const fileName = `ticket-${folio}.pdf`;
|
||||||
doc.save(fileName);
|
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;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user