feat: Se implementó el componente para subir archivos xml y pdf para facturas
This commit is contained in:
parent
4dfeeeea20
commit
c9251e0c8f
@ -10,6 +10,12 @@ VITE_APP_API_SECURE=false
|
|||||||
VITE_MICROSERVICE_STOCK=http://localhost:3000/api
|
VITE_MICROSERVICE_STOCK=http://localhost:3000/api
|
||||||
VITE_APP_NOTIFICATIONS=false
|
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_ID=
|
||||||
VITE_REVERB_APP_KEY=
|
VITE_REVERB_APP_KEY=
|
||||||
VITE_REVERB_APP_SECRET=
|
VITE_REVERB_APP_SECRET=
|
||||||
|
|||||||
@ -1,227 +0,0 @@
|
|||||||
<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>
|
|
||||||
346
src/components/POS/UploadFiles.vue
Normal file
346
src/components/POS/UploadFiles.vue
Normal file
@ -0,0 +1,346 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { useForm, apiURL } from '@Services/Api';
|
||||||
|
|
||||||
|
import Modal from '@Holos/Modal.vue';
|
||||||
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
|
|
||||||
|
/** Props */
|
||||||
|
const props = defineProps({
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
request: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Emits */
|
||||||
|
const emit = defineEmits(['close', 'refresh']);
|
||||||
|
|
||||||
|
/** Estado */
|
||||||
|
const processing = ref(false);
|
||||||
|
const uploadForm = useForm({
|
||||||
|
invoice_xml: null,
|
||||||
|
invoice_pdf: null,
|
||||||
|
cfdi_uuid: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Métodos */
|
||||||
|
const closeModal = () => {
|
||||||
|
uploadForm.reset();
|
||||||
|
emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleXmlChange = (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
if (file.name.endsWith('.xml') || file.type === 'text/xml' || file.type === 'application/xml') {
|
||||||
|
uploadForm.invoice_xml = file;
|
||||||
|
} else {
|
||||||
|
window.Notify.warning('Por favor selecciona un archivo XML válido');
|
||||||
|
event.target.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePdfChange = (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
if (file.name.endsWith('.pdf') || file.type === 'application/pdf') {
|
||||||
|
uploadForm.invoice_pdf = file;
|
||||||
|
} else {
|
||||||
|
window.Notify.warning('Por favor selecciona un archivo PDF válido');
|
||||||
|
event.target.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitUpload = () => {
|
||||||
|
// Debug: ver qué datos se van a enviar
|
||||||
|
console.log('Datos del formulario antes de enviar:', {
|
||||||
|
invoice_xml: uploadForm.invoice_xml,
|
||||||
|
invoice_pdf: uploadForm.invoice_pdf,
|
||||||
|
cfdi_uuid: uploadForm.cfdi_uuid
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validación: al menos debe haber PDF
|
||||||
|
if (!uploadForm.invoice_pdf) {
|
||||||
|
window.Notify.warning('Por favor selecciona el archivo PDF');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
processing.value = true;
|
||||||
|
|
||||||
|
// Crear FormData manualmente para mayor control
|
||||||
|
const formData = new FormData();
|
||||||
|
|
||||||
|
// Agregar archivos si existen
|
||||||
|
if (uploadForm.invoice_xml) {
|
||||||
|
formData.append('xml_file', uploadForm.invoice_xml, uploadForm.invoice_xml.name);
|
||||||
|
console.log('XML agregado:', uploadForm.invoice_xml.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploadForm.invoice_pdf) {
|
||||||
|
formData.append('invoice_pdf', uploadForm.invoice_pdf, uploadForm.invoice_pdf.name);
|
||||||
|
console.log('PDF agregado:', uploadForm.invoice_pdf.name, 'Tamaño:', uploadForm.invoice_pdf.size, 'bytes');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Agregar UUID si existe
|
||||||
|
if (uploadForm.cfdi_uuid && uploadForm.cfdi_uuid.trim()) {
|
||||||
|
formData.append('cfdi_uuid', uploadForm.cfdi_uuid.trim());
|
||||||
|
console.log('UUID agregado:', uploadForm.cfdi_uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug: mostrar todo lo que hay en FormData
|
||||||
|
console.log('=== FormData a enviar ===');
|
||||||
|
for (let pair of formData.entries()) {
|
||||||
|
console.log(pair[0] + ':', pair[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enviar con axios directamente
|
||||||
|
window.axios({
|
||||||
|
method: 'POST',
|
||||||
|
url: apiURL(`invoice-requests/${props.request.id}/upload`),
|
||||||
|
data: formData,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Authorization': `Bearer ${sessionStorage.token}`,
|
||||||
|
'X-CSRF-TOKEN': localStorage.csrfToken
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
console.log('Respuesta exitosa:', response.data);
|
||||||
|
if (response.data.status === 'success') {
|
||||||
|
window.Notify.success('Archivos de factura subidos correctamente');
|
||||||
|
uploadForm.reset();
|
||||||
|
emit('refresh');
|
||||||
|
emit('close');
|
||||||
|
} else if (response.data.status === 'fail') {
|
||||||
|
console.error('Error del backend:', response.data);
|
||||||
|
window.Notify.error(response.data.data?.message || 'Error al subir los archivos');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error completo:', error);
|
||||||
|
console.error('Respuesta del servidor:', error.response?.data);
|
||||||
|
|
||||||
|
if (error.response?.status === 422) {
|
||||||
|
// Errores de validación
|
||||||
|
const errors = error.response.data.errors || {};
|
||||||
|
console.error('Errores de validación:', errors);
|
||||||
|
window.Notify.error(Object.values(errors).flat().join(', '));
|
||||||
|
} else {
|
||||||
|
window.Notify.error(error.response?.data?.message || 'Error al subir los archivos');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
processing.value = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal :show="show" max-width="lg" @close="closeModal">
|
||||||
|
<div class="p-6">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-center gap-3 mb-6">
|
||||||
|
<div class="flex items-center justify-center w-12 h-12 rounded-full bg-blue-100 dark:bg-blue-900/30">
|
||||||
|
<GoogleIcon name="upload_file" class="text-2xl text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<h3 class="text-lg font-bold text-gray-900 dark:text-gray-100">
|
||||||
|
Subir Archivos de Factura
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Solicitud #{{ request.id }} - {{ request.client?.name }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
@click="closeModal"
|
||||||
|
:disabled="processing"
|
||||||
|
class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Información de la venta -->
|
||||||
|
<div class="mb-6 p-4 bg-gray-50 dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||||
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<GoogleIcon name="receipt_long" class="text-lg text-gray-600 dark:text-gray-400" />
|
||||||
|
<span class="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
||||||
|
Folio: {{ request.sale?.invoice_number || 'N/A' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
RFC: <span class="font-mono font-semibold">{{ request.client?.rfc || 'N/A' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Formulario -->
|
||||||
|
<form @submit.prevent="submitUpload" class="space-y-5">
|
||||||
|
<!-- UUID del CFDI -->
|
||||||
|
<div>
|
||||||
|
<label class="flex items-center gap-2 text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
<GoogleIcon name="key" class="text-lg text-purple-600 dark:text-purple-400" />
|
||||||
|
UUID del CFDI
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="uploadForm.cfdi_uuid"
|
||||||
|
type="text"
|
||||||
|
placeholder="Ej: 123e4567-e89b-12d3-a456-426614174000"
|
||||||
|
:disabled="processing"
|
||||||
|
class="w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-purple-500 focus:border-transparent disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-mono text-sm"
|
||||||
|
/>
|
||||||
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Identificador único del CFDI timbrado
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Archivo XML -->
|
||||||
|
<div>
|
||||||
|
<label class="flex items-center gap-2 text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
<GoogleIcon name="description" class="text-lg text-blue-600 dark:text-blue-400" />
|
||||||
|
Archivo XML
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".xml,application/xml,text/xml"
|
||||||
|
:disabled="processing"
|
||||||
|
@change="handleXmlChange"
|
||||||
|
class="w-full text-sm text-gray-500 dark:text-gray-400
|
||||||
|
file:mr-4 file:py-2.5 file:px-4
|
||||||
|
file:rounded-lg file:border-0
|
||||||
|
file:text-sm file:font-semibold
|
||||||
|
file:bg-blue-50 file:text-blue-700
|
||||||
|
hover:file:bg-blue-100
|
||||||
|
file:cursor-pointer
|
||||||
|
dark:file:bg-blue-900/30 dark:file:text-blue-400
|
||||||
|
dark:hover:file:bg-blue-900/50
|
||||||
|
file:transition-colors
|
||||||
|
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
/>
|
||||||
|
<GoogleIcon
|
||||||
|
v-if="uploadForm.invoice_xml"
|
||||||
|
name="check_circle"
|
||||||
|
class="absolute right-3 top-1/2 -translate-y-1/2 text-2xl text-green-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p v-if="uploadForm.invoice_xml" class="mt-2 flex items-center gap-2 text-xs">
|
||||||
|
<GoogleIcon name="insert_drive_file" class="text-base text-blue-600 dark:text-blue-400" />
|
||||||
|
<span class="font-medium text-gray-700 dark:text-gray-300">{{ uploadForm.invoice_xml.name }}</span>
|
||||||
|
<span class="text-gray-500 dark:text-gray-400">
|
||||||
|
({{ (uploadForm.xml_file.size / 1024).toFixed(2) }} KB)
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p v-else class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Archivo XML del CFDI timbrado por el PAC
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Archivo PDF -->
|
||||||
|
<div>
|
||||||
|
<label class="flex items-center gap-2 text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
|
||||||
|
<GoogleIcon name="picture_as_pdf" class="text-lg text-red-600 dark:text-red-400" />
|
||||||
|
Archivo PDF
|
||||||
|
</label>
|
||||||
|
<div class="relative">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".pdf,application/pdf"
|
||||||
|
:disabled="processing"
|
||||||
|
@change="handlePdfChange"
|
||||||
|
class="w-full text-sm text-gray-500 dark:text-gray-400
|
||||||
|
file:mr-4 file:py-2.5 file:px-4
|
||||||
|
file:rounded-lg file:border-0
|
||||||
|
file:text-sm file:font-semibold
|
||||||
|
file:bg-red-50 file:text-red-700
|
||||||
|
hover:file:bg-red-100
|
||||||
|
file:cursor-pointer
|
||||||
|
dark:file:bg-red-900/30 dark:file:text-red-400
|
||||||
|
dark:hover:file:bg-red-900/50
|
||||||
|
file:transition-colors
|
||||||
|
disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
/>
|
||||||
|
<GoogleIcon
|
||||||
|
v-if="uploadForm.invoice_pdf"
|
||||||
|
name="check_circle"
|
||||||
|
class="absolute right-3 top-1/2 -translate-y-1/2 text-2xl text-green-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p v-if="uploadForm.invoice_pdf" class="mt-2 flex items-center gap-2 text-xs">
|
||||||
|
<GoogleIcon name="picture_as_pdf" class="text-base text-red-600 dark:text-red-400" />
|
||||||
|
<span class="font-medium text-gray-700 dark:text-gray-300">{{ uploadForm.invoice_pdf.name }}</span>
|
||||||
|
<span class="text-gray-500 dark:text-gray-400">
|
||||||
|
({{ (uploadForm.invoice_pdf.size / 1024).toFixed(2) }} KB)
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<p v-else class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
Representación impresa del CFDI en formato PDF
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Nota informativa -->
|
||||||
|
<div class="flex gap-3 p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||||
|
<GoogleIcon name="info" class="text-xl text-blue-600 dark:text-blue-400 flex-shrink-0" />
|
||||||
|
<div class="text-xs text-blue-800 dark:text-blue-300">
|
||||||
|
<p class="font-semibold mb-1">Información importante:</p>
|
||||||
|
<ul class="list-disc list-inside space-y-1">
|
||||||
|
<li>Los archivos deben corresponder a la factura timbrada</li>
|
||||||
|
<li>El UUID debe coincidir con el del XML timbrado</li>
|
||||||
|
<li>Tamaño máximo por archivo: 5MB</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Acciones -->
|
||||||
|
<div class="flex items-center justify-end gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="closeModal"
|
||||||
|
:disabled="processing"
|
||||||
|
class="px-5 py-2.5 text-sm font-semibold text-gray-700 dark:text-gray-300 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
:disabled="processing"
|
||||||
|
class="inline-flex items-center gap-2 px-5 py-2.5 text-sm font-semibold text-white bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-blue-500/30"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="upload" class="text-lg" />
|
||||||
|
{{ processing ? 'Subiendo...' : 'Subir Archivos' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Animación para el check de archivo seleccionado */
|
||||||
|
@keyframes checkIn {
|
||||||
|
0% {
|
||||||
|
transform: translate(50%, -50%) scale(0);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translate(50%, -50%) scale(1.2);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(50%, -50%) scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.absolute.right-3 {
|
||||||
|
animation: checkIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -6,6 +6,7 @@ import { formatCurrency, formatDate } from '@/utils/formatters';
|
|||||||
import Modal from '@Holos/Modal.vue';
|
import Modal from '@Holos/Modal.vue';
|
||||||
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
import GoogleIcon from '@Shared/GoogleIcon.vue';
|
||||||
import Input from '@Holos/Form/Input.vue';
|
import Input from '@Holos/Form/Input.vue';
|
||||||
|
import UploadFiles from '@Components/POS/UploadFiles.vue';
|
||||||
|
|
||||||
/** Props */
|
/** Props */
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -22,6 +23,7 @@ const emit = defineEmits(['close', 'refresh']);
|
|||||||
const processing = ref(false);
|
const processing = ref(false);
|
||||||
const showProcessModal = ref(false);
|
const showProcessModal = ref(false);
|
||||||
const showRejectModal = ref(false);
|
const showRejectModal = ref(false);
|
||||||
|
const showUploadModal = ref(false);
|
||||||
|
|
||||||
const processForm = useForm({
|
const processForm = useForm({
|
||||||
notes: ''
|
notes: ''
|
||||||
@ -90,6 +92,14 @@ const openRejectModal = () => {
|
|||||||
showRejectModal.value = true;
|
showRejectModal.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openUploadModal = () => {
|
||||||
|
showUploadModal.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeUploadModal = () => {
|
||||||
|
showUploadModal.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
const submitProcess = () => {
|
const submitProcess = () => {
|
||||||
processing.value = true;
|
processing.value = true;
|
||||||
processForm.put(apiURL(`invoice-requests/${props.request.id}/process`), {
|
processForm.put(apiURL(`invoice-requests/${props.request.id}/process`), {
|
||||||
@ -209,6 +219,87 @@ const submitReject = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Información del CFDI -->
|
||||||
|
<div v-if="request.cfdi_uuid || request.invoice_xml_url || request.invoice_pdf_url" class="bg-gradient-to-br from-purple-50 to-purple-100 dark:from-purple-900/20 dark:to-purple-800/20 rounded-xl p-5 border border-purple-200 dark:border-purple-800">
|
||||||
|
<div class="flex items-center gap-2 mb-4">
|
||||||
|
<GoogleIcon name="verified" class="text-xl text-purple-600 dark:text-purple-400" />
|
||||||
|
<h4 class="text-sm font-bold text-purple-700 dark:text-purple-300 uppercase tracking-wide">
|
||||||
|
Información del CFDI
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- UUID -->
|
||||||
|
<div v-if="request.cfdi_uuid" class="mb-4">
|
||||||
|
<label class="block text-xs font-semibold text-purple-600 dark:text-purple-400 uppercase mb-2">
|
||||||
|
UUID del CFDI
|
||||||
|
</label>
|
||||||
|
<div class="flex items-center gap-2 p-3 bg-white/50 dark:bg-gray-800/50 rounded-lg">
|
||||||
|
<p class="flex-1 text-sm font-mono text-purple-900 dark:text-purple-100 break-all">
|
||||||
|
{{ request.cfdi_uuid }}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
@click="navigator.clipboard.writeText(request.cfdi_uuid); window.Notify.success('UUID copiado al portapapeles')"
|
||||||
|
class="flex-shrink-0 p-2 rounded-lg bg-purple-100 hover:bg-purple-200 dark:bg-purple-900/30 dark:hover:bg-purple-900/50 transition-colors"
|
||||||
|
title="Copiar UUID"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="content_copy" class="text-lg text-purple-600 dark:text-purple-400" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Archivos -->
|
||||||
|
<div v-if="request.invoice_xml_url || request.invoice_pdf_url">
|
||||||
|
<label class="block text-xs font-semibold text-purple-600 dark:text-purple-400 uppercase mb-2">
|
||||||
|
Archivos de Facturación
|
||||||
|
</label>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<!-- XML -->
|
||||||
|
<a
|
||||||
|
v-if="request.invoice_xml_url"
|
||||||
|
:href="request.invoice_xml_url"
|
||||||
|
target="_blank"
|
||||||
|
download
|
||||||
|
class="flex-1 flex items-center gap-3 p-3 bg-white/50 dark:bg-gray-800/50 rounded-lg hover:bg-white dark:hover:bg-gray-800 transition-colors border border-blue-200 dark:border-blue-800"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-blue-100 dark:bg-blue-900/30">
|
||||||
|
<GoogleIcon name="description" class="text-2xl text-blue-600 dark:text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-xs font-semibold text-blue-700 dark:text-blue-300">
|
||||||
|
Archivo XML
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-blue-600 dark:text-blue-400">
|
||||||
|
Descargar CFDI
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<GoogleIcon name="download" class="text-xl text-blue-600 dark:text-blue-400" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- PDF -->
|
||||||
|
<a
|
||||||
|
v-if="request.invoice_pdf_url"
|
||||||
|
:href="request.invoice_pdf_url"
|
||||||
|
target="_blank"
|
||||||
|
download
|
||||||
|
class="flex-1 flex items-center gap-3 p-3 bg-white/50 dark:bg-gray-800/50 rounded-lg hover:bg-white dark:hover:bg-gray-800 transition-colors border border-red-200 dark:border-red-800"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-red-100 dark:bg-red-900/30">
|
||||||
|
<GoogleIcon name="picture_as_pdf" class="text-2xl text-red-600 dark:text-red-400" />
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-xs font-semibold text-red-700 dark:text-red-300">
|
||||||
|
Archivo PDF
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-red-600 dark:text-red-400">
|
||||||
|
Descargar Factura
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<GoogleIcon name="download" class="text-xl text-red-600 dark:text-red-400" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Información de la Venta -->
|
<!-- Información de la Venta -->
|
||||||
<div class="bg-gradient-to-br from-green-50 to-green-100 dark:from-green-900/20 dark:to-green-800/20 rounded-xl p-5 border border-green-200 dark:border-green-800">
|
<div class="bg-gradient-to-br from-green-50 to-green-100 dark:from-green-900/20 dark:to-green-800/20 rounded-xl p-5 border border-green-200 dark:border-green-800">
|
||||||
<div class="flex items-center gap-2 mb-4">
|
<div class="flex items-center gap-2 mb-4">
|
||||||
@ -389,6 +480,15 @@ const submitReject = () => {
|
|||||||
<GoogleIcon name="cancel" class="text-lg" />
|
<GoogleIcon name="cancel" class="text-lg" />
|
||||||
Rechazar
|
Rechazar
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="openUploadModal"
|
||||||
|
:disabled="processing"
|
||||||
|
class="inline-flex items-center gap-2 px-5 py-2.5 text-sm font-semibold text-white bg-blue-600 hover:bg-blue-700 rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="upload_file" class="text-lg" />
|
||||||
|
Subir Archivos
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="openProcessModal"
|
@click="openProcessModal"
|
||||||
@ -501,4 +601,11 @@ const submitReject = () => {
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<UploadFiles
|
||||||
|
:show="showUploadModal"
|
||||||
|
:request="request"
|
||||||
|
@close="closeUploadModal"
|
||||||
|
@refresh="emit('refresh')"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -198,6 +198,8 @@ onMounted(() => {
|
|||||||
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">RFC</th>
|
<th class="px-6 py-3 text-left text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">RFC</th>
|
||||||
<th class="px-6 py-3 text-right text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">TOTAL</th>
|
<th class="px-6 py-3 text-right text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">TOTAL</th>
|
||||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">ESTADO</th>
|
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">ESTADO</th>
|
||||||
|
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">UUID CFDI</th>
|
||||||
|
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">ARCHIVOS</th>
|
||||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">FECHA SOLICITUD</th>
|
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">FECHA SOLICITUD</th>
|
||||||
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">ACCIONES</th>
|
<th class="px-6 py-3 text-center text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider">ACCIONES</th>
|
||||||
</template>
|
</template>
|
||||||
@ -246,6 +248,61 @@ onMounted(() => {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<!-- UUID del CFDI -->
|
||||||
|
<td class="px-6 py-4 text-center">
|
||||||
|
<div v-if="request.cfdi_uuid" class="flex flex-col items-center">
|
||||||
|
<p class="text-xs font-mono text-gray-700 dark:text-gray-300 break-all max-w-[200px]">
|
||||||
|
{{ request.cfdi_uuid }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<span v-else class="text-xs text-gray-400 dark:text-gray-500">
|
||||||
|
Sin UUID
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<!-- Archivos disponibles -->
|
||||||
|
<td class="px-6 py-4 text-center">
|
||||||
|
<div class="flex items-center justify-center gap-2">
|
||||||
|
<!-- XML -->
|
||||||
|
<a
|
||||||
|
v-if="request.invoice_xml_url"
|
||||||
|
:href="request.invoice_xml_url"
|
||||||
|
target="_blank"
|
||||||
|
download
|
||||||
|
class="inline-flex items-center justify-center w-8 h-8 rounded-lg bg-blue-100 hover:bg-blue-200 dark:bg-blue-900/30 dark:hover:bg-blue-900/50 transition-colors"
|
||||||
|
title="Descargar XML"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="description" class="text-lg text-blue-600 dark:text-blue-400" />
|
||||||
|
</a>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="inline-flex items-center justify-center w-8 h-8 rounded-lg bg-gray-100 dark:bg-gray-800"
|
||||||
|
title="Sin XML"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="description" class="text-lg text-gray-400" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- PDF -->
|
||||||
|
<a
|
||||||
|
v-if="request.invoice_pdf_url"
|
||||||
|
:href="request.invoice_pdf_url"
|
||||||
|
target="_blank"
|
||||||
|
download
|
||||||
|
class="inline-flex items-center justify-center w-8 h-8 rounded-lg bg-red-100 hover:bg-red-200 dark:bg-red-900/30 dark:hover:bg-red-900/50 transition-colors"
|
||||||
|
title="Descargar PDF"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="picture_as_pdf" class="text-lg text-red-600 dark:text-red-400" />
|
||||||
|
</a>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="inline-flex items-center justify-center w-8 h-8 rounded-lg bg-gray-100 dark:bg-gray-800"
|
||||||
|
title="Sin PDF"
|
||||||
|
>
|
||||||
|
<GoogleIcon name="picture_as_pdf" class="text-lg text-gray-400" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
<td class="px-6 py-4 text-center">
|
<td class="px-6 py-4 text-center">
|
||||||
<p class="text-sm text-gray-700 dark:text-gray-300">
|
<p class="text-sm text-gray-700 dark:text-gray-300">
|
||||||
{{ formatDate(request.requested_at) }}
|
{{ formatDate(request.requested_at) }}
|
||||||
@ -268,7 +325,7 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<td colspan="7" class="table-cell text-center">
|
<td colspan="9" class="table-cell text-center">
|
||||||
<div class="flex flex-col items-center justify-center py-12 text-gray-500">
|
<div class="flex flex-col items-center justify-center py-12 text-gray-500">
|
||||||
<GoogleIcon
|
<GoogleIcon
|
||||||
name="receipt_long"
|
name="receipt_long"
|
||||||
|
|||||||
@ -17,7 +17,6 @@ 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();
|
||||||
@ -38,9 +37,6 @@ 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'),
|
||||||
@ -301,7 +297,7 @@ const handleConfirmSale = async (paymentData) => {
|
|||||||
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
|
autoPrint: true // Abre diálogo de impresión automáticamente
|
||||||
});
|
});
|
||||||
|
|
||||||
// Notificación de éxito
|
// Notificación de éxito
|
||||||
@ -399,15 +395,6 @@ onUnmounted(() => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
<button
|
|
||||||
@click="showPrinterConfig = true"
|
|
||||||
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"
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<GoogleIcon name="print" class="text-xl" />
|
|
||||||
Impresora
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
@click="toggleScanMode"
|
@click="toggleScanMode"
|
||||||
:class="[
|
:class="[
|
||||||
@ -423,7 +410,6 @@ onUnmounted(() => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Contenido Principal -->
|
<!-- Contenido Principal -->
|
||||||
<div class="flex gap-6 h-[calc(100%-120px)] px-6">
|
<div class="flex gap-6 h-[calc(100%-120px)] px-6">
|
||||||
@ -598,11 +584,5 @@ 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>
|
||||||
|
|||||||
@ -1,286 +1,15 @@
|
|||||||
/**
|
/**
|
||||||
* Servicio de impresión usando QZ Tray
|
* Servicio de impresión simplificado
|
||||||
* Maneja la conexión con impresoras térmicas y de tickets
|
* Usa el diálogo de impresión nativo del navegador (window.print)
|
||||||
*
|
|
||||||
* Requiere que QZ Tray esté cargado globalmente (window.qz)
|
|
||||||
* Se carga automáticamente desde CDN si no está disponible
|
|
||||||
*/
|
*/
|
||||||
class PrintService {
|
class PrintService {
|
||||||
constructor() {
|
|
||||||
this.isConnected = false;
|
|
||||||
this.defaultPrinter = null;
|
|
||||||
this.qz = null;
|
|
||||||
this.loadQZ();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cargar QZ Tray desde CDN si no está disponible
|
* Imprimir PDF usando window.print()
|
||||||
*/
|
* Abre el PDF en una nueva ventana e invoca el diálogo de impresión del navegador
|
||||||
async loadQZ() {
|
* @param {string} pdfDataUri - PDF como data URI o base64
|
||||||
// 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}
|
* @returns {Promise}
|
||||||
*/
|
*/
|
||||||
async printPDF(base64Data, printerName = null) {
|
async printPDF(pdfDataUri) {
|
||||||
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
// Asegurar que tenemos el data URI completo
|
// Asegurar que tenemos el data URI completo
|
||||||
@ -317,7 +46,7 @@ class PrintService {
|
|||||||
try {
|
try {
|
||||||
printWindow.print();
|
printWindow.print();
|
||||||
|
|
||||||
// Cerrar ventana después de imprimir (opcional)
|
// Cerrar ventana después de imprimir
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
printWindow.close();
|
printWindow.close();
|
||||||
URL.revokeObjectURL(blobUrl);
|
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
|
// Exportar instancia singleton
|
||||||
|
|||||||
@ -3,30 +3,6 @@ import QRCode from 'qrcode';
|
|||||||
import { formatMoney, PAYMENT_METHODS } from '@/utils/formatters';
|
import { formatMoney, PAYMENT_METHODS } from '@/utils/formatters';
|
||||||
import printService from '@Services/printService';
|
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
|
* Servicio para generar tickets de venta en formato PDF
|
||||||
*/
|
*/
|
||||||
@ -37,9 +13,10 @@ const ticketService = {
|
|||||||
*/
|
*/
|
||||||
async getUserLocation() {
|
async getUserLocation() {
|
||||||
return {
|
return {
|
||||||
city: import.meta.env.VITE_BUSINESS_CITY,
|
city: import.meta.env.VITE_BUSINESS_CITY || 'Ciudad',
|
||||||
state: import.meta.env.VITE_BUSINESS_STATE,
|
state: import.meta.env.VITE_BUSINESS_STATE || 'Estado',
|
||||||
country: import.meta.env.VITE_BUSINESS_COUNTRY
|
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} 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.autoDownload - Descargar automáticamente el PDF
|
||||||
* @param {boolean} options.autoPrint - Imprimir automáticamente con QZ Tray
|
* @param {boolean} options.autoPrint - Imprimir automáticamente (abre diálogo del navegador)
|
||||||
* @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,
|
autoPrint = false
|
||||||
printerName = null
|
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
// Detectar ubicación del usuario
|
// Detectar ubicación del usuario
|
||||||
const location = await this.getUserLocation();
|
const location = await this.getUserLocation();
|
||||||
const businessAddress = `${location.city}, ${location.state}, ${location.country}`;
|
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
|
// Crear documento PDF - Ticket térmico 80mm de ancho
|
||||||
const doc = new jsPDF({
|
const doc = new jsPDF({
|
||||||
@ -391,70 +366,16 @@ const ticketService = {
|
|||||||
// Imprimir automáticamente si se solicita
|
// Imprimir automáticamente si se solicita
|
||||||
if (autoPrint) {
|
if (autoPrint) {
|
||||||
try {
|
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
|
// Convertir PDF a base64 data URI
|
||||||
const pdfBase64 = doc.output('datauristring');
|
const pdfBase64 = doc.output('datauristring');
|
||||||
|
|
||||||
// Intentar QZ Tray Direct PDF
|
// Abrir diálogo de impresión del navegador
|
||||||
try {
|
await printService.printPDF(pdfBase64);
|
||||||
await printService.printPDF(pdfBase64, printerName);
|
|
||||||
} catch (qzError) {
|
console.log('Ticket enviado a impresión');
|
||||||
console.warn('QZ Tray PDF falló, abriendo diálogo:', qzError);
|
|
||||||
await printService.printPDFWithDialog(pdfBase64);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error general imprimiendo ticket:', 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.');
|
||||||
// Fallback final
|
|
||||||
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.');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -501,7 +422,9 @@ const ticketService = {
|
|||||||
doc.setFontSize(8);
|
doc.setFontSize(8);
|
||||||
doc.setFont('helvetica', 'normal');
|
doc.setFont('helvetica', 'normal');
|
||||||
doc.setTextColor(...darkGrayColor);
|
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;
|
yPosition += 6;
|
||||||
|
|
||||||
// Línea
|
// Línea
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user