- Se cerró el flujo funcional en empleados:
consultar, editar y borrar lógico desde la misma pantalla.
- Se completó la UI de empleados con tabla, acciones por
fila, panel de detalle y formulario de edición.
- Se ajustó backend para alinear el nombre del SP de
inserción con base de datos.
- Se recompilaron frontend y backend.
- Se ejecutaron pruebas API end-to-end: insertar ->
editar -> borrar -> verificar que ya no aparece.
SELECT Id, Nombre,
ValorDocumentoIdentidad, idPuesto FROM [dbo].[Empleado]
WHERE Nombre LIKE '%' +
@filtroNomvbrePuesto + '%'))
SELECT Id, Nombre,
ValorDocumentoIdentidad, idPuesto FROM [dbo].[Empleado]
WHERE Nombre LIKE '%' +
@filtroNomvbrePuesto + '%'))
- Se reemplazó Activo por EsActivo.
- Se eliminó el segundo WHERE y se cambió por AND.
2) Desalineación backend vs SP en inserción de empleados.
- Se alineó el nombre del SP en controller.
- Se volvieron a desplegar SPs de empleado para sincronizar
la base.
- Se definió que la pantalla de empleados iba primero con
base visual (HTML/CSS) y luego comportamiento TS.
- Se decidió mantener todo en una sola vista (tabla +
detalle + edición) en lugar de separar en pantallas distintas.
/**
* empleados.ts
* Lógica de la pantalla de empleados.
*
* Este archivo solo se encarga de la interfaz del navegador:
* - leer el filtro
* - llamar al backend
* - pintar la tabla
* - mostrar mensajes de estado
*/
type Empleado = {
Id: number;
Nombre: string;
ValorDocumentoIdentidad: string;
idPuesto: number;
};
type EmpleadoDetalle = {
ValorDocumentoIdentidad: string;
Nombre: string;
idPuesto: number;
NombrePuesto: string;
FechaContratación?: string;
FechaContratacion?: string;
SaldoVacaciones: number;
EsActivo: number;
};
class EmpleadosPage {
private filtroInput: HTMLInputElement;
private buscarBtn: HTMLButtonElement;
private limpiarBtn: HTMLButtonElement;
private mensajeDiv: HTMLElement;
private contadorSpan: HTMLElement;
private empleadosBody: HTMLTableSectionElement;
private detallePanel: HTMLElement;
private detalleContenido: HTMLElement;
private detalleTitulo: HTMLElement;
private detalleEstado: HTMLElement;
private editarForm: HTMLFormElement;
private documentoDespuesInput: HTMLInputElement;
private nombreDespuesInput: HTMLInputElement;
private idPuestoDespuesInput: HTMLInputElement;
private cancelarEdicionBtn: HTMLButtonElement;
private detalleActual: EmpleadoDetalle | null = null;
private documentoActual: string | null = null;
constructor() {
this.filtroInput = document.getElementById('filtro') as HTMLInputElement;
this.buscarBtn = document.getElementById('buscarBtn') as HTMLButtonElement;
this.limpiarBtn = document.getElementById('limpiarBtn') as HTMLButtonElement;
this.mensajeDiv = document.getElementById('mensaje') as HTMLElement;
this.contadorSpan = document.getElementById('contador') as HTMLElement;
this.empleadosBody = document.getElementById('empleadosBody') as HTMLTableSectionElement;
this.detallePanel = document.getElementById('detallePanel') as HTMLElement;
this.detalleContenido = document.getElementById('detalleContenido') as HTMLElement;
this.detalleTitulo = document.getElementById('detalleTitulo') as HTMLElement;
this.detalleEstado = document.getElementById('detalleEstado') as HTMLElement;
this.editarForm = document.getElementById('editarForm') as HTMLFormElement;
this.documentoDespuesInput = document.getElementById('documentoDespues') as HTMLInputElement;
this.nombreDespuesInput = document.getElementById('nombreDespues') as HTMLInputElement;
this.idPuestoDespuesInput = document.getElementById('idPuestoDespues') as HTMLInputElement;
this.cancelarEdicionBtn = document.getElementById('cancelarEdicionBtn') as HTMLButtonElement;
this.bindEvents();
this.cargarEmpleados();
}
private bindEvents(): void {
// Botón principal: ejecutar la búsqueda
this.buscarBtn.addEventListener('click', () => {
void this.cargarEmpleados();
});
// Botón secundario: limpiar el filtro y volver a cargar todo
this.limpiarBtn.addEventListener('click', () => {
this.filtroInput.value = '';
void this.cargarEmpleados();
});
// Permitir Enter dentro de la caja de texto
this.filtroInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
event.preventDefault();
void this.cargarEmpleados();
}
});
// Delegación de eventos: una sola escucha para todos los botones de la tabla
this.empleadosBody.addEventListener('click', (event) => {
const target = event.target as HTMLElement | null;
const button = target?.closest('button[data-accion]') as HTMLButtonElement | null;
if (!button) {
return;
}
const documento = button.dataset.documento;
if (!documento) {
return;
}
const accion = button.dataset.accion;
if (accion === 'consultar') {
void this.consultarEmpleado(documento);
return;
}
if (accion === 'editar') {
void this.abrirEdicion(documento);
return;
}
if (accion === 'borrar') {
void this.borrarEmpleado(documento);
}
});
this.editarForm.addEventListener('submit', (event) => {
event.preventDefault();
void this.guardarEdicion();
});
this.cancelarEdicionBtn.addEventListener('click', () => {
this.editarForm.classList.add('hidden');
});
}
private async cargarEmpleados(): Promise<void> {
const filtro = this.filtroInput.value.trim();
const username = localStorage.getItem('username') || 'UsuarioScripts';
this.setEstado('Cargando empleados...', 'info');
this.setBotones(false);
try {
const response = await fetch(`/api/empleados?filtro=${encodeURIComponent(filtro)}`, {
method: 'GET',
headers: {
'x-username': username,
},
});
const payload = await response.json() as {
success: boolean;
outResultCode: number;
message?: string;
data?: Empleado[];
};
if (!response.ok || !payload.success) {
this.limpiarTabla();
this.setEstado(payload.message || 'No se pudieron obtener los empleados.', 'error');
this.contadorSpan.textContent = '0 resultados';
return;
}
const empleados = payload.data ?? [];
this.renderTabla(empleados);
this.contadorSpan.textContent = `${empleados.length} resultado${empleados.length === 1 ? '' : 's'}`;
if (empleados.length === 0) {
this.setEstado('No se encontraron empleados con ese filtro.', 'warning');
} else {
this.setEstado('Empleados cargados correctamente.', 'success');
}
} catch (error) {
console.error('Error cargando empleados:', error);
this.limpiarTabla();
this.contadorSpan.textContent = '0 resultados';
this.setEstado('Error de conexión con el servidor.', 'error');
} finally {
this.setBotones(true);
}
}
private renderTabla(empleados: Empleado[]): void {
this.empleadosBody.innerHTML = '';
if (empleados.length === 0) {
this.empleadosBody.innerHTML = `
<tr>
<td colspan="5" class="empty-state">Todavía no hay datos cargados</td>
</tr>
`;
return;
}
for (const empleado of empleados) {
const fila = document.createElement('tr');
fila.innerHTML = `
<td>${empleado.Id}</td>
<td>${empleado.Nombre}</td>
<td>${empleado.ValorDocumentoIdentidad}</td>
<td>${empleado.idPuesto}</td>
<td>
<button type="button" class="action-button action-view" data-accion="consultar" data-documento="${empleado.ValorDocumentoIdentidad}">
Consultar
</button>
<button type="button" class="action-button action-edit" data-accion="editar" data-documento="${empleado.ValorDocumentoIdentidad}">
Editar
</button>
<button type="button" class="action-button action-delete" data-accion="borrar" data-documento="${empleado.ValorDocumentoIdentidad}">
Borrar
</button>
</td>
`;
this.empleadosBody.appendChild(fila);
}
}
private limpiarTabla(): void {
this.empleadosBody.innerHTML = `
<tr>
<td colspan="5" class="empty-state">Todavía no hay datos cargados</td>
</tr>
`;
}
private async consultarEmpleado(valorDocumentoIdentidad: string): Promise<void> {
const username = localStorage.getItem('username') || 'UsuarioScripts';
this.detallePanel.classList.remove('hidden');
this.editarForm.classList.add('hidden');
this.detalleTitulo.textContent = `Consulta de ${valorDocumentoIdentidad}`;
this.detalleEstado.textContent = 'Cargando detalle del empleado...';
this.detalleEstado.className = 'status info';
this.detalleContenido.innerHTML = '';
try {
const response = await fetch(`/api/empleados/${encodeURIComponent(valorDocumentoIdentidad)}`, {
method: 'GET',
headers: {
'x-username': username,
},
});
const payload = await response.json() as {
success: boolean;
outResultCode: number;
message?: string;
data?: EmpleadoDetalle | null;
};
if (!response.ok || !payload.success || !payload.data) {
this.detalleEstado.textContent = payload.message || 'No se pudo cargar el detalle.';
this.detalleEstado.className = 'status error';
this.detalleContenido.innerHTML = '';
return;
}
const detalle = payload.data;
const fechaContratacion = detalle.FechaContratación ?? detalle.FechaContratacion ?? '';
this.detalleActual = detalle;
this.documentoActual = detalle.ValorDocumentoIdentidad;
this.detalleEstado.textContent = 'Detalle cargado correctamente.';
this.detalleEstado.className = 'status success';
this.detalleContenido.innerHTML = `
<div class="detalle-grid">
<div class="detalle-item">
<span class="detalle-label">Documento</span>
<span class="detalle-valor">${detalle.ValorDocumentoIdentidad}</span>
</div>
<div class="detalle-item">
<span class="detalle-label">Nombre</span>
<span class="detalle-valor">${detalle.Nombre}</span>
</div>
<div class="detalle-item">
<span class="detalle-label">Puesto</span>
<span class="detalle-valor">${detalle.NombrePuesto}</span>
</div>
<div class="detalle-item">
<span class="detalle-label">Id Puesto</span>
<span class="detalle-valor">${detalle.idPuesto}</span>
</div>
<div class="detalle-item">
<span class="detalle-label">Fecha contratación</span>
<span class="detalle-valor">${fechaContratacion}</span>
</div>
<div class="detalle-item">
<span class="detalle-label">Saldo vacaciones</span>
<span class="detalle-valor">${detalle.SaldoVacaciones}</span>
</div>
<div class="detalle-item">
<span class="detalle-label">Estado</span>
<span class="detalle-valor">${detalle.EsActivo === 1 ? 'Activo' : 'Inactivo'}</span>
</div>
</div>
`;
} catch (error) {
console.error('Error consultando empleado:', error);
this.detalleEstado.textContent = 'Error de conexión con el servidor.';
this.detalleEstado.className = 'status error';
this.detalleContenido.innerHTML = '';
}
}
private async abrirEdicion(valorDocumentoIdentidad: string): Promise<void> {
await this.consultarEmpleado(valorDocumentoIdentidad);
if (!this.detalleActual) {
return;
}
this.documentoDespuesInput.value = this.detalleActual.ValorDocumentoIdentidad;
this.nombreDespuesInput.value = this.detalleActual.Nombre;
this.idPuestoDespuesInput.value = String(this.detalleActual.idPuesto);
this.editarForm.classList.remove('hidden');
this.detalleEstado.textContent = 'Edita los campos y guarda cambios.';
this.detalleEstado.className = 'status info';
}
private async guardarEdicion(): Promise<void> {
if (!this.detalleActual || !this.documentoActual) {
this.detalleEstado.textContent = 'Primero consulta un empleado antes de editar.';
this.detalleEstado.className = 'status warning';
return;
}
const username = localStorage.getItem('username') || 'UsuarioScripts';
const valorDocumentoIdentidadDespues = this.documentoDespuesInput.value.trim();
const nombreDespues = this.nombreDespuesInput.value.trim();
const idPuestoDespues = Number(this.idPuestoDespuesInput.value);
if (!valorDocumentoIdentidadDespues || !nombreDespues || Number.isNaN(idPuestoDespues)) {
this.detalleEstado.textContent = 'Completa todos los campos para editar.';
this.detalleEstado.className = 'status warning';
return;
}
this.detalleEstado.textContent = 'Guardando cambios...';
this.detalleEstado.className = 'status info';
try {
const response = await fetch(`/api/empleados/${encodeURIComponent(this.documentoActual)}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
'x-username': username,
},
body: JSON.stringify({
valorDocumentoIdentidadDespues,
nombreAntes: this.detalleActual.Nombre,
nombreDespues,
idPuestoAntes: this.detalleActual.idPuesto,
idPuestoDespues,
}),
});
const payload = await response.json() as {
success: boolean;
outResultCode: number;
message?: string;
};
if (!response.ok || !payload.success) {
this.detalleEstado.textContent = payload.message || 'No se pudo actualizar el empleado.';
this.detalleEstado.className = 'status error';
return;
}
this.detalleEstado.textContent = 'Empleado actualizado correctamente.';
this.detalleEstado.className = 'status success';
this.editarForm.classList.add('hidden');
await this.cargarEmpleados();
await this.consultarEmpleado(valorDocumentoIdentidadDespues);
} catch (error) {
console.error('Error actualizando empleado:', error);
this.detalleEstado.textContent = 'Error de conexión al actualizar el empleado.';
this.detalleEstado.className = 'status error';
}
}
private async borrarEmpleado(valorDocumentoIdentidad: string): Promise<void> {
const username = localStorage.getItem('username') || 'UsuarioScripts';
const confirmar = window.confirm(`¿Seguro que deseas eliminar lógicamente al empleado ${valorDocumentoIdentidad}?`);
try {
if (!confirmar) {
await fetch(`/api/empleados/${encodeURIComponent(valorDocumentoIdentidad)}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'x-username': username,
},
body: JSON.stringify({ confirmado: false }),
});
this.setEstado('Intento de borrado registrado (no confirmado).', 'warning');
return;
}
const response = await fetch(`/api/empleados/${encodeURIComponent(valorDocumentoIdentidad)}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'x-username': username,
},
body: JSON.stringify({ confirmado: true }),
});
const payload = await response.json() as {
success: boolean;
outResultCode: number;
message?: string;
};
if (!response.ok || !payload.success) {
this.setEstado(payload.message || 'No se pudo eliminar el empleado.', 'error');
return;
}
this.setEstado('Empleado eliminado lógicamente.', 'success');
this.detallePanel.classList.add('hidden');
this.detalleActual = null;
this.documentoActual = null;
await this.cargarEmpleados();
} catch (error) {
console.error('Error eliminando empleado:', error);
this.setEstado('Error de conexión al eliminar empleado.', 'error');
}
}
private setEstado(texto: string, tipo: 'info' | 'success' | 'warning' | 'error'): void {
this.mensajeDiv.textContent = texto;
this.mensajeDiv.className = `status ${tipo}`;
}
private setBotones(habilitado: boolean): void {
this.buscarBtn.disabled = !habilitado;
this.limpiarBtn.disabled = !habilitado;
}
}
document.addEventListener('DOMContentLoaded', () => {
new EmpleadosPage();
});
- Confirmar siempre que nombre de SP en código y SQL sea
exactamente igual.
- Probar primero por API antes de dar por cerrada una
funcionalidad de UI.
- Cuando hay contenido dinámico en tablas, usar delegación
de eventos simplifica mucho el mantenimiento.
- Separar compilación frontend/backend ayuda a detectar
rápido dónde está el problema.
- Resolver definitivamente el tema de codificación de fecha
en sp_GetEmpleadoById para devolver la fecha real.
- Cambiar idPuesto en edición por un select de puestos
(mejor UX y menos errores de entrada).
- Cerrar validaciones de formulario de edición (rangos,
strings vacíos y mensajes de error más claros).
Comentarios
Publicar un comentario