From 2bdccbe6c623e126698f037192359955394060de Mon Sep 17 00:00:00 2001 From: "edgar.mendez" Date: Fri, 27 Feb 2026 12:10:04 -0600 Subject: [PATCH] feat: enhance warehouse inventory management by adding product selection modal and manual entry mode --- .../components/WarehouseAddInventory.vue | 419 ++++++++++++++++-- .../warehouse/components/WarehouseDetails.vue | 6 +- 2 files changed, 386 insertions(+), 39 deletions(-) diff --git a/src/modules/warehouse/components/WarehouseAddInventory.vue b/src/modules/warehouse/components/WarehouseAddInventory.vue index aaaf5b4..2865517 100644 --- a/src/modules/warehouse/components/WarehouseAddInventory.vue +++ b/src/modules/warehouse/components/WarehouseAddInventory.vue @@ -8,12 +8,19 @@ import Dropdown from 'primevue/dropdown'; import InputText from 'primevue/inputtext'; import Badge from 'primevue/badge'; import Toast from 'primevue/toast'; +import Dialog from 'primevue/dialog'; +import DataTable from 'primevue/datatable'; +import Column from 'primevue/column'; +import IconField from 'primevue/iconfield'; +import InputIcon from 'primevue/inputicon'; import { useToast } from 'primevue/usetoast'; import { purchaseServices } from '../../purchases/services/purchaseServices'; import type { PurchaseDetailResponse } from '../../purchases/types/purchases'; import { useWarehouseStore } from '../../../stores/warehouseStore'; +import { useProductStore } from '../../products/stores/productStore'; import { inventoryWarehouseServices } from '../services/inventoryWarehouse.services'; import type { InventoryProductItem, CreateInventoryRequest } from '../types/warehouse.inventory'; +import type { Product as ProductType } from '../../products/types/product'; interface SerialNumber { serial: string; @@ -38,11 +45,22 @@ const toast = useToast(); const route = useRoute(); const router = useRouter(); const warehouseStore = useWarehouseStore(); +const productStore = useProductStore(); // Data from API const purchaseData = ref(null); const loading = ref(false); +// Modo de operación: 'purchase' o 'manual' +const operationMode = computed(() => { + return route.query.purchaseId ? 'purchase' : 'manual'; +}); + +// Almacén de destino (para modo manual) +const targetWarehouseId = computed(() => { + return route.query.warehouse ? Number(route.query.warehouse) : null; +}); + // Data const purchaseOrderNumber = ref('ORD-2023-001'); const totalItemsPO = ref(12); @@ -92,6 +110,12 @@ const expandedRows = ref([]); const newSerialNumber = ref(''); const newSerialWarehouse = ref(1); +// Modal de productos +const showProductModal = ref(false); +const productSearch = ref(''); +const selectedProducts = ref([]); +const loadingProducts = ref(false); + const totalReceived = computed(() => { return products.value.reduce((sum, p) => sum + p.quantityReceived, 0); }); @@ -123,11 +147,16 @@ const warehouseSummary = computed(() => { }); const isFormValid = computed(() => { + // En modo manual sin productos, no es válido + if (operationMode.value === 'manual' && products.value.length === 0) { + return false; + } + return products.value.every(product => { if (product.requiresSerial) { return product.serialNumbers.length === product.quantityOrdered; } else { - return product.warehouseId !== null; + return product.warehouseId !== null && product.quantityReceived > 0; } }); }); @@ -183,12 +212,6 @@ async function confirmReceipt() { return; } - const purchaseId = route.query.purchaseId; - if (!purchaseId) { - toast.add({ severity: 'error', summary: 'Error', detail: 'ID de compra no válido', life: 3000 }); - return; - } - try { loading.value = true; @@ -229,8 +252,10 @@ async function confirmReceipt() { // Enviar al API const response = await inventoryWarehouseServices.addInventory(requestData); - // Actualizar estado de la compra a "Ingresada a Inventario" (4) - await purchaseServices.updatePurchaseStatus(Number(purchaseId), '4'); + // Si es desde una compra, actualizar estado + if (operationMode.value === 'purchase' && route.query.purchaseId) { + await purchaseServices.updatePurchaseStatus(Number(route.query.purchaseId), '4'); + } toast.add({ severity: 'success', @@ -239,7 +264,7 @@ async function confirmReceipt() { life: 4000 }); - // Regresar a la vista de compras + // Regresar a la vista anterior setTimeout(() => { router.back(); }, 1000); @@ -261,13 +286,7 @@ async function fetchPurchaseDetails() { const purchaseId = route.query.purchaseId; if (!purchaseId) { - toast.add({ - severity: 'error', - summary: 'Error', - detail: 'No se proporcionó un ID de compra válido', - life: 3000 - }); - router.back(); + // Si no hay purchaseId, estamos en modo manual return; } @@ -309,18 +328,136 @@ async function fetchPurchaseDetails() { } } +function initializeManualMode() { + // En modo manual, limpiar productos de ejemplo y establecer almacén por defecto + products.value = []; + purchaseOrderNumber.value = 'Entrada Manual'; + totalItemsPO.value = 0; + + // Obtener el nombre del almacén si se especificó + if (targetWarehouseId.value) { + const warehouse = warehouseStore.warehouses.find(w => w.id === targetWarehouseId.value); + if (warehouse) { + toast.add({ + severity: 'info', + summary: 'Entrada Manual', + detail: `Agregando inventario al almacén: ${warehouse.name}`, + life: 3000 + }); + } + } +} + onMounted(async () => { await warehouseStore.fetchWarehouses(); // Establecer el primer almacén activo como predeterminado if (warehouseStore.activeWarehouses.length > 0) { newSerialWarehouse.value = warehouseStore.activeWarehouses[0]?.id || 1; } - await fetchPurchaseDetails(); + + // Cargar datos según el modo + if (operationMode.value === 'purchase') { + await fetchPurchaseDetails(); + } else { + initializeManualMode(); + } }); function cancel() { - // Lógica para cancelar y regresar + router.back(); } + +// Funciones para el modal de productos +const openProductModal = async () => { + showProductModal.value = true; + loadingProducts.value = true; + try { + await productStore.fetchProducts(); + } catch (error) { + console.error('Error al cargar productos:', error); + toast.add({ + severity: 'error', + summary: 'Error', + detail: 'No se pudieron cargar los productos', + life: 3000 + }); + } finally { + loadingProducts.value = false; + } +}; + +const closeProductModal = () => { + showProductModal.value = false; + selectedProducts.value = []; + productSearch.value = ''; +}; + +const filteredProducts = computed(() => { + if (!productSearch.value) { + return productStore.activeProducts; + } + + const search = productSearch.value.toLowerCase(); + return productStore.activeProducts.filter(p => + p.name.toLowerCase().includes(search) || + p.sku.toLowerCase().includes(search) || + (p.code && p.code.toLowerCase().includes(search)) + ); +}); + +const addSelectedProducts = () => { + if (selectedProducts.value.length === 0) { + toast.add({ + severity: 'warn', + summary: 'Selección Requerida', + detail: 'Por favor seleccione al menos un producto', + life: 3000 + }); + return; + } + + selectedProducts.value.forEach(product => { + // Verificar si el producto ya está en la lista + const exists = products.value.find(p => p.id === product.id); + if (!exists) { + products.value.push({ + id: product.id, + name: product.name, + sku: product.sku, + category: product.description || 'Sin categoría', + quantityOrdered: 1, // Cantidad inicial + quantityReceived: 0, + warehouseId: targetWarehouseId.value || null, + requiresSerial: product.is_serial, + serialNumbers: [], + purchaseCost: 0, // El usuario puede ajustar esto + attributes: product.attributes || undefined, + }); + } + }); + + toast.add({ + severity: 'success', + summary: 'Productos Agregados', + detail: `Se agregaron ${selectedProducts.value.length} producto(s)`, + life: 3000 + }); + + closeProductModal(); +}; + +const removeProduct = (productId: number) => { + const index = products.value.findIndex(p => p.id === productId); + if (index > -1) { + products.value.splice(index, 1); + toast.add({ + severity: 'info', + summary: 'Producto Eliminado', + detail: 'El producto ha sido eliminado de la lista', + life: 2000 + }); + } +}; diff --git a/src/modules/warehouse/components/WarehouseDetails.vue b/src/modules/warehouse/components/WarehouseDetails.vue index d041e9a..e5acdc2 100644 --- a/src/modules/warehouse/components/WarehouseDetails.vue +++ b/src/modules/warehouse/components/WarehouseDetails.vue @@ -426,7 +426,11 @@ const getMovementTypeSeverity = (type: string) => { }; const openBatchAdd = () => { - router.push({ name: 'BatchAddInventory' }); + const warehouseId = route.params.id; + router.push({ + name: 'WarehouseAddInventory', + query: { warehouse: warehouseId } + }); }; const viewItem = (item: any) => {