Fecha: 28 y 30 de abril de 2026
Inicio: [1:39] | Fin: [2:24] U Inicio: [2:48] | Fin: [3:59] U Inicio: [11:27] | Fin: [12:08] Inicio: [13:10] | Fin: [14:36] y Inicio: [9:21] | Fin: [10:04] (bitacora) || Total: [4 horas y 36 minutos]
Presente: Sebastián Ramírez Abarca
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
¿QUÉ HICIMOS HOY?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- Se creó la página insertar movimiento con insertarMovimiento.html.
- Se creó insertarMovimiento.ts para y se compiló para crear insertarMovimiento.js que maneja la página de insertarMovimiento, específicamente hace:
+ Lee el ?documento=... de la URL para saber a qué empleado corresponde.
+ Carga los datos del empleado llamando a /api/empleados/:doc y los muestra.
+ Carga la dropdown list de tipos de movimiento llamando a /api/tiposMovimiento.
+ Valida que el monto no deje el saldo negativo cuando el tipo de movimiento es Débito
- Correción de sp_
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PROBLEMAS DETECTADOS Y CÓMO RESOLVERLOS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- Uncaught TypeError: Cannot set properties of null (setting 'href')
at new InsertarMovimientoPage (insertarMovimiento.js:19:29)
at insertarMovimiento.js:195:1
Soluciones:
Correcciones en insertarMovimientos.html
1. volverBtn tenía dos href, puse como href lo que era id.
2. El id en html del botón insertar no era el que buscaba en el javascript
3. Puse class="editor-actions" y lo correcto era class="editar-actions".
- :3000/api/empleados/$%7BencodeURIComponent(this.documentoIdentidad)%7D:1 Failed to load resource: the server responded with a status of 500 (Internal Server Error)
Solución: Usaba comillas simples (') en el fetch en vez de backticks (`), reemplacé las comillas simples por backticks.
- No cargaba la dropdown list de tiposMovimiento: Failed to load resource: the server responded with a status of 404 (Not Found)Understand this error
insertarMovimiento.js:102 Error cargando tipos de movimiento: SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
Solución: Resultó que pusé mal la dirección en el fetch en index.ts. puse '/api/tiposMovimientos' y era '/api/tiposMovimiento', por lo que lo corregí.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
DUDAS Y DIVERGENCIAS DE CRITERIOS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- No se discutieron dudas ni divergencias en esta sesión.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
AVANCE DEL CÓDIGO
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
insertarMovimiento.html:
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Insertar Movimiento - Sistema de Vacaciones</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body class="empleados-page">
<div class="empleados-shell">
<header class="empleados-header">
<div>
<h1>Insertar movimiento</h1>
<p class="subtitle">Registrar un nuevo movimiento de vacaciones al empleado</p>
</div>
<div class="empleados-actions-top">
<a id="volverBtn" class="secondary-link" href="/movimientos.html">Volver a movimientos</a>
</div>
</header>
<section class="panel">
<div class="panel-title">
<h2 id="tituloEmpleado">Cargando empleado...</h2>
</div>
<div id="estado" class="status info">
Cargando datos del empleado...
</div>
<div id="resumenEmpleado" class="detalle-contenido"></div>
</section>
<section class="panel">
<div class="panel-title">
<h2>Nuevo movimiento</h2>
</div>
<div class="editar-form">
<div class="editar-grid">
<div class="editar-field">
<label for="tipoMovimiento">
Tipo de movimiento
</label>
<select id="tipoMovimiento" class="fullwidth" required>
<option value="">
Cargando tipos...
</option>
</select>
</div>
<div class="editar-field">
<label for="monto"> Monto</label>
<input id="monto" type="number"
min="0.01" step="0.01"
required placeholder="0.00">
</div>
</div>
<div id="estadoForm" class="status info hidden">
Completa los campos para registrar el movimiento.
</div>
<div class="editar-actions">
<button id="insertarBtn" class="primary-btn" type="button" disabled>
Insertar movimiento
</button>
<a id="cancelarBtn" class="ghost-btn" href="/movimientos.html">
Cancelar
</a>
</div>
</div>
</section>
</div>
<script type="module" src="js/insertarMovimiento.js"></script>
</body>
</html>
Cambios en sp_GetMovimientos:
CREATE PROCEDURE sp_GetTiposMovimiento
@outResultCode INT OUTPUT
AS
BEGIN
@@ -19,7 +16,7 @@ BEGIN
BEGIN TRY
SELECT t.id, t.Nombre, t.TipoAccion
FROM dbo.TipoMovimiento t
ORDER BY Nombre ASC;
SET @outResultCode = 0;
Cambio en movimientos.html:
<a id="insertarMovimientoBtn" class="primary-btn" href="/insertarMovimiento.html"> Agregar movimiento</a>
insertarMovimiento.ts:
type EmpleadoDatosMovimiento = {
ValorDocumentoIdentidad: string;
Nombre: string;
SaldoVacaciones: number;
};
type TipoMovimiento = {
id: number;
Nombre: string;
TipoAccion: string;
};
class InsertarMovimientoPage {
private readonly documentoIdentidad: string;
private readonly tituloEmpleado: HTMLElement;
private readonly resumenEmpleado: HTMLElement;
private estadoDiv: HTMLElement;
private readonly estadoFormDiv: HTMLElement;
private readonly tipoMovimientoSelect: HTMLSelectElement;
private readonly montoInput: HTMLInputElement;
private readonly insertarBtn: HTMLButtonElement;
private readonly volverBtn: HTMLAnchorElement;
private readonly cancelarBtn: HTMLAnchorElement;
private saldoActual: number = 0;
private empleadoCargado: boolean = false;
constructor() {
const params = new URLSearchParams(window.location.search);
this.documentoIdentidad = params.get('documento')?.trim() ?? '';
this.tituloEmpleado = document.getElementById('tituloEmpleado') as HTMLElement;
this.resumenEmpleado = document.getElementById('resumenEmpleado') as HTMLElement;
this.estadoDiv = document.getElementById('estado') as HTMLElement;
this.estadoFormDiv = document.getElementById('estadoForm') as HTMLElement;
this.tipoMovimientoSelect = document.getElementById('tipoMovimiento') as HTMLSelectElement;
this.montoInput = document.getElementById('monto') as HTMLInputElement;
this.insertarBtn = document.getElementById('insertarBtn') as HTMLButtonElement;
this.volverBtn = document.getElementById('volverBtn') as HTMLAnchorElement;
this.cancelarBtn = document.getElementById('cancelarBtn') as HTMLAnchorElement;
// Apuntar los enlaces de volver y cancelar al empleado correcto
const urlMovimientos = `/movimientos.html?documento=${encodeURIComponent(this.documentoIdentidad)}`;
this.volverBtn.href = urlMovimientos;
this.cancelarBtn.href = urlMovimientos;
this.bindEvents();
void this.cargarVista();
}
private bindEvents(): void {
this.insertarBtn.addEventListener('click', () => {
void this.insertar();
});
// Validar el monto en tiempo real
this.montoInput.addEventListener('input', () => {
this.validarMontoEnTiempoReal();
});
}
private async cargarVista(): Promise<void> {
if (!this.documentoIdentidad){
this.tituloEmpleado.textContent = 'Sin empleado seleccionado';
this.setEstado('Regresa a la lista de empleados y selecciona uno.', 'warning');
return;
}
this.setEstado('Cargando datos del empleado...', 'info');
await this.cargarEmpleado();
if (this.empleadoCargado){
await this.cargarTiposMovimiento();
}
}
private async cargarEmpleado(): Promise<void>{
try {
const response = await fetch(`/api/empleados/${encodeURIComponent(this.documentoIdentidad)}`, {
method: 'GET',
});
const payload = await response.json() as {
success: boolean
message?: string;
data?: EmpleadoDatosMovimiento | null;
};
if (!response.ok || !payload.success || !payload.data){
this.tituloEmpleado.textContent = 'Error al cargar empleado';
this.setEstado(payload.message ?? 'No se pudo cargar el empleado.', 'error');
return;
}
const empleado = payload.data;
this.saldoActual = empleado.SaldoVacaciones;
this.empleadoCargado = true;
this.tituloEmpleado.textContent = `Insertar movimientos - ${empleado.Nombre}`;
this.resumenEmpleado.innerHTML = `
<div class="detalle-grid">
<div class="detalle-item">
<span class="detalle-label">Documento</span>
<span class="detalle-valor">${empleado.ValorDocumentoIdentidad}</span>
</div>
<div class="detalle-item">
<span class="detalle-label">Nombre</span>
<span class="detalle-valor">${empleado.Nombre}</span>
</div>
<div class="detalle-item">
<span class="detalle-label">Saldo vacaciones</span>
<span class="detalle-valor">${empleado.SaldoVacaciones}</span>
</div>
</div>
`;
this.setEstado('Empleado cargado. Selecciona el tipo de movimiento y el monto.', 'success');
} catch (error){
console.error('Error cargando empleado:', error);
this.tituloEmpleado.textContent = 'Error al cargar empleado';
this.setEstado('Error de conexión al cargar el empleado.', 'error');
}
}
private async cargarTiposMovimiento(): Promise<void>{
try{
const response = await fetch('/api/tiposMovimiento', {method: 'GET' });
const payload = await response.json() as {
success: boolean;
message?: string;
data?: TipoMovimiento[];
};
if (!response.ok || !payload.success || !payload.data?.length){
this.tipoMovimientoSelect.innerHTML =
'<option value="">No hay tipos disponibles</option>';
return;
}
this.tipoMovimientoSelect.innerHTML =
'<option value="">Selecciona un tipo</option>' +
payload.data.map((t) =>
`<option value="${t.Nombre}" data-accion="${t.TipoAccion}">${t.Nombre}</option>`
).join('');
// Habilitar boton solo cuando hay tipos disponibles
this.insertarBtn.disabled = false;
} catch (error){
console.error('Error cargando tipos de movimiento:', error);
this.tipoMovimientoSelect.innerHTML = '<option value"">Error al cargar tipos</option>';
}
}
private validarMontoEnTiempoReal(): void {
const monto = parseFloat(this.montoInput.value);
const selectedOption = this.tipoMovimientoSelect.selectedOptions[0];
const tipoAccion = selectedOption?.dataset['accion'] ?? '';
if (isNaN(monto) || monto <= 0){
this.setEstadoForm('El monto debe ser mayor a 0.', 'warning');
return;
}
// Verificar que no haya saldo negativo si el tipo es R (retiro/debito)
if (tipoAccion === 'R' && monto > this.saldoActual){
this.setEstadoForm(
`El monto supera el saldo actual (${this.saldoActual}). El saldo no puede ser negativo.`,
'error'
);
return;
}
this.setEstadoForm('', 'info');
}
private validar(): string | null {
const tipoMovimiento = this.tipoMovimientoSelect.value.trim();
const monto = parseFloat(this.montoInput.value);
const selectedOption = this.tipoMovimientoSelect.selectedOptions[0];
const tipoAccion = selectedOption?.dataset['accion'] ?? '';
if (!tipoMovimiento){
return 'Debes seleccionar un tipo de movimiento.';
}
if (isNaN(monto) || monto <= 0){
return 'El monto debe ser mayor a 0.';
}
if (tipoAccion === 'R' && monto > this.saldoActual){
return `El monto supera el saldo actual (${this.saldoActual}). El saldo no puede ser negativo.`;
}
return null;
}
private async insertar(): Promise<void> {
const error = this.validar();
if (error){
this.setEstadoForm(error, 'error');
return;
}
const tipoMovimiento = this.tipoMovimientoSelect.value.trim();
const monto = parseFloat(this.montoInput.value);
const username = localStorage.getItem('username') ?? 'UsuarioScripts';
const fecha = new Date().toISOString().split('T')[0];
this.insertarBtn.disabled = true;
this.setEstadoForm('Insertando movimiento...', 'info');
try{
const response = await fetch('/api/movimientos', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-username': username,
},
body: JSON.stringify({
valorDocumentoIdentidad: this.documentoIdentidad,
nombreTipoMovimiento: tipoMovimiento,
monto,
fecha,
}),
});
const payload = await response.json() as {
success: boolean;
outResultCode: number;
message?: string;
};
if (!response.ok || !payload.success){
this.setEstadoForm(payload.message ?? 'No se pudo insertar el movimiento.', 'error');
this.insertarBtn.disabled = false;
return;
}
this.setEstadoForm('Movimiento insertado correctamente. Redirigiendo...', 'success');
// Redirigir a movimientos despues de exito
setTimeout(() => {
window.location.href = `/movimientos.html?documento=${encodeURIComponent(this.documentoIdentidad)}`;
}, 1500);
} catch (error) {
console.error('Error insertanto movimiento:', error);
this.setEstadoForm('Error de conexion al registrar el movimiento.', 'error');
this.insertarBtn.disabled = false;
}
}
private setEstado(mensaje: string, tipo: 'info' | 'success' | 'warning' | 'error'): void{
this.estadoDiv.textContent = mensaje;
this.estadoDiv.className = `status ${tipo}`;
}
private setEstadoForm(mensaje: string, tipo: 'info' | 'success' | 'warning' | 'error'): void{
if (!mensaje){
this.estadoFormDiv.classList.add('hidden');
return;
}
this.estadoFormDiv.textContent = mensaje;
this.estadoFormDiv.className = `status ${tipo}`;
this.estadoFormDiv.classList.remove('hidden');
}
}
new InsertarMovimientoPage();
insertarMovimiento.js:
"use strict";
class InsertarMovimientoPage {
constructor() {
this.saldoActual = 0;
this.empleadoCargado = false;
const params = new URLSearchParams(window.location.search);
this.documentoIdentidad = params.get('documento')?.trim() ?? '';
this.tituloEmpleado = document.getElementById('tituloEmpleado');
this.resumenEmpleado = document.getElementById('resumenEmpleado');
this.estadoDiv = document.getElementById('estado');
this.estadoFormDiv = document.getElementById('estadoForm');
this.tipoMovimientoSelect = document.getElementById('tipoMovimiento');
this.montoInput = document.getElementById('monto');
this.insertarBtn = document.getElementById('insertarBtn');
this.volverBtn = document.getElementById('volverBtn');
this.cancelarBtn = document.getElementById('cancelarBtn');
// Apuntar los enlaces de volver y cancelar al empleado correcto
const urlMovimientos = `/movimientos.html?documento=${encodeURIComponent(this.documentoIdentidad)}`;
this.volverBtn.href = urlMovimientos;
this.cancelarBtn.href = urlMovimientos;
this.bindEvents();
void this.cargarVista();
}
bindEvents() {
this.insertarBtn.addEventListener('click', () => {
void this.insertar();
});
// Validar el monto en tiempo real
this.montoInput.addEventListener('input', () => {
this.validarMontoEnTiempoReal();
});
}
async cargarVista() {
if (!this.documentoIdentidad) {
this.tituloEmpleado.textContent = 'Sin empleado seleccionado';
this.setEstado('Regresa a la lista de empleados y selecciona uno.', 'warning');
return;
}
this.setEstado('Cargando datos del empleado...', 'info');
await this.cargarEmpleado();
if (this.empleadoCargado) {
await this.cargarTiposMovimiento();
}
}
async cargarEmpleado() {
try {
const response = await fetch(`/api/empleados/${encodeURIComponent(this.documentoIdentidad)}`, {
method: 'GET',
});
const payload = await response.json();
if (!response.ok || !payload.success || !payload.data) {
this.tituloEmpleado.textContent = 'Error al cargar empleado';
this.setEstado(payload.message ?? 'No se pudo cargar el empleado.', 'error');
return;
}
const empleado = payload.data;
this.saldoActual = empleado.SaldoVacaciones;
this.empleadoCargado = true;
this.tituloEmpleado.textContent = `Insertar movimientos - ${empleado.Nombre}`;
this.resumenEmpleado.innerHTML = `
<div class="detalle-grid">
<div class="detalle-item">
<span class="detalle-label">Documento</span>
<span class="detalle-valor">${empleado.ValorDocumentoIdentidad}</span>
</div>
<div class="detalle-item">
<span class="detalle-label">Nombre</span>
<span class="detalle-valor">${empleado.Nombre}</span>
</div>
<div class="detalle-item">
<span class="detalle-label">Saldo vacaciones</span>
<span class="detalle-valor">${empleado.SaldoVacaciones}</span>
</div>
</div>
`;
this.setEstado('Empleado cargado. Selecciona el tipo de movimiento y el monto.', 'success');
}
catch (error) {
console.error('Error cargando empleado:', error);
this.tituloEmpleado.textContent = 'Error al cargar empleado';
this.setEstado('Error de conexión al cargar el empleado.', 'error');
}
}
async cargarTiposMovimiento() {
try {
const response = await fetch('/api/tiposMovimiento', { method: 'GET' });
const payload = await response.json();
if (!response.ok || !payload.success || !payload.data?.length) {
this.tipoMovimientoSelect.innerHTML =
'<option value="">No hay tipos disponibles</option>';
return;
}
this.tipoMovimientoSelect.innerHTML =
'<option value="">Selecciona un tipo</option>' +
payload.data.map((t) => `<option value="${t.Nombre}" data-accion="${t.TipoAccion}">${t.Nombre}</option>`).join('');
// Habilitar boton solo cuando hay tipos disponibles
this.insertarBtn.disabled = false;
}
catch (error) {
console.error('Error cargando tipos de movimiento:', error);
this.tipoMovimientoSelect.innerHTML = '<option value"">Error al cargar tipos</option>';
}
}
validarMontoEnTiempoReal() {
const monto = parseFloat(this.montoInput.value);
const selectedOption = this.tipoMovimientoSelect.selectedOptions[0];
const tipoAccion = selectedOption?.dataset['accion'] ?? '';
if (isNaN(monto) || monto <= 0) {
this.setEstadoForm('El monto debe ser mayor a 0.', 'warning');
return;
}
// Verificar que no haya saldo negativo si el tipo es R (retiro/debito)
if (tipoAccion === 'R' && monto > this.saldoActual) {
this.setEstadoForm(`El monto supera el saldo actual (${this.saldoActual}). El saldo no puede ser negativo.`, 'error');
return;
}
this.setEstadoForm('', 'info');
}
validar() {
const tipoMovimiento = this.tipoMovimientoSelect.value.trim();
const monto = parseFloat(this.montoInput.value);
const selectedOption = this.tipoMovimientoSelect.selectedOptions[0];
const tipoAccion = selectedOption?.dataset['accion'] ?? '';
if (!tipoMovimiento) {
return 'Debes seleccionar un tipo de movimiento.';
}
if (isNaN(monto) || monto <= 0) {
return 'El monto debe ser mayor a 0.';
}
if (tipoAccion === 'R' && monto > this.saldoActual) {
return `El monto supera el saldo actual (${this.saldoActual}). El saldo no puede ser negativo.`;
}
return null;
}
async insertar() {
const error = this.validar();
if (error) {
this.setEstadoForm(error, 'error');
return;
}
const tipoMovimiento = this.tipoMovimientoSelect.value.trim();
const monto = parseFloat(this.montoInput.value);
const username = localStorage.getItem('username') ?? 'UsuarioScripts';
const fecha = new Date().toISOString().split('T')[0];
this.insertarBtn.disabled = true;
this.setEstadoForm('Insertando movimiento...', 'info');
try {
const response = await fetch('/api/movimientos', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-username': username,
},
body: JSON.stringify({
valorDocumentoIdentidad: this.documentoIdentidad,
nombreTipoMovimiento: tipoMovimiento,
monto,
fecha,
}),
});
const payload = await response.json();
if (!response.ok || !payload.success) {
this.setEstadoForm(payload.message ?? 'No se pudo insertar el movimiento.', 'error');
this.insertarBtn.disabled = false;
return;
}
this.setEstadoForm('Movimiento insertado correctamente. Redirigiendo...', 'success');
// Redirigir a movimientos despues de exito
setTimeout(() => {
window.location.href = `/movimientos.html?documento=${encodeURIComponent(this.documentoIdentidad)}`;
}, 1500);
}
catch (error) {
console.error('Error insertanto movimiento:', error);
this.setEstadoForm('Error de conexion al registrar el movimiento.', 'error');
this.insertarBtn.disabled = false;
}
}
setEstado(mensaje, tipo) {
this.estadoDiv.textContent = mensaje;
this.estadoDiv.className = `status ${tipo}`;
}
setEstadoForm(mensaje, tipo) {
if (!mensaje) {
this.estadoFormDiv.classList.add('hidden');
return;
}
this.estadoFormDiv.textContent = mensaje;
this.estadoFormDiv.className = `status ${tipo}`;
this.estadoFormDiv.classList.remove('hidden');
}
}
new InsertarMovimientoPage();
Cambio en movimiento.ts:
const insertarBtn = document.getElementById('insertarMovimientoBtn') as HTMLAnchorElement;
if (insertarBtn){
insertarBtn.href = `/insertarMovimiento.html?documento=${encodeURIComponent(this.documentoIdentidad)}`;
}
Cambio en movimiento.js:
const insertarBtn = document.getElementById('insertarMovimientoBtn');
if (insertarBtn) {
insertarBtn.href = `/insertarMovimiento.html?documento=${encodeURIComponent(this.documentoIdentidad)}`;
}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
MORALEJAS / BUENAS PRÁCTICAS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- Es mejor hacer poco a poco para que no se acumulen errores.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
PRÓXIMA SESIÓN: ¿QUÉ SIGUE?
- Aún por definir.
Comentarios
Publicar un comentario