Merge pull request #1 from josedario87/feat/empleado-ui-chat-integration
feat: Implement empleado UI and chat integration
This commit is contained in:
@@ -5,10 +5,69 @@ import { log } from './logger.js';
|
|||||||
// Ya no se necesitan: sendText, fetchChatMessages, setTypingStatus, askGemini aquí
|
// Ya no se necesitan: sendText, fetchChatMessages, setTypingStatus, askGemini aquí
|
||||||
import { processMessage } from './utils/processMessage.js';
|
import { processMessage } from './utils/processMessage.js';
|
||||||
import { saveMedia } from './utils/saveMedia.js';
|
import { saveMedia } from './utils/saveMedia.js';
|
||||||
import { respuestaNormal } from './respuestas/respuestaNormal.js'; // <- NUEVA IMPORTACIÓN
|
// import { respuestaNormal } from './respuestas/respuestaNormal.js'; // <- NUEVA IMPORTACIÓN
|
||||||
import { respuestaBrave } from './respuestas/respuestaBrave.js'; // <- NUEVA IMPORTACIÓN
|
import { respuestaBrave } from './respuestas/respuestaBrave.js'; // <- NUEVA IMPORTACIÓN
|
||||||
import { sendText } from './whatsapp.js'; // <- NUEVA IMPORTACIÓN
|
import { sendText } from './whatsapp.js'; // <- NUEVA IMPORTACIÓN
|
||||||
|
|
||||||
|
// Mock Data for Employees
|
||||||
|
const mockEmployees = [
|
||||||
|
{
|
||||||
|
id: '1', // Ensure ID is string if components expect string
|
||||||
|
name: 'Ana García Mock',
|
||||||
|
cedula: 123456789, // Ensure cedula is number
|
||||||
|
avatar_url: 'https://randomuser.me/api/portraits/women/60.jpg',
|
||||||
|
telefono: '0991234567',
|
||||||
|
ubicacion: 'Oficina Mock Central',
|
||||||
|
idciat: 'AG001M',
|
||||||
|
grupo_estudio: 'Desarrollo Frontend Mock',
|
||||||
|
empleado: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: 'Carlos Rodriguez Mock',
|
||||||
|
cedula: 987654321,
|
||||||
|
avatar_url: 'https://randomuser.me/api/portraits/men/45.jpg',
|
||||||
|
telefono: '0987654321',
|
||||||
|
ubicacion: 'Sucursal Mock Norte',
|
||||||
|
idciat: 'CR002M',
|
||||||
|
grupo_estudio: 'Backend Services Mock',
|
||||||
|
empleado: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
name: 'Luisa Martinez Mock',
|
||||||
|
cedula: 112233445,
|
||||||
|
avatar_url: 'https://randomuser.me/api/portraits/women/61.jpg',
|
||||||
|
telefono: '0976543210',
|
||||||
|
ubicacion: 'Remoto Mock',
|
||||||
|
idciat: 'LM003M',
|
||||||
|
grupo_estudio: 'QA Mock',
|
||||||
|
empleado: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
name: 'Jorge Herrera Mock',
|
||||||
|
cedula: 223344556,
|
||||||
|
avatar_url: 'https://randomuser.me/api/portraits/men/50.jpg',
|
||||||
|
telefono: '0965432109',
|
||||||
|
ubicacion: 'Oficina Mock Sur',
|
||||||
|
idciat: 'JH004M',
|
||||||
|
grupo_estudio: 'DevOps Mock',
|
||||||
|
empleado: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5',
|
||||||
|
name: 'Patricia Fernández Mock',
|
||||||
|
cedula: 334455667,
|
||||||
|
avatar_url: 'https://randomuser.me/api/portraits/women/62.jpg',
|
||||||
|
telefono: '0954321098',
|
||||||
|
ubicacion: 'Oficina Mock Central',
|
||||||
|
idciat: 'PF005M',
|
||||||
|
grupo_estudio: 'Diseño UX/UI Mock',
|
||||||
|
empleado: true,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
/* carpeta raíz donde saveMedia deja todo */
|
/* carpeta raíz donde saveMedia deja todo */
|
||||||
const MEDIA_DIR = '/media';
|
const MEDIA_DIR = '/media';
|
||||||
await fs.mkdir(MEDIA_DIR, { recursive: true });
|
await fs.mkdir(MEDIA_DIR, { recursive: true });
|
||||||
@@ -29,23 +88,57 @@ export async function processIncoming(raw) {
|
|||||||
const msg = processMessage(raw);
|
const msg = processMessage(raw);
|
||||||
const text = msg.text || '';
|
const text = msg.text || '';
|
||||||
|
|
||||||
/* ----- comando @nucleo ----- */
|
// Logica para componentes UI de Empleados
|
||||||
if (/^@nucleo(\s|$)/i.test(text)) {
|
if (/^Quiero crear un nuevo @empleado/i.test(text)) {
|
||||||
// Llama a la función importada
|
log('info', `Comando recibido: Crear nuevo empleado. Enviando componente EmpleadoForm.`);
|
||||||
// await respuestaNormal(msg); // <- LLAMADA A LA FUNCIÓN EXTERNA
|
sendText(msg.chatId, 'CHAT_UI_COMPONENT::EmpleadoForm');
|
||||||
await respuestaMCP(msg); // <- LLAMADA A LA FUNCIÓN EXTERNA
|
return; // Termina el procesamiento para este comando
|
||||||
} else {
|
|
||||||
// Lógica para otros mensajes (si aplica)
|
|
||||||
// log('debug', 'Mensaje recibido no es comando @nucleo:', text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/^@nucleo.(\s|$)/i.test(text)) {
|
const verEmpleadoMatch = text.match(/^Ver @empleado(\d+)/i);
|
||||||
|
if (verEmpleadoMatch && verEmpleadoMatch[1]) {
|
||||||
|
const cedula = parseInt(verEmpleadoMatch[1], 10);
|
||||||
|
log('info', `Comando recibido: Ver empleado con cédula ${cedula}.`);
|
||||||
|
const employee = mockEmployees.find(emp => emp.cedula === cedula);
|
||||||
|
if (employee) {
|
||||||
|
log('info', `Empleado encontrado: ${employee.name}. Enviando componente cardEmpleado.`);
|
||||||
|
// La cédula se pasa como parámetro para que el frontend la use si es necesario para buscar o mostrar.
|
||||||
|
sendText(msg.chatId, `CHAT_UI_COMPONENT::cardEmpleado::${cedula}`);
|
||||||
|
} else {
|
||||||
|
log('warn', `Empleado con cédula ${cedula} no encontrado.`);
|
||||||
|
sendText(msg.chatId, `No se encontró un empleado con la cédula ${cedula}.`);
|
||||||
|
}
|
||||||
|
return; // Termina el procesamiento para este comando
|
||||||
|
}
|
||||||
|
|
||||||
|
const mostrarEmpleadosMatch = text.match(/^Mostrame los primeros (\d+) @empleados/i);
|
||||||
|
if (mostrarEmpleadosMatch && mostrarEmpleadosMatch[1]) {
|
||||||
|
const count = parseInt(mostrarEmpleadosMatch[1], 10);
|
||||||
|
log('info', `Comando recibido: Mostrar los primeros ${count} empleados.`);
|
||||||
|
// El count se pasa como parámetro para que el frontend lo use para determinar cuántos mostrar.
|
||||||
|
// La lógica de obtener los X primeros empleados realmente estará en el frontend o en una API.
|
||||||
|
// Aquí solo indicamos el componente y el count deseado.
|
||||||
|
sendText(msg.chatId, `CHAT_UI_COMPONENT::tablaEmpleados::${count}`);
|
||||||
|
return; // Termina el procesamiento para este comando
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----- comando @nucleo ----- */
|
||||||
|
// Se comenta la condicion original de @nucleo para evitar doble respuesta si no se hace return antes.
|
||||||
|
// if (/^@nucleo(\s|$)/i.test(text)) {
|
||||||
|
// // Llama a la función importada
|
||||||
|
// // await respuestaNormal(msg); // Ya no se usa respuestaNormal aquí directamente.
|
||||||
|
// await respuestaMCP(msg); // respuestaMCP ya no es relevante en este flujo si @nucleo siempre va a brave.
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (/^@nucleo(\s|$)/i.test(text)) { // Modificado para que @nucleo solo dispare respuestaBrave
|
||||||
log('info', '🧠 Generando respuesta para @nucleo...');
|
log('info', '🧠 Generando respuesta para @nucleo...');
|
||||||
const respuestaObjMCP = await respuestaBrave(msg); // <- LLAMADA A LA FUNCIÓN EXTERNA
|
const respuestaObjMCP = await respuestaBrave(msg);
|
||||||
log('info', 'Respuesta de MCP:', respuestaObjMCP);
|
log('info', 'Respuesta de @nucleo (Brave):', respuestaObjMCP);
|
||||||
sendText(msg.chatId, respuestaObjMCP);
|
sendText(msg.chatId, respuestaObjMCP);
|
||||||
} else {
|
} else {
|
||||||
// Lógica para otros mensajes (si aplica)
|
// Lógica para otros mensajes si no son comandos de UI ni @nucleo
|
||||||
// log('debug', 'Mensaje recibido no es comando @nucleo:', text);
|
log('debug', 'Mensaje no reconocido como comando UI o @nucleo:', text);
|
||||||
|
// Considerar si se debe enviar una respuesta por defecto o ninguna si no coincide con nada.
|
||||||
|
// Por ahora, no se envía nada si no es un comando específico.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1 +1,112 @@
|
|||||||
|
<template>
|
||||||
|
<div class="bg-white shadow-lg rounded-lg p-6 m-4 w-full max-w-sm hover:shadow-xl transition-shadow duration-300 ease-in-out">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<img
|
||||||
|
:src="employee.avatar_url || 'https://via.placeholder.com/150'"
|
||||||
|
alt="Avatar del empleado"
|
||||||
|
class="w-20 h-20 rounded-full mr-6 border-2 border-blue-500 object-cover"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-2xl font-bold text-gray-800">{{ employee.name }}</h2>
|
||||||
|
<p class="text-gray-600 text-sm">Cédula: {{ employee.cedula }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div v-if="employee.telefono" class="flex items-center text-gray-700">
|
||||||
|
<svg class="w-5 h-5 mr-2 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.308 1.154a11.042 11.042 0 005.516 5.516l1.154-2.308a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"></path></svg>
|
||||||
|
<span>{{ employee.telefono }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="employee.ubicacion" class="flex items-center text-gray-700">
|
||||||
|
<svg class="w-5 h-5 mr-2 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"></path></svg>
|
||||||
|
<span>{{ employee.ubicacion }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="employee.idciat" class="flex items-center text-gray-700">
|
||||||
|
<svg class="w-5 h-5 mr-2 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V8a2 2 0 00-2-2h-5m-4 0V5a2 2 0 012-2h2a2 2 0 012 2v1m-4 0h4m-6 10v-5m0 5v0z"></path></svg>
|
||||||
|
<span>ID CIAT: {{ employee.idciat }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="employee.grupo_estudio" class="flex items-center text-gray-700">
|
||||||
|
<svg class="w-5 h-5 mr-2 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 14l9-5-9-5-9 5 9 5z"></path><path d="M12 14l6.16-3.422a12.083 12.083 0 01.665 6.479A11.952 11.952 0 0012 20.055a11.952 11.952 0 00-6.824-5.998 12.083 12.083 0 01.665-6.479L12 14z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 14l9-5-9-5-9 5 9 5zm0 0v3.945m0-3.945L6.161 10.58M17.839 10.58L12 14m5.839-3.42L12 14m0 0l6.161 3.42m-6.161-3.42L5.839 14.002"></path></svg>
|
||||||
|
<span>Grupo Estudio: {{ employee.grupo_estudio }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 flex justify-end space-x-3">
|
||||||
|
<button @click="handleEdit" class="px-4 py-2 bg-blue-500 text-white text-sm font-medium rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-offset-2">
|
||||||
|
Editar
|
||||||
|
</button>
|
||||||
|
<button @click="handleViewDetails" class="px-4 py-2 bg-gray-200 text-gray-700 text-sm font-medium rounded-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-400 focus:ring-offset-2">
|
||||||
|
Ver Detalles
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { PropType } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
// Define the structure of the employee object based on the Prisma schema
|
||||||
|
interface Employee {
|
||||||
|
id: string | number // Changed from BigInt to string | number for easier handling in frontend
|
||||||
|
name: string
|
||||||
|
cedula: number // Changed from BigInt
|
||||||
|
avatar_url?: string
|
||||||
|
telefono?: string
|
||||||
|
ubicacion: string
|
||||||
|
idciat?: string
|
||||||
|
grupo_estudio?: string
|
||||||
|
// created_at and updated_at are usually not displayed directly in a summary card
|
||||||
|
// empleado: boolean // This is implicit as it's an employee card
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
employee: {
|
||||||
|
type: Object as PropType<Employee>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const handleEdit = () => {
|
||||||
|
// Ensure employee.id is available and correctly typed for URL
|
||||||
|
router.push(`/empleados/edit/${props.employee.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleViewDetails = () => {
|
||||||
|
// This could navigate to a more detailed employee page if one exists
|
||||||
|
// For now, it can also navigate to an edit page or a specific detail view
|
||||||
|
// Depending on the application's routing structure, this might be the same as edit or a different view
|
||||||
|
router.push(`/empleados/view/${props.employee.id}`) // Assuming a dedicated view route exists or will be created
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Scoped styles for the card ensure they don't leak */
|
||||||
|
.max-w-sm {
|
||||||
|
max-width: 24rem; /* Consistent with Tailwind's sm breakpoint for max-width */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Adding some subtle enhancements for visual appeal */
|
||||||
|
.text-2xl {
|
||||||
|
line-height: 1.2; /* Adjust line height for the title for better readability */
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex svg {
|
||||||
|
flex-shrink: 0; /* Prevent SVGs from shrinking if text is long, ensuring icon consistency */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styling for buttons for a more refined and interactive look */
|
||||||
|
button {
|
||||||
|
transition: background-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out, transform 0.1s ease-in-out;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
transform: translateY(-1px); /* Slight lift on hover */
|
||||||
|
}
|
||||||
|
button:active {
|
||||||
|
transform: translateY(0px); /* Reset lift on click */
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded-full {
|
||||||
|
object-fit: cover; /* Ensures the avatar image covers the area without distortion */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1 +1,163 @@
|
|||||||
|
<template>
|
||||||
|
<div class="overflow-x-auto bg-white shadow-md rounded-lg p-4">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Avatar
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Nombre
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Cédula
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Teléfono
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Ubicación
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
ID CIAT
|
||||||
|
</th>
|
||||||
|
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||||
|
Acciones
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white divide-y divide-gray-200">
|
||||||
|
<tr v-if="!employees || employees.length === 0">
|
||||||
|
<td colspan="7" class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-center">
|
||||||
|
No hay empleados para mostrar.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-for="employee in employees" :key="employee.id" class="hover:bg-gray-100 transition-colors duration-150 ease-in-out">
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<img
|
||||||
|
:src="employee.avatar_url || 'https://via.placeholder.com/40'"
|
||||||
|
alt="Avatar"
|
||||||
|
class="w-10 h-10 rounded-full object-cover border border-gray-300 shadow-sm"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
|
<div class="text-sm font-semibold text-gray-900">{{ employee.name }}</div>
|
||||||
|
<div v-if="employee.grupo_estudio" class="text-xs text-gray-500">
|
||||||
|
Grupo: {{ employee.grupo_estudio }}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
|
||||||
|
{{ employee.cedula }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
|
||||||
|
{{ employee.telefono || '-' }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
|
||||||
|
{{ employee.ubicacion }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600">
|
||||||
|
{{ employee.idciat || '-' }}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium space-x-2">
|
||||||
|
<button
|
||||||
|
@click="handleEdit(employee.id)"
|
||||||
|
class="text-indigo-600 hover:text-indigo-800 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 p-1 rounded-md hover:bg-indigo-100 transition-all duration-150 ease-in-out"
|
||||||
|
title="Editar Empleado"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path d="M17.414 2.586a2 2 0 00-2.828 0L7 10.172V13h2.828l7.586-7.586a2 2 0 000-2.828z" />
|
||||||
|
<path fill-rule="evenodd" d="M2 6a2 2 0 012-2h4a1 1 0 010 2H4v10h10v-4a1 1 0 112 0v4a2 2 0 01-2 2H4a2 2 0 01-2-2V6z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="handleViewDetails(employee.id)"
|
||||||
|
class="text-green-600 hover:text-green-800 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 p-1 rounded-md hover:bg-green-100 transition-all duration-150 ease-in-out"
|
||||||
|
title="Ver Detalles del Empleado"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { PropType } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
// Interface for Employee object structure, aligning with prisma model (excluding sensitive or large fields for table view)
|
||||||
|
interface Employee {
|
||||||
|
id: string | number; // Primary key for navigation and :key
|
||||||
|
name: string;
|
||||||
|
cedula: number; // Assuming cedula is a number; adjust if it's a string
|
||||||
|
avatar_url?: string;
|
||||||
|
telefono?: string;
|
||||||
|
ubicacion: string; // As per schema, this has a default and likely always present
|
||||||
|
idciat?: string;
|
||||||
|
grupo_estudio?: string;
|
||||||
|
// Omitting created_at, updated_at, empleado boolean for brevity in table
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
employees: {
|
||||||
|
type: Array as PropType<Employee[]>,
|
||||||
|
required: true,
|
||||||
|
default: () => [], // Provides a default empty array if no prop is passed
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const handleEdit = (employeeId: string | number) => {
|
||||||
|
router.push(`/empleados/edit/${employeeId}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleViewDetails = (employeeId: string | number) => {
|
||||||
|
// This could navigate to a dedicated detail view or the edit view itself
|
||||||
|
router.push(`/empleados/view/${employeeId}`); // Adjust route as per application structure
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Enhancing table aesthetics and interactivity */
|
||||||
|
.min-w-full {
|
||||||
|
border-collapse: separate; /* Allows for rounded corners on table if desired, or better spacing */
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
font-weight: 600; /* semibold for headers */
|
||||||
|
color: #4b5563; /* gray-600 for header text */
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
color: #374151; /* gray-700 for cell text */
|
||||||
|
}
|
||||||
|
|
||||||
|
.rounded-full {
|
||||||
|
object-fit: cover; /* Ensures avatar images are displayed nicely within their circular bounds */
|
||||||
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); /* Subtle shadow for avatars */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Action button icon styling */
|
||||||
|
button svg {
|
||||||
|
transition: transform 0.15s ease-in-out;
|
||||||
|
}
|
||||||
|
button:hover svg {
|
||||||
|
transform: scale(1.15); /* Slightly larger pop on hover for icons */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure consistent padding and alignment */
|
||||||
|
.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
|
||||||
|
.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
|
||||||
|
.py-4 { padding-top: 1rem; padding-bottom: 1rem; }
|
||||||
|
|
||||||
|
/* Adding a subtle border to the table container itself */
|
||||||
|
.overflow-x-auto {
|
||||||
|
border: 1px solid #e5e7eb; /* Tailwind's gray-200 */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1 +1,272 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-6 bg-gray-100 min-h-screen">
|
||||||
|
<h1 class="text-3xl font-bold mb-8 text-center text-gray-700">
|
||||||
|
{{ isEditMode ? 'Editar Empleado' : 'Crear Empleado' }}
|
||||||
|
</h1>
|
||||||
|
<form
|
||||||
|
@submit.prevent="handleSubmit"
|
||||||
|
class="max-w-lg mx-auto bg-white p-8 rounded-lg shadow-lg"
|
||||||
|
>
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="name" class="block text-gray-700 font-semibold mb-2">Nombre Completo</label>
|
||||||
|
<input
|
||||||
|
id="name"
|
||||||
|
v-model="form.name"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="Ej: Juan Pérez"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="cedula" class="block text-gray-700 font-semibold mb-2">Cédula</label>
|
||||||
|
<input
|
||||||
|
id="cedula"
|
||||||
|
v-model="form.cedula"
|
||||||
|
type="number"
|
||||||
|
required
|
||||||
|
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="Ej: 123456789"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="ubicacion" class="block text-gray-700 font-semibold mb-2">Ubicación</label>
|
||||||
|
<input
|
||||||
|
id="ubicacion"
|
||||||
|
v-model="form.ubicacion"
|
||||||
|
type="text"
|
||||||
|
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="Ej: Oficina Principal"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="grupo_estudio" class="block text-gray-700 font-semibold mb-2">Grupo de Estudio</label>
|
||||||
|
<input
|
||||||
|
id="grupo_estudio"
|
||||||
|
v-model="form.grupo_estudio"
|
||||||
|
type="text"
|
||||||
|
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="Ej: Grupo A"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="avatar_url" class="block text-gray-700 font-semibold mb-2">URL del Avatar</label>
|
||||||
|
<input
|
||||||
|
id="avatar_url"
|
||||||
|
v-model="form.avatar_url"
|
||||||
|
type="url"
|
||||||
|
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="Ej: https://example.com/avatar.png"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="telefono" class="block text-gray-700 font-semibold mb-2">Teléfono</label>
|
||||||
|
<input
|
||||||
|
id="telefono"
|
||||||
|
v-model="form.telefono"
|
||||||
|
type="tel"
|
||||||
|
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="Ej: 0991234567"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-6">
|
||||||
|
<label for="idciat" class="block text-gray-700 font-semibold mb-2">ID CIAT</label>
|
||||||
|
<input
|
||||||
|
id="idciat"
|
||||||
|
v-model="form.idciat"
|
||||||
|
type="text"
|
||||||
|
class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
|
placeholder="Ej: CIAT123"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="handleCancel"
|
||||||
|
class="mr-4 px-6 py-2 text-gray-700 border border-gray-300 rounded-md hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-400"
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="px-6 py-2 bg-blue-600 text-white font-semibold rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2"
|
||||||
|
>
|
||||||
|
{{ isEditMode ? 'Actualizar' : 'Crear' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, computed } from 'vue'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
interface EmpleadoForm {
|
||||||
|
name: string
|
||||||
|
cedula: number | null
|
||||||
|
ubicacion: string
|
||||||
|
grupo_estudio?: string
|
||||||
|
avatar_url?: string
|
||||||
|
telefono?: string
|
||||||
|
idciat?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const form = ref<EmpleadoForm>({
|
||||||
|
name: '',
|
||||||
|
cedula: null,
|
||||||
|
ubicacion: '.', // Default value as per schema
|
||||||
|
grupo_estudio: '',
|
||||||
|
avatar_url: '',
|
||||||
|
telefono: '',
|
||||||
|
idciat: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const employeeId = ref<string | null>(null)
|
||||||
|
|
||||||
|
const isEditMode = computed(() => !!employeeId.value)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (route.params.id) {
|
||||||
|
employeeId.value = route.params.id as string
|
||||||
|
// In a real application, you would fetch the employee data here
|
||||||
|
// For example:
|
||||||
|
// const response = await fetch(`/api/employees/${employeeId.value}`)
|
||||||
|
// const data = await response.json()
|
||||||
|
// form.value = data // Populate form with fetched data
|
||||||
|
// For now, we'll simulate fetching data or indicate that it needs to be done
|
||||||
|
if (employeeId.value) {
|
||||||
|
console.log(`Editing employee ID: ${employeeId.value}. Need to fetch data.`);
|
||||||
|
// Example: Pre-populate form for an existing employee
|
||||||
|
// form.value = {
|
||||||
|
// name: 'Juan Pérez Existente',
|
||||||
|
// cedula: 123456789,
|
||||||
|
// ubicacion: 'Oficina Antigua',
|
||||||
|
// grupo_estudio: 'Grupo Z',
|
||||||
|
// avatar_url: 'https://example.com/avatar_existente.png',
|
||||||
|
// telefono: '0987654321',
|
||||||
|
// idciat: 'CIATXYZ'
|
||||||
|
// };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
// Ensure cedula is a number if provided, or null otherwise
|
||||||
|
const cedulaValue = form.value.cedula ? Number(form.value.cedula) : null;
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
...form.value,
|
||||||
|
cedula: cedulaValue,
|
||||||
|
empleado: true, // This form is specifically for employees
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isEditMode.value) {
|
||||||
|
console.log('Actualizando empleado:', employeeId.value, payload)
|
||||||
|
// Replace with actual API call
|
||||||
|
// const response = await fetch(`/api/clientes/${employeeId.value}`, {
|
||||||
|
// method: 'PUT',
|
||||||
|
// headers: { 'Content-Type': 'application/json' },
|
||||||
|
// body: JSON.stringify(payload),
|
||||||
|
// })
|
||||||
|
// if (!response.ok) throw new Error('Failed to update employee')
|
||||||
|
// const updatedEmployee = await response.json()
|
||||||
|
// console.log('Empleado actualizado:', updatedEmployee)
|
||||||
|
} else {
|
||||||
|
console.log('Creando empleado:', payload)
|
||||||
|
// Replace with actual API call
|
||||||
|
// const response = await fetch('/api/clientes', {
|
||||||
|
// method: 'POST',
|
||||||
|
// headers: { 'Content-Type': 'application/json' },
|
||||||
|
// body: JSON.stringify(payload),
|
||||||
|
// })
|
||||||
|
// if (!response.ok) throw new Error('Failed to create employee')
|
||||||
|
// const newEmployee = await response.json()
|
||||||
|
// console.log('Empleado creado:', newEmployee)
|
||||||
|
}
|
||||||
|
router.push('/empleados') // Or wherever the employee list is
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error submitting form:', error)
|
||||||
|
// Handle error (e.g., show a notification to the user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = ()_=> {
|
||||||
|
router.go(-1) // Go back to the previous page or to a default route
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Custom styles for better visual appeal if needed */
|
||||||
|
input:required:invalid {
|
||||||
|
border-color: #e53e3e; /* Tailwind's red-600 */
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus, button:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 0 2px #3b82f6; /* Tailwind's blue-500 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styling for a more elegant look and feel */
|
||||||
|
.form-container {
|
||||||
|
background-color: #f9fafb; /* Tailwind's gray-50 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-card {
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); /* Tailwind's shadow-xl */
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-title {
|
||||||
|
color: #1f2937; /* Tailwind's gray-800 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
color: #374151; /* Tailwind's gray-700 */
|
||||||
|
font-weight: 600; /* semibold */
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
border-color: #d1d5db; /* Tailwind's gray-300 */
|
||||||
|
transition: border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
.form-input:focus {
|
||||||
|
border-color: #2563eb; /* Tailwind's blue-600 */
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.5); /* Ring focus */
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background-color: #2563eb; /* Tailwind's blue-600 */
|
||||||
|
color: white;
|
||||||
|
font-weight: 600; /* semibold */
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 0.375rem; /* rounded-md */
|
||||||
|
transition: background-color 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
background-color: #1d4ed8; /* Tailwind's blue-700 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: #e5e7eb; /* Tailwind's gray-200 */
|
||||||
|
color: #374151; /* Tailwind's gray-700 */
|
||||||
|
font-weight: 600; /* semibold */
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 0.375rem; /* rounded-md */
|
||||||
|
border: 1px solid #d1d5db; /* Tailwind's gray-300 */
|
||||||
|
transition: background-color 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background-color: #d1d5db; /* Tailwind's gray-300 */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1 +1,208 @@
|
|||||||
|
<template>
|
||||||
|
<div class="p-6 bg-gray-50 min-h-screen">
|
||||||
|
<header class="mb-8">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<h1 class="text-4xl font-bold text-gray-800">Gestión de Empleados</h1>
|
||||||
|
<button
|
||||||
|
@click="goToCreateEmployee"
|
||||||
|
class="px-6 py-3 bg-blue-600 text-white font-semibold rounded-lg shadow-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition duration-150 ease-in-out"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 inline-block mr-2" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
Crear Empleado
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 text-gray-600">Visualiza, crea y gestiona los empleados de la organización.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="mb-6 flex justify-end items-center space-x-3">
|
||||||
|
<span class="text-sm font-medium text-gray-700">Cambiar Vista:</span>
|
||||||
|
<button
|
||||||
|
@click="currentView = 'card'"
|
||||||
|
:class="[
|
||||||
|
'px-4 py-2 rounded-md text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2',
|
||||||
|
currentView === 'card'
|
||||||
|
? 'bg-blue-500 text-white shadow-sm focus:ring-blue-400'
|
||||||
|
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 focus:ring-gray-400',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
Tarjetas
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="currentView = 'table'"
|
||||||
|
:class="[
|
||||||
|
'px-4 py-2 rounded-md text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2',
|
||||||
|
currentView === 'table'
|
||||||
|
? 'bg-blue-500 text-white shadow-sm focus:ring-blue-400'
|
||||||
|
: 'bg-gray-200 text-gray-700 hover:bg-gray-300 focus:ring-gray-400',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
Tabla
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div v-if="loading" class="text-center py-10">
|
||||||
|
<p class="text-gray-500 text-xl">Cargando empleados...</p>
|
||||||
|
<!-- You can add a spinner here -->
|
||||||
|
</div>
|
||||||
|
<div v-else-if="error" class="text-center py-10">
|
||||||
|
<p class="text-red-500 text-xl">Error al cargar los empleados: {{ error }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<!-- Card View -->
|
||||||
|
<div v-if="currentView === 'card'" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||||
|
<CardEmpleado v-for="employee in employees" :key="employee.id" :employee="employee" />
|
||||||
|
<div v-if="employees.length === 0" class="col-span-full text-center py-10">
|
||||||
|
<p class="text-gray-500 text-xl">No hay empleados para mostrar en la vista de tarjetas.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Table View -->
|
||||||
|
<div v-if="currentView === 'table'">
|
||||||
|
<TablaEmpleados :employees="employees" />
|
||||||
|
<div v-if="employees.length === 0" class="text-center py-10 bg-white shadow-md rounded-lg mt-4">
|
||||||
|
<p class="text-gray-500 text-xl">No hay empleados para mostrar en la vista de tabla.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import CardEmpleado from '@/components/empleados/cardEmpleado.vue' // Adjusted path
|
||||||
|
import TablaEmpleados from '@/components/empleados/tablaEmpleados.vue' // Adjusted path
|
||||||
|
|
||||||
|
// Define the structure of the employee object
|
||||||
|
interface Employee {
|
||||||
|
id: string | number
|
||||||
|
name: string
|
||||||
|
cedula: number
|
||||||
|
avatar_url?: string
|
||||||
|
telefono?: string
|
||||||
|
ubicacion: string
|
||||||
|
idciat?: string
|
||||||
|
grupo_estudio?: string
|
||||||
|
empleado: boolean // To ensure we are dealing with employees
|
||||||
|
}
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const currentView = ref<'card' | 'table'>('card') // Default view
|
||||||
|
const employees = ref<Employee[]>([])
|
||||||
|
const loading = ref(true)
|
||||||
|
const error = ref<string | null>(null)
|
||||||
|
|
||||||
|
// Mock data for employees - replace with actual API call
|
||||||
|
const mockEmployees: Employee[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
name: 'Ana García',
|
||||||
|
cedula: 123456789,
|
||||||
|
avatar_url: 'https://randomuser.me/api/portraits/women/60.jpg',
|
||||||
|
telefono: '0991234567',
|
||||||
|
ubicacion: 'Oficina Central',
|
||||||
|
idciat: 'AG001',
|
||||||
|
grupo_estudio: 'Desarrollo Frontend',
|
||||||
|
empleado: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
name: 'Carlos Rodriguez',
|
||||||
|
cedula: 987654321,
|
||||||
|
avatar_url: 'https://randomuser.me/api/portraits/men/45.jpg',
|
||||||
|
telefono: '0987654321',
|
||||||
|
ubicacion: 'Sucursal Norte',
|
||||||
|
idciat: 'CR002',
|
||||||
|
grupo_estudio: 'Backend Services',
|
||||||
|
empleado: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
name: 'Luisa Martinez',
|
||||||
|
cedula: 112233445,
|
||||||
|
// avatar_url: '', // Test fallback avatar
|
||||||
|
telefono: '0976543210',
|
||||||
|
ubicacion: 'Remoto',
|
||||||
|
idciat: 'LM003',
|
||||||
|
// grupo_estudio: '', // Test missing optional field
|
||||||
|
empleado: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
name: 'Jorge Herrera',
|
||||||
|
cedula: 223344556,
|
||||||
|
avatar_url: 'https://randomuser.me/api/portraits/men/50.jpg',
|
||||||
|
telefono: '0965432109',
|
||||||
|
ubicacion: 'Oficina Central',
|
||||||
|
idciat: 'JH004',
|
||||||
|
grupo_estudio: 'QA Team',
|
||||||
|
empleado: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const fetchEmployees = async () => {
|
||||||
|
loading.value = true
|
||||||
|
error.value = null
|
||||||
|
try {
|
||||||
|
// Simulate API call delay
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
|
// In a real application, you would fetch from your API:
|
||||||
|
// const response = await fetch('/api/clientes?empleado=true');
|
||||||
|
// if (!response.ok) {
|
||||||
|
// throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
// }
|
||||||
|
// const data = await response.json();
|
||||||
|
// employees.value = data.filter((cliente: Employee) => cliente.empleado);
|
||||||
|
|
||||||
|
// Using mock data for now
|
||||||
|
employees.value = mockEmployees.filter(e => e.empleado);
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to fetch employees:", e);
|
||||||
|
if (e instanceof Error) {
|
||||||
|
error.value = e.message;
|
||||||
|
} else {
|
||||||
|
error.value = "Ocurrió un error desconocido."
|
||||||
|
}
|
||||||
|
employees.value = []; // Clear employees on error or set to empty if preferred
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchEmployees()
|
||||||
|
})
|
||||||
|
|
||||||
|
const goToCreateEmployee = () => {
|
||||||
|
router.push('/empleados/new') // Assuming this is the route for EmpleadoForm.vue for creation
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Additional styling for the index page */
|
||||||
|
.min-h-screen {
|
||||||
|
min-height: calc(100vh - var(--navbar-height, 0px)); /* Adjust if you have a fixed navbar */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styling for active/inactive toggle buttons */
|
||||||
|
button.focus\:ring-blue-400:focus { /* More specific focus for blue buttons */
|
||||||
|
box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.5); /* Tailwind's blue-400 with 50% opacity */
|
||||||
|
}
|
||||||
|
button.focus\:ring-gray-400:focus { /* More specific focus for gray buttons */
|
||||||
|
box-shadow: 0 0 0 3px rgba(156, 163, 175, 0.5); /* Tailwind's gray-400 with 50% opacity */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Transition for views, if desired (can be more complex) */
|
||||||
|
.view-enter-active, .view-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
.view-enter-from, .view-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user