El precio unitario perdía los céntimos. Qué fallaba: al dar de alta una línea, el precio se guardaba con parseInt(), de modo que
«10,50 €» se convertía en «10 €» y todos los importes quedaban descuadrados.
Cómo lo detecté: añadí una línea con precio 10.50 y la columna «Precio» mostraba 10,00 €;
el subtotal era 10,00 € en lugar de 10,50 €.
Cómo lo arreglé: el precio se parsea con parseFloat() (y se valida >= 0), conservando los céntimos.
Al recargar se perdían las fechas (la página dejaba de funcionar). Qué fallaba:JSON.stringify() convierte los objetos Date en texto ISO, así que al
recargar l.fecha era una cadena y l.fecha.getTime() lanzaba
TypeError: l.fecha.getTime is not a function: ni se pintaba la tabla ni el filtro «Solo últimos 7 días».
Cómo lo detecté: cargué varias líneas, recargué la página y revisé la consola: error de getTime.
Cómo lo arreglé: al cargar, cada línea se rehidrata reconstruyendo la fecha (new Date(l.fecha)),
descartando las entradas con fecha inválida.
La paginación mostraba 4 líneas en vez de 5. Qué fallaba: el slice() terminaba en … + POR_PAGINA - 1, así que cada página
mostraba 4 filas (la 5.ª se saltaba a la página siguiente).
Cómo lo detecté: con 6 líneas añadidas, la página 1 mostraba 4 filas y la 2 mostraba 2, no 5 y 1.
Cómo lo arreglé:slice(inicio, inicio + POR_PAGINA).
El IVA se calculaba sobre el subtotal sin descontar. Qué fallaba: el IVA se aplicaba a sub en lugar de a la base ya descontada, así que con
descuento el total salía demasiado alto (spec: «el IVA se calcula sobre la base ya descontada»).
Cómo lo detecté: con un subtotal de 100,00 € y 10 % de descuento, la página mostraba IVA 21,00 € y
total 111,00 €, cuando debe ser IVA 18,90 € y total 108,90 €.
Cómo lo arreglé: se calcula base = sub - desc, iva = base * IVA y
total = base + iva.
El botón ✕ borraba la línea equivocada. Qué fallaba: el manejador hacía lineas.splice(i, 1) usando el índice de la fila dentro de la
tabla visible, no el índice real en lineas; con filtro, orden o paginación se borraba otra línea (o ninguna).
Cómo lo detecté: con 6 líneas, en la página 2, al pulsar ✕ en la última fila se borraba la primera línea
de la lista original en lugar de esa.
Cómo lo arreglé: cada fila conserva su línea original y el borrado usa lineas.indexOf(l)
para localizar el índice real antes de hacer el splice.
La búsqueda distinguía mayúsculas de minúsculas. Qué fallaba: se comparaba concepto.toLowerCase().includes(filtro) sin normalizar lo que el
usuario escribía, así que buscar «agua» no encontraba «Agua».
Cómo lo detecté: añadí «Agua mineral» y busqué «agua»: no aparecía ninguna fila.
Cómo lo arreglé: el filtro se normaliza a minúsculas al comparar (filtro.toLowerCase()).
«Ordenar» ordenaba de menor a mayor. Qué fallaba: el comparador importe(a) < importe(b) devuelve un booleano y ordenaba en sentido
ascendente, cuando debe ser de mayor a menor.
Cómo lo detecté: con importes 30 €, 10 € y 20 €, la tabla quedaba 10 €, 20 €, 30 €.
Cómo lo arreglé:lineas.sort((a, b) => importe(b) - importe(a)) (y se guarda el orden,
de modo que se mantiene al recargar).
Inyección de HTML en el concepto. Qué fallaba: la fila se construía con innerHTML interpolando el concepto del usuario, así que
un concepto como <img src=x onerror=alert(1)> se ejecutaba como HTML.
Cómo lo detecté: di de alta una línea con ese concepto y observé que se insertaba un elemento img
en la tabla en lugar de mostrarse como texto.
Cómo lo arreglé: la fila se monta con createElement() y textContent, de modo que
cualquier texto del usuario se muestra literalmente y nunca se interpreta como HTML.