Entrada 10

 Bitácora de Sesión

Fecha: 05/06/2026

Inicio: [20:00] | Fin: [22:00] || Total: [2 horas]

Presente: Matías Benavides Sandoval

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

¿QUÉ HICIMOS HOY?

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

Se cerró la única inconsistencia pendiente en el catálogo de eventos (data/Datos.xml) detectada al revisar R07 del PDF contra los eventos ya listados, agregando los 7 tipos que faltaban para alinearse con la tabla de eventos del enunciado. El cambio fue puramente aditivo (sin modificar nombres ni ids existentes), por lo que ningún SP que resuelve TipoEvento por Nombre se vio afectado.

Luego se completó R03 (Impersonar empleado) y R06 (Regresar a interfaz de administrador) end-to-end,  faltaba el cableado del backend (controller + route) y, sobre todo, la UI para invocarlos. Se creó un nuevo controller y route de Express, se registró bajo /api/auth, se agregó el botón "Impersonar" en cada fila de la tabla de empleados (empleados.js), y se creó la página placeholder empleado.html con su handler para el botón "Regresar a admin".

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

PROBLEMAS DETECTADOS Y CÓMO SE RESOLVIERON

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

Problema: faltaban TiposEvento en data/Datos.xml para cubrir R07 del PDF.

Causa: la spec R07 lista 15 eventos (Login, Logout, Listar empleados, Listar con filtro, Insertar empleado, Eliminar empleado, Asociar deducción, Desasociar deducción, Consultar planilla semanal, Consultar planilla mensual, Editar empleado, Impersonar empleado, Regresar a admin, Ingreso de marcas de asistencia, Ingreso nuevas jornadas), pero el XML solo tenía 16 que cubrían parcialmente.

Solución: se agregaron Ids 17-23. Cambio aditivo, sin tocar Ids 1-16 (los SPs existentes que resuelven por Nombre siguen funcionando idéntico).

Problema: no había forma de probar R03/R06 desde la UI, solo a nivel SP.

Causa: los SPs existían pero no había controller/route/botón que los invocaran.

Solución: se implementó el cableado completo (controller + route + botón admin + página empleado con botón de regreso).

Problema: el placeholder de la vista de empleado (empleado.html) no existía.

Causa: Sebastián aún no implementa R04/R05, pero el botón R06 necesita una página donde vivir.

Solución: se creó empleado.html como placeholder mínimo (header con nombre del empleado, botón "Regresar a admin" prominente, cuerpo pendiente). Sebastián reemplaza solo el cuerpo, sin tocar el header.

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

AVANCE DEL CÓDIGO

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

/**

* controllers/impersonarController.ts

* Controlador de impersonación (R03) y regreso a admin (R06).

*

* - impersonarEmpleado: registra en BitacoraEvento la impersonación

*   y devuelve el id del empleado impersonado para que el frontend

*   sepa a quién está "viendo".

* - regresarAdmin: registra en BitacoraEvento el regreso a la vista admin.

*

* Ambos SPs dejan la traza correspondiente en BitacoraEvento; el id del

* usuario admin se resuelve a partir del header 'x-username', igual que

* en los demás controllers.

*/


import { Request, Response } from 'express';

import { getPool, sql } from '../db/connection';

import { getErrorMessage } from '../utils/errorhelper';


async function resolveUsuarioId(

    pool: Awaited<ReturnType<typeof getPool>>,

    username: string,

): Promise<number | null> {

    const usuarioResult = await pool

        .request()

        .input('inUsername', sql.VarChar(128), username)

        .query('SELECT id FROM Usuario WHERE Username = @inUsername');


    return usuarioResult.recordset?.[0]?.id ?? null;

}


// POST /api/auth/impersonar

// Body: { valorDocumentoIdentidad: "110011001" }

// Respuesta: { success, outResultCode, idEmpleado, message }

export async function impersonarEmpleado(req: Request, res: Response): Promise<void> {

    const valorDocumentoIdentidad = String(req.body?.valorDocumentoIdentidad ?? '').trim();


    if (!valorDocumentoIdentidad) {

        res.status(400).json({

            success: false,

            outResultCode: 50000,

            message: 'valorDocumentoIdentidad es requerido',

        });

        return;

    }


    const username = String(req.headers['x-username'] ?? '').trim();

    const ipPostIn = req.ip ?? '';

    const postTime = new Date();


    if (!username) {

        res.status(401).json({

            success: false,

            outResultCode: 50001,

            message: 'Sesión no válida. Vuelve a iniciar sesión.',

        });

        return;

    }


    try {

        const pool = await getPool();

        const idUsuarioAdmin = await resolveUsuarioId(pool, username);


        if (!idUsuarioAdmin) {

            res.status(401).json({

                success: false,

                outResultCode: 50001,

                message: 'Usuario de sesión no encontrado',

            });

            return;

        }


        const result = await pool

            .request()

            .input('inValorDocumento', sql.VarChar(32), valorDocumentoIdentidad)

            .input('inIdUsuarioAdmin', sql.Int, idUsuarioAdmin)

            .input('inIpPostIn', sql.VarChar(64), ipPostIn)

            .input('inPostTime', sql.DateTime, postTime)

            .output('outIdEmpleado', sql.Int)

            .output('outResultCode', sql.Int)

            .execute('sp_ImpersonarEmpleado');


        const outResultCode: number = Number(result.output.outResultCode ?? 50008);

        const outIdEmpleado: number | null = result.output.outIdEmpleado ?? null;


        if (outResultCode !== 0) {

            res.status(outResultCode === 50012 ? 404 : 400).json({

                success: false,

                outResultCode,

                message: await getErrorMessage(outResultCode),

            });

            return;

        }


        res.status(200).json({

            success: true,

            outResultCode,

            idEmpleado: outIdEmpleado,

            message: 'Impersonación iniciada correctamente',

        });

    } catch (error) {

        console.error('Error en impersonarEmpleado:', error);

        res.status(500).json({

            success: false,

            outResultCode: 50008,

            message: 'Error interno del servidor',

        });

    }

}


// POST /api/auth/regresar-admin

// Body: (ninguno)

// Respuesta: { success, outResultCode, message }

export async function regresarAdmin(req: Request, res: Response): Promise<void> {

    const username = String(req.headers['x-username'] ?? '').trim();

    const ipPostIn = req.ip ?? '';

    const postTime = new Date();


    if (!username) {

        res.status(401).json({

            success: false,

            outResultCode: 50001,

            message: 'Sesión no válida. Vuelve a iniciar sesión.',

        });

        return;

    }


    try {

        const pool = await getPool();

        const idUsuarioAdmin = await resolveUsuarioId(pool, username);


        if (!idUsuarioAdmin) {

            res.status(401).json({

                success: false,

                outResultCode: 50001,

                message: 'Usuario de sesión no encontrado',

            });

            return;

        }


        const result = await pool

            .request()

            .input('inIdUsuarioAdmin', sql.Int, idUsuarioAdmin)

            .input('inIpPostIn', sql.VarChar(64), ipPostIn)

            .input('inPostTime', sql.DateTime, postTime)

            .output('outResultCode', sql.Int)

            .execute('sp_RegresarAdmin');


        const outResultCode: number = Number(result.output.outResultCode ?? 50008);


        if (outResultCode !== 0) {

            res.status(outResultCode === 50013 ? 403 : 400).json({

                success: false,

                outResultCode,

                message: await getErrorMessage(outResultCode),

            });

            return;

        }


        res.status(200).json({

            success: true,

            outResultCode,

            message: 'Regreso a interfaz de administrador',

        });

    } catch (error) {

        console.error('Error en regresarAdmin:', error);

        res.status(500).json({

            success: false,

            outResultCode: 50008,

            message: 'Error interno del servidor',

        });

    }

}

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

MORALEJAS / BUENAS PRÁCTICAS

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

Los SPs que resuelven TiposEvento por Nombre son robustos a reordenamientos de Ids en el XML. Mantener ese patrón.

Para R03/R06, montar las rutas bajo /api/auth (no /api/empleados) porque la sesión activa viaja en x-username, igual que login/logout. Consistencia de namespace.

Crear placeholders mínimos (header + botón principal) para vistas que otro compañero implementará después, en vez de bloquear la integración. Sebastián puede ahora conectar sus SPs sin tener que pelearse con la navegación.

En frontend, duplicar info en localStorage + query params (sobrevive a F5) y limpiar las keys de impersonación al regresar a admin.

Patrón de outResultCode → HTTP status: códigos de dominio (50012 no existe, 50013 no es admin) → 404/403, resto → 400, server errors → 500. El controller es quien decide el status, no el SP.

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

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

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

 implementar el visor de bitácora (R07 admin) — nueva página bitacora.html + sp_GetBitacora + ruta GET. Pendiente.

Levantar SQL Server local + pnpm install + pnpm run dev para validar end-to-end R03/R06 (clic → SP → bitácora → redirige).

Ejecutar sp_CargarCatalogosXML con el XML actualizado (Ids 17-23) para que los nuevos eventos estén en la BD antes de que Sebastián los use.

Comentarios