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.
This commit is contained in:
parent
ecc053c138
commit
ad264107f6
@ -64,7 +64,8 @@ const menuItems = ref<MenuItem[]>([
|
|||||||
icon: 'pi pi-users',
|
icon: 'pi pi-users',
|
||||||
items: [
|
items: [
|
||||||
{ label: 'Puestos laborales', icon: 'pi pi-user', to: '/rh/positions' },
|
{ label: 'Puestos laborales', icon: 'pi pi-user', to: '/rh/positions' },
|
||||||
{ label: 'Departamentos', icon: 'pi pi-briefcase', to: '/rh/departments' }
|
{ label: 'Departamentos', icon: 'pi pi-briefcase', to: '/rh/departments' },
|
||||||
|
{ label: 'Empleados', icon: 'pi pi-id-card', to: '/rh/employees' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -14,8 +14,8 @@ import Dialog from 'primevue/dialog';
|
|||||||
import Toast from 'primevue/toast';
|
import Toast from 'primevue/toast';
|
||||||
import { useRequisitionStore } from './stores/requisitionStore';
|
import { useRequisitionStore } from './stores/requisitionStore';
|
||||||
import type { RequisitionItem } from './types/requisition.interfaces';
|
import type { RequisitionItem } from './types/requisition.interfaces';
|
||||||
import { DepartmentsService } from '@/modules/rh/services/departments.services';
|
import { DepartmentsService } from '@/modules/rh/components/departments/departments.services';
|
||||||
import type { Department } from '@/modules/rh/types/departments.interface';
|
import type { Department } from '@/modules/rh/components/departments/departments.interface';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
|||||||
@ -16,8 +16,8 @@ import Dialog from 'primevue/dialog';
|
|||||||
import Textarea from 'primevue/textarea';
|
import Textarea from 'primevue/textarea';
|
||||||
import { useRequisitionStore } from './stores/requisitionStore';
|
import { useRequisitionStore } from './stores/requisitionStore';
|
||||||
import type { Requisition } from './types/requisition.interfaces';
|
import type { Requisition } from './types/requisition.interfaces';
|
||||||
import { DepartmentsService } from '@/modules/rh/services/departments.services';
|
import { DepartmentsService } from '@/modules/rh/components/departments/departments.services';
|
||||||
import type { Department } from '@/modules/rh/types/departments.interface';
|
import type { Department } from '@/modules/rh/components/departments/departments.interface';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|||||||
269
src/modules/rh/components/departments/DepartmentDetailModal.vue
Normal file
269
src/modules/rh/components/departments/DepartmentDetailModal.vue
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
:visible="visible"
|
||||||
|
@update:visible="$emit('update:visible', $event)"
|
||||||
|
:modal="true"
|
||||||
|
:style="{ width: '900px' }"
|
||||||
|
:closable="true"
|
||||||
|
class="p-0"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex flex-col gap-1 w-full">
|
||||||
|
<h2 class="text-gray-900 dark:text-white text-2xl font-bold tracking-tight">
|
||||||
|
Detalles del Departamento
|
||||||
|
</h2>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400 text-base font-normal leading-normal">
|
||||||
|
Información completa del departamento y su estructura organizacional
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="loading" class="p-8 flex justify-center items-center">
|
||||||
|
<ProgressSpinner style="width: 50px; height: 50px" strokeWidth="4" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="department" class="p-6 pt-0 space-y-6">
|
||||||
|
<!-- Department Info Card -->
|
||||||
|
<Card class="shadow-sm border border-gray-200 dark:border-gray-800">
|
||||||
|
<template #content>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="flex items-start gap-4">
|
||||||
|
<div
|
||||||
|
class="size-16 rounded-full flex items-center justify-center text-white font-bold text-2xl shrink-0"
|
||||||
|
:style="{ backgroundColor: department.color || '#3b82f6' }"
|
||||||
|
>
|
||||||
|
<i :class="department.icon || 'pi pi-building'"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<h3 class="text-2xl font-bold text-gray-900 dark:text-white mb-1">
|
||||||
|
{{ department.name }}
|
||||||
|
</h3>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400 text-sm">
|
||||||
|
{{ department.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- Parent Department -->
|
||||||
|
<div v-if="department.parent" class="space-y-3">
|
||||||
|
<h4 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||||
|
<i class="pi pi-sitemap text-primary"></i>
|
||||||
|
Departamento Padre
|
||||||
|
</h4>
|
||||||
|
<Card class="shadow-sm border border-gray-200 dark:border-gray-800">
|
||||||
|
<template #content>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="size-10 rounded-full bg-gray-100 dark:bg-gray-800 flex items-center justify-center">
|
||||||
|
<i class="pi pi-building text-gray-600 dark:text-gray-400"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="font-semibold text-gray-900 dark:text-white">
|
||||||
|
{{ department.parent.name }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ department.parent.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="space-y-3">
|
||||||
|
<h4 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||||
|
<i class="pi pi-sitemap text-primary"></i>
|
||||||
|
Jerarquía
|
||||||
|
</h4>
|
||||||
|
<div class="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-800">
|
||||||
|
<p class="text-sm text-blue-800 dark:text-blue-300 flex items-center gap-2">
|
||||||
|
<i class="pi pi-info-circle"></i>
|
||||||
|
Este es un departamento de nivel raíz (sin departamento padre)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Children Departments (Subdepartamentos) -->
|
||||||
|
<div class="space-y-3">
|
||||||
|
<h4 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||||
|
<i class="pi pi-share-alt text-primary"></i>
|
||||||
|
Subdepartamentos
|
||||||
|
<Tag :value="department.children?.length || 0" severity="success" />
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div v-if="department.children && department.children.length > 0" class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||||
|
<Card
|
||||||
|
v-for="child in department.children"
|
||||||
|
:key="child.id"
|
||||||
|
class="shadow-sm border border-gray-200 dark:border-gray-800 hover:shadow-md transition-shadow cursor-pointer"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="size-10 rounded-full bg-primary-100 dark:bg-primary-900/30 flex items-center justify-center">
|
||||||
|
<i class="pi pi-building text-primary-600 dark:text-primary-400"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p class="font-semibold text-gray-900 dark:text-white truncate">
|
||||||
|
{{ child.name }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-gray-500 dark:text-gray-400 truncate">
|
||||||
|
{{ child.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="p-8 text-center bg-gray-50 dark:bg-gray-900/30 rounded-lg border border-gray-200 dark:border-gray-800">
|
||||||
|
<i class="pi pi-folder-open text-4xl text-gray-300 dark:text-gray-700 mb-3"></i>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400">
|
||||||
|
No hay subdepartamentos asignados
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Job Positions -->
|
||||||
|
<div class="space-y-3">
|
||||||
|
<h4 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||||
|
<i class="pi pi-users text-primary"></i>
|
||||||
|
Puestos Laborales
|
||||||
|
<Tag :value="department.job_positions?.length || 0" severity="info" />
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<div v-if="department.job_positions && department.job_positions.length > 0" class="space-y-2">
|
||||||
|
<Card
|
||||||
|
v-for="position in department.job_positions"
|
||||||
|
:key="position.id"
|
||||||
|
class="shadow-sm border border-gray-200 dark:border-gray-800"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<div class="flex items-start justify-between gap-4">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="flex items-center gap-3 mb-2">
|
||||||
|
<span class="px-2.5 py-1 rounded-full text-xs font-bold bg-primary-100 text-primary-700 dark:bg-primary-900/30 dark:text-primary-300">
|
||||||
|
{{ position.code }}
|
||||||
|
</span>
|
||||||
|
<h5 class="font-semibold text-gray-900 dark:text-white">
|
||||||
|
{{ position.name }}
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{{ position.description }}
|
||||||
|
</p>
|
||||||
|
<div v-if="position.parent_id" class="mt-2 flex items-center gap-2 text-xs text-gray-600 dark:text-gray-400">
|
||||||
|
<i class="pi pi-arrow-up-right"></i>
|
||||||
|
<span>Reporta a puesto ID: {{ position.parent_id }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Tag
|
||||||
|
:value="position.is_active ? 'Activo' : 'Inactivo'"
|
||||||
|
:severity="position.is_active ? 'success' : 'danger'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="p-8 text-center bg-gray-50 dark:bg-gray-900/30 rounded-lg border border-gray-200 dark:border-gray-800">
|
||||||
|
<i class="pi pi-inbox text-4xl text-gray-300 dark:text-gray-700 mb-3"></i>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400">
|
||||||
|
No hay puestos laborales asignados a este departamento
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Metadata -->
|
||||||
|
<div class="pt-4 border-t border-gray-200 dark:border-gray-800">
|
||||||
|
<div class="grid grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<span class="text-gray-500 dark:text-gray-400">Creado:</span>
|
||||||
|
<span class="ml-2 text-gray-900 dark:text-white font-medium">
|
||||||
|
{{ formatDate(department.created_at) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-gray-500 dark:text-gray-400">Actualizado:</span>
|
||||||
|
<span class="ml-2 text-gray-900 dark:text-white font-medium">
|
||||||
|
{{ formatDate(department.updated_at) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex items-center justify-end gap-4 pt-4 border-t border-gray-200 dark:border-gray-800">
|
||||||
|
<Button
|
||||||
|
label="Cerrar"
|
||||||
|
icon="pi pi-times"
|
||||||
|
severity="secondary"
|
||||||
|
@click="$emit('update:visible', false)"
|
||||||
|
class="px-6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import Dialog from 'primevue/dialog';
|
||||||
|
import Button from 'primevue/button';
|
||||||
|
import Card from 'primevue/card';
|
||||||
|
import Tag from 'primevue/tag';
|
||||||
|
import ProgressSpinner from 'primevue/progressspinner';
|
||||||
|
import type { Department } from './departments.interface';
|
||||||
|
import { DepartmentsService } from './departments.services';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
visible: boolean;
|
||||||
|
departmentId: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:visible', value: boolean): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
const departmentsService = new DepartmentsService();
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const department = ref<Department | null>(null);
|
||||||
|
|
||||||
|
// Fetch department details when modal opens
|
||||||
|
watch(() => [props.visible, props.departmentId], async ([isVisible, id]) => {
|
||||||
|
if (isVisible && id) {
|
||||||
|
await fetchDepartmentDetails(id as number);
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
|
const fetchDepartmentDetails = async (id: number) => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await departmentsService.getDepartmentById(id);
|
||||||
|
department.value = response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading department details:', error);
|
||||||
|
department.value = null;
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (date: Date | string) => {
|
||||||
|
return new Date(date).toLocaleDateString('es-MX', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Estilos adicionales si son necesarios */
|
||||||
|
</style>
|
||||||
@ -34,6 +34,25 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Parent Department Dropdown -->
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-gray-900 dark:text-gray-200 text-base font-semibold leading-normal">
|
||||||
|
Departamento Padre (Opcional)
|
||||||
|
</label>
|
||||||
|
<Dropdown
|
||||||
|
v-model="localFormData.parent_id"
|
||||||
|
:options="departmentOptions"
|
||||||
|
optionLabel="label"
|
||||||
|
optionValue="value"
|
||||||
|
placeholder="Seleccionar departamento padre"
|
||||||
|
class="w-full h-14"
|
||||||
|
showClear
|
||||||
|
/>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400 text-xs font-normal">
|
||||||
|
Seleccione un departamento padre para crear una jerarquía organizacional. Déjelo vacío para departamentos de nivel raíz.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Department Description Field -->
|
<!-- Department Description Field -->
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="text-gray-900 dark:text-gray-200 text-base font-semibold leading-normal">
|
<label class="text-gray-900 dark:text-gray-200 text-base font-semibold leading-normal">
|
||||||
@ -69,17 +88,19 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch, computed } from 'vue';
|
||||||
import Dialog from 'primevue/dialog';
|
import Dialog from 'primevue/dialog';
|
||||||
import Button from 'primevue/button';
|
import Button from 'primevue/button';
|
||||||
import InputText from 'primevue/inputtext';
|
import InputText from 'primevue/inputtext';
|
||||||
import Textarea from 'primevue/textarea';
|
import Textarea from 'primevue/textarea';
|
||||||
import type { Department } from '../../types/departments.interface';
|
import Dropdown from 'primevue/dropdown';
|
||||||
|
import type { Department } from './departments.interface';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
mode: 'create' | 'edit';
|
mode: 'create' | 'edit';
|
||||||
formData: Partial<Department>;
|
formData: Partial<Department>;
|
||||||
|
departments?: Department[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
@ -93,6 +114,28 @@ const emit = defineEmits<Emits>();
|
|||||||
|
|
||||||
const localFormData = ref<Partial<Department>>({ ...props.formData });
|
const localFormData = ref<Partial<Department>>({ ...props.formData });
|
||||||
|
|
||||||
|
// Computed: Lista de departamentos disponibles para seleccionar como padre
|
||||||
|
// Excluir el departamento actual si estamos en modo edición
|
||||||
|
const availableDepartments = computed(() => {
|
||||||
|
if (!props.departments) return [];
|
||||||
|
|
||||||
|
// Si estamos editando, excluir el departamento actual para evitar referencias circulares
|
||||||
|
if (props.mode === 'edit' && localFormData.value.id) {
|
||||||
|
return props.departments.filter(dept => dept.id !== localFormData.value.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.departments;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Opciones para el dropdown (incluye opción "Sin departamento padre")
|
||||||
|
const departmentOptions = computed(() => [
|
||||||
|
{ label: 'Sin departamento padre (Nivel raíz)', value: null },
|
||||||
|
...availableDepartments.value.map(dept => ({
|
||||||
|
label: dept.name,
|
||||||
|
value: dept.id
|
||||||
|
}))
|
||||||
|
]);
|
||||||
|
|
||||||
// Watch for external formData changes
|
// Watch for external formData changes
|
||||||
watch(() => props.formData, (newData) => {
|
watch(() => props.formData, (newData) => {
|
||||||
localFormData.value = { ...newData };
|
localFormData.value = { ...newData };
|
||||||
|
|||||||
@ -47,9 +47,14 @@
|
|||||||
>
|
>
|
||||||
<i :class="slotProps.data.icon"></i>
|
<i :class="slotProps.data.icon"></i>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
<span class="font-semibold text-gray-900 dark:text-white">
|
<span class="font-semibold text-gray-900 dark:text-white">
|
||||||
{{ slotProps.data.name }}
|
{{ slotProps.data.name }}
|
||||||
</span>
|
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
@ -100,10 +105,17 @@
|
|||||||
v-model:visible="showDialog"
|
v-model:visible="showDialog"
|
||||||
:mode="dialogMode"
|
:mode="dialogMode"
|
||||||
:formData="formData"
|
:formData="formData"
|
||||||
|
:departments="departments"
|
||||||
@save="saveDepartment"
|
@save="saveDepartment"
|
||||||
@cancel="showDialog = false"
|
@cancel="showDialog = false"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Detail Modal -->
|
||||||
|
<DepartmentDetailModal
|
||||||
|
v-model:visible="showDetailModal"
|
||||||
|
:departmentId="selectedDepartmentId"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Delete Confirmation Dialog -->
|
<!-- Delete Confirmation Dialog -->
|
||||||
<Dialog
|
<Dialog
|
||||||
v-model:visible="showDeleteDialog"
|
v-model:visible="showDeleteDialog"
|
||||||
@ -143,7 +155,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue';
|
import { ref, onMounted } from 'vue';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { DepartmentsService } from '../../services/departments.services';
|
import { DepartmentsService } from './departments.services';
|
||||||
|
|
||||||
// PrimeVue Components
|
// PrimeVue Components
|
||||||
import Toast from 'primevue/toast';
|
import Toast from 'primevue/toast';
|
||||||
@ -154,8 +166,9 @@ import DataTable from 'primevue/datatable';
|
|||||||
import Column from 'primevue/column';
|
import Column from 'primevue/column';
|
||||||
import Dialog from 'primevue/dialog';
|
import Dialog from 'primevue/dialog';
|
||||||
|
|
||||||
import type { Department, CreateDepartmentDTO, UpdateDepartmentDTO } from '../../types/departments.interface';
|
import type { Department, CreateDepartmentDTO, UpdateDepartmentDTO } from './departments.interface';
|
||||||
import DepartmentForm from './DepartmentForm.vue';
|
import DepartmentForm from './DepartmentForm.vue';
|
||||||
|
import DepartmentDetailModal from './DepartmentDetailModal.vue';
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const departmentsService = new DepartmentsService();
|
const departmentsService = new DepartmentsService();
|
||||||
@ -164,8 +177,10 @@ const departmentsService = new DepartmentsService();
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const showDialog = ref(false);
|
const showDialog = ref(false);
|
||||||
const showDeleteDialog = ref(false);
|
const showDeleteDialog = ref(false);
|
||||||
|
const showDetailModal = ref(false);
|
||||||
const dialogMode = ref<'create' | 'edit'>('create');
|
const dialogMode = ref<'create' | 'edit'>('create');
|
||||||
const departmentToDelete = ref<Department | null>(null);
|
const departmentToDelete = ref<Department | null>(null);
|
||||||
|
const selectedDepartmentId = ref<number | null>(null);
|
||||||
|
|
||||||
// Breadcrumb
|
// Breadcrumb
|
||||||
const breadcrumbHome = ref({ icon: 'pi pi-home', to: '/' });
|
const breadcrumbHome = ref({ icon: 'pi pi-home', to: '/' });
|
||||||
@ -225,6 +240,7 @@ const openCreateDialog = () => {
|
|||||||
formData.value = {
|
formData.value = {
|
||||||
name: '',
|
name: '',
|
||||||
description: '',
|
description: '',
|
||||||
|
parent_id: null,
|
||||||
employeeCount: 0,
|
employeeCount: 0,
|
||||||
color: '#3b82f6',
|
color: '#3b82f6',
|
||||||
icon: 'pi pi-building'
|
icon: 'pi pi-building'
|
||||||
@ -233,12 +249,8 @@ const openCreateDialog = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const viewDepartment = (department: Department) => {
|
const viewDepartment = (department: Department) => {
|
||||||
toast.add({
|
selectedDepartmentId.value = department.id;
|
||||||
severity: 'info',
|
showDetailModal.value = true;
|
||||||
summary: 'Ver Departamento',
|
|
||||||
detail: `Visualizando: ${department.name}`,
|
|
||||||
life: 3000
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const editDepartment = (department: Department) => {
|
const editDepartment = (department: Department) => {
|
||||||
@ -297,7 +309,8 @@ const saveDepartment = async (data: Partial<Department>) => {
|
|||||||
// Create new department
|
// Create new department
|
||||||
const createData: CreateDepartmentDTO = {
|
const createData: CreateDepartmentDTO = {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
description: data.description || ''
|
description: data.description || '',
|
||||||
|
parent_id: data.parent_id || null
|
||||||
};
|
};
|
||||||
|
|
||||||
await departmentsService.createDepartment(createData);
|
await departmentsService.createDepartment(createData);
|
||||||
@ -322,7 +335,8 @@ const saveDepartment = async (data: Partial<Department>) => {
|
|||||||
|
|
||||||
const updateData: UpdateDepartmentDTO = {
|
const updateData: UpdateDepartmentDTO = {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
description: data.description
|
description: data.description,
|
||||||
|
parent_id: data.parent_id || null
|
||||||
};
|
};
|
||||||
|
|
||||||
await departmentsService.updateDepartment(data.id, updateData);
|
await departmentsService.updateDepartment(data.id, updateData);
|
||||||
|
|||||||
@ -1,7 +1,24 @@
|
|||||||
|
export interface JobPosition {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
description: string;
|
||||||
|
is_active: number;
|
||||||
|
created_at: Date;
|
||||||
|
updated_at: Date;
|
||||||
|
deleted_at: Date | null;
|
||||||
|
parent_id?: number | null;
|
||||||
|
department_id: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Department {
|
export interface Department {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
parent_id?: number | null;
|
||||||
|
parent?: Department;
|
||||||
|
children?: Department[];
|
||||||
|
job_positions?: JobPosition[];
|
||||||
employeeCount?: number;
|
employeeCount?: number;
|
||||||
color?: string;
|
color?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
@ -14,6 +31,10 @@ export interface ResponseDepartmentsDTO {
|
|||||||
data: Department[];
|
data: Department[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ResponseDepartmentDTO {
|
||||||
|
data: Department;
|
||||||
|
}
|
||||||
|
|
||||||
export interface DeleteDepartmentDTO {
|
export interface DeleteDepartmentDTO {
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
@ -21,6 +42,7 @@ export interface DeleteDepartmentDTO {
|
|||||||
export interface CreateDepartmentDTO {
|
export interface CreateDepartmentDTO {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
parent_id?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateDepartmentDTO extends Partial<CreateDepartmentDTO> {}
|
export interface UpdateDepartmentDTO extends Partial<CreateDepartmentDTO> {}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
import type { CreateDepartmentDTO, DeleteDepartmentDTO, ResponseDepartmentsDTO, UpdateDepartmentDTO } from "../types/departments.interface";
|
import type { CreateDepartmentDTO, DeleteDepartmentDTO, ResponseDepartmentsDTO, ResponseDepartmentDTO, UpdateDepartmentDTO } from "./departments.interface";
|
||||||
import api from "@/services/api";
|
import api from "@/services/api";
|
||||||
|
|
||||||
export class DepartmentsService {
|
export class DepartmentsService {
|
||||||
@ -14,6 +14,16 @@ export class DepartmentsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getDepartmentById(id: number): Promise<ResponseDepartmentDTO> {
|
||||||
|
try {
|
||||||
|
const response = await api.get(`/api/rh/departments/${id}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching department:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async createDepartment(data: CreateDepartmentDTO): Promise<ResponseDepartmentsDTO> {
|
public async createDepartment(data: CreateDepartmentDTO): Promise<ResponseDepartmentsDTO> {
|
||||||
try {
|
try {
|
||||||
const response = await api.post('/api/rh/departments', data);
|
const response = await api.post('/api/rh/departments', data);
|
||||||
512
src/modules/rh/components/employees/Employees.vue
Normal file
512
src/modules/rh/components/employees/Employees.vue
Normal file
@ -0,0 +1,512 @@
|
|||||||
|
<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 Empleados
|
||||||
|
</h1>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400 text-sm">
|
||||||
|
Total: {{ employees.length }} empleados registrados
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
label="Nuevo Empleado"
|
||||||
|
icon="pi pi-plus"
|
||||||
|
@click="openCreateDialog"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Employees Table -->
|
||||||
|
<Card class="shadow-sm">
|
||||||
|
<template #content>
|
||||||
|
<DataTable
|
||||||
|
:value="employees"
|
||||||
|
:paginator="true"
|
||||||
|
:rows="10"
|
||||||
|
:rowsPerPageOptions="[10, 25, 50]"
|
||||||
|
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown CurrentPageReport"
|
||||||
|
currentPageReportTemplate="Mostrando {first} a {last} de {totalRecords} empleados"
|
||||||
|
:loading="loading"
|
||||||
|
stripedRows
|
||||||
|
responsiveLayout="scroll"
|
||||||
|
class="text-sm"
|
||||||
|
>
|
||||||
|
<Column header="Empleado" sortable>
|
||||||
|
<template #body="slotProps">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<img
|
||||||
|
:src="slotProps.data.user.profile_photo_url"
|
||||||
|
:alt="getFullName(slotProps.data)"
|
||||||
|
class="w-10 h-10 rounded-full"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="font-semibold text-gray-900 dark:text-white">
|
||||||
|
{{ getFullName(slotProps.data) }}
|
||||||
|
</span>
|
||||||
|
<span class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
{{ slotProps.data.user.email }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column field="gender" header="Género" sortable>
|
||||||
|
<template #body="slotProps">
|
||||||
|
<Tag
|
||||||
|
:value="slotProps.data.gender === 1 ? 'Masculino' : 'Femenino'"
|
||||||
|
:severity="slotProps.data.gender === 1 ? 'info' : 'warn'"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column field="department" header="Departamento" sortable>
|
||||||
|
<template #body="slotProps">
|
||||||
|
<span v-if="slotProps.data.department" class="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
<i class="pi pi-building text-xs mr-1"></i>
|
||||||
|
{{ slotProps.data.department.name }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="text-xs text-gray-400 italic">
|
||||||
|
Sin asignar
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column field="job_position" header="Puesto" sortable>
|
||||||
|
<template #body="slotProps">
|
||||||
|
<span v-if="slotProps.data.job_position" class="text-sm text-gray-700 dark:text-gray-300">
|
||||||
|
<i class="pi pi-briefcase text-xs mr-1"></i>
|
||||||
|
{{ slotProps.data.job_position.name }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="text-xs text-gray-400 italic">
|
||||||
|
Sin asignar
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column field="hire_date" header="Fecha de Contratación" sortable>
|
||||||
|
<template #body="slotProps">
|
||||||
|
<span v-if="slotProps.data.hire_date" class="text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{{ formatDate(slotProps.data.hire_date) }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="text-xs text-gray-400 italic">
|
||||||
|
No registrada
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
|
||||||
|
<Column field="user.status" header="Estado" sortable>
|
||||||
|
<template #body="slotProps">
|
||||||
|
<Tag
|
||||||
|
:value="slotProps.data.user.status === 1 ? 'Activo' : 'Inactivo'"
|
||||||
|
:severity="slotProps.data.user.status === 1 ? 'success' : 'danger'"
|
||||||
|
/>
|
||||||
|
</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="viewEmployee(slotProps.data)"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon="pi pi-pencil"
|
||||||
|
outlined
|
||||||
|
rounded
|
||||||
|
size="small"
|
||||||
|
@click="editEmployee(slotProps.data)"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
icon="pi pi-trash"
|
||||||
|
outlined
|
||||||
|
rounded
|
||||||
|
size="small"
|
||||||
|
severity="danger"
|
||||||
|
@click="confirmDelete(slotProps.data)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
</template>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<!-- 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 al empleado <strong>{{ employeeToDelete ? getFullName(employeeToDelete) : '' }}</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="deleteEmployee"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<!-- Create/Edit Employee Form -->
|
||||||
|
<EmployeesForm
|
||||||
|
ref="employeeFormRef"
|
||||||
|
v-model:visible="showDialog"
|
||||||
|
:mode="dialogMode"
|
||||||
|
:formData="formData"
|
||||||
|
:departments="departments"
|
||||||
|
:positions="positions"
|
||||||
|
@save="saveEmployee"
|
||||||
|
@cancel="showDialog = false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import { employeesService } from './employees.services';
|
||||||
|
import type { Employee, CreateEmployeeDTO, UpdateEmployeeDTO, DepartmentReference, JobPositionReference } from './employees.interfaces';
|
||||||
|
import { DepartmentsService } from '../departments/departments.services';
|
||||||
|
import { PositionsService } from '../positions/positions.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 Tag from 'primevue/tag';
|
||||||
|
import EmployeesForm from './EmployeesForm.vue';
|
||||||
|
|
||||||
|
const toast = useToast();
|
||||||
|
const departmentsService = new DepartmentsService();
|
||||||
|
const positionsService = new PositionsService();
|
||||||
|
|
||||||
|
// State
|
||||||
|
const loading = ref(false);
|
||||||
|
const showDialog = ref(false);
|
||||||
|
const showDeleteDialog = ref(false);
|
||||||
|
const dialogMode = ref<'create' | 'edit'>('create');
|
||||||
|
const employeeToDelete = ref<Employee | null>(null);
|
||||||
|
const employeeFormRef = ref<InstanceType<typeof EmployeesForm> | null>(null);
|
||||||
|
|
||||||
|
// Breadcrumb
|
||||||
|
const breadcrumbHome = ref({ icon: 'pi pi-home', to: '/' });
|
||||||
|
const breadcrumbItems = ref([
|
||||||
|
{ label: 'RH', to: '/rh' },
|
||||||
|
{ label: 'Empleados' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Data
|
||||||
|
const employees = ref<Employee[]>([]);
|
||||||
|
const departments = ref<DepartmentReference[]>([]);
|
||||||
|
const positions = ref<JobPositionReference[]>([]);
|
||||||
|
|
||||||
|
// Form Data
|
||||||
|
const formData = ref<CreateEmployeeDTO | UpdateEmployeeDTO>({
|
||||||
|
name: '',
|
||||||
|
paternal: '',
|
||||||
|
maternal: '',
|
||||||
|
birthdate: '',
|
||||||
|
gender: 0,
|
||||||
|
hire_date: '',
|
||||||
|
department_id: null,
|
||||||
|
job_position_id: null,
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
password_confirmation: '',
|
||||||
|
address: {
|
||||||
|
country: '',
|
||||||
|
postal_code: '',
|
||||||
|
state: '',
|
||||||
|
municipality: '',
|
||||||
|
city: '',
|
||||||
|
street: '',
|
||||||
|
num_ext: '',
|
||||||
|
num_int: ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch Employees from API
|
||||||
|
const fetchEmployees = async (showToast = true) => {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await employeesService.getEmployees({ paginate: false });
|
||||||
|
|
||||||
|
if ('data' in response) {
|
||||||
|
employees.value = response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showToast) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: 'Empleados Cargados',
|
||||||
|
detail: `Se cargaron ${employees.value.length} empleados`,
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al cargar empleados:', error);
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Error',
|
||||||
|
detail: 'No se pudieron cargar los empleados',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch Departments from API
|
||||||
|
const fetchDepartments = async () => {
|
||||||
|
try {
|
||||||
|
const response = await departmentsService.getDepartments();
|
||||||
|
departments.value = response.data.map(dept => ({
|
||||||
|
id: dept.id,
|
||||||
|
name: dept.name,
|
||||||
|
description: dept.description
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al cargar departamentos:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fetch Positions from API
|
||||||
|
const fetchPositions = async () => {
|
||||||
|
try {
|
||||||
|
const response = await positionsService.getPositions();
|
||||||
|
positions.value = response.data.map(pos => ({
|
||||||
|
id: pos.id,
|
||||||
|
name: pos.name,
|
||||||
|
code: pos.code
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al cargar puestos:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
onMounted(() => {
|
||||||
|
fetchEmployees();
|
||||||
|
fetchDepartments();
|
||||||
|
fetchPositions();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helper Methods
|
||||||
|
const getFullName = (employee: Employee): string => {
|
||||||
|
return `${employee.name} ${employee.paternal} ${employee.maternal}`.trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString: string | null): string => {
|
||||||
|
if (!dateString) return 'N/A';
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString('es-MX', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
const openCreateDialog = () => {
|
||||||
|
dialogMode.value = 'create';
|
||||||
|
formData.value = {
|
||||||
|
name: '',
|
||||||
|
paternal: '',
|
||||||
|
maternal: '',
|
||||||
|
birthdate: '',
|
||||||
|
gender: 0,
|
||||||
|
hire_date: '',
|
||||||
|
department_id: null,
|
||||||
|
job_position_id: null,
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
password_confirmation: '',
|
||||||
|
address: {
|
||||||
|
country: 'México',
|
||||||
|
postal_code: '',
|
||||||
|
state: '',
|
||||||
|
municipality: '',
|
||||||
|
city: '',
|
||||||
|
street: '',
|
||||||
|
num_ext: '',
|
||||||
|
num_int: ''
|
||||||
|
}
|
||||||
|
};
|
||||||
|
showDialog.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const viewEmployee = (employee: Employee) => {
|
||||||
|
toast.add({
|
||||||
|
severity: 'info',
|
||||||
|
summary: 'Ver Empleado',
|
||||||
|
detail: `Visualizando: ${getFullName(employee)}`,
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const editEmployee = (employee: Employee) => {
|
||||||
|
dialogMode.value = 'edit';
|
||||||
|
|
||||||
|
// Map EmployeeAddress to AddressDTO structure
|
||||||
|
const addressData = employee.addresses && employee.addresses.length > 0 && employee.addresses[0]
|
||||||
|
? {
|
||||||
|
country: employee.addresses[0].country,
|
||||||
|
postal_code: employee.addresses[0].postal_code,
|
||||||
|
state: employee.addresses[0].state,
|
||||||
|
municipality: employee.addresses[0].city, // Using city as municipality if no municipality field
|
||||||
|
city: employee.addresses[0].city,
|
||||||
|
street: employee.addresses[0].street,
|
||||||
|
num_ext: employee.addresses[0].num_ext,
|
||||||
|
num_int: employee.addresses[0].num_int
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
country: 'México',
|
||||||
|
postal_code: '',
|
||||||
|
state: '',
|
||||||
|
municipality: '',
|
||||||
|
city: '',
|
||||||
|
street: '',
|
||||||
|
num_ext: '',
|
||||||
|
num_int: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
formData.value = {
|
||||||
|
id: employee.id,
|
||||||
|
name: employee.name,
|
||||||
|
paternal: employee.paternal,
|
||||||
|
maternal: employee.maternal,
|
||||||
|
birthdate: employee.birthdate || '',
|
||||||
|
gender: employee.gender,
|
||||||
|
hire_date: employee.hire_date || '',
|
||||||
|
department_id: employee.department_id,
|
||||||
|
job_position_id: employee.job_position_id,
|
||||||
|
email: employee.user.email,
|
||||||
|
address: addressData
|
||||||
|
};
|
||||||
|
showDialog.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmDelete = (employee: Employee) => {
|
||||||
|
employeeToDelete.value = employee;
|
||||||
|
showDeleteDialog.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveEmployee = async (data: CreateEmployeeDTO | UpdateEmployeeDTO) => {
|
||||||
|
try {
|
||||||
|
if (dialogMode.value === 'create') {
|
||||||
|
await employeesService.createEmployee(data as CreateEmployeeDTO);
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: 'Empleado Creado',
|
||||||
|
detail: 'El empleado ha sido registrado exitosamente',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const updateData = data as UpdateEmployeeDTO;
|
||||||
|
await employeesService.updateEmployee(updateData.id, updateData);
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: 'Empleado Actualizado',
|
||||||
|
detail: 'Los datos del empleado han sido actualizados',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await fetchEmployees(false);
|
||||||
|
showDialog.value = false;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error al guardar empleado:', error);
|
||||||
|
|
||||||
|
if (error.response && error.response.status === 422 && error.response.data.errors) {
|
||||||
|
if (employeeFormRef.value) {
|
||||||
|
employeeFormRef.value.setErrors(error.response.data.errors);
|
||||||
|
}
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Error de Validación',
|
||||||
|
detail: 'Por favor revise los campos marcados en rojo',
|
||||||
|
life: 4000
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Error',
|
||||||
|
detail: 'No se pudo guardar el empleado',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteEmployee = async () => {
|
||||||
|
if (!employeeToDelete.value) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await employeesService.deleteEmployee(employeeToDelete.value.id);
|
||||||
|
await fetchEmployees(false);
|
||||||
|
|
||||||
|
toast.add({
|
||||||
|
severity: 'success',
|
||||||
|
summary: 'Empleado Eliminado',
|
||||||
|
detail: `${getFullName(employeeToDelete.value)} ha sido eliminado exitosamente`,
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al eliminar empleado:', error);
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Error',
|
||||||
|
detail: 'No se pudo eliminar el empleado',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
showDeleteDialog.value = false;
|
||||||
|
employeeToDelete.value = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.line-clamp-1 {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
line-clamp: 1;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
533
src/modules/rh/components/employees/EmployeesForm.vue
Normal file
533
src/modules/rh/components/employees/EmployeesForm.vue
Normal file
@ -0,0 +1,533 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
:visible="visible"
|
||||||
|
@update:visible="$emit('update:visible', $event)"
|
||||||
|
:modal="true"
|
||||||
|
:style="{ width: '900px' }"
|
||||||
|
:closable="true"
|
||||||
|
class="p-0"
|
||||||
|
>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex flex-col gap-1 w-full">
|
||||||
|
<h2 class="text-gray-900 dark:text-white text-2xl font-bold tracking-tight">
|
||||||
|
{{ mode === 'create' ? 'Registrar Nuevo Empleado' : 'Editar Empleado' }}
|
||||||
|
</h2>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400 text-base font-normal leading-normal">
|
||||||
|
Complete la información del empleado para su registro en el sistema.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="p-6 pt-0 space-y-6">
|
||||||
|
<!-- Información Personal -->
|
||||||
|
<div class="border-b pb-4">
|
||||||
|
<h3 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Información Personal</h3>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-gray-900 dark:text-gray-200 text-sm font-semibold">
|
||||||
|
Nombre(s) *
|
||||||
|
</label>
|
||||||
|
<InputText
|
||||||
|
v-model="localFormData.name"
|
||||||
|
placeholder="Ej. Juan"
|
||||||
|
class="w-full"
|
||||||
|
:class="{ 'p-invalid': errors.name }"
|
||||||
|
/>
|
||||||
|
<small v-if="errors.name" class="text-red-600">{{ errors.name }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-gray-900 dark:text-gray-200 text-sm font-semibold">
|
||||||
|
Apellido Paterno *
|
||||||
|
</label>
|
||||||
|
<InputText
|
||||||
|
v-model="localFormData.paternal"
|
||||||
|
placeholder="Ej. Pérez"
|
||||||
|
class="w-full"
|
||||||
|
:class="{ 'p-invalid': errors.paternal }"
|
||||||
|
/>
|
||||||
|
<small v-if="errors.paternal" class="text-red-600">{{ errors.paternal }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-gray-900 dark:text-gray-200 text-sm font-semibold">
|
||||||
|
Apellido Materno *
|
||||||
|
</label>
|
||||||
|
<InputText
|
||||||
|
v-model="localFormData.maternal"
|
||||||
|
placeholder="Ej. García"
|
||||||
|
class="w-full"
|
||||||
|
:class="{ 'p-invalid': errors.maternal }"
|
||||||
|
/>
|
||||||
|
<small v-if="errors.maternal" class="text-red-600">{{ errors.maternal }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-gray-900 dark:text-gray-200 text-sm font-semibold">
|
||||||
|
Género *
|
||||||
|
</label>
|
||||||
|
<Dropdown
|
||||||
|
v-model="localFormData.gender"
|
||||||
|
:options="genderOptions"
|
||||||
|
optionLabel="label"
|
||||||
|
optionValue="value"
|
||||||
|
placeholder="Seleccionar género"
|
||||||
|
class="w-full"
|
||||||
|
:class="{ 'p-invalid': errors.gender }"
|
||||||
|
/>
|
||||||
|
<small v-if="errors.gender" class="text-red-600">{{ errors.gender }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-gray-900 dark:text-gray-200 text-sm font-semibold">
|
||||||
|
Fecha de Nacimiento *
|
||||||
|
</label>
|
||||||
|
<Calendar
|
||||||
|
v-model="localFormData.birthdate"
|
||||||
|
dateFormat="yy-mm-dd"
|
||||||
|
placeholder="AAAA-MM-DD"
|
||||||
|
class="w-full"
|
||||||
|
:class="{ 'p-invalid': errors.birthdate }"
|
||||||
|
:maxDate="new Date()"
|
||||||
|
showIcon
|
||||||
|
/>
|
||||||
|
<small v-if="errors.birthdate" class="text-red-600">{{ errors.birthdate }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Información Laboral -->
|
||||||
|
<div class="border-b pb-4">
|
||||||
|
<h3 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Información Laboral</h3>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-gray-900 dark:text-gray-200 text-sm font-semibold">
|
||||||
|
Departamento
|
||||||
|
</label>
|
||||||
|
<Dropdown
|
||||||
|
v-model="localFormData.department_id"
|
||||||
|
:options="departmentOptions"
|
||||||
|
optionLabel="label"
|
||||||
|
optionValue="value"
|
||||||
|
placeholder="Seleccionar departamento"
|
||||||
|
class="w-full"
|
||||||
|
showClear
|
||||||
|
:class="{ 'p-invalid': errors.department_id }"
|
||||||
|
/>
|
||||||
|
<small v-if="errors.department_id" class="text-red-600">{{ errors.department_id }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-gray-900 dark:text-gray-200 text-sm font-semibold">
|
||||||
|
Puesto
|
||||||
|
</label>
|
||||||
|
<Dropdown
|
||||||
|
v-model="localFormData.job_position_id"
|
||||||
|
:options="positionOptions"
|
||||||
|
optionLabel="label"
|
||||||
|
optionValue="value"
|
||||||
|
placeholder="Seleccionar puesto"
|
||||||
|
class="w-full"
|
||||||
|
showClear
|
||||||
|
:class="{ 'p-invalid': errors.job_position_id }"
|
||||||
|
/>
|
||||||
|
<small v-if="errors.job_position_id" class="text-red-600">{{ errors.job_position_id }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-gray-900 dark:text-gray-200 text-sm font-semibold">
|
||||||
|
Fecha de Contratación *
|
||||||
|
</label>
|
||||||
|
<Calendar
|
||||||
|
v-model="localFormData.hire_date"
|
||||||
|
dateFormat="yy-mm-dd"
|
||||||
|
placeholder="AAAA-MM-DD"
|
||||||
|
class="w-full"
|
||||||
|
:class="{ 'p-invalid': errors.hire_date }"
|
||||||
|
showIcon
|
||||||
|
/>
|
||||||
|
<small v-if="errors.hire_date" class="text-red-600">{{ errors.hire_date }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Credenciales de Usuario -->
|
||||||
|
<div class="border-b pb-4">
|
||||||
|
<h3 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Credenciales de Acceso</h3>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div class="flex flex-col gap-2 md:col-span-2">
|
||||||
|
<label class="text-gray-900 dark:text-gray-200 text-sm font-semibold">
|
||||||
|
Correo Electrónico *
|
||||||
|
</label>
|
||||||
|
<InputText
|
||||||
|
v-model="localFormData.email"
|
||||||
|
type="email"
|
||||||
|
placeholder="ejemplo@empresa.com"
|
||||||
|
class="w-full"
|
||||||
|
:class="{ 'p-invalid': errors.email }"
|
||||||
|
/>
|
||||||
|
<small v-if="errors.email" class="text-red-600">{{ errors.email }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="mode === 'create'" class="flex flex-col gap-2">
|
||||||
|
<label class="text-gray-900 dark:text-gray-200 text-sm font-semibold">
|
||||||
|
Contraseña *
|
||||||
|
</label>
|
||||||
|
<Password
|
||||||
|
v-model="localFormData.password"
|
||||||
|
placeholder="Mínimo 8 caracteres"
|
||||||
|
class="w-full"
|
||||||
|
:class="{ 'p-invalid': errors.password }"
|
||||||
|
toggleMask
|
||||||
|
:feedback="false"
|
||||||
|
/>
|
||||||
|
<small v-if="errors.password" class="text-red-600">{{ errors.password }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="mode === 'create'" class="flex flex-col gap-2">
|
||||||
|
<label class="text-gray-900 dark:text-gray-200 text-sm font-semibold">
|
||||||
|
Confirmar Contraseña *
|
||||||
|
</label>
|
||||||
|
<Password
|
||||||
|
v-model="localFormData.password_confirmation"
|
||||||
|
placeholder="Repetir contraseña"
|
||||||
|
class="w-full"
|
||||||
|
:class="{ 'p-invalid': errors.password_confirmation }"
|
||||||
|
toggleMask
|
||||||
|
:feedback="false"
|
||||||
|
/>
|
||||||
|
<small v-if="errors.password_confirmation" class="text-red-600">{{ errors.password_confirmation }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dirección -->
|
||||||
|
<div>
|
||||||
|
<h3 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Dirección</h3>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-gray-900 dark:text-gray-200 text-sm font-semibold">
|
||||||
|
Calle *
|
||||||
|
</label>
|
||||||
|
<InputText
|
||||||
|
v-model="localFormData.address.street"
|
||||||
|
placeholder="Ej. Av. Constitución"
|
||||||
|
class="w-full"
|
||||||
|
:class="{ 'p-invalid': errors['address.street'] }"
|
||||||
|
/>
|
||||||
|
<small v-if="errors['address.street']" class="text-red-600">{{ errors['address.street'] }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-gray-900 dark:text-gray-200 text-sm font-semibold">
|
||||||
|
Número Exterior *
|
||||||
|
</label>
|
||||||
|
<InputText
|
||||||
|
v-model="localFormData.address.num_ext"
|
||||||
|
placeholder="Ej. 123"
|
||||||
|
class="w-full"
|
||||||
|
:class="{ 'p-invalid': errors['address.num_ext'] }"
|
||||||
|
/>
|
||||||
|
<small v-if="errors['address.num_ext']" class="text-red-600">{{ errors['address.num_ext'] }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-gray-900 dark:text-gray-200 text-sm font-semibold">
|
||||||
|
Número Interior
|
||||||
|
</label>
|
||||||
|
<InputText
|
||||||
|
v-model="localFormData.address.num_int"
|
||||||
|
placeholder="Ej. 4A"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-gray-900 dark:text-gray-200 text-sm font-semibold">
|
||||||
|
Código Postal *
|
||||||
|
</label>
|
||||||
|
<InputText
|
||||||
|
v-model="localFormData.address.postal_code"
|
||||||
|
placeholder="Ej. 64000"
|
||||||
|
class="w-full"
|
||||||
|
:class="{ 'p-invalid': errors['address.postal_code'] }"
|
||||||
|
/>
|
||||||
|
<small v-if="errors['address.postal_code']" class="text-red-600">{{ errors['address.postal_code'] }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-gray-900 dark:text-gray-200 text-sm font-semibold">
|
||||||
|
Ciudad *
|
||||||
|
</label>
|
||||||
|
<InputText
|
||||||
|
v-model="localFormData.address.city"
|
||||||
|
placeholder="Ej. Monterrey"
|
||||||
|
class="w-full"
|
||||||
|
:class="{ 'p-invalid': errors['address.city'] }"
|
||||||
|
/>
|
||||||
|
<small v-if="errors['address.city']" class="text-red-600">{{ errors['address.city'] }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-gray-900 dark:text-gray-200 text-sm font-semibold">
|
||||||
|
Municipio *
|
||||||
|
</label>
|
||||||
|
<InputText
|
||||||
|
v-model="localFormData.address.municipality"
|
||||||
|
placeholder="Ej. Monterrey"
|
||||||
|
class="w-full"
|
||||||
|
:class="{ 'p-invalid': errors['address.municipality'] }"
|
||||||
|
/>
|
||||||
|
<small v-if="errors['address.municipality']" class="text-red-600">{{ errors['address.municipality'] }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-gray-900 dark:text-gray-200 text-sm font-semibold">
|
||||||
|
Estado *
|
||||||
|
</label>
|
||||||
|
<InputText
|
||||||
|
v-model="localFormData.address.state"
|
||||||
|
placeholder="Ej. Nuevo León"
|
||||||
|
class="w-full"
|
||||||
|
:class="{ 'p-invalid': errors['address.state'] }"
|
||||||
|
/>
|
||||||
|
<small v-if="errors['address.state']" class="text-red-600">{{ errors['address.state'] }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-gray-900 dark:text-gray-200 text-sm font-semibold">
|
||||||
|
País *
|
||||||
|
</label>
|
||||||
|
<InputText
|
||||||
|
v-model="localFormData.address.country"
|
||||||
|
placeholder="Ej. México"
|
||||||
|
class="w-full"
|
||||||
|
:class="{ 'p-invalid': errors['address.country'] }"
|
||||||
|
/>
|
||||||
|
<small v-if="errors['address.country']" class="text-red-600">{{ errors['address.country'] }}</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex items-center justify-end gap-4 pt-4 border-t border-gray-200 dark:border-gray-800">
|
||||||
|
<Button
|
||||||
|
label="Cancelar"
|
||||||
|
severity="secondary"
|
||||||
|
text
|
||||||
|
@click="handleCancel"
|
||||||
|
class="px-6"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
:label="mode === 'create' ? 'Registrar Empleado' : 'Actualizar Empleado'"
|
||||||
|
icon="pi pi-save"
|
||||||
|
@click="handleSave"
|
||||||
|
class="px-8"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, computed } from 'vue';
|
||||||
|
import Dialog from 'primevue/dialog';
|
||||||
|
import Button from 'primevue/button';
|
||||||
|
import InputText from 'primevue/inputtext';
|
||||||
|
import Dropdown from 'primevue/dropdown';
|
||||||
|
import Calendar from 'primevue/calendar';
|
||||||
|
import Password from 'primevue/password';
|
||||||
|
import type { CreateEmployeeDTO, UpdateEmployeeDTO, AddressDTO } from './employees.interfaces';
|
||||||
|
import type { DepartmentReference, JobPositionReference } from './employees.interfaces';
|
||||||
|
|
||||||
|
// Local form data type that always has address
|
||||||
|
interface LocalFormData {
|
||||||
|
id?: number;
|
||||||
|
name: string;
|
||||||
|
paternal: string;
|
||||||
|
maternal: string;
|
||||||
|
birthdate: Date | null;
|
||||||
|
gender: number;
|
||||||
|
hire_date: Date | null;
|
||||||
|
department_id: number | null;
|
||||||
|
job_position_id: number | null;
|
||||||
|
email: string;
|
||||||
|
password?: string;
|
||||||
|
password_confirmation?: string;
|
||||||
|
address: AddressDTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
visible: boolean;
|
||||||
|
mode: 'create' | 'edit';
|
||||||
|
formData: CreateEmployeeDTO | UpdateEmployeeDTO;
|
||||||
|
departments?: DepartmentReference[];
|
||||||
|
positions?: JobPositionReference[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:visible', value: boolean): void;
|
||||||
|
(e: 'save', data: CreateEmployeeDTO | UpdateEmployeeDTO): void;
|
||||||
|
(e: 'cancel'): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const emit = defineEmits<Emits>();
|
||||||
|
|
||||||
|
// Convert string date to Date object
|
||||||
|
const parseDate = (dateString: string | undefined | null): Date | null => {
|
||||||
|
if (!dateString) return null;
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return isNaN(date.getTime()) ? null : date;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize local form data with proper structure
|
||||||
|
const initializeFormData = (data: CreateEmployeeDTO | UpdateEmployeeDTO): LocalFormData => {
|
||||||
|
return {
|
||||||
|
id: 'id' in data ? data.id : undefined,
|
||||||
|
name: data.name || '',
|
||||||
|
paternal: data.paternal || '',
|
||||||
|
maternal: data.maternal || '',
|
||||||
|
birthdate: parseDate(data.birthdate),
|
||||||
|
gender: data.gender || 0,
|
||||||
|
hire_date: parseDate(data.hire_date),
|
||||||
|
department_id: data.department_id || null,
|
||||||
|
job_position_id: data.job_position_id || null,
|
||||||
|
email: data.email || '',
|
||||||
|
password: 'password' in data ? data.password : undefined,
|
||||||
|
password_confirmation: 'password_confirmation' in data ? data.password_confirmation : undefined,
|
||||||
|
address: data.address || {
|
||||||
|
country: '',
|
||||||
|
postal_code: '',
|
||||||
|
state: '',
|
||||||
|
municipality: '',
|
||||||
|
city: '',
|
||||||
|
street: '',
|
||||||
|
num_ext: '',
|
||||||
|
num_int: ''
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Local form data
|
||||||
|
const localFormData = ref<LocalFormData>(initializeFormData(props.formData));
|
||||||
|
|
||||||
|
// Validation errors
|
||||||
|
const errors = ref<Record<string, string>>({});
|
||||||
|
|
||||||
|
// Gender options
|
||||||
|
const genderOptions = [
|
||||||
|
{ label: 'Femenino', value: 0 },
|
||||||
|
{ label: 'Masculino', value: 1 }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Department options
|
||||||
|
const departmentOptions = computed(() => {
|
||||||
|
return props.departments?.map(dept => ({
|
||||||
|
label: dept.name,
|
||||||
|
value: dept.id
|
||||||
|
})) || [];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Position options
|
||||||
|
const positionOptions = computed(() => {
|
||||||
|
return props.positions?.map(pos => ({
|
||||||
|
label: pos.name,
|
||||||
|
value: pos.id
|
||||||
|
})) || [];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Watch for formData changes
|
||||||
|
watch(() => props.formData, (newData) => {
|
||||||
|
localFormData.value = initializeFormData(newData);
|
||||||
|
errors.value = {};
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
// Format date for API (Calendar works with Date objects)
|
||||||
|
const formatDateForAPI = (date: Date | null): string | null => {
|
||||||
|
if (!date || !(date instanceof Date)) return null;
|
||||||
|
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
const handleSave = () => {
|
||||||
|
errors.value = {};
|
||||||
|
|
||||||
|
// Prepare data for API
|
||||||
|
if (props.mode === 'create') {
|
||||||
|
const dataToSave: CreateEmployeeDTO = {
|
||||||
|
name: localFormData.value.name,
|
||||||
|
paternal: localFormData.value.paternal,
|
||||||
|
maternal: localFormData.value.maternal,
|
||||||
|
birthdate: formatDateForAPI(localFormData.value.birthdate) || '',
|
||||||
|
gender: localFormData.value.gender,
|
||||||
|
hire_date: formatDateForAPI(localFormData.value.hire_date) || '',
|
||||||
|
department_id: localFormData.value.department_id,
|
||||||
|
job_position_id: localFormData.value.job_position_id,
|
||||||
|
email: localFormData.value.email,
|
||||||
|
password: localFormData.value.password || '',
|
||||||
|
password_confirmation: localFormData.value.password_confirmation || '',
|
||||||
|
address: localFormData.value.address
|
||||||
|
};
|
||||||
|
emit('save', dataToSave);
|
||||||
|
} else {
|
||||||
|
const dataToSave: UpdateEmployeeDTO = {
|
||||||
|
id: localFormData.value.id!,
|
||||||
|
name: localFormData.value.name,
|
||||||
|
paternal: localFormData.value.paternal,
|
||||||
|
maternal: localFormData.value.maternal,
|
||||||
|
birthdate: formatDateForAPI(localFormData.value.birthdate) || '',
|
||||||
|
gender: localFormData.value.gender,
|
||||||
|
hire_date: formatDateForAPI(localFormData.value.hire_date) || '',
|
||||||
|
department_id: localFormData.value.department_id,
|
||||||
|
job_position_id: localFormData.value.job_position_id,
|
||||||
|
email: localFormData.value.email,
|
||||||
|
address: localFormData.value.address
|
||||||
|
};
|
||||||
|
emit('save', dataToSave);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
errors.value = {};
|
||||||
|
emit('cancel');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Expose method to set errors from parent component
|
||||||
|
const setErrors = (validationErrors: Record<string, string[]>) => {
|
||||||
|
errors.value = {};
|
||||||
|
Object.keys(validationErrors).forEach((key) => {
|
||||||
|
const errorMessages = validationErrors[key];
|
||||||
|
if (errorMessages && errorMessages[0]) {
|
||||||
|
errors.value[key] = errorMessages[0];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
setErrors
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.p-password) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.p-password input) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.p-calendar) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.p-calendar input) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
144
src/modules/rh/components/employees/employees.interfaces.ts
Normal file
144
src/modules/rh/components/employees/employees.interfaces.ts
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
// User Interface
|
||||||
|
export interface User {
|
||||||
|
id: number;
|
||||||
|
email: string;
|
||||||
|
email_verified_at: string | null;
|
||||||
|
status: number;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
deleted_at: string | null;
|
||||||
|
full_name: string;
|
||||||
|
last_name: string;
|
||||||
|
profile_photo_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Department Reference (simplified)
|
||||||
|
export interface DepartmentReference {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Job Position Reference (simplified)
|
||||||
|
export interface JobPositionReference {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Address Interface
|
||||||
|
export interface EmployeeAddress {
|
||||||
|
id: number;
|
||||||
|
street: string;
|
||||||
|
num_ext: string;
|
||||||
|
num_int?: string;
|
||||||
|
postal_code: string;
|
||||||
|
city: string;
|
||||||
|
state: string;
|
||||||
|
country: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Employee Interface
|
||||||
|
export interface Employee {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
paternal: string;
|
||||||
|
maternal: string;
|
||||||
|
birthdate: string | null;
|
||||||
|
gender: number;
|
||||||
|
hire_date: string | null;
|
||||||
|
department_id: number | null;
|
||||||
|
job_position_id: number | null;
|
||||||
|
user_id: number;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
deleted_at: string | null;
|
||||||
|
user: User;
|
||||||
|
department: DepartmentReference | null;
|
||||||
|
job_position: JobPositionReference | null;
|
||||||
|
addresses: EmployeeAddress[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pagination Link
|
||||||
|
export interface PaginationLink {
|
||||||
|
url: string | null;
|
||||||
|
label: string;
|
||||||
|
active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paginated Response
|
||||||
|
export interface PaginatedEmployeesResponse {
|
||||||
|
current_page: number;
|
||||||
|
data: Employee[];
|
||||||
|
first_page_url: string;
|
||||||
|
from: number;
|
||||||
|
last_page: number;
|
||||||
|
last_page_url: string;
|
||||||
|
links: PaginationLink[];
|
||||||
|
next_page_url: string | null;
|
||||||
|
path: string;
|
||||||
|
per_page: number;
|
||||||
|
prev_page_url: string | null;
|
||||||
|
to: number;
|
||||||
|
total: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple Response (non-paginated)
|
||||||
|
export interface SimpleEmployeesResponse {
|
||||||
|
data: Employee[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query Parameters
|
||||||
|
export interface EmployeeQueryParams {
|
||||||
|
page?: number;
|
||||||
|
per_page?: number;
|
||||||
|
name?: string;
|
||||||
|
department_id?: number;
|
||||||
|
job_position_id?: number;
|
||||||
|
paginate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Address DTO for Create/Update
|
||||||
|
export interface AddressDTO {
|
||||||
|
country: string;
|
||||||
|
postal_code: string;
|
||||||
|
state: string;
|
||||||
|
municipality: string;
|
||||||
|
city: string;
|
||||||
|
street: string;
|
||||||
|
num_ext: string;
|
||||||
|
num_int?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Employee DTO
|
||||||
|
export interface CreateEmployeeDTO {
|
||||||
|
name: string;
|
||||||
|
paternal: string;
|
||||||
|
maternal: string;
|
||||||
|
birthdate: string;
|
||||||
|
gender: number;
|
||||||
|
hire_date: string;
|
||||||
|
department_id: number | null;
|
||||||
|
job_position_id: number | null;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
password_confirmation: string;
|
||||||
|
address: AddressDTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update Employee DTO
|
||||||
|
export interface UpdateEmployeeDTO {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
paternal: string;
|
||||||
|
maternal: string;
|
||||||
|
birthdate: string;
|
||||||
|
gender: number;
|
||||||
|
hire_date: string;
|
||||||
|
department_id: number | null;
|
||||||
|
job_position_id: number | null;
|
||||||
|
email: string;
|
||||||
|
password?: string;
|
||||||
|
password_confirmation?: string;
|
||||||
|
address: AddressDTO;
|
||||||
|
}
|
||||||
88
src/modules/rh/components/employees/employees.services.ts
Normal file
88
src/modules/rh/components/employees/employees.services.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import api from "@/services/api";
|
||||||
|
import type {
|
||||||
|
PaginatedEmployeesResponse,
|
||||||
|
SimpleEmployeesResponse,
|
||||||
|
EmployeeQueryParams,
|
||||||
|
CreateEmployeeDTO,
|
||||||
|
UpdateEmployeeDTO,
|
||||||
|
Employee
|
||||||
|
} from "./employees.interfaces";
|
||||||
|
|
||||||
|
export class EmployeesService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all employees with optional pagination
|
||||||
|
* @param params - Query parameters for filtering and pagination
|
||||||
|
* @returns PaginatedEmployeesResponse or SimpleEmployeesResponse
|
||||||
|
*/
|
||||||
|
public async getEmployees(params?: EmployeeQueryParams): Promise<PaginatedEmployeesResponse | SimpleEmployeesResponse> {
|
||||||
|
try {
|
||||||
|
const response = await api.get('/api/rh/employees', { params });
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching employees:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new employee
|
||||||
|
* @param data - Employee data
|
||||||
|
* @returns Created employee
|
||||||
|
*/
|
||||||
|
public async createEmployee(data: CreateEmployeeDTO): Promise<Employee> {
|
||||||
|
try {
|
||||||
|
const response = await api.post('/api/rh/employees', data);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating employee:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing employee
|
||||||
|
* @param id - Employee ID
|
||||||
|
* @param data - Updated employee data
|
||||||
|
* @returns Updated employee
|
||||||
|
*/
|
||||||
|
public async updateEmployee(id: number, data: UpdateEmployeeDTO): Promise<Employee> {
|
||||||
|
try {
|
||||||
|
const response = await api.put(`/api/rh/employees/${id}`, data);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating employee:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an employee
|
||||||
|
* @param id - Employee ID
|
||||||
|
*/
|
||||||
|
public async deleteEmployee(id: number): Promise<void> {
|
||||||
|
try {
|
||||||
|
await api.delete(`/api/rh/employees/${id}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting employee:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get employee by ID
|
||||||
|
* @param id - Employee ID
|
||||||
|
* @returns Employee details
|
||||||
|
*/
|
||||||
|
public async getEmployeeById(id: number): Promise<Employee> {
|
||||||
|
try {
|
||||||
|
const response = await api.get(`/api/rh/employees/${id}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching employee:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const employeesService = new EmployeesService();
|
||||||
@ -37,10 +37,16 @@
|
|||||||
</Column>
|
</Column>
|
||||||
<Column field="name" header="Nombre del Puesto" sortable>
|
<Column field="name" header="Nombre del Puesto" sortable>
|
||||||
<template #body="slotProps">
|
<template #body="slotProps">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex flex-col">
|
||||||
<span class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
<span class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||||
{{ slotProps.data.name }}
|
{{ slotProps.data.name }}
|
||||||
</span>
|
</span>
|
||||||
|
<span v-if="slotProps.data.department" class="text-xs text-gray-500 dark:text-gray-400">
|
||||||
|
<i class="pi pi-building text-xs"></i> {{ slotProps.data.department.name }}
|
||||||
|
</span>
|
||||||
|
<span v-if="slotProps.data.parent" class="text-xs text-primary-600 dark:text-primary-400">
|
||||||
|
<i class="pi pi-arrow-up-right text-xs"></i> {{ slotProps.data.parent.name }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
@ -68,8 +74,15 @@
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<!-- Create/Edit Dialog -->
|
<!-- Create/Edit Dialog -->
|
||||||
<PositionsForm v-model:visible="showDialog" :mode="dialogMode" :formData="formData" @save="savePosition"
|
<PositionsForm
|
||||||
@cancel="showDialog = false" />
|
v-model:visible="showDialog"
|
||||||
|
:mode="dialogMode"
|
||||||
|
:formData="formData"
|
||||||
|
:departments="departments"
|
||||||
|
:positions="positions"
|
||||||
|
@save="savePosition"
|
||||||
|
@cancel="showDialog = false"
|
||||||
|
/>
|
||||||
|
|
||||||
<!-- Delete Confirmation Dialog -->
|
<!-- Delete Confirmation Dialog -->
|
||||||
<Dialog v-model:visible="showDeleteDialog" header="Confirmar Eliminación" :modal="true"
|
<Dialog v-model:visible="showDeleteDialog" header="Confirmar Eliminación" :modal="true"
|
||||||
@ -97,8 +110,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue';
|
import { ref, computed, onMounted } from 'vue';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { PositionsService } from '../../services/positions.services';
|
import { PositionsService } from './positions.services';
|
||||||
import type { Position, CreatePositionDTO, UpdatePositionDTO } from '../../types/positions.interface';
|
import { DepartmentsService } from '../departments/departments.services';
|
||||||
|
import type { Position, CreatePositionDTO, UpdatePositionDTO } from './positions.interface';
|
||||||
|
import type { Department } from '../departments/departments.interface';
|
||||||
|
|
||||||
// PrimeVue Components
|
// PrimeVue Components
|
||||||
import Toast from 'primevue/toast';
|
import Toast from 'primevue/toast';
|
||||||
@ -112,6 +127,7 @@ import PositionsForm from './PositionsForm.vue';
|
|||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const positionsService = new PositionsService();
|
const positionsService = new PositionsService();
|
||||||
|
const departmentsService = new DepartmentsService();
|
||||||
|
|
||||||
// State
|
// State
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
@ -134,11 +150,14 @@ const breadcrumbItems = ref([
|
|||||||
const formData = ref<CreatePositionDTO | UpdatePositionDTO>({
|
const formData = ref<CreatePositionDTO | UpdatePositionDTO>({
|
||||||
name: '',
|
name: '',
|
||||||
code: '',
|
code: '',
|
||||||
description: ''
|
description: '',
|
||||||
|
department_id: null,
|
||||||
|
parent_id: null
|
||||||
});
|
});
|
||||||
|
|
||||||
// Positions Data
|
// Positions Data
|
||||||
const positions = ref<Position[]>([]);
|
const positions = ref<Position[]>([]);
|
||||||
|
const departments = ref<Department[]>([]);
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
const filteredPositions = computed(() => {
|
const filteredPositions = computed(() => {
|
||||||
@ -182,8 +201,25 @@ const fetchPositions = async (showToast = true) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Fetch Departments from API
|
||||||
|
const fetchDepartments = async () => {
|
||||||
|
try {
|
||||||
|
const response = await departmentsService.getDepartments();
|
||||||
|
departments.value = response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error al cargar departamentos:', error);
|
||||||
|
toast.add({
|
||||||
|
severity: 'error',
|
||||||
|
summary: 'Error',
|
||||||
|
detail: 'No se pudieron cargar los departamentos',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Lifecycle
|
// Lifecycle
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
fetchDepartments();
|
||||||
fetchPositions();
|
fetchPositions();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -192,7 +228,9 @@ const openCreateDialog = () => {
|
|||||||
formData.value = {
|
formData.value = {
|
||||||
name: '',
|
name: '',
|
||||||
code: '',
|
code: '',
|
||||||
description: ''
|
description: '',
|
||||||
|
department_id: 0,
|
||||||
|
parent_id: null
|
||||||
};
|
};
|
||||||
showDialog.value = true;
|
showDialog.value = true;
|
||||||
};
|
};
|
||||||
@ -251,7 +289,17 @@ const savePosition = async (data: CreatePositionDTO | UpdatePositionDTO) => {
|
|||||||
toast.add({
|
toast.add({
|
||||||
severity: 'warn',
|
severity: 'warn',
|
||||||
summary: 'Validación',
|
summary: 'Validación',
|
||||||
detail: 'Nombre y departamento son requeridos',
|
detail: 'Nombre y código son requeridos',
|
||||||
|
life: 3000
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!data.department_id) {
|
||||||
|
toast.add({
|
||||||
|
severity: 'warn',
|
||||||
|
summary: 'Validación',
|
||||||
|
detail: 'El departamento es requerido',
|
||||||
life: 3000
|
life: 3000
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@ -263,7 +311,9 @@ const savePosition = async (data: CreatePositionDTO | UpdatePositionDTO) => {
|
|||||||
const createData: CreatePositionDTO = {
|
const createData: CreatePositionDTO = {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
code: data.code,
|
code: data.code,
|
||||||
description: data.description || null
|
description: data.description || null,
|
||||||
|
department_id: data.department_id,
|
||||||
|
parent_id: data.parent_id || null
|
||||||
};
|
};
|
||||||
|
|
||||||
await positionsService.createPosition(createData);
|
await positionsService.createPosition(createData);
|
||||||
@ -290,7 +340,9 @@ const savePosition = async (data: CreatePositionDTO | UpdatePositionDTO) => {
|
|||||||
const updateData: UpdatePositionDTO = {
|
const updateData: UpdatePositionDTO = {
|
||||||
name: updateDataWithId.name,
|
name: updateDataWithId.name,
|
||||||
code: updateDataWithId.code,
|
code: updateDataWithId.code,
|
||||||
description: updateDataWithId.description || null
|
description: updateDataWithId.description || null,
|
||||||
|
department_id: updateDataWithId.department_id,
|
||||||
|
parent_id: updateDataWithId.parent_id || null
|
||||||
};
|
};
|
||||||
|
|
||||||
await positionsService.updatePosition(updateDataWithId.id, updateData);
|
await positionsService.updatePosition(updateDataWithId.id, updateData);
|
||||||
|
|||||||
@ -19,6 +19,43 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="p-6 pt-0 space-y-6">
|
<div class="p-6 pt-0 space-y-6">
|
||||||
|
<!-- Departamento -->
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-gray-900 dark:text-gray-200 text-base font-semibold leading-normal">
|
||||||
|
Departamento *
|
||||||
|
</label>
|
||||||
|
<Dropdown
|
||||||
|
v-model="localFormData.department_id"
|
||||||
|
:options="departmentOptions"
|
||||||
|
optionLabel="label"
|
||||||
|
optionValue="value"
|
||||||
|
placeholder="Seleccionar departamento"
|
||||||
|
class="w-full h-14"
|
||||||
|
/>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400 text-xs font-normal">
|
||||||
|
Departamento al que pertenece este puesto.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Puesto Padre (Opcional) -->
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<label class="text-gray-900 dark:text-gray-200 text-base font-semibold leading-normal">
|
||||||
|
Puesto Padre (Opcional)
|
||||||
|
</label>
|
||||||
|
<Dropdown
|
||||||
|
v-model="localFormData.parent_id"
|
||||||
|
:options="positionOptions"
|
||||||
|
optionLabel="label"
|
||||||
|
optionValue="value"
|
||||||
|
placeholder="Seleccionar puesto superior"
|
||||||
|
class="w-full h-14"
|
||||||
|
showClear
|
||||||
|
/>
|
||||||
|
<p class="text-gray-500 dark:text-gray-400 text-xs font-normal">
|
||||||
|
Define la jerarquía organizacional. Déjelo vacío para puestos de nivel superior.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col gap-2">
|
<div class="flex flex-col gap-2">
|
||||||
<label class="text-gray-900 dark:text-gray-200 text-base font-semibold leading-normal">
|
<label class="text-gray-900 dark:text-gray-200 text-base font-semibold leading-normal">
|
||||||
Nombre del Puesto
|
Nombre del Puesto
|
||||||
@ -87,17 +124,21 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch, computed } from 'vue';
|
||||||
import Dialog from 'primevue/dialog';
|
import Dialog from 'primevue/dialog';
|
||||||
import Button from 'primevue/button';
|
import Button from 'primevue/button';
|
||||||
import InputText from 'primevue/inputtext';
|
import InputText from 'primevue/inputtext';
|
||||||
import Textarea from 'primevue/textarea';
|
import Textarea from 'primevue/textarea';
|
||||||
import type { CreatePositionDTO, UpdatePositionDTO } from '../../types/positions.interface';
|
import Dropdown from 'primevue/dropdown';
|
||||||
|
import type { CreatePositionDTO, UpdatePositionDTO, Position } from './positions.interface';
|
||||||
|
import type { Department } from '../departments/departments.interface';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
mode: 'create' | 'edit';
|
mode: 'create' | 'edit';
|
||||||
formData: CreatePositionDTO | UpdatePositionDTO;
|
formData: CreatePositionDTO | UpdatePositionDTO;
|
||||||
|
departments?: Department[];
|
||||||
|
positions?: Position[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
@ -111,6 +152,37 @@ const emit = defineEmits<Emits>();
|
|||||||
|
|
||||||
const localFormData = ref<CreatePositionDTO | UpdatePositionDTO>({ ...props.formData });
|
const localFormData = ref<CreatePositionDTO | UpdatePositionDTO>({ ...props.formData });
|
||||||
|
|
||||||
|
// Computed: Opciones de departamentos para el dropdown
|
||||||
|
const departmentOptions = computed(() => {
|
||||||
|
if (!props.departments) return [];
|
||||||
|
return props.departments.map(dept => ({
|
||||||
|
label: dept.name,
|
||||||
|
value: dept.id
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Computed: Opciones de puestos disponibles para seleccionar como padre
|
||||||
|
const availablePositions = computed(() => {
|
||||||
|
if (!props.positions) return [];
|
||||||
|
|
||||||
|
// Si estamos editando, excluir el puesto actual para evitar referencias circulares
|
||||||
|
const dataWithId = localFormData.value as UpdatePositionDTO & { id?: number };
|
||||||
|
if (props.mode === 'edit' && dataWithId.id) {
|
||||||
|
return props.positions.filter(pos => pos.id !== dataWithId.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.positions;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Opciones para el dropdown de puestos (incluye opción "Sin puesto padre")
|
||||||
|
const positionOptions = computed(() => [
|
||||||
|
{ label: 'Sin puesto padre (Nivel raíz)', value: null },
|
||||||
|
...availablePositions.value.map(pos => ({
|
||||||
|
label: `${pos.name} (${pos.code})`,
|
||||||
|
value: pos.id
|
||||||
|
}))
|
||||||
|
]);
|
||||||
|
|
||||||
// Watch for external formData changes
|
// Watch for external formData changes
|
||||||
watch(() => props.formData, (newData) => {
|
watch(() => props.formData, (newData) => {
|
||||||
localFormData.value = { ...newData };
|
localFormData.value = { ...newData };
|
||||||
|
|||||||
@ -3,6 +3,13 @@ export interface Position {
|
|||||||
name: string;
|
name: string;
|
||||||
code: string;
|
code: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
|
department_id: number | null;
|
||||||
|
parent_id?: number | null;
|
||||||
|
department?: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
parent?: Position;
|
||||||
is_active: number;
|
is_active: number;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
updated_at: Date;
|
updated_at: Date;
|
||||||
@ -13,6 +20,8 @@ export interface CreatePositionDTO {
|
|||||||
name: string;
|
name: string;
|
||||||
code: string;
|
code: string;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
|
department_id: number | null;
|
||||||
|
parent_id?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdatePositionDTO extends Partial<CreatePositionDTO> {}
|
export interface UpdatePositionDTO extends Partial<CreatePositionDTO> {}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import api from "@/services/api";
|
import api from "@/services/api";
|
||||||
import type { CreatePositionDTO, ResponsePositionsDTO, UpdatePositionDTO } from "../types/positions.interface";
|
import type { CreatePositionDTO, ResponsePositionsDTO, UpdatePositionDTO } from "./positions.interface";
|
||||||
|
|
||||||
export class PositionsService {
|
export class PositionsService {
|
||||||
|
|
||||||
@ -33,6 +33,7 @@ import CreateRequisition from '../modules/requisitions/CreateRequisition.vue';
|
|||||||
import ClassificationsComercial from '../modules/catalog/components/comercial-classification/ClassificationsComercial.vue';
|
import ClassificationsComercial from '../modules/catalog/components/comercial-classification/ClassificationsComercial.vue';
|
||||||
import WarehouseOutInventory from '../modules/warehouse/components/WarehouseOutInventory.vue';
|
import WarehouseOutInventory from '../modules/warehouse/components/WarehouseOutInventory.vue';
|
||||||
import companiesRouter from '@/modules/catalog/components/companies/companies.router';
|
import companiesRouter from '@/modules/catalog/components/companies/companies.router';
|
||||||
|
import Employees from '../modules/rh/components/employees/Employees.vue';
|
||||||
|
|
||||||
const routes: RouteRecordRaw[] = [
|
const routes: RouteRecordRaw[] = [
|
||||||
{
|
{
|
||||||
@ -332,6 +333,15 @@ const routes: RouteRecordRaw[] = [
|
|||||||
requiresAuth: true
|
requiresAuth: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'employees',
|
||||||
|
name: 'Employees',
|
||||||
|
component: Employees,
|
||||||
|
meta: {
|
||||||
|
title: 'Gestión de Empleados',
|
||||||
|
requiresAuth: true
|
||||||
|
}
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user