From b55c6c1ef08408b8d8bc9722bca0adfa117f4263 Mon Sep 17 00:00:00 2001
From: "edgar.mendez"
Date: Wed, 4 Mar 2026 09:04:39 -0600
Subject: [PATCH] feat: add WarehouseOutInventory component and related
services
- Created a new component for managing warehouse inventory exits (WarehouseOutInventory.vue).
- Implemented inventory movement services to handle API requests for inventory movements.
- Added new interfaces for inventory movements and stock management.
- Updated routing to include the new inventory exit page.
- Enhanced existing services to support inventory exit functionality.
- Added validation and user feedback for inventory exit operations.
---
components.d.ts | 2 +
src/modules/rh/components/Positions.vue | 529 -----------------
.../rh/components/departments/Departments.vue | 58 +-
.../rh/components/positions/Positions.vue | 319 ++++++++++
.../rh/components/positions/PositionsForm.vue | 127 ++++
src/modules/rh/services/positions.services.ts | 45 ++
src/modules/rh/types/positions.interface.ts | 26 +
.../warehouse/components/ModalProducts.vue | 212 +++++++
.../components/ModalStockProducts.vue | 376 ++++++++++++
.../components/WarehouseAddInventory.vue | 198 +------
.../warehouse/components/WarehouseDetails.vue | 316 ++++------
.../warehouse/components/WarehouseIndex.vue | 1 +
.../components/WarehouseOutInventory.vue | 552 ++++++++++++++++++
.../services/inventory-movements.services.ts | 16 +
.../services/inventoryWarehouse.services.ts | 14 +
.../warehouse/services/stock.services.ts | 15 +
.../types/inventory-movements.interfaces.ts | 102 ++++
.../warehouse/types/stock.interfaces.ts | 66 +++
.../types/warehouse-inventory.interfaces.ts | 93 +++
src/router/index.ts | 12 +-
20 files changed, 2090 insertions(+), 989 deletions(-)
delete mode 100644 src/modules/rh/components/Positions.vue
create mode 100644 src/modules/rh/components/positions/Positions.vue
create mode 100644 src/modules/rh/components/positions/PositionsForm.vue
create mode 100644 src/modules/rh/services/positions.services.ts
create mode 100644 src/modules/rh/types/positions.interface.ts
create mode 100644 src/modules/warehouse/components/ModalProducts.vue
create mode 100644 src/modules/warehouse/components/ModalStockProducts.vue
create mode 100644 src/modules/warehouse/components/WarehouseOutInventory.vue
create mode 100644 src/modules/warehouse/services/inventory-movements.services.ts
create mode 100644 src/modules/warehouse/services/stock.services.ts
create mode 100644 src/modules/warehouse/types/inventory-movements.interfaces.ts
create mode 100644 src/modules/warehouse/types/stock.interfaces.ts
create mode 100644 src/modules/warehouse/types/warehouse-inventory.interfaces.ts
diff --git a/components.d.ts b/components.d.ts
index 544ac24..cf2230f 100644
--- a/components.d.ts
+++ b/components.d.ts
@@ -37,6 +37,7 @@ declare module 'vue' {
Menu: typeof import('primevue/menu')['default']
Message: typeof import('primevue/message')['default']
Paginator: typeof import('primevue/paginator')['default']
+ Panel: typeof import('primevue/panel')['default']
ProgressSpinner: typeof import('primevue/progressspinner')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
@@ -45,6 +46,7 @@ declare module 'vue' {
Tag: typeof import('primevue/tag')['default']
Textarea: typeof import('primevue/textarea')['default']
Toast: typeof import('primevue/toast')['default']
+ Toolbar: typeof import('primevue/toolbar')['default']
TopBar: typeof import('./src/components/layout/TopBar.vue')['default']
}
export interface GlobalDirectives {
diff --git a/src/modules/rh/components/Positions.vue b/src/modules/rh/components/Positions.vue
deleted file mode 100644
index a012796..0000000
--- a/src/modules/rh/components/Positions.vue
+++ /dev/null
@@ -1,529 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
- Puestos de Trabajo
-
-
- Administre y organice los roles operativos y administrativos de la planta.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ slotProps.data.name }}
-
-
-
-
-
-
-
-
- {{ slotProps.data.department.toUpperCase() }}
-
-
-
-
-
-
-
- {{ slotProps.data.description }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Total de Puestos
-
{{ positions.length }} Roles
-
-
-
-
-
-
-
-
-
-
-
-
-
Departamentos Activos
-
{{ activeDepartments }} Áreas
-
-
-
-
-
-
-
-
-
-
-
-
-
Última Actualización
-
Hoy, 09:12 AM
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/modules/rh/components/departments/Departments.vue b/src/modules/rh/components/departments/Departments.vue
index 6698c13..afc1ab2 100644
--- a/src/modules/rh/components/departments/Departments.vue
+++ b/src/modules/rh/components/departments/Departments.vue
@@ -23,57 +23,6 @@
/>
-
-
-
-
-
-
-
-
-
-
- Plantilla Total
-
-
{{ totalEmployees }}
-
-
-
-
-
-
-
-
-
-
-
-
-
- Nuevas Vacantes
-
-
5
-
-
-
-
-
-
-
-
-
-
-
-
-
- Próximas Evaluaciones
-
-
12
-
-
-
-
-
-
@@ -192,7 +141,7 @@
diff --git a/src/modules/rh/components/positions/PositionsForm.vue b/src/modules/rh/components/positions/PositionsForm.vue
new file mode 100644
index 0000000..5fd429e
--- /dev/null
+++ b/src/modules/rh/components/positions/PositionsForm.vue
@@ -0,0 +1,127 @@
+
+
+
+
+
diff --git a/src/modules/rh/services/positions.services.ts b/src/modules/rh/services/positions.services.ts
new file mode 100644
index 0000000..f5f3caf
--- /dev/null
+++ b/src/modules/rh/services/positions.services.ts
@@ -0,0 +1,45 @@
+import api from "@/services/api";
+import type { CreatePositionDTO, ResponsePositionsDTO, UpdatePositionDTO } from "../types/positions.interface";
+
+export class PositionsService {
+
+ public async getPositions(): Promise {
+ try {
+ const response = await api.get('/api/rh/job-positions');
+ return response.data;
+ } catch (error) {
+ console.error('Error fetching positions:', error);
+ throw error;
+ }
+ }
+
+ public async createPosition(data: CreatePositionDTO): Promise {
+ try {
+ const response = await api.post('/api/rh/job-positions', data);
+ return response.data;
+ } catch (error) {
+ console.error('Error creating position:', error);
+ throw error;
+ }
+ }
+
+ public async updatePosition(id: number, data: UpdatePositionDTO): Promise {
+ try {
+ const response = await api.put(`/api/rh/job-positions/${id}`, data);
+ return response.data;
+ } catch (error) {
+ console.error('Error updating position:', error);
+ throw error;
+ }
+ }
+
+ public async deletePosition(id: number): Promise {
+ try {
+ await api.delete(`/api/rh/job-positions/${id}`);
+ } catch (error) {
+ console.error('Error deleting position:', error);
+ throw error;
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/modules/rh/types/positions.interface.ts b/src/modules/rh/types/positions.interface.ts
new file mode 100644
index 0000000..314e6ae
--- /dev/null
+++ b/src/modules/rh/types/positions.interface.ts
@@ -0,0 +1,26 @@
+export interface Position {
+ id: number;
+ name: string;
+ code: string;
+ description: string | null;
+ is_active: number;
+ created_at: Date;
+ updated_at: Date;
+ deleted_at: Date | null;
+}
+
+export interface CreatePositionDTO {
+ name: string;
+ code: string;
+ description: string | null;
+}
+
+export interface UpdatePositionDTO extends Partial {}
+
+export interface ResponsePositionsDTO {
+ data: Position[];
+}
+
+export interface DeletePositionDTO {
+ message: string;
+}
\ No newline at end of file
diff --git a/src/modules/warehouse/components/ModalProducts.vue b/src/modules/warehouse/components/ModalProducts.vue
new file mode 100644
index 0000000..361cb5f
--- /dev/null
+++ b/src/modules/warehouse/components/ModalProducts.vue
@@ -0,0 +1,212 @@
+
+
+
+
+
diff --git a/src/modules/warehouse/components/ModalStockProducts.vue b/src/modules/warehouse/components/ModalStockProducts.vue
new file mode 100644
index 0000000..25fa2fd
--- /dev/null
+++ b/src/modules/warehouse/components/ModalStockProducts.vue
@@ -0,0 +1,376 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/warehouse/components/WarehouseAddInventory.vue b/src/modules/warehouse/components/WarehouseAddInventory.vue
index 70f5476..1dcb195 100644
--- a/src/modules/warehouse/components/WarehouseAddInventory.vue
+++ b/src/modules/warehouse/components/WarehouseAddInventory.vue
@@ -8,19 +8,14 @@ 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';
+import ModalProducts from './ModalProducts.vue';
interface SerialNumber {
serial: string;
@@ -45,7 +40,6 @@ const toast = useToast();
const route = useRoute();
const router = useRouter();
const warehouseStore = useWarehouseStore();
-const productStore = useProductStore();
// Data from API
const purchaseData = ref(null);
@@ -112,9 +106,6 @@ 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);
@@ -368,55 +359,12 @@ function cancel() {
}
// Funciones para el modal de productos
-const openProductModal = async () => {
+const openProductModal = () => {
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 => {
+const handleProductsSelected = (selectedProductsList: ProductType[]) => {
+ selectedProductsList.forEach(product => {
// Verificar si el producto ya está en la lista
const exists = products.value.find(p => p.id === product.id);
if (!exists) {
@@ -439,11 +387,9 @@ const addSelectedProducts = () => {
toast.add({
severity: 'success',
summary: 'Productos Agregados',
- detail: `Se agregaron ${selectedProducts.value.length} producto(s)`,
+ detail: `Se agregaron ${selectedProductsList.length} producto(s)`,
life: 3000
});
-
- closeProductModal();
};
const removeProduct = (productId: number) => {
@@ -823,138 +769,10 @@ const removeProduct = (productId: number) => {
-
+ @products-selected="handleProductsSelected"
+ />
diff --git a/src/modules/warehouse/components/WarehouseDetails.vue b/src/modules/warehouse/components/WarehouseDetails.vue
index e5acdc2..5761606 100644
--- a/src/modules/warehouse/components/WarehouseDetails.vue
+++ b/src/modules/warehouse/components/WarehouseDetails.vue
@@ -31,79 +31,11 @@
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Stock Total
-
- {{warehouseData?.stocks.reduce((sum, s) => sum + s.stock, 0).toLocaleString() || 0}}
-
-
-
- Unidades en almacén
-
-
-
-
-
-
@@ -198,7 +130,7 @@
:rowsPerPageOptions="[10, 25, 50]"
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown CurrentPageReport"
currentPageReportTemplate="Mostrando {first} a {last} de {totalRecords} resultados"
- :loading="loading" stripedRows responsiveLayout="scroll" class="text-sm">
+ :loading="loadingMovements" stripedRows responsiveLayout="scroll" class="text-sm">
@@ -219,77 +151,6 @@
-
-
-
-
-
-
-
-
-
@@ -298,7 +159,9 @@ import { ref, onMounted, computed } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { useToast } from 'primevue/usetoast';
import { warehouseService } from '../services/warehouseService';
+import { InventoryMovementsServices } from '../services/inventory-movements.services';
import type { WarehouseDetailData } from '../types/warehouse';
+import type { InventoryMovement } from '../types/inventory-movements.interfaces';
// PrimeVue Components
import Toast from 'primevue/toast';
@@ -316,12 +179,16 @@ const router = useRouter();
const route = useRoute();
const toast = useToast();
+// Services
+const inventoryMovementsService = new InventoryMovementsServices();
+
// Reactive State
const warehouseData = ref(null);
const loading = ref(false);
const activeTab = ref(0);
const selectedCategory = ref('all');
const selectedStockLevel = ref('all');
+const loadingMovements = ref(false);
// Breadcrumb
const breadcrumbHome = ref({ icon: 'pi pi-home', to: '/' });
@@ -348,68 +215,15 @@ const stockLevelOptions = [
// Inventory Data from API
const inventoryData = ref([]);
-// Mock Data - Movement History
-const movementHistory = ref([
- {
- date: '2024-01-30 14:30',
- type: 'Entrada',
- product: 'Laptop Dell XPS 15',
- quantity: '+50',
- user: 'Juan Pérez',
- reference: 'PO-2024-156'
- },
- {
- date: '2024-01-30 12:15',
- type: 'Salida',
- product: 'Monitor LG 27" 4K',
- quantity: '-25',
- user: 'María García',
- reference: 'SO-2024-892'
- },
- {
- date: '2024-01-30 10:00',
- type: 'Entrada',
- product: 'Silla Ergonómica Pro',
- quantity: '+100',
- user: 'Carlos López',
- reference: 'PO-2024-155'
- },
-]);
-
-// Mock Data - Recent Activities (unused - template is commented out)
-// const recentActivities = ref([
-// {
-// id: 1,
-// type: 'in',
-// action: 'Entrada de Stock',
-// product: 'Laptop Dell XPS 15 - 50 unidades',
-// quantity: '+50',
-// time: 'Hace 2 horas'
-// },
-// {
-// id: 2,
-// type: 'out',
-// action: 'Salida de Stock',
-// product: 'Monitor LG 27" - 25 unidades',
-// quantity: '-25',
-// time: 'Hace 4 horas'
-// },
-// {
-// id: 3,
-// type: 'in',
-// action: 'Entrada de Stock',
-// product: 'Silla Ergonómica - 100 unidades',
-// quantity: '+100',
-// time: 'Hace 6 horas'
-// },
-// ]);
-
+// Movement History from API
+const movementHistory = ref([]);
// Methods
const getStatusSeverity = (status: string) => {
const severityMap: Record = {
'En Stock': 'success',
'Stock Bajo': 'warn',
'Stock Crítico': 'danger',
+ 'Sin Stock': 'danger',
'Sobrestock': 'info',
};
return severityMap[status] || 'secondary';
@@ -433,6 +247,14 @@ const openBatchAdd = () => {
});
};
+const openBatchRemove = () => {
+ const warehouseId = route.params.id;
+ router.push({
+ name: 'WarehouseOutOfStock',
+ query: { warehouse: warehouseId }
+ });
+};
+
const viewItem = (item: any) => {
toast.add({
severity: 'info',
@@ -451,24 +273,10 @@ const editItem = (item: any) => {
});
};
-// Unused methods (template sections are commented out)
-// const viewAllMovements = () => {
-// activeTab.value = 1;
-// };
-
-// const generateReport = () => {
-// toast.add({
-// severity: 'success',
-// summary: 'Generando Reporte',
-// detail: 'El reporte se está generando...',
-// life: 3000
-// });
-// };
-
// Helper function to get stock status
const getStockStatus = (stock: number, stockMin: number | null) => {
- if (!stockMin) return 'En Stock';
if (stock === 0) return 'Sin Stock';
+ if (!stockMin) return 'En Stock';
if (stock < stockMin) return 'Stock Bajo';
if (stock < stockMin * 1.5) return 'Stock Crítico';
return 'En Stock';
@@ -487,6 +295,87 @@ const formatRelativeTime = (dateString: string) => {
return 'Hace menos de 1 hora';
};
+// Helper function to format date for movements
+const formatDate = (dateString: string) => {
+ const date = new Date(dateString);
+ return date.toLocaleString('es-MX', {
+ year: 'numeric',
+ month: '2-digit',
+ day: '2-digit',
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+};
+
+// Helper function to capitalize first letter
+const capitalizeFirstLetter = (str: string) => {
+ return str.charAt(0).toUpperCase() + str.slice(1);
+};
+
+// Load inventory movements
+const loadInventoryMovements = async (warehouseId: number) => {
+ loadingMovements.value = true;
+ try {
+ const response = await inventoryMovementsService.getInventoryMovements(warehouseId);
+
+ // Map API data to table format - flatten to show each product as a row
+ movementHistory.value = response.data.flatMap((movement: InventoryMovement) => {
+ const reference = movement.reference_document_id
+ ? `${movement.reference_type || 'DOC'}-${movement.reference_document_id}`
+ : 'Sin referencia';
+
+ // If no products, show basic movement info
+ if (!movement.products || movement.products.length === 0) {
+ return [{
+ date: formatDate(movement.created_at),
+ type: capitalizeFirstLetter(movement.type),
+ product: 'Sin productos',
+ quantity: '0',
+ user: movement.user?.email || 'Sistema',
+ reference: reference
+ }];
+ }
+
+ // Create a row for each product in the movement
+ return movement.products.map((productItem) => {
+ const qty = typeof productItem.quantity === 'string'
+ ? parseFloat(productItem.quantity)
+ : productItem.quantity;
+
+ const formattedQty = movement.type === 'entrada'
+ ? `+${qty}`
+ : `-${qty}`;
+
+ return {
+ date: formatDate(movement.created_at),
+ type: capitalizeFirstLetter(movement.type),
+ product: productItem.product.name,
+ quantity: formattedQty,
+ user: movement.user?.email || 'Sistema',
+ reference: reference
+ };
+ });
+ });
+
+ toast.add({
+ severity: 'success',
+ summary: 'Movimientos Cargados',
+ detail: `${response.data.length} movimientos encontrados`,
+ life: 3000
+ });
+ } catch (error) {
+ console.error('Error al cargar movimientos:', error);
+ toast.add({
+ severity: 'error',
+ summary: 'Error',
+ detail: 'Error al cargar el historial de movimientos',
+ life: 3000
+ });
+ } finally {
+ loadingMovements.value = false;
+ }
+};
+
// Lifecycle
onMounted(async () => {
const warehouseId = route.params.id;
@@ -529,6 +418,9 @@ onMounted(async () => {
detail: `Almacén ${response.data.warehouse.name} cargado exitosamente`,
life: 3000
});
+
+ // Load inventory movements
+ await loadInventoryMovements(Number(warehouseId));
} catch (error) {
console.error('Error al cargar los datos del almacén:', error);
toast.add({
diff --git a/src/modules/warehouse/components/WarehouseIndex.vue b/src/modules/warehouse/components/WarehouseIndex.vue
index 6caa16d..4fd9795 100644
--- a/src/modules/warehouse/components/WarehouseIndex.vue
+++ b/src/modules/warehouse/components/WarehouseIndex.vue
@@ -93,6 +93,7 @@ onMounted(async () => {
Administra, rastrea y organiza todos tus almacenes en un solo lugar.
+