edgar.mendez b55c6c1ef0 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.
2026-03-04 09:04:39 -06:00

356 lines
12 KiB
Vue

<template>
<div class="space-y-6">
<!-- Toast Notifications -->
<Toast position="bottom-right" />
<!-- Breadcrumb -->
<Breadcrumb :home="breadcrumbHome" :model="breadcrumbItems" />
<!-- Page Header -->
<div class="flex flex-wrap items-baseline justify-between gap-4">
<div class="flex flex-col gap-2">
<h1 class="text-gray-900 dark:text-white text-3xl md:text-4xl font-black leading-tight tracking-tight">
Gestión de Departamentos
</h1>
<p class="text-gray-500 dark:text-gray-400 text-sm">
Total: {{ departments.length }} unidades operativas
</p>
</div>
<Button
label="Nuevo Departamento"
icon="pi pi-plus"
@click="openCreateDialog"
/>
</div>
<!-- Departments Table -->
<Card class="shadow-sm">
<template #content>
<DataTable
:value="departments"
:paginator="true"
:rows="10"
:rowsPerPageOptions="[10, 25, 50]"
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown CurrentPageReport"
currentPageReportTemplate="Mostrando {first} a {last} de {totalRecords} departamentos"
:loading="loading"
stripedRows
responsiveLayout="scroll"
class="text-sm"
>
<Column field="name" header="Nombre del Departamento" sortable>
<template #body="slotProps">
<div class="flex items-center gap-3">
<div
class="size-10 rounded-full flex items-center justify-center text-white font-bold"
:style="{ backgroundColor: slotProps.data.color }"
>
<i :class="slotProps.data.icon"></i>
</div>
<span class="font-semibold text-gray-900 dark:text-white">
{{ slotProps.data.name }}
</span>
</div>
</template>
</Column>
<Column field="description" header="Descripción" sortable>
<template #body="slotProps">
<span class="text-sm text-gray-500 dark:text-gray-400">
{{ slotProps.data.description }}
</span>
</template>
</Column>
<Column header="Acciones" :exportable="false">
<template #body="slotProps">
<div class="flex gap-2">
<Button
icon="pi pi-eye"
outlined
rounded
size="small"
severity="secondary"
@click="viewDepartment(slotProps.data)"
/>
<Button
icon="pi pi-pencil"
outlined
rounded
size="small"
@click="editDepartment(slotProps.data)"
/>
<Button
icon="pi pi-trash"
outlined
rounded
size="small"
severity="danger"
@click="confirmDelete(slotProps.data)"
/>
</div>
</template>
</Column>
</DataTable>
</template>
</Card>
<!-- Create/Edit Dialog -->
<DepartmentForm
v-model:visible="showDialog"
:mode="dialogMode"
:formData="formData"
@save="saveDepartment"
@cancel="showDialog = false"
/>
<!-- Delete Confirmation Dialog -->
<Dialog
v-model:visible="showDeleteDialog"
header="Confirmar Eliminación"
:modal="true"
:style="{ width: '450px' }"
>
<div class="flex items-start gap-4">
<i class="pi pi-exclamation-triangle text-3xl text-red-500"></i>
<div>
<p class="text-gray-900 dark:text-white mb-2">
¿Estás seguro de que deseas eliminar el departamento <strong>{{ departmentToDelete?.name }}</strong>?
</p>
<p class="text-sm text-gray-500 dark:text-gray-400">
Esta acción no se puede deshacer.
</p>
</div>
</div>
<template #footer>
<Button
label="Cancelar"
severity="secondary"
@click="showDeleteDialog = false"
/>
<Button
label="Eliminar"
severity="danger"
icon="pi pi-trash"
@click="deleteDepartment"
/>
</template>
</Dialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { useToast } from 'primevue/usetoast';
import { DepartmentsService } from '../../services/departments.services';
// PrimeVue Components
import Toast from 'primevue/toast';
import Breadcrumb from 'primevue/breadcrumb';
import Card from 'primevue/card';
import Button from 'primevue/button';
import DataTable from 'primevue/datatable';
import Column from 'primevue/column';
import Dialog from 'primevue/dialog';
import type { Department, CreateDepartmentDTO, UpdateDepartmentDTO } from '../../types/departments.interface';
import DepartmentForm from './DepartmentForm.vue';
const toast = useToast();
const departmentsService = new DepartmentsService();
// State
const loading = ref(false);
const showDialog = ref(false);
const showDeleteDialog = ref(false);
const dialogMode = ref<'create' | 'edit'>('create');
const departmentToDelete = ref<Department | null>(null);
// Breadcrumb
const breadcrumbHome = ref({ icon: 'pi pi-home', to: '/' });
const breadcrumbItems = ref([
{ label: 'RH', to: '/rh' },
{ label: 'Departamentos' }
]);
// Form Data
const formData = ref<Partial<Department>>({
name: '',
description: '',
employeeCount: 0,
color: '#3b82f6',
icon: 'pi pi-building'
});
// Departments Data
const departments = ref<Department[]>([]);
// Fetch Departments from API
const fetchDepartments = async (showToast = true) => {
loading.value = true;
try {
const response = await departmentsService.getDepartments();
departments.value = response.data;
if (showToast) {
toast.add({
severity: 'success',
summary: 'Departamentos Cargados',
detail: `Se cargaron ${response.data.length} departamentos`,
life: 3000
});
}
} catch (error) {
console.error('Error al cargar departamentos:', error);
toast.add({
severity: 'error',
summary: 'Error',
detail: 'No se pudieron cargar los departamentos',
life: 3000
});
} finally {
loading.value = false;
}
};
// Lifecycle
onMounted(() => {
fetchDepartments();
});
// Methods
const openCreateDialog = () => {
dialogMode.value = 'create';
formData.value = {
name: '',
description: '',
employeeCount: 0,
color: '#3b82f6',
icon: 'pi pi-building'
};
showDialog.value = true;
};
const viewDepartment = (department: Department) => {
toast.add({
severity: 'info',
summary: 'Ver Departamento',
detail: `Visualizando: ${department.name}`,
life: 3000
});
};
const editDepartment = (department: Department) => {
dialogMode.value = 'edit';
formData.value = { ...department };
showDialog.value = true;
};
const confirmDelete = (department: any) => {
departmentToDelete.value = department;
showDeleteDialog.value = true;
};
const deleteDepartment = async () => {
if (!departmentToDelete.value) return;
try {
await departmentsService.deleteDepartment(departmentToDelete.value.id);
// Recargar la lista de departamentos
await fetchDepartments(false);
toast.add({
severity: 'success',
summary: 'Departamento Eliminado',
detail: `${departmentToDelete.value.name} ha sido eliminado exitosamente`,
life: 3000
});
} catch (error) {
console.error('Error al eliminar departamento:', error);
toast.add({
severity: 'error',
summary: 'Error',
detail: 'No se pudo eliminar el departamento',
life: 3000
});
} finally {
showDeleteDialog.value = false;
departmentToDelete.value = null;
}
};
const saveDepartment = async (data: Partial<Department>) => {
if (!data.name) {
toast.add({
severity: 'warn',
summary: 'Validación',
detail: 'El nombre del departamento es requerido',
life: 3000
});
return;
}
try {
if (dialogMode.value === 'create') {
// Create new department
const createData: CreateDepartmentDTO = {
name: data.name,
description: data.description || ''
};
await departmentsService.createDepartment(createData);
toast.add({
severity: 'success',
summary: 'Departamento Creado',
detail: `${data.name} ha sido creado exitosamente`,
life: 3000
});
} else {
// Update existing department
if (!data.id) {
toast.add({
severity: 'error',
summary: 'Error',
detail: 'ID del departamento no encontrado',
life: 3000
});
return;
}
const updateData: UpdateDepartmentDTO = {
name: data.name,
description: data.description
};
await departmentsService.updateDepartment(data.id, updateData);
toast.add({
severity: 'success',
summary: 'Departamento Actualizado',
detail: `${data.name} ha sido actualizado exitosamente`,
life: 3000
});
}
// Recargar la lista de departamentos
await fetchDepartments(false);
showDialog.value = false;
} catch (error) {
console.error('Error al guardar departamento:', error);
toast.add({
severity: 'error',
summary: 'Error',
detail: `No se pudo ${dialogMode.value === 'create' ? 'crear' : 'actualizar'} el departamento`,
life: 3000
});
}
};
</script>
<style scoped>
/* Estilos adicionales si son necesarios */
</style>