actualizada API empleados, conectando UI con API
Some checks failed
build-and-deploy / filter (push) Successful in 3s
Sync to GitHub / sync (push) Failing after 1s
build-and-deploy / build (push) Successful in 13s
build-and-deploy / deploy (push) Successful in 14s

This commit is contained in:
2025-05-30 10:51:21 -06:00
parent 30f85bf602
commit 2a844d275d
4 changed files with 232 additions and 246 deletions

View File

@@ -1,142 +1,126 @@
import express from 'express';
const router = express.Router();
import { PrismaClient } from '../../prisma/generated/client/index.js';
const prisma = new PrismaClient();
import express from 'express'
import { PrismaClient } from '../../prisma/generated/client/index.js'
// GET all empleados
router.get('/', async (req, res) => {
const router = express.Router()
const prisma = new PrismaClient()
// ⚙️ helper: evita el crash al serializar BigInt
const fixBigInt = (data) =>
JSON.parse(JSON.stringify(data, (_, v) => (typeof v === 'bigint' ? v.toString() : v)))
// ───── GET todos los empleados ─────
router.get('/', async (_req, res) => {
try {
const empleados = await prisma.cliente.findMany({
where: { empleado: true },
});
res.json(empleados);
} catch (error) {
console.error(error); // Log the error for debugging
res.status(500).json({ error: 'Error al obtener empleados.' });
const empleados = await prisma.cliente.findMany({ where: { empleado: true } })
res.json(fixBigInt(empleados))
} catch (e) {
console.error(e)
res.status(500).json({ error: 'Error al obtener empleados.' })
}
});
})
// GET empleado by ID
// ───── GET empleado por ID ─────
router.get('/:id', async (req, res) => {
const { id } = req.params;
const id = BigInt(req.params.id)
try {
const empleado = await prisma.cliente.findFirst({
where: {
id: parseInt(id),
empleado: true
},
});
if (empleado) {
res.json(empleado);
} else {
res.status(404).json({ error: 'Empleado no encontrado.' });
const empleado = await prisma.cliente.findFirst({ where: { id, empleado: true } })
if (!empleado) return res.status(404).json({ error: 'Empleado no encontrado.' })
res.json(fixBigInt(empleado))
} catch (e) {
console.error(e)
res.status(500).json({ error: 'Error al obtener empleado.' })
}
} catch (error) {
console.error(error); // Log the error for debugging
res.status(500).json({ error: 'Error al obtener empleado.' });
}
});
})
// POST create new empleado
// ───── POST crear empleado ─────
router.post('/', async (req, res) => {
const { nombre, apellido, dni, telefono, direccion, email } = req.body;
try {
const nuevoEmpleado = await prisma.cliente.create({
data: {
nombre,
apellido,
dni,
const {
name,
cedula,
telefono,
direccion,
email,
empleado: true, // Ensure empleado is set to true
},
});
res.status(201).json(nuevoEmpleado);
} catch (error) {
console.error(error); // Log the error for debugging
if (error.code === 'P2002' && error.meta?.target?.includes('dni')) {
return res.status(400).json({ error: 'Ya existe un cliente con este DNI.' });
}
if (error.code === 'P2002' && error.meta?.target?.includes('email')) {
return res.status(400).json({ error: 'Ya existe un cliente con este Email.' });
}
res.status(500).json({ error: 'Error al crear empleado.' });
}
});
ubicacion = '.',
grupo_estudio,
avatar_url,
idciat,
} = req.body
// PUT update empleado by ID
try {
const nuevo = await prisma.cliente.create({
data: {
name,
cedula: BigInt(cedula),
telefono,
ubicacion,
grupo_estudio,
avatar_url,
idciat,
empleado: true,
},
})
res.status(201).json(fixBigInt(nuevo))
} catch (e) {
console.error(e)
if (e.code === 'P2002' && e.meta?.target?.includes('cedula'))
return res.status(400).json({ error: 'Ya existe un cliente con esa cédula.' })
res.status(500).json({ error: 'Error al crear empleado.' })
}
})
// ───── PUT actualizar empleado ─────
router.put('/:id', async (req, res) => {
const { id } = req.params;
const { nombre, apellido, dni, telefono, direccion, email } = req.body;
try {
// First, check if the employee exists and is an employee
const existingEmpleado = await prisma.cliente.findFirst({
where: {
id: parseInt(id),
empleado: true,
},
});
if (!existingEmpleado) {
return res.status(404).json({ error: 'Empleado no encontrado.' });
}
const empleadoActualizado = await prisma.cliente.update({
where: { id: parseInt(id) },
data: {
nombre,
apellido,
dni,
const id = BigInt(req.params.id)
const {
name,
cedula,
telefono,
direccion,
email,
// empleado: true, // Keep it as an employee, or allow changing this? For now, keep as true.
},
});
res.json(empleadoActualizado);
} catch (error) {
console.error(error); // Log the error for debugging
if (error.code === 'P2002' && error.meta?.target?.includes('dni')) {
return res.status(400).json({ error: 'Ya existe un cliente con este DNI.' });
}
if (error.code === 'P2002' && error.meta?.target?.includes('email')) {
return res.status(400).json({ error: 'Ya existe un cliente con este Email.' });
}
if (error.code === 'P2025') { // Record to update not found
return res.status(404).json({ error: 'Empleado no encontrado para actualizar.' });
}
res.status(500).json({ error: 'Error al actualizar empleado.' });
}
});
ubicacion,
grupo_estudio,
avatar_url,
idciat,
} = req.body
// DELETE empleado by ID
router.delete('/:id', async (req, res) => {
const { id } = req.params;
try {
// First, check if the employee exists and is an employee
const existingEmpleado = await prisma.cliente.findFirst({
where: {
id: parseInt(id),
empleado: true,
const existe = await prisma.cliente.findFirst({ where: { id, empleado: true } })
if (!existe) return res.status(404).json({ error: 'Empleado no encontrado.' })
const actualizado = await prisma.cliente.update({
where: { id },
data: {
name,
cedula: cedula !== undefined ? BigInt(cedula) : undefined,
telefono,
ubicacion,
grupo_estudio,
avatar_url,
idciat,
},
});
if (!existingEmpleado) {
return res.status(404).json({ error: 'Empleado no encontrado para eliminar.' });
})
res.json(fixBigInt(actualizado))
} catch (e) {
console.error(e)
if (e.code === 'P2002' && e.meta?.target?.includes('cedula'))
return res.status(400).json({ error: 'Ya existe un cliente con esa cédula.' })
if (e.code === 'P2025')
return res.status(404).json({ error: 'Empleado no encontrado para actualizar.' })
res.status(500).json({ error: 'Error al actualizar empleado.' })
}
})
await prisma.cliente.delete({
where: { id: parseInt(id) },
});
res.status(204).send(); // No content
} catch (error) {
console.error(error); // Log the error for debugging
if (error.code === 'P2025') { // Record to delete not found
return res.status(404).json({ error: 'Empleado no encontrado para eliminar.' });
}
res.status(500).json({ error: 'Error al eliminar empleado.' });
}
});
// ───── DELETE eliminar empleado ─────
router.delete('/:id', async (req, res) => {
const id = BigInt(req.params.id)
try {
const existe = await prisma.cliente.findFirst({ where: { id, empleado: true } })
if (!existe) return res.status(404).json({ error: 'Empleado no encontrado.' })
export default router;
await prisma.cliente.delete({ where: { id } })
res.status(204).send()
} catch (e) {
console.error(e)
if (e.code === 'P2025')
return res.status(404).json({ error: 'Empleado no encontrado para eliminar.' })
res.status(500).json({ error: 'Error al eliminar empleado.' })
}
})
export default router

View File

@@ -20,9 +20,9 @@
<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">
<div v-if="employee.cedula" 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>
<span>ID CIAT: {{ employee.cedula }}</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>

View File

@@ -47,8 +47,9 @@ export const useEmpleadosStore = defineStore('empleados', {
async createEmpleado (empleadoData) {
try {
await apiClient.post('/api/empleados', empleadoData);
const {data} = await apiClient.post('/api/empleados/', empleadoData);
await this.fetchEmpleados();
return data
} catch (err) {
console.error('Error creando empleado:', err);
throw err; // para que el form muestre feedback
@@ -57,9 +58,10 @@ export const useEmpleadosStore = defineStore('empleados', {
async updateEmpleado (id, empleadoData) {
try {
await apiClient.put(`/api/empleados/${id}`, empleadoData);
const {data} = await apiClient.put(`/api/empleados/${id}`, empleadoData);
await this.fetchEmpleados();
this.clearCurrentEmpleado();
return data
} catch (err) {
console.error(`Error actualizando empleado ${id}:`, err);
throw err;

View File

@@ -3,100 +3,140 @@
<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"
>
<!-- Nombre -->
<div class="mb-6">
<label for="name" class="block text-gray-700 font-semibold mb-2">Nombre Completo</label>
<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"
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>
<!-- Cédula -->
<div class="mb-6">
<label for="cedula" class="block text-gray-700 font-semibold mb-2">Cédula</label>
<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"
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>
<!-- Ubicación -->
<div class="mb-6">
<label for="ubicacion" class="block text-gray-700 font-semibold mb-2">Ubicación</label>
<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"
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>
<!-- Grupo de estudio -->
<div class="mb-6">
<label for="grupo_estudio" class="block text-gray-700 font-semibold mb-2">Grupo de Estudio</label>
<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"
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>
<!-- Avatar -->
<div class="mb-6">
<label for="avatar_url" class="block text-gray-700 font-semibold mb-2">URL del Avatar</label>
<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"
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>
<!-- Teléfono -->
<div class="mb-6">
<label for="telefono" class="block text-gray-700 font-semibold mb-2">Teléfono</label>
<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"
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>
<!-- ID CIAT -->
<div class="mb-6">
<label for="idciat" class="block text-gray-700 font-semibold mb-2">ID CIAT</label>
<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"
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>
<!-- Botones -->
<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"
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"
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>
@@ -108,7 +148,10 @@
<script setup lang="ts">
import { ref, onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { storeToRefs } from 'pinia'
import { useEmpleadosStore } from '@/stores/useEmpleados.js'
/* ───── Tipos ───── */
interface EmpleadoForm {
name: string
cedula: number | null
@@ -119,154 +162,111 @@ interface EmpleadoForm {
idciat?: string
}
const route = useRoute()
const router = useRouter()
const form = ref<EmpleadoForm>({
/* ───── helpers ───── */
const defaultForm = (): EmpleadoForm => ({
name: '',
cedula: null,
ubicacion: '.', // Default value as per schema
ubicacion: '.', // default del schema
grupo_estudio: '',
avatar_url: '',
telefono: '',
idciat: '',
})
const employeeId = ref<string | null>(null)
/* ───── Router ───── */
const route = useRoute()
const router = useRouter()
/* ───── Store ───── */
const empleadosStore = useEmpleadosStore()
const { currentEmpleado } = storeToRefs(empleadosStore)
/* ───── State ───── */
const form = ref<EmpleadoForm>(defaultForm())
const employeeId = computed(() => route.params.id as string | undefined)
const isEditMode = computed(() => !!employeeId.value)
/* ───── Lifecycle ───── */
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'
// };
if (isEditMode.value && employeeId.value) {
try {
await empleadosStore.fetchEmpleadoById(employeeId.value)
// copiamos solo campos que el form necesita
form.value = {
name : currentEmpleado.value.name ?? '',
cedula : currentEmpleado.value.cedula ?? null,
ubicacion : currentEmpleado.value.ubicacion ?? '.',
grupo_estudio : currentEmpleado.value.grupo_estudio ?? '',
avatar_url : currentEmpleado.value.avatar_url ?? '',
telefono : currentEmpleado.value.telefono ?? '',
idciat : currentEmpleado.value.idciat ?? '',
}
} catch (err) {
console.error('Error cargando empleado:', err)
}
}
})
/* ───── Submit ───── */
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
}
const payload = { ...form.value, empleado: true }
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)
if (isEditMode.value && employeeId.value) {
await empleadosStore.updateEmpleado(employeeId.value, payload)
} 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)
await empleadosStore.createEmpleado(payload)
}
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)
router.push('/empleados')
} catch (err) {
console.error('Error al guardar empleado:', err)
// aquí podrías disparar una notificación
}
}
/* ───── Cancel ───── */
const handleCancel = () => {
router.go(-1) // Go back to the previous page or to a default route
empleadosStore.clearCurrentEmpleado()
router.go(-1)
}
</script>
<style scoped>
/* Custom styles for better visual appeal if needed */
/* --- Validación rápida de inputs requeridos --- */
input:required:invalid {
border-color: #e53e3e; /* Tailwind's red-600 */
border-color: #e53e3e; /* red-600 */
}
input:focus, button:focus {
/* --- Focus global para inputs y botones --- */
input:focus,
button:focus {
outline: none;
box-shadow: 0 0 0 2px #3b82f6; /* Tailwind's blue-500 */
box-shadow: 0 0 0 2px #3b82f6; /* blue-500 */
}
/* Styling for a more elegant look and feel */
.form-container {
background-color: #f9fafb; /* Tailwind's gray-50 */
}
/* --- Look & feel extra (opcional, podés ajustar) --- */
.form-container { background-color: #f9fafb; } /* gray-50 */
.form-card { box-shadow: 0 10px 15px -3px rgba(0,0,0,.1),
0 4px 6px -2px rgba(0,0,0,.05); }
.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-title { color: #1f2937; } /* gray-800 */
.form-label { color: #374151; font-weight: 600; } /* gray-700 */
.form-input { border-color: #d1d5db; transition: border-color .2s, box-shadow .2s; }
.form-input:focus {
border-color: #2563eb; /* Tailwind's blue-600 */
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.5); /* Ring focus */
border-color: #2563eb; /* blue-600 */
box-shadow: 0 0 0 3px rgba(59,130,246,.5);
}
.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 */
background-color: #2563eb; color: #fff; font-weight: 600;
padding: .75rem 1.5rem; border-radius: .375rem; transition: background-color .2s;
}
.btn-primary:hover { background-color: #1d4ed8; }
.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 */
background-color: #e5e7eb; color: #374151; font-weight: 600;
padding: .75rem 1.5rem; border-radius: .375rem; border: 1px solid #d1d5db;
transition: background-color .2s;
}
.btn-secondary:hover { background-color: #d1d5db; }
</style>