From a45cc247c19ec0771d547dc6e9f26b7135273bb9 Mon Sep 17 00:00:00 2001 From: Juan Felipe Zapata Moreno Date: Tue, 3 Feb 2026 15:25:19 -0600 Subject: [PATCH] =?UTF-8?q?feat:=20mejorar=20gesti=C3=B3n=20de=20facturaci?= =?UTF-8?q?=C3=B3n=20y=20b=C3=BAsqueda=20por=20scanner?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Agrega estadísticas detalladas y manejo de carga en BillingRequests.vue. - Actualiza tablas para mostrar historial, montos y estados con badges. - Previene solicitudes duplicadas hasta que la anterior sea procesada. - Implementa hook useBarcodeScanner en Point.vue para búsqueda por código/serie --- src/composables/useBarcodeScanner.js | 45 ++ .../POS/Clients/BillingRequestDetailModal.vue | 542 +++++++++++++----- src/pages/POS/Clients/BillingRequests.vue | 219 +++++-- src/pages/POS/Factura/Index.vue | 215 ++++++- src/pages/POS/Point.vue | 82 ++- 5 files changed, 884 insertions(+), 219 deletions(-) create mode 100644 src/composables/useBarcodeScanner.js diff --git a/src/composables/useBarcodeScanner.js b/src/composables/useBarcodeScanner.js new file mode 100644 index 0000000..7c2dff0 --- /dev/null +++ b/src/composables/useBarcodeScanner.js @@ -0,0 +1,45 @@ +import { onMounted, onUnmounted } from 'vue'; + +export function useBarcodeScanner(options = {}) { + const { + onScan, + minLength = 3, + scanTimeout = 100, + enterKey = true + } = options; + + let barcode = ''; + let timeout; + + const handleKeyPress = (e) => { + // Ignorar si está escribiendo en inputs + if (['INPUT', 'TEXTAREA'].includes(e.target.tagName)) { + return; + } + + if (e.key === 'Enter' && enterKey) { + if (barcode.length >= minLength) { + onScan?.(barcode); + barcode = ''; + } + } else if (e.key.length === 1) { + clearTimeout(timeout); + barcode += e.key; + + timeout = setTimeout(() => { + barcode = ''; + }, scanTimeout); + } + }; + + onMounted(() => { + document.addEventListener('keypress', handleKeyPress); + }); + + onUnmounted(() => { + document.removeEventListener('keypress', handleKeyPress); + clearTimeout(timeout); + }); + + return {}; +} diff --git a/src/pages/POS/Clients/BillingRequestDetailModal.vue b/src/pages/POS/Clients/BillingRequestDetailModal.vue index 622b14c..2849442 100644 --- a/src/pages/POS/Clients/BillingRequestDetailModal.vue +++ b/src/pages/POS/Clients/BillingRequestDetailModal.vue @@ -1,9 +1,11 @@ + + +
+
+
+ +
+
+

+ Marcar como Procesada +

+

+ Solicitud #{{ request.id }} +

+
+
+ +
+ + +
+ + +
+
+
+
+ + + +
+
+
+ +
+
+

+ Rechazar Solicitud +

+

+ Solicitud #{{ request.id }} +

+
+
+ +
+ + +
+ + +
+
+
+
+ diff --git a/src/pages/POS/Clients/BillingRequests.vue b/src/pages/POS/Clients/BillingRequests.vue index c5c2c97..23cf2f3 100644 --- a/src/pages/POS/Clients/BillingRequests.vue +++ b/src/pages/POS/Clients/BillingRequests.vue @@ -1,7 +1,7 @@ - + \ No newline at end of file diff --git a/src/pages/POS/Point.vue b/src/pages/POS/Point.vue index d932cc8..5c71373 100644 --- a/src/pages/POS/Point.vue +++ b/src/pages/POS/Point.vue @@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n'; import { useSearcher, apiURL } from '@Services/Api'; import { page } from '@Services/Page'; import { formatCurrency } from '@/utils/formatters'; +import { useBarcodeScanner } from '@/composables/useBarcodeScanner'; import useCart from '@Stores/cart'; import salesService from '@Services/salesService'; import ticketService from '@Services/ticketService'; @@ -142,31 +143,73 @@ const handleCodeDetected = async (barcode) => { try { window.Notify.info('Buscando producto...'); - // Buscar producto por código de barras - const response = await fetch(apiURL(`inventario?q=${encodeURIComponent(barcode)}`), { - headers: { - 'Authorization': `Bearer ${sessionStorage.token}`, - 'Accept': 'application/json' + // Intentar buscar como código de barras del producto + const productResponse = await fetch( + apiURL(`inventario?q=${encodeURIComponent(barcode)}`), + { + headers: { + 'Authorization': `Bearer ${sessionStorage.token}`, + 'Accept': 'application/json' + } } - }); + ); - const result = await response.json(); + const productResult = await productResponse.json(); - // Verificar si se encontró el producto - if (result.data && result.data.products && result.data.products.data && result.data.products.data.length > 0) { - const product = result.data.products.data[0]; + if (productResult.data?.products?.data?.length > 0) { + const product = productResult.data.products.data[0]; - // Verificar si el producto tiene stock if (product.stock <= 0) { - window.Notify.error(`${product.name} no tiene stock disponible`); + window.Notify.warning(`El producto "${product.name}" no tiene stock disponible`); return; } - // Agregar producto al carrito addToCart(product); - } else { - window.Notify.error('Producto no encontrado'); + return; } + + // Si no se encontró como producto, buscar como número de serie + window.Notify.info('Buscando por número de serie...'); + + const serialResponse = await fetch( + apiURL(`serials/search?serial_number=${encodeURIComponent(barcode)}`), + { + headers: { + 'Authorization': `Bearer ${sessionStorage.token}`, + 'Accept': 'application/json' + } + } + ); + + const serialResult = await serialResponse.json(); + + if (serialResult.data?.serial) { + const serial = serialResult.data.serial; + const product = serial.inventory; + + if (!product) { + window.Notify.error('Producto del serial no encontrado'); + return; + } + + if (serial.status !== 'disponible') { + window.Notify.error(`Serial ${barcode} no está disponible (${serial.status})`); + return; + } + + // Agregar producto con ese serial específico + product.track_serials = true; + cart.addProductWithSerials(product, 1, { + serialNumbers: [serial.serial_number], + selectionMode: 'manual' + }); + + window.Notify.success(`${product.name} (SN: ${barcode}) agregado al carrito`); + return; + } + + window.Notify.error('Producto o serial no encontrado'); + } catch (error) { console.error('Error buscando producto:', error); window.Notify.error('Error al buscar el producto'); @@ -314,6 +357,15 @@ const handleOpenSerialSelector = (event) => { } }; +useBarcodeScanner({ + onScan: (barcode) => { + // Reutilizar la misma lógica que ya tienes + handleCodeDetected(barcode); + }, + minLength: 5, // Ajusta según tus códigos de barras + scanTimeout: 100 +}); + /** Ciclo de vida */ onMounted(() => { searcher.search();