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
Publicar un comentario