- Sidebar fija en todas las pantallas (md+): logo, nav, logout - index.html: grid 3/5 + 2/5 en desktop (movimientos | donut chart) - add.html: 2 columnas desktop (form | monto+numpad), botones mobile/desktop unificados - historial.html: max-w-3xl, header con botón Nuevo integrado - dashboard.js: formato de moneda con toLocaleString, skeleton loading, mes dinámico - add.js: lógica simplificada con doSave(), teclado ignora inputs de texto - auth.php + login.php: cookie con httponly y SameSite=Lax - docs/plan/00_PLAN.md: estado actualizado, CLI marcado como fase futura Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
111 lines
4.5 KiB
JavaScript
111 lines
4.5 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
|
let currentMonto = "0";
|
|
let selectedTipo = "gasto";
|
|
|
|
// Hay dos displays de monto (desktop / mobile) y dos botones guardar
|
|
const montoEls = ['ui-monto', 'ui-monto-mobile'].map(id => document.getElementById(id)).filter(Boolean);
|
|
const btnGasto = document.getElementById('btn-gasto');
|
|
const btnIngreso = document.getElementById('btn-ingreso');
|
|
const numpad = document.getElementById('ui-numpad');
|
|
const iptFecha = document.getElementById('ui-fecha');
|
|
const saveButtons = ['btn-save', 'btn-save-mobile'].map(id => document.getElementById(id)).filter(Boolean);
|
|
|
|
// Default a hoy
|
|
iptFecha.valueAsDate = new Date();
|
|
|
|
// Cargar categorías desde la API
|
|
apiFetch('api/movimientos.php')
|
|
.then(r => r ? r.json() : Promise.reject('no-auth'))
|
|
.then(data => {
|
|
if (!data || !data.categorias) return;
|
|
const sel = document.getElementById('ui-categoria');
|
|
sel.innerHTML = data.categorias.map(c =>
|
|
`<option value="${c.id}">${c.nombre}</option>`
|
|
).join('');
|
|
})
|
|
.catch(() => {});
|
|
|
|
// Tipo selector
|
|
const updateTipoUI = () => {
|
|
const isGasto = selectedTipo === 'gasto';
|
|
btnGasto.classList.toggle('bg-surface-container-lowest', isGasto);
|
|
btnGasto.classList.toggle('text-primary', isGasto);
|
|
btnGasto.classList.toggle('shadow-sm', isGasto);
|
|
btnGasto.classList.toggle('text-on-surface-variant', !isGasto);
|
|
btnIngreso.classList.toggle('bg-surface-container-lowest', !isGasto);
|
|
btnIngreso.classList.toggle('text-primary', !isGasto);
|
|
btnIngreso.classList.toggle('shadow-sm', !isGasto);
|
|
btnIngreso.classList.toggle('text-on-surface-variant', isGasto);
|
|
};
|
|
btnGasto.addEventListener('click', () => { selectedTipo = 'gasto'; updateTipoUI(); });
|
|
btnIngreso.addEventListener('click', () => { selectedTipo = 'ingreso'; updateTipoUI(); });
|
|
|
|
// Numpad
|
|
function applyInput(val) {
|
|
if (val === 'backspace') {
|
|
currentMonto = currentMonto.slice(0, -1) || "0";
|
|
} else if (/^[0-9]$/.test(val)) {
|
|
currentMonto = currentMonto === "0" ? val : currentMonto + val;
|
|
} else if (val === '.') {
|
|
if (!currentMonto.includes('.')) currentMonto += '.';
|
|
}
|
|
montoEls.forEach(el => el.textContent = currentMonto);
|
|
}
|
|
|
|
numpad.addEventListener('click', (e) => {
|
|
const btn = e.target.closest('button');
|
|
if (!btn) return;
|
|
applyInput(btn.querySelector('[data-icon="backspace"]') ? 'backspace' : btn.textContent.trim());
|
|
});
|
|
|
|
// Teclado físico
|
|
document.addEventListener('keydown', (e) => {
|
|
if (document.activeElement.tagName === 'INPUT') return;
|
|
if (e.key >= '0' && e.key <= '9') applyInput(e.key);
|
|
else if (e.key === '.' || e.key === ',') applyInput('.');
|
|
else if (e.key === 'Backspace') applyInput('backspace');
|
|
else if (e.key === 'Enter') doSave();
|
|
});
|
|
|
|
// Guardar
|
|
async function doSave() {
|
|
const payload = {
|
|
tipo: selectedTipo,
|
|
monto: parseFloat(currentMonto),
|
|
fecha: iptFecha.value,
|
|
notas: document.getElementById('ui-notas').value,
|
|
id_categoria: document.getElementById('ui-categoria').value
|
|
};
|
|
|
|
if (isNaN(payload.monto) || payload.monto <= 0) {
|
|
rmModal.alert('Ingresá un monto mayor a 0.', 'Monto inválido');
|
|
return;
|
|
}
|
|
|
|
saveButtons.forEach(b => { b.disabled = true; b.textContent = 'Guardando…'; });
|
|
|
|
try {
|
|
const r = await apiFetch('api/movimientos.php', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
if (!r) return;
|
|
const data = await r.json();
|
|
|
|
if (data.status === 'success') {
|
|
window.location.href = 'index.html';
|
|
} else {
|
|
await rmModal.alert(data.message, 'Error al guardar');
|
|
saveButtons.forEach(b => { b.disabled = false; b.textContent = 'Guardar Movimiento'; });
|
|
}
|
|
} catch (err) {
|
|
console.error(err);
|
|
await rmModal.alert('No se pudo conectar con el servidor.', 'Error de conexión');
|
|
saveButtons.forEach(b => { b.disabled = false; b.textContent = 'Guardar Movimiento'; });
|
|
}
|
|
}
|
|
|
|
saveButtons.forEach(btn => btn.addEventListener('click', doSave));
|
|
});
|