Entrada 12 (CRUD)

# Bitácora de Sesión

Fecha: 26/04/2026

Inicio: [19:30] | Fin: [22:30] || Total: [3 horas ]

Presentes: Matías Benavides Sandoval

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

¿QUÉ HICIMOS HOY?

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

- 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 implementó en frontend la lógica de:

    - carga y filtrado de empleados

    - consulta por empleado

    - edición (PATCH)

    - borrado lógico (DELETE con confirmado true/false)

- 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.

 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

PROBLEMAS ENCONTRADOS Y CÓMO SE RESOLVIERON

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

1) Error literal reportado en sesión:

Decia que invalid column name Activo  y incorrect syntax near ORDER

 Código antes (con error):

```sql

IF @tipoFiltro = 'NOMBRE_PUESTO'

        SELECT Id, Nombre, ValorDocumentoIdentidad, idPuesto FROM [dbo].[Empleado]

        WHERE (Nombre LIKE '%' + @filtroNomvbrePuesto + '%' OR

                        idPuesto IN (

                                SELECT Id

                                FROM [dbo].[Puesto]

                                WHERE Nombre LIKE '%' + @filtroNomvbrePuesto + '%'))

        WHERE Activo = 1

        ORDER BY Nombre ASC;

```


Código después (corregido):

```sql

IF @tipoFiltro = 'NOMBRE_PUESTO'

        SELECT Id, Nombre, ValorDocumentoIdentidad, idPuesto FROM [dbo].[Empleado]

        WHERE (Nombre LIKE '%' + @filtroNomvbrePuesto + '%' OR

                        idPuesto IN (

                                SELECT Id

                                FROM [dbo].[Puesto]

                                WHERE Nombre LIKE '%' + @filtroNomvbrePuesto + '%'))

        AND EsActivo = 1

        ORDER BY Nombre ASC;

```

 

Solucion:

- 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.

 

Código antes (backend):

```ts

.execute('sp_InsertEmpleado');

```

 

Código después (backend):

```ts

.execute('sp_InsertarEmpleado');

```

 

Resolución aplicada:

- Se alineó el nombre del SP en controller.

- Se volvieron a desplegar SPs de empleado para sincronizar la base.

 

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

DUDAS Y DIVERGENCIAS DE CRITERIO

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

- 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.

- Se confirmó que en borrado se debía cubrir ambas ramas:

    - confirmado = false (registrar intento)

    - confirmado = true (borrado lógico real)

 

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

AVANCE DEL CÓDIGO

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

/**
* 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();
});

 

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

MORALEJAS / BUENAS PRÁCTICAS

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

- 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.

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

PRÓXIMA SESIÓN: ¿QUÉ SIGUE?

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

- 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

Entradas más populares de este blog

Entrada 16 (Controlador de movimientos)

Entrada 20 (Lógica de insertarMovimientos, conectarla y probarla)

Entrada 21 (Análisis de resultados)