- Se cerró la mejora de presentación para que
el listado de empleados muestre el nombre del puesto y no solo el id.
- Se ajustó `sp_GetEmpleados` para devolver `NombrePuesto`
junto con `idPuesto`, usando `JOIN` con `Puesto` en los tres caminos del
filtro.
- Se actualizó el frontend de empleados para que la tabla
pinte el nombre del puesto y el detalle mantenga el nombre como dato visible
principal.
- Se dejó el select de edición alimentado desde
`/api/puestos`, así la UI no depende de escribir ids manualmente.
- El listado seguía mostrando solo `idPuesto` porque el SP
de empleados no estaba trayendo el nombre del puesto; se resolvió agregando el
`JOIN` con `Puesto`.
- Había riesgo de mostrar dos versiones distintas de la
misma información entre tabla y detalle; se normalizó para que el nombre del
puesto sea el dato visible principal.
- Se decidió mantener `idPuesto` solo como dato técnico para
edición y auditoría, pero no como texto principal para el usuario.
"use strict";
/**
* 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
*/
class EmpleadosPage {
constructor() {
this.detalleActual = null;
this.documentoActual = null;
this.documentoPendienteBorrado = null;
this.puestos = [];
this.filtroInput = document.getElementById('filtro');
this.buscarBtn = document.getElementById('buscarBtn');
this.limpiarBtn = document.getElementById('limpiarBtn');
this.mensajeDiv = document.getElementById('mensaje');
this.contadorSpan = document.getElementById('contador');
this.empleadosBody = document.getElementById('empleadosBody');
this.detallePanel = document.getElementById('detallePanel');
this.detalleContenido = document.getElementById('detalleContenido');
this.detalleTitulo = document.getElementById('detalleTitulo');
this.detalleEstado = document.getElementById('detalleEstado');
this.editarForm = document.getElementById('editarForm');
this.documentoDespuesInput = document.getElementById('documentoDespues');
this.nombreDespuesInput = document.getElementById('nombreDespues');
this.idPuestoDespuesInput = document.getElementById('idPuestoDespues');
this.cancelarEdicionBtn = document.getElementById('cancelarEdicionBtn');
this.deleteModal = document.getElementById('deleteModal');
this.deleteModalEstado = document.getElementById('deleteModalEstado');
this.deleteDocumentoTexto = document.getElementById('deleteDocumentoTexto');
this.confirmDeleteBtn = document.getElementById('confirmDeleteBtn');
this.cancelDeleteBtn = document.getElementById('cancelDeleteBtn');
this.closeDeleteModalBtn = document.getElementById('closeDeleteModalBtn');
this.bindEvents();
void this.cargarPuestos();
this.cargarEmpleados();
}
bindEvents() {
// 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;
const button = target?.closest('button[data-accion]');
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') {
this.abrirModalBorrado(documento);
}
});
this.editarForm.addEventListener('submit', (event) => {
event.preventDefault();
void this.guardarEdicion();
});
this.cancelarEdicionBtn.addEventListener('click', () => {
this.editarForm.classList.add('hidden');
});
this.confirmDeleteBtn.addEventListener('click', () => {
void this.confirmarBorradoPendiente();
});
this.cancelDeleteBtn.addEventListener('click', () => {
void this.cancelarBorradoPendiente();
});
this.closeDeleteModalBtn.addEventListener('click', () => {
void this.cancelarBorradoPendiente();
});
this.deleteModal.addEventListener('click', (event) => {
if (event.target === this.deleteModal) {
void this.cancelarBorradoPendiente();
}
});
}
async cargarEmpleados() {
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();
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);
}
}
async cargarPuestos() {
try {
const response = await fetch('/api/puestos', { method: 'GET' });
const payload = await response.json();
if (!response.ok || !payload.success) {
this.renderPuestos([]);
return;
}
this.puestos = payload.data ?? [];
this.renderPuestos(this.puestos);
}
catch (error) {
console.error('Error cargando puestos:', error);
this.renderPuestos([]);
}
}
renderPuestos(puestos) {
const options = puestos.length === 0
? '<option value="">No hay puestos disponibles</option>'
: '<option value="">Selecciona un puesto</option>' + puestos.map((puesto) => (`<option value="${puesto.id}">${puesto.Nombre}</option>`)).join('');
this.idPuestoDespuesInput.innerHTML = options;
this.idPuestoDespuesInput.disabled = puestos.length === 0;
}
renderTabla(empleados) {
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.NombrePuesto}</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);
}
}
limpiarTabla() {
this.empleadosBody.innerHTML = `
<tr>
<td colspan="5" class="empty-state">Todavía no hay datos cargados</td>
</tr>
`;
}
async consultarEmpleado(valorDocumentoIdentidad) {
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();
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">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 = '';
}
}
async abrirEdicion(valorDocumentoIdentidad) {
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';
}
validarEdicion() {
const documento = this.documentoDespuesInput.value.trim();
const nombre = this.nombreDespuesInput.value.trim();
const idPuesto = Number(this.idPuestoDespuesInput.value);
if (!documento) {
return 'El documento después es obligatorio.';
}
if (!/^\d{3,32}$/.test(documento)) {
return 'El documento debe tener solo números y al menos 3 dígitos.';
}
if (!nombre) {
return 'El nombre después es obligatorio.';
}
if (nombre.length < 3 || nombre.length > 128) {
return 'El nombre debe tener entre 3 y 128 caracteres.';
}
if (!/^[A-Za-zÁÉÍÓÚÜÑáéíóúüñ0-9.' -]+$/.test(nombre)) {
return 'El nombre contiene caracteres no permitidos.';
}
if (Number.isNaN(idPuesto) || idPuesto <= 0) {
return 'Debes seleccionar un puesto válido.';
}
return null;
}
async guardarEdicion() {
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);
const errorValidacion = this.validarEdicion();
if (errorValidacion) {
this.detalleEstado.textContent = errorValidacion;
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();
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';
}
}
abrirModalBorrado(valorDocumentoIdentidad) {
this.documentoPendienteBorrado = valorDocumentoIdentidad;
this.deleteDocumentoTexto.textContent = `¿Deseas eliminar lógicamente al empleado ${valorDocumentoIdentidad}?`;
this.deleteModalEstado.textContent = 'Esta acción desactiva al empleado, no lo elimina físicamente.';
this.deleteModalEstado.className = 'status warning';
this.deleteModal.classList.remove('hidden');
}
cerrarModalBorrado() {
this.deleteModal.classList.add('hidden');
this.documentoPendienteBorrado = null;
}
async confirmarBorradoPendiente() {
if (!this.documentoPendienteBorrado) {
return;
}
await this.borrarEmpleado(this.documentoPendienteBorrado, true);
}
async cancelarBorradoPendiente() {
if (!this.documentoPendienteBorrado) {
this.cerrarModalBorrado();
return;
}
await this.borrarEmpleado(this.documentoPendienteBorrado, false);
}
async borrarEmpleado(valorDocumentoIdentidad, confirmado) {
const username = localStorage.getItem('username') || 'UsuarioScripts';
this.confirmDeleteBtn.disabled = true;
this.cancelDeleteBtn.disabled = true;
this.closeDeleteModalBtn.disabled = true;
try {
const response = await fetch(`/api/empleados/${encodeURIComponent(valorDocumentoIdentidad)}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'x-username': username,
},
body: JSON.stringify({ confirmado }),
});
const payload = await response.json();
if (!response.ok || !payload.success) {
this.deleteModalEstado.textContent = payload.message || 'No se pudo eliminar el empleado.';
this.deleteModalEstado.className = 'status error';
return;
}
this.cerrarModalBorrado();
if (!confirmado) {
this.setEstado('Intento de borrado registrado (no confirmado).', 'warning');
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.deleteModalEstado.textContent = 'Error de conexión al eliminar empleado.';
this.deleteModalEstado.className = 'status error';
}
finally {
this.confirmDeleteBtn.disabled = false;
this.cancelDeleteBtn.disabled = false;
this.closeDeleteModalBtn.disabled = false;
}
}
setEstado(texto, tipo) {
this.mensajeDiv.textContent = texto;
this.mensajeDiv.className = `status ${tipo}`;
}
setBotones(habilitado) {
this.buscarBtn.disabled = !habilitado;
this.limpiarBtn.disabled = !habilitado;
}
}
document.addEventListener('DOMContentLoaded', () => {
new EmpleadosPage();
});
- Si un dato se va a mostrar muchas veces en pantalla,
conviene traerlo resuelto desde SQL y no reconstruirlo en el frontend.
- La tabla puede mostrar el nombre de negocio y guardar el
id como soporte interno para el formulario.
- Revisar si conviene mostrar también el nombre del puesto
en otras vistas del módulo o dejarlo solo en listado y detalle.
- Seguir cerrando la documentación del módulo con el estado
final real del CRUD.
Comentarios
Publicar un comentario