- 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.
370 lines
12 KiB
Vue
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>
|