edgar.mendez ad264107f6 feat: add departments and employees management components
- Implement DepartmentsService for CRUD operations on departments.
- Create Employees.vue for managing employee listings, including viewing, editing, and deleting employees.
- Add EmployeesForm.vue for creating and editing employee details with validation.
- Introduce employees.interfaces.ts to define employee-related TypeScript interfaces.
- Implement EmployeesService for API interactions related to employees.
- Add positions.interface.ts and positions.services.ts for managing job positions.
2026-03-10 16:13:26 -06:00

370 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>
<div class="flex flex-col">
<span class="font-semibold text-gray-900 dark:text-white">
{{ slotProps.data.name }}
</span>
<span v-if="slotProps.data.parent" class="text-xs text-gray-500 dark:text-gray-400">
<i class="pi pi-arrow-up-right text-xs"></i> {{ slotProps.data.parent.name }}
</span>
</div>
</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"
:departments="departments"
@save="saveDepartment"
@cancel="showDialog = false"
/>
<!-- Detail Modal -->
<DepartmentDetailModal
v-model:visible="showDetailModal"
:departmentId="selectedDepartmentId"
/>
<!-- 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 './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 './departments.interface';
import DepartmentForm from './DepartmentForm.vue';
import DepartmentDetailModal from './DepartmentDetailModal.vue';
const toast = useToast();
const departmentsService = new DepartmentsService();
// State
const loading = ref(false);
const showDialog = ref(false);
const showDeleteDialog = ref(false);
const showDetailModal = ref(false);
const dialogMode = ref<'create' | 'edit'>('create');
const departmentToDelete = ref<Department | null>(null);
const selectedDepartmentId = ref<number | 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: '',
parent_id: null,
employeeCount: 0,
color: '#3b82f6',
icon: 'pi pi-building'
};
showDialog.value = true;
};
const viewDepartment = (department: Department) => {
selectedDepartmentId.value = department.id;
showDetailModal.value = true;
};
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 || '',
parent_id: data.parent_id || null
};
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,
parent_id: data.parent_id || null
};
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>