feat: remove index.html and restructure RH components

- Deleted the index.html file from the RH components.
- Created new DepartmentForm.vue and Departments.vue components for managing departments.
- Updated the router to reflect the new path for Departments component.
- Added DepartmentsService for API interactions related to departments.
- Introduced types for departments in departments.interface.ts.
- Updated WarehouseAddInventory.vue to change background color classes.
- Configured TypeScript paths in tsconfig.app.json for easier imports.
- Enhanced Vite configuration to support aliasing for src directory.
This commit is contained in:
Edgar Méndez Mendoza 2026-02-27 13:33:59 -06:00
parent 2bdccbe6c6
commit 983c3265bc
9 changed files with 328 additions and 518 deletions

View File

@ -0,0 +1,113 @@
<template>
<Dialog
:visible="visible"
@update:visible="$emit('update:visible', $event)"
:modal="true"
:style="{ width: '800px' }"
: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' ? 'Crear Nuevo Departamento' : 'Editar Departamento' }}
</h2>
<p class="text-gray-500 dark:text-gray-400 text-base font-normal leading-normal">
Configure los detalles básicos para habilitar el nuevo departamento en la estructura organizacional.
</p>
</div>
</template>
<div class="p-6 pt-0 space-y-6">
<!-- Department Name Field -->
<div class="flex flex-col gap-2">
<label class="text-gray-900 dark:text-gray-200 text-base font-semibold leading-normal">
Nombre del Departamento
</label>
<InputText
v-model="localFormData.name"
placeholder="Ej. Logística, Producción o Calidad"
class="w-full h-14"
/>
<p class="text-gray-500 dark:text-gray-400 text-xs font-normal">
Este nombre será visible en todos los reportes de RH.
</p>
</div>
<!-- Department Description Field -->
<div class="flex flex-col gap-2">
<label class="text-gray-900 dark:text-gray-200 text-base font-semibold leading-normal">
Descripción del Departamento
</label>
<Textarea
v-model="localFormData.description"
rows="5"
placeholder="Describa las funciones, responsabilidades y objetivos principales de este departamento..."
class="w-full"
/>
</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' ? 'Guardar Departamento' : 'Actualizar Departamento'"
icon="pi pi-save"
@click="handleSave"
class="px-8"
/>
</div>
</template>
</Dialog>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import Dialog from 'primevue/dialog';
import Button from 'primevue/button';
import InputText from 'primevue/inputtext';
import Textarea from 'primevue/textarea';
import type { Department } from '../../types/departments.interface';
interface Props {
visible: boolean;
mode: 'create' | 'edit';
formData: Partial<Department>;
}
interface Emits {
(e: 'update:visible', value: boolean): void;
(e: 'save', data: Partial<Department>): void;
(e: 'cancel'): void;
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
const localFormData = ref<Partial<Department>>({ ...props.formData });
// Watch for external formData changes
watch(() => props.formData, (newData) => {
localFormData.value = { ...newData };
}, { deep: true });
const handleSave = () => {
emit('save', localFormData.value);
};
const handleCancel = () => {
emit('update:visible', false);
emit('cancel');
};
</script>
<style scoped>
/* Estilos adicionales si son necesarios */
</style>

View File

@ -113,15 +113,6 @@
</template> </template>
</Column> </Column>
<Column field="employeeCount" header="Número de Empleados" sortable class="text-center">
<template #body="slotProps">
<Tag
:value="`${slotProps.data.employeeCount} empleados`"
severity="secondary"
/>
</template>
</Column>
<Column header="Acciones" :exportable="false"> <Column header="Acciones" :exportable="false">
<template #body="slotProps"> <template #body="slotProps">
<div class="flex gap-2"> <div class="flex gap-2">
@ -156,72 +147,13 @@
</Card> </Card>
<!-- Create/Edit Dialog --> <!-- Create/Edit Dialog -->
<Dialog <DepartmentForm
v-model:visible="showDialog" v-model:visible="showDialog"
:modal="true" :mode="dialogMode"
:style="{ width: '800px' }" :formData="formData"
:closable="true" @save="saveDepartment"
class="p-0" @cancel="showDialog = false"
> />
<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">
{{ dialogMode === 'create' ? 'Crear Nuevo Departamento' : 'Editar Departamento' }}
</h2>
<p class="text-gray-500 dark:text-gray-400 text-base font-normal leading-normal">
Configure los detalles básicos para habilitar el nuevo departamento en la estructura organizacional.
</p>
</div>
</template>
<div class="p-6 pt-0 space-y-6">
<!-- Department Name Field -->
<div class="flex flex-col gap-2">
<label class="text-gray-900 dark:text-gray-200 text-base font-semibold leading-normal">
Nombre del Departamento
</label>
<InputText
v-model="formData.name"
placeholder="Ej. Logística, Producción o Calidad"
class="w-full h-14"
/>
<p class="text-gray-500 dark:text-gray-400 text-xs font-normal">
Este nombre será visible en todos los reportes de RH.
</p>
</div>
<!-- Department Description Field -->
<div class="flex flex-col gap-2">
<label class="text-gray-900 dark:text-gray-200 text-base font-semibold leading-normal">
Descripción del Departamento
</label>
<Textarea
v-model="formData.description"
rows="5"
placeholder="Describa las funciones, responsabilidades y objetivos principales de este departamento..."
class="w-full"
/>
</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="showDialog = false"
class="px-6"
/>
<Button
:label="dialogMode === 'create' ? 'Guardar Departamento' : 'Actualizar Departamento'"
icon="pi pi-save"
@click="saveDepartment"
class="px-8"
/>
</div>
</template>
</Dialog>
<!-- Delete Confirmation Dialog --> <!-- Delete Confirmation Dialog -->
<Dialog <Dialog
@ -260,8 +192,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue'; import { ref, computed, onMounted } from 'vue';
import { useToast } from 'primevue/usetoast'; import { useToast } from 'primevue/usetoast';
import { DepartmentsService } from '../../services/departments.services';
// PrimeVue Components // PrimeVue Components
import Toast from 'primevue/toast'; import Toast from 'primevue/toast';
@ -271,17 +204,19 @@ import Button from 'primevue/button';
import DataTable from 'primevue/datatable'; 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 InputText from 'primevue/inputtext';
import Textarea from 'primevue/textarea'; import type { Department, CreateDepartmentDTO, UpdateDepartmentDTO } from '../../types/departments.interface';
import Tag from 'primevue/tag'; import DepartmentForm from './DepartmentForm.vue';
const toast = useToast(); const toast = useToast();
const departmentsService = new DepartmentsService();
// State // State
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 dialogMode = ref<'create' | 'edit'>('create'); const dialogMode = ref<'create' | 'edit'>('create');
const departmentToDelete = ref<any>(null); const departmentToDelete = ref<Department | null>(null);
// Breadcrumb // Breadcrumb
const breadcrumbHome = ref({ icon: 'pi pi-home', to: '/' }); const breadcrumbHome = ref({ icon: 'pi pi-home', to: '/' });
@ -291,7 +226,7 @@ const breadcrumbItems = ref([
]); ]);
// Form Data // Form Data
const formData = ref<{id?: number; name: string; description: string; employeeCount: number; color: string; icon: string}>({ const formData = ref<Partial<Department>>({
name: '', name: '',
description: '', description: '',
employeeCount: 0, employeeCount: 0,
@ -299,69 +234,45 @@ const formData = ref<{id?: number; name: string; description: string; employeeCo
icon: 'pi pi-building' icon: 'pi pi-building'
}); });
// Color Options (for future use) // Departments Data
// const colorOptions = [ const departments = ref<Department[]>([]);
// { label: 'Azul', value: '#3b82f6' },
// { label: 'Verde', value: '#10b981' },
// { label: 'Púrpura', value: '#8b5cf6' },
// { label: 'Naranja', value: '#f97316' },
// { label: 'Rosa', value: '#ec4899' },
// { label: 'Rojo', value: '#ef4444' },
// { label: 'Amarillo', value: '#f59e0b' },
// { label: 'Índigo', value: '#6366f1' },
// ];
// Icon Options (for future use)
// const iconOptions = [
// { label: 'Edificio', value: 'pi pi-building' },
// { label: 'Usuarios', value: 'pi pi-users' },
// { label: 'Cog', value: 'pi pi-cog' },
// { label: 'Herramientas', value: 'pi pi-wrench' },
// { label: 'Camión', value: 'pi pi-truck' },
// { label: 'Gráfico', value: 'pi pi-chart-line' },
// { label: 'Escudo', value: 'pi pi-shield' },
// { label: 'Estrella', value: 'pi pi-star' },
// ];
// Mock Data - Departments
const departments = ref([
{
id: 1,
name: 'Producción',
description: 'Línea de ensamblaje y manufactura de productos terminados.',
employeeCount: 45,
color: '#3b82f6',
icon: 'pi pi-building'
},
{
id: 2,
name: 'Logística',
description: 'Gestión de envíos nacionales, recepción y almacenamiento.',
employeeCount: 22,
color: '#10b981',
icon: 'pi pi-truck'
},
{
id: 3,
name: 'Control de Calidad',
description: 'Inspección de estándares de calidad y auditoría de procesos.',
employeeCount: 12,
color: '#8b5cf6',
icon: 'pi pi-shield'
},
{
id: 4,
name: 'Mantenimiento',
description: 'Soporte técnico, reparaciones y mantenimiento preventivo.',
employeeCount: 8,
color: '#f97316',
icon: 'pi pi-wrench'
},
]);
// Computed // Computed
const totalEmployees = computed(() => { const totalEmployees = computed(() => {
return departments.value.reduce((sum, dept) => sum + dept.employeeCount, 0); return departments.value.reduce((sum, dept) => sum + (dept.employeeCount || 0), 0);
});
// 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 // Methods
@ -377,7 +288,7 @@ const openCreateDialog = () => {
showDialog.value = true; showDialog.value = true;
}; };
const viewDepartment = (department: any) => { const viewDepartment = (department: Department) => {
toast.add({ toast.add({
severity: 'info', severity: 'info',
summary: 'Ver Departamento', summary: 'Ver Departamento',
@ -386,7 +297,7 @@ const viewDepartment = (department: any) => {
}); });
}; };
const editDepartment = (department: any) => { const editDepartment = (department: Department) => {
dialogMode.value = 'edit'; dialogMode.value = 'edit';
formData.value = { ...department }; formData.value = { ...department };
showDialog.value = true; showDialog.value = true;
@ -397,25 +308,37 @@ const confirmDelete = (department: any) => {
showDeleteDialog.value = true; showDeleteDialog.value = true;
}; };
const deleteDepartment = () => { const deleteDepartment = async () => {
if (departmentToDelete.value) { if (!departmentToDelete.value) return;
const index = departments.value.findIndex(d => d.id === departmentToDelete.value.id);
if (index > -1) { try {
departments.value.splice(index, 1); await departmentsService.deleteDepartment(departmentToDelete.value.id);
toast.add({
severity: 'success', // Recargar la lista de departamentos
summary: 'Departamento Eliminado', await fetchDepartments(false);
detail: `${departmentToDelete.value.name} ha sido eliminado`,
life: 3000 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;
} }
showDeleteDialog.value = false;
departmentToDelete.value = null;
}; };
const saveDepartment = () => { const saveDepartment = async (data: Partial<Department>) => {
if (!formData.value.name) { if (!data.name) {
toast.add({ toast.add({
severity: 'warn', severity: 'warn',
summary: 'Validación', summary: 'Validación',
@ -425,47 +348,61 @@ const saveDepartment = () => {
return; return;
} }
if (dialogMode.value === 'create') { try {
// Create new department if (dialogMode.value === 'create') {
const newDepartment = { // Create new department
id: Math.max(...departments.value.map(d => d.id)) + 1, const createData: CreateDepartmentDTO = {
name: formData.value.name, name: data.name,
description: formData.value.description, description: data.description || ''
employeeCount: formData.value.employeeCount,
color: formData.value.color,
icon: formData.value.icon
};
departments.value.push(newDepartment);
toast.add({
severity: 'success',
summary: 'Departamento Creado',
detail: `${formData.value.name} ha sido creado exitosamente`,
life: 3000
});
} else {
// Update existing department
const index = departments.value.findIndex(d => d.id === formData.value.id);
if (index > -1) {
const existingDept = departments.value[index]!;
departments.value[index] = {
id: existingDept.id,
name: formData.value.name,
description: formData.value.description,
employeeCount: formData.value.employeeCount,
color: formData.value.color,
icon: formData.value.icon
}; };
await departmentsService.createDepartment(createData);
toast.add({
severity: 'success',
summary: 'Departamento Creado',
detail: `${data.name} ha sido creado exitosamente`,
life: 3000
});
} else {
// Update existing department
if (!data.id) {
toast.add({
severity: 'error',
summary: 'Error',
detail: 'ID del departamento no encontrado',
life: 3000
});
return;
}
const updateData: UpdateDepartmentDTO = {
name: data.name,
description: data.description
};
await departmentsService.updateDepartment(data.id, updateData);
toast.add({ toast.add({
severity: 'success', severity: 'success',
summary: 'Departamento Actualizado', summary: 'Departamento Actualizado',
detail: `${formData.value.name} ha sido actualizado`, detail: `${data.name} ha sido actualizado exitosamente`,
life: 3000 life: 3000
}); });
} }
}
showDialog.value = false; // 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> </script>

View File

@ -1,323 +0,0 @@
<!DOCTYPE html>
<html class="light" lang="es"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Gestión de Puestos de Trabajo | RH Portal</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
colors: {
"primary": "#137fec",
"background-light": "#f6f7f8",
"background-dark": "#101922",
},
fontFamily: {
"display": ["Inter"]
},
borderRadius: { "DEFAULT": "0.25rem", "lg": "0.5rem", "xl": "0.75rem", "full": "9999px" },
},
},
}
</script>
<style>
body {
font-family: 'Inter', sans-serif;
}
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
</style>
</head>
<body class="bg-background-light dark:bg-background-dark text-[#111418] dark:text-white font-display">
<div class="flex h-screen overflow-hidden">
<!-- Sidebar Navigation -->
<aside class="w-64 bg-white dark:bg-gray-900 border-r border-gray-200 dark:border-gray-800 flex flex-col shrink-0">
<div class="p-6 flex items-center gap-3">
<div class="size-10 rounded-lg bg-primary flex items-center justify-center text-white">
<span class="material-symbols-outlined">factory</span>
</div>
<div class="flex flex-col">
<h1 class="text-sm font-bold leading-none">Warehouse System</h1>
<p class="text-xs text-gray-500 mt-1">Admin Portal</p>
</div>
</div>
<nav class="flex-1 px-4 space-y-1 overflow-y-auto">
<a class="flex items-center gap-3 px-3 py-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors" href="#">
<span class="material-symbols-outlined">dashboard</span>
<span class="text-sm font-medium">Dashboard</span>
</a>
<a class="flex items-center gap-3 px-3 py-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors" href="#">
<span class="material-symbols-outlined">inventory_2</span>
<span class="text-sm font-medium">Inventario</span>
</a>
<a class="flex items-center gap-3 px-3 py-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors" href="#">
<span class="material-symbols-outlined">precision_manufacturing</span>
<span class="text-sm font-medium">Producción</span>
</a>
<a class="flex items-center gap-3 px-3 py-2 bg-primary/10 text-primary rounded-lg transition-colors" href="#">
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">groups</span>
<span class="text-sm font-medium">Recursos Humanos</span>
</a>
<a class="flex items-center gap-3 px-3 py-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors" href="#">
<span class="material-symbols-outlined">settings</span>
<span class="text-sm font-medium">Configuración</span>
</a>
</nav>
<div class="p-4 border-t border-gray-200 dark:border-gray-800">
<div class="flex items-center gap-3 p-2">
<div class="size-8 rounded-full bg-cover bg-center" data-alt="User profile avatar of admin" style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuAZE4yg7YhxPnYbTfdwrgMaXbIwT17qinrNA2sczwJTMCecZuuRxWI9crz6h75iNsn4VKTmHSNkkwtpFOx2I72wWVgtbhDUeUX8ngUzF0A_22GqWF9F46TErldseaBQgTwjXDP5wWLyOU8S8Mf64JntceSLjq2U9exTu9Pkhgo6OPG1GkinNVD3rBNsVjwN-xNbtWLt6jYnwVcAkrCqawJsiK_Pf8R3chrYkUJNmzJgn9XSTOtBLUDT6A7K1V_G1jXX6k1jzSdWnFdg')"></div>
<div class="flex-1 min-w-0">
<p class="text-xs font-semibold truncate">Marcos Pérez</p>
<p class="text-[10px] text-gray-500 truncate">HR Manager</p>
</div>
<span class="material-symbols-outlined text-gray-400 text-sm">logout</span>
</div>
</div>
</aside>
<!-- Main Content Area -->
<main class="flex-1 flex flex-col min-w-0 overflow-hidden">
<!-- Top Navigation Bar -->
<header class="h-16 bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800 flex items-center justify-between px-8 shrink-0">
<div class="flex items-center gap-4">
<div class="flex items-center gap-2">
<a class="text-xs font-medium text-gray-500 hover:text-primary transition-colors" href="#">INICIO</a>
<span class="text-xs text-gray-400">/</span>
<a class="text-xs font-medium text-gray-500 hover:text-primary transition-colors" href="#">RH</a>
<span class="text-xs text-gray-400">/</span>
<span class="text-xs font-bold text-gray-900 dark:text-white uppercase tracking-wider">Puestos</span>
</div>
</div>
<div class="flex items-center gap-4">
<div class="relative">
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-sm">search</span>
<input class="h-9 w-64 bg-gray-100 dark:bg-gray-800 border-none rounded-lg pl-9 text-xs focus:ring-2 focus:ring-primary/50 transition-all" placeholder="Buscar..." type="text"/>
</div>
<button class="size-9 flex items-center justify-center rounded-lg hover:bg-gray-100 dark:hover:bg-gray-800 relative">
<span class="material-symbols-outlined text-gray-600 dark:text-gray-400">notifications</span>
<span class="absolute top-2 right-2 size-2 bg-red-500 rounded-full border-2 border-white dark:border-gray-900"></span>
</button>
<div class="h-8 w-px bg-gray-200 dark:bg-gray-800 mx-2"></div>
<div class="flex items-center gap-2">
<div class="size-8 rounded-full bg-primary/20 text-primary flex items-center justify-center font-bold text-xs">AD</div>
</div>
</div>
</header>
<!-- Page Content -->
<div class="flex-1 overflow-y-auto p-8 bg-background-light dark:bg-background-dark">
<div class="max-w-6xl mx-auto space-y-6">
<!-- Page Heading -->
<div class="flex flex-col md:flex-row md:items-end justify-between gap-4">
<div class="space-y-1">
<h2 class="text-3xl font-black text-gray-900 dark:text-white tracking-tight">Puestos de Trabajo</h2>
<p class="text-gray-500 dark:text-gray-400 text-sm">Administre y organice los roles operativos y administrativos de la planta.</p>
</div>
<button class="bg-primary hover:bg-primary/90 text-white px-5 py-2.5 rounded-lg flex items-center gap-2 text-sm font-bold shadow-lg shadow-primary/20 transition-all">
<span class="material-symbols-outlined text-lg">add_circle</span>
Nuevo Puesto
</button>
</div>
<!-- Search and Filter Bar -->
<div class="bg-white dark:bg-gray-900 p-4 rounded-xl shadow-sm border border-gray-200 dark:border-gray-800 flex gap-4">
<div class="flex-1 relative">
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">search</span>
<input class="w-full h-11 bg-gray-50 dark:bg-gray-800 border-none rounded-lg pl-10 text-sm focus:ring-2 focus:ring-primary/50" placeholder="Buscar por nombre o departamento..." type="text"/>
</div>
<button class="flex items-center gap-2 px-4 py-2 text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 transition-colors">
<span class="material-symbols-outlined">filter_list</span>
<span class="text-sm font-medium">Filtros</span>
</button>
</div>
<!-- Data Table Card -->
<div class="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-800 overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-gray-50 dark:bg-gray-800/50 border-b border-gray-200 dark:border-gray-800">
<th class="px-6 py-4 text-xs font-bold text-gray-500 uppercase tracking-wider w-1/4">Nombre del Puesto</th>
<th class="px-6 py-4 text-xs font-bold text-gray-500 uppercase tracking-wider">Departamento</th>
<th class="px-6 py-4 text-xs font-bold text-gray-500 uppercase tracking-wider w-1/3">Descripción / Actividades</th>
<th class="px-6 py-4 text-xs font-bold text-gray-500 uppercase tracking-wider text-right">Acciones</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 dark:divide-gray-800">
<!-- Row 1 -->
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/30 transition-colors group">
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div class="size-8 rounded bg-blue-50 dark:bg-blue-900/20 flex items-center justify-center text-blue-600">
<span class="material-symbols-outlined text-base">supervisor_account</span>
</div>
<span class="text-sm font-semibold text-gray-900 dark:text-gray-100">Supervisor de Almacén</span>
</div>
</td>
<td class="px-6 py-4 text-sm">
<span class="px-2.5 py-1 rounded-full text-[11px] font-bold bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300">LOGÍSTICA</span>
</td>
<td class="px-6 py-4">
<p class="text-sm text-gray-500 dark:text-gray-400 line-clamp-1">Coordinar la recepción de mercancías, gestión de inventarios y despacho...</p>
</td>
<td class="px-6 py-4 text-right">
<div class="flex items-center justify-end gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button class="p-2 text-gray-400 hover:text-primary transition-colors" title="Ver Detalle">
<span class="material-symbols-outlined text-lg">visibility</span>
</button>
<button class="p-2 text-gray-400 hover:text-primary transition-colors" title="Editar">
<span class="material-symbols-outlined text-lg">edit</span>
</button>
<button class="p-2 text-gray-400 hover:text-red-500 transition-colors" title="Eliminar">
<span class="material-symbols-outlined text-lg">delete</span>
</button>
</div>
</td>
</tr>
<!-- Row 2 -->
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/30 transition-colors group">
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div class="size-8 rounded bg-green-50 dark:bg-green-900/20 flex items-center justify-center text-green-600">
<span class="material-symbols-outlined text-base">precision_manufacturing</span>
</div>
<span class="text-sm font-semibold text-gray-900 dark:text-gray-100">Operario de Línea A</span>
</div>
</td>
<td class="px-6 py-4 text-sm">
<span class="px-2.5 py-1 rounded-full text-[11px] font-bold bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300">PRODUCCIÓN</span>
</td>
<td class="px-6 py-4">
<p class="text-sm text-gray-500 dark:text-gray-400 line-clamp-1">Operación de maquinaria de ensamblado y control de calidad primario...</p>
</td>
<td class="px-6 py-4 text-right">
<div class="flex items-center justify-end gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button class="p-2 text-gray-400 hover:text-primary transition-colors">
<span class="material-symbols-outlined text-lg">visibility</span>
</button>
<button class="p-2 text-gray-400 hover:text-primary transition-colors">
<span class="material-symbols-outlined text-lg">edit</span>
</button>
<button class="p-2 text-gray-400 hover:text-red-500 transition-colors">
<span class="material-symbols-outlined text-lg">delete</span>
</button>
</div>
</td>
</tr>
<!-- Row 3 -->
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/30 transition-colors group">
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div class="size-8 rounded bg-orange-50 dark:bg-orange-900/20 flex items-center justify-center text-orange-600">
<span class="material-symbols-outlined text-base">forklift</span>
</div>
<span class="text-sm font-semibold text-gray-900 dark:text-gray-100">Montacarguista</span>
</div>
</td>
<td class="px-6 py-4 text-sm">
<span class="px-2.5 py-1 rounded-full text-[11px] font-bold bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300">ALMACÉN</span>
</td>
<td class="px-6 py-4">
<p class="text-sm text-gray-500 dark:text-gray-400 line-clamp-1">Traslado de pallets a zona de racks y carga de camiones de distribución...</p>
</td>
<td class="px-6 py-4 text-right">
<div class="flex items-center justify-end gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button class="p-2 text-gray-400 hover:text-primary transition-colors">
<span class="material-symbols-outlined text-lg">visibility</span>
</button>
<button class="p-2 text-gray-400 hover:text-primary transition-colors">
<span class="material-symbols-outlined text-lg">edit</span>
</button>
<button class="p-2 text-gray-400 hover:text-red-500 transition-colors">
<span class="material-symbols-outlined text-lg">delete</span>
</button>
</div>
</td>
</tr>
<!-- Row 4 -->
<tr class="hover:bg-gray-50 dark:hover:bg-gray-800/30 transition-colors group">
<td class="px-6 py-4">
<div class="flex items-center gap-3">
<div class="size-8 rounded bg-purple-50 dark:bg-purple-900/20 flex items-center justify-center text-purple-600">
<span class="material-symbols-outlined text-base">security</span>
</div>
<span class="text-sm font-semibold text-gray-900 dark:text-gray-100">Analista de Seguridad</span>
</div>
</td>
<td class="px-6 py-4 text-sm">
<span class="px-2.5 py-1 rounded-full text-[11px] font-bold bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300">EHS</span>
</td>
<td class="px-6 py-4">
<p class="text-sm text-gray-500 dark:text-gray-400 line-clamp-1">Monitoreo de protocolos de seguridad e higiene industrial en planta...</p>
</td>
<td class="px-6 py-4 text-right">
<div class="flex items-center justify-end gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button class="p-2 text-gray-400 hover:text-primary transition-colors">
<span class="material-symbols-outlined text-lg">visibility</span>
</button>
<button class="p-2 text-gray-400 hover:text-primary transition-colors">
<span class="material-symbols-outlined text-lg">edit</span>
</button>
<button class="p-2 text-gray-400 hover:text-red-500 transition-colors">
<span class="material-symbols-outlined text-lg">delete</span>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Pagination Footer -->
<div class="px-6 py-4 bg-gray-50 dark:bg-gray-800/50 border-t border-gray-200 dark:border-gray-800 flex items-center justify-between">
<span class="text-xs text-gray-500 dark:text-gray-400">Mostrando 1 a 4 de 12 puestos</span>
<div class="flex gap-2">
<button class="size-8 flex items-center justify-center rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-gray-600 dark:text-gray-400 disabled:opacity-50" disabled="">
<span class="material-symbols-outlined text-sm">chevron_left</span>
</button>
<button class="size-8 flex items-center justify-center rounded bg-primary text-white font-bold text-xs">1</button>
<button class="size-8 flex items-center justify-center rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-gray-600 dark:text-gray-400 hover:bg-gray-50 transition-colors text-xs font-bold">2</button>
<button class="size-8 flex items-center justify-center rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-gray-600 dark:text-gray-400 hover:bg-gray-50 transition-colors text-xs font-bold">3</button>
<button class="size-8 flex items-center justify-center rounded border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-gray-600 dark:text-gray-400 hover:bg-gray-50 transition-colors">
<span class="material-symbols-outlined text-sm">chevron_right</span>
</button>
</div>
</div>
</div>
<!-- Summary Info Cards (Subtle Details) -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="bg-white dark:bg-gray-900 p-6 rounded-xl border border-gray-200 dark:border-gray-800 shadow-sm flex items-center gap-4">
<div class="size-12 rounded-full bg-primary/10 flex items-center justify-center text-primary">
<span class="material-symbols-outlined">work</span>
</div>
<div>
<p class="text-xs text-gray-500 font-medium">Total de Puestos</p>
<p class="text-xl font-black">12 Roles</p>
</div>
</div>
<div class="bg-white dark:bg-gray-900 p-6 rounded-xl border border-gray-200 dark:border-gray-800 shadow-sm flex items-center gap-4">
<div class="size-12 rounded-full bg-green-500/10 flex items-center justify-center text-green-600">
<span class="material-symbols-outlined">domain</span>
</div>
<div>
<p class="text-xs text-gray-500 font-medium">Departamentos Activos</p>
<p class="text-xl font-black">5 Áreas</p>
</div>
</div>
<div class="bg-white dark:bg-gray-900 p-6 rounded-xl border border-gray-200 dark:border-gray-800 shadow-sm flex items-center gap-4">
<div class="size-12 rounded-full bg-amber-500/10 flex items-center justify-center text-amber-600">
<span class="material-symbols-outlined">history</span>
</div>
<div>
<p class="text-xs text-gray-500 font-medium">Última Actualización</p>
<p class="text-xl font-black">Hoy, 09:12 AM</p>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
</body></html>

View File

@ -0,0 +1,47 @@
import type { CreateDepartmentDTO, DeleteDepartmentDTO, ResponseDepartmentsDTO, UpdateDepartmentDTO } from "../types/departments.interface";
import api from "@/services/api";
export class DepartmentsService {
public async getDepartments(): Promise<ResponseDepartmentsDTO> {
try {
const response = await api.get('/api/rh/departments');
return response.data;
} catch (error) {
console.error('Error fetching departments:', error);
throw error;
}
}
public async createDepartment(data: CreateDepartmentDTO): Promise<ResponseDepartmentsDTO> {
try {
const response = await api.post('/api/rh/departments', data);
return response.data;
} catch (error) {
console.error('Error creating department:', error);
throw error;
}
}
public async updateDepartment(id: number, data: UpdateDepartmentDTO): Promise<ResponseDepartmentsDTO> {
try {
const response = await api.put(`/api/rh/departments/${id}`, data);
return response.data;
} catch (error) {
console.error('Error updating department:', error);
throw error;
}
}
public async deleteDepartment(id: number): Promise<DeleteDepartmentDTO> {
try {
const response = await api.delete(`/api/rh/departments/${id}`);
return response.data;
} catch (error) {
console.error('Error deleting department:', error);
throw error;
}
}
}

View File

@ -0,0 +1,26 @@
export interface Department {
id: number;
name: string;
description: string;
employeeCount?: number;
color?: string;
icon?: string;
created_at: Date;
updated_at: Date;
deleted_at: Date | null;
}
export interface ResponseDepartmentsDTO {
data: Department[];
}
export interface DeleteDepartmentDTO {
message: string;
}
export interface CreateDepartmentDTO {
name: string;
description: string;
}
export interface UpdateDepartmentDTO extends Partial<CreateDepartmentDTO> {}

View File

@ -570,7 +570,7 @@ const removeProduct = (productId: number) => {
<template v-for="product in products" :key="product.id"> <template v-for="product in products" :key="product.id">
<tr :class="[ <tr :class="[
'hover:bg-slate-50 transition-colors', 'hover:bg-slate-50 transition-colors',
isRowExpanded(product) ? 'bg-primary/[0.02]' : '' isRowExpanded(product) ? 'bg-primary/2' : ''
]"> ]">
<td class="px-6 py-4" :class="{ 'border-l-4 border-primary': isRowExpanded(product) }"> <td class="px-6 py-4" :class="{ 'border-l-4 border-primary': isRowExpanded(product) }">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
@ -659,7 +659,7 @@ const removeProduct = (productId: number) => {
</tr> </tr>
<!-- Serial Numbers Expansion --> <!-- Serial Numbers Expansion -->
<tr v-if="isRowExpanded(product)" class="bg-primary/[0.02]"> <tr v-if="isRowExpanded(product)" class="bg-primary/2">
<td colspan="6" class="px-10 pb-6 pt-2"> <td colspan="6" class="px-10 pb-6 pt-2">
<Card class="border border-primary/20 shadow-sm"> <Card class="border border-primary/20 shadow-sm">
<template #header> <template #header>

View File

@ -18,7 +18,7 @@ import RoleForm from '../modules/users/components/RoleForm.vue';
import UserIndex from '../modules/users/components/UserIndex.vue'; import UserIndex from '../modules/users/components/UserIndex.vue';
import StoreDetails from '../modules/stores/components/StoreDetails.vue'; import StoreDetails from '../modules/stores/components/StoreDetails.vue';
import Positions from '../modules/rh/components/Positions.vue'; import Positions from '../modules/rh/components/Positions.vue';
import Departments from '../modules/rh/components/Departments.vue'; import Departments from '../modules/rh/components/departments/Departments.vue';
import '../modules/catalog/components/suppliers/Suppliers.vue'; import '../modules/catalog/components/suppliers/Suppliers.vue';
import Suppliers from '../modules/catalog/components/suppliers/Suppliers.vue'; import Suppliers from '../modules/catalog/components/suppliers/Suppliers.vue';

View File

@ -3,6 +3,10 @@
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"types": ["vite/client"], "types": ["vite/client"],
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
/* Linting */ /* Linting */
"strict": true, "strict": true,

View File

@ -3,6 +3,7 @@ import vue from "@vitejs/plugin-vue";
import tailwindcss from "@tailwindcss/vite"; import tailwindcss from "@tailwindcss/vite";
import Components from 'unplugin-vue-components/vite'; import Components from 'unplugin-vue-components/vite';
import { PrimeVueResolver } from '@primevue/auto-import-resolver'; import { PrimeVueResolver } from '@primevue/auto-import-resolver';
import { fileURLToPath, URL } from 'node:url';
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
@ -13,5 +14,10 @@ export default defineConfig({
PrimeVueResolver() PrimeVueResolver()
] ]
}) })
] ],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
}); });