Entrada 5

Fecha: 22/03/2025

Inicio: [13:00] | Fin: [15:30] | Total: [2 horas y 30 minutos]

Presentes: Matías Benavides Sandoval y  (parcialmente) Sebastián Ramírez Abarca

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

¿QUÉ HICIMOS HOY?

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

Sesión enfocada en resolver la conexión de Hamachi y empezar con el frontend de la aplicación.

1. Resolvimos el problema de conexión de Hamachi de Sebastián. Resultó que no estaba correctamente conectado a la red aunque parecía estarlo. Se solucionó apagando y encendiendo Hamachi, tras lo cual se conectó automáticamente.

2. Creamos la estructura del frontend separando los archivos Typescript del frontend en su propia carpeta src/frontend/ para no mezclarlos con el backend.

3. Creamos el archivo insertar.html en public/ con el formulario de inserción de empleados, incluyendo campos de Nombre y Salario, botones de Insertar y Regresar, y espacios para mensajes de error.

4. Creamos src/frontend/insertar.ts con toda la lógica del formulario:

- Validación de nombre (solo letras y guiones)

- Validación de salario (valor monetario bien formado)

- Llamada al endpoint POST /api/empleados

- Manejo de respuesta: redirección si éxito,

- Mensaje de error si nombre duplicado

5. Creamos public/css/style.css con los estilos compartidos para ambas páginas.

6. Creamos tsconfig.frontend.json separado del tsconfig principal para compilar solo los archivos del frontend hacia public/js/.

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

PROBLEMAS ENCONTRADOS Y CÓMO SE RESOLVIERON

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

Problema 1:

Descripción: Hamachi del compañero no conectaba a la BD aunque aparecía como conectado. Mensaje de error: "ConnectionError: Failed to connect to 25.0.119.25:1433 in 15000ms - code: ETIMEOUT"

Solución:

Apagar y encender Hamachi en la máquina del compañero. Se reconectó solo y el ping funcionó correctamente. Moraleja: Si Hamachi parece conectado pero no funciona, reiniciarlo es el primer paso.

Problema 2:

Descripción: Al compilar con npx tsc, también compilaba index.ts y connection.ts del backend y los metía en public/js/, donde no deben estar.

  Solución:

Crear un tsconfig.frontend.json separado que solo incluye src/frontend/ y mover insertar.ts a esa carpeta. Para compilar el frontend se usa: npx tsc --project tsconfig.frontend.json

Problema 3:

Descripción: Error al usar document en TypeScript. Mensaje de error: "Cannot find name 'document'. Do you need to change your target library? Try changing the 'lib' compiler option to include 'dom'."

Solución:

Agregar "DOM" al array lib en el tsconfig.frontend.json: "lib": ["ES2020", "DOM"]

Causa: TypeScript no incluye las definiciones del browser por defecto, hay que indicarle explícitamente que el código va a correr en un navegador.

Problema 4:

Descripción: npx no funcionaba en PowerShell. Mensaje de error:

"npx.ps1 cannot be loaded because running scripts is disabled on this system"

Solución:

Ejecutar nuevamente: Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned

Nota: Volvemos a poner este problema porque notamos que aparece cada vez que se abre una terminal nueva en una máquina diferente.

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

DUDAS Y DIVERGENCIAS DE CRITERIO

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

Surgió la pregunta de cómo manejar la compilación de TypeScript cuando hay código tanto de backend como de frontend en el mismo proyecto. La solución fue usar dos tsconfig separados: el principal para el backend (usado por ts-node) y uno específico para el frontend que compila a public/js/.

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

DIVISIÓN DE TRABAJO

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

Matías: pantalla de Insertar Empleado (insertar.html + src/frontend/insertar.ts)

Sebastián: pantalla de Lista de Empleados (index.html + src/frontend/index.ts)

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

AVANCE DEL CÓDIGO

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

Versión con error (tsconfig.json): "lib": ["ES2020"]

→ Resultado: "Cannot find name 'document'"

Versión corregida (tsconfig.frontend.json): "lib": ["ES2020", "DOM"]

→ Resultado: compila correctamente

- - - - - - - - - - - - - - - - - src/frontend/insertar.ts: - - - - - - - - - - - - - - - - - - - - - 

const NOMBRE_REGEX = /^[a-zA-ZáéíóúÁÉÍÓÚüÜñÑ\- ]+$/;

const SALARIO_REGEX = /^\d+(\.\d{1,2})?$/;

function setError(id: string, msg: string): void {

    const el = document.getElementById(id) as HTMLElement;

    el.textContent = msg;

}


function limpiarErrores(): void {

    setError('error-nombre', '');

    setError('error-salario', '');

}


function mostrarMensaje(texto: string, tipo: 'exito' | 'error'): void {

    const el = document.getElementById('mensaje') as HTMLElement;

    el.textContent = texto;

    el.className = `mensaje ${tipo}`;

}


function validar(nombre: string, salario: string): boolean {

    let valido = true;


    if (!nombre.trim()) {

    setError('error-nombre', 'El nombre no puede estar vacío.');

    valido = false;

    } else if (!NOMBRE_REGEX.test(nombre.trim())) {

    setError('error-nombre', 'Solo se permiten letras y guiones.');

    valido = false;

    }


    if (!salario.trim()) {

    setError('error-salario', 'El salario no puede estar vacío.');

    valido = false;

    } else if (!SALARIO_REGEX.test(salario.trim())) {

    setError('error-salario', 'Ingrese un valor válido. Ej: 850000.00');

    valido = false;

    } else if (parseFloat(salario) <= 0) {

    setError('error-salario', 'El salario debe ser mayor a cero.');

    valido = false;

    }


    return valido;

}


async function insertar(): Promise<void> {

    limpiarErrores();


    const nombre = (document.getElementById('nombre') as HTMLInputElement).value;

    const salario = (document.getElementById('salario') as HTMLInputElement).value;


    if (!validar(nombre, salario)) return;


    const btn = document.getElementById('btn-insertar') as HTMLButtonElement;

    btn.disabled = true;


    try {

    const resp = await fetch('/api/empleados', {

        method: 'POST',

        headers: { 'Content-Type': 'application/json' },

        body: JSON.stringify({ nombre: nombre.trim(), salario: parseFloat(salario) }),

    });


    const json = await resp.json();


    if (json.success) {

        window.location.href = '/index.html?exito=1';

    } else {

        mostrarMensaje(json.message, 'error');

        btn.disabled = false;

    }

    } catch {

    mostrarMensaje('No se pudo conectar al servidor.', 'error');

    btn.disabled = false;

    }

}


document.getElementById('btn-insertar')!.addEventListener('click', insertar);

document.getElementById('btn-regresar')!.addEventListener('click', () => {

    window.location.href = '/index.html';

});


- - - - - - - - - - - - /public/js/insertar.js: - - - - - - - - - - - - - - - - - - - - - 

"use strict";

const NOMBRE_REGEX = /^[a-zA-ZáéíóúÁÉÍÓÚüÜñÑ\- ]+$/;

const SALARIO_REGEX = /^\d+(\.\d{1,2})?$/;

function setError(id, msg) {

    const el = document.getElementById(id);

    el.textContent = msg;

}

function limpiarErrores() {

    setError('error-nombre', '');

    setError('error-salario', '');

}

function mostrarMensaje(texto, tipo) {

    const el = document.getElementById('mensaje');

    el.textContent = texto;

    el.className = `mensaje ${tipo}`;

}

function validar(nombre, salario) {

    let valido = true;

    if (!nombre.trim()) {

        setError('error-nombre', 'El nombre no puede estar vacío.');

        valido = false;

    }

    else if (!NOMBRE_REGEX.test(nombre.trim())) {

        setError('error-nombre', 'Solo se permiten letras y guiones.');

        valido = false;

    }

    if (!salario.trim()) {

        setError('error-salario', 'El salario no puede estar vacío.');

        valido = false;

    }

    else if (!SALARIO_REGEX.test(salario.trim())) {

        setError('error-salario', 'Ingrese un valor válido. Ej: 850000.00');

        valido = false;

    }

    else if (parseFloat(salario) <= 0) {

        setError('error-salario', 'El salario debe ser mayor a cero.');

        valido = false;

    }

    return valido;

}

async function insertar() {

    limpiarErrores();

    const nombre = document.getElementById('nombre').value;

    const salario = document.getElementById('salario').value;

    if (!validar(nombre, salario))

        return;

    const btn = document.getElementById('btn-insertar');

    btn.disabled = true;

    try {

        const resp = await fetch('/api/empleados', {

            method: 'POST',

            headers: { 'Content-Type': 'application/json' },

            body: JSON.stringify({ nombre: nombre.trim(), salario: parseFloat(salario) }),

        });

        const json = await resp.json();

        if (json.success) {

            window.location.href = '/index.html?exito=1';

        }

        else {

            mostrarMensaje(json.message, 'error');

            btn.disabled = false;

        }

    }

    catch {

        mostrarMensaje('No se pudo conectar al servidor.', 'error');

        btn.disabled = false;

    }

}

document.getElementById('btn-insertar').addEventListener('click', insertar);

document.getElementById('btn-regresar').addEventListener('click', () => {

    window.location.href = '/index.html';

});


- - - - - - - - - - - - - - - - - - - /public/insertar.html: - - - - - - - - - - - - - - - - 

<!DOCTYPE html>

<html lang="es">

<head>

    <meta charset="UTF-8"/>

    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>

    <title>Insertar Empleado</title>

    <link rel="stylesheet" href="css/style.css"/>

</head>

<body>

    <div class="container">

    <h1>Insertar Empleado</h1>


    <div id="mensaje" class="mensaje hidden"></div>


    <div class="form-grupo">

        <label for="nombre">Nombre:</label>

        <input type="text" id="nombre" placeholder="Ej: María Rodríguez"/>

        <span class="error" id="error-nombre"></span>

    </div>


    <div class="form-grupo">

        <label for="salario">Salario:</label>

        <input type="text" id="salario" placeholder="Ej: 850000.00"/>

        <span class="error" id="error-salario"></span>

    </div>


    <div class="botones">

        <button id="btn-insertar">Insertar</button>

        <button id="btn-regresar">Regresar</button>

    </div>

    </div>


    <script src="js/insertar.js"></script>

</body>

</html>


- - - - - - - - - - - - - - - - /public/style.css: - - - - - - - - - - - - - - - - -

body {

    font-family: Arial, sans-serif;

    background: #f0f2f5;

    display: flex;

    justify-content: center;

    padding: 40px 20px;

}


.container {

    background: #fff;

    padding: 32px;

    border-radius: 8px;

    box-shadow: 0 2px 8px rgba(0,0,0,0.1);

    width: 100%;

    max-width: 480px;

}


h1 {

    margin-bottom: 24px;

    font-size: 1.4rem;

    color: #1a56db;

}


.form-grupo {

    display: flex;

    flex-direction: column;

    margin-bottom: 16px;

}


label {

    font-weight: bold;

    margin-bottom: 4px;

}


input {

    padding: 8px 12px;

    border: 1.5px solid #ccc;

    border-radius: 6px;

    font-size: 1rem;

}


input:focus {

    outline: none;

    border-color: #1a56db;

}


.error {

    color: red;

    font-size: 0.8rem;

    margin-top: 4px;

}


.botones {

    display: flex;

    justify-content: space-between;

    margin-top: 24px;

}


button {

    padding: 10px 24px;

    border: none;

    border-radius: 6px;

    font-size: 1rem;

    cursor: pointer;

}


#btn-insertar { background: #1a56db; color: white; }

#btn-insertar:hover { background: #1e429f; }

#btn-insertar:disabled { background: #9ca3af; }

#btn-regresar { background: #e5e7eb; color: #374151; }

#btn-regresar:hover { background: #d1d5db; }


.mensaje { padding: 10px; border-radius: 6px; margin-bottom: 16px; }

.exito { background: #f0fdf4; color: #16a34a; }

.error-msg { background: #fef2f2; color: #dc2626; }

.hidden { display: none; }

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

MORALEJAS / BUENAS PRÁCTICAS

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

- En proyectos que utilicen backend y frontend TypeScript, usar tsconfig separados evita que el compilador mezcle los archivos de salida.

- Siempre reiniciar Hamachi si hay problemas de conexión antes de buscar el error en el código.

- La política de ejecución de PowerShell en Windows hay que configurarla en cada máquina del equipo.

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

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

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

- Sebastián termina index.html e index.ts con la tabla de empleados.

- Crear el controller y las rutas del backend (empleadoController.ts y routes/empleados.ts).

- Probar el flujo completo: lista → insertar → regresar con tabla actualizada.

- Hacer commit de todo lo avanzado hoy.

Comentarios

Entradas más populares de este blog

Entrada 4