UI: rediseño responsive desktop + fix categorías + plan actualizado

- 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>
This commit is contained in:
Ricardo Monla
2026-06-03 16:41:59 -03:00
co-authored by Claude Sonnet 4.6
parent ead1bf8ffc
commit 7cb6107cb4
8 changed files with 623 additions and 650 deletions
+76 -68
View File
@@ -1,80 +1,88 @@
document.addEventListener('DOMContentLoaded', async () => {
try {
const response = await apiFetch('api/movimientos.php');
if (!response) return;
const data = await response.json();
if (data.status === 'success') {
// Update balance
document.getElementById('ui-balance').textContent = `$${data.balance.toFixed(2)}`;
// Render movements
const uiMovimientos = document.getElementById('ui-movimientos');
uiMovimientos.innerHTML = '';
data.movimientos.forEach(mov => {
const isExpense = mov.tipo === 'gasto';
const sign = isExpense ? '-' : '+';
const amountColor = isExpense ? 'text-error' : 'text-primary-container';
uiMovimientos.innerHTML += `
<div class="flex items-center justify-between mt-6">
<div class="flex items-center gap-4">
<div class="w-12 h-12 rounded-2xl bg-surface-container-highest flex items-center justify-center">
<span class="material-symbols-outlined text-primary">${mov.categoria_icono || 'payments'}</span>
</div>
<div>
<p class="font-bold text-on-surface">${mov.notas || mov.categoria_nombre}</p>
<p class="text-xs text-on-surface-variant">${mov.fecha}</p>
</div>
</div>
<p class="font-bold ${amountColor}">${sign}$${parseFloat(mov.monto).toFixed(2)}</p>
</div>
`;
});
if (data.status !== 'success') return;
// Dynamize Donut Chart
if (data.grafico && data.grafico.length > 0) {
const totalGastos = data.grafico.reduce((sum, item) => sum + parseFloat(item.total), 0);
let svgHtml = `<svg class="w-full h-full transform -rotate-90" viewbox="0 0 36 36">`;
let legendHtml = '';
let currentOffset = 0;
// Balance
const balance = data.balance ?? 0;
document.getElementById('ui-balance').textContent = formatMoney(balance);
document.getElementById('ui-balance-resumen').textContent =
balance >= 0 ? 'Saldo disponible' : 'Saldo negativo';
data.grafico.forEach(cat => {
const pct = (parseFloat(cat.total) / totalGastos) * 100;
// Add SVG segment
svgHtml += `<circle cx="18" cy="18" fill="none" r="16" stroke="${cat.color_hex}" stroke-dasharray="${pct}, 100" stroke-dashoffset="-${currentOffset}" stroke-linecap="round" stroke-width="4"></circle>`;
// Add Legend entry
legendHtml += `
<div class="flex items-center justify-between p-3 bg-surface-container-lowest rounded-xl">
<div class="flex items-center gap-3">
<div class="w-3 h-3 rounded-full" style="background-color: ${cat.color_hex}"></div>
<span class="text-sm font-semibold">${cat.nombre}</span>
</div>
<span class="text-sm text-on-surface-variant">${pct.toFixed(1)}%</span>
</div>
`;
currentOffset += pct;
});
svgHtml += `</svg>
<div class="absolute inset-0 flex flex-col items-center justify-center">
<span class="text-2xl font-bold">$${totalGastos.toFixed(0)}</span>
<span class="text-[10px] uppercase tracking-widest text-on-surface-variant font-bold">Gastos</span>
// Movimientos recientes
const container = document.getElementById('ui-movimientos');
if (!data.movimientos.length) {
container.innerHTML = `
<div class="flex flex-col items-center py-10 gap-2 text-on-surface-variant">
<span class="material-symbols-outlined text-4xl opacity-30">receipt_long</span>
<p class="text-sm">Sin movimientos aún</p>
</div>`;
} else {
container.innerHTML = data.movimientos.map(mov => {
const isGasto = mov.tipo === 'gasto';
const colorAmt = isGasto ? 'text-[#ba1a1a]' : 'text-[#005050]';
const signo = isGasto ? '-' : '+';
const icono = mov.categoria_icono || 'payments';
const titulo = mov.notas?.trim() ? mov.notas : (mov.categoria_nombre || '—');
const fecha = new Date(mov.fecha + 'T12:00:00')
.toLocaleDateString('es-AR', { day:'2-digit', month:'short' });
return `
<div class="flex items-center gap-4 px-5 py-4">
<div class="w-11 h-11 rounded-xl bg-surface-container-high flex items-center justify-center flex-shrink-0">
<span class="material-symbols-outlined text-primary text-[20px]">${icono}</span>
</div>
<div class="flex-1 min-w-0">
<p class="font-semibold text-on-surface text-sm truncate">${titulo}</p>
<p class="text-xs text-on-surface-variant">${fecha} · ${mov.categoria_nombre || '—'}</p>
</div>
<p class="font-bold ${colorAmt} text-sm flex-shrink-0">${signo}${formatMoney(mov.monto)}</p>
</div>`;
}).join('');
}
document.getElementById('ui-donut-chart').innerHTML = svgHtml;
document.getElementById('ui-donut-legend').innerHTML = legendHtml;
} else {
document.getElementById('ui-donut-chart').innerHTML = `<div class="flex h-full items-center justify-center text-sm text-on-surface-variant">Sin gastos registrados</div>`;
document.getElementById('ui-donut-legend').innerHTML = '';
}
// Donut
if (data.grafico?.length) {
const total = data.grafico.reduce((s, c) => s + parseFloat(c.total), 0);
let svgCircles = '', legend = '', offset = 0;
data.grafico.forEach(cat => {
const pct = (parseFloat(cat.total) / total) * 100;
svgCircles += `<circle cx="18" cy="18" fill="none" r="16"
stroke="${cat.color_hex}" stroke-dasharray="${pct} ${100 - pct}"
stroke-dashoffset="${-offset}" stroke-linecap="round" stroke-width="4"/>`;
legend += `
<div class="flex items-center justify-between py-2 px-3 bg-surface-container-lowest rounded-xl">
<div class="flex items-center gap-2">
<div class="w-2.5 h-2.5 rounded-full flex-shrink-0" style="background:${cat.color_hex}"></div>
<span class="text-sm font-semibold truncate">${cat.nombre}</span>
</div>
<span class="text-xs text-on-surface-variant font-medium ml-2">${pct.toFixed(1)}%</span>
</div>`;
offset += pct;
});
document.getElementById('ui-donut-chart').innerHTML = `
<svg class="w-full h-full -rotate-90" viewBox="0 0 36 36">${svgCircles}</svg>
<div class="absolute inset-0 flex flex-col items-center justify-center">
<span class="text-xl font-bold">${formatMoney(total)}</span>
<span class="text-[9px] uppercase tracking-widest text-on-surface-variant font-bold">Gastos</span>
</div>`;
document.getElementById('ui-donut-legend').innerHTML = `<div class="space-y-1.5">${legend}</div>`;
} else {
document.getElementById('ui-donut-chart').innerHTML = `
<svg class="w-full h-full -rotate-90" viewBox="0 0 36 36">
<circle cx="18" cy="18" fill="none" r="16" stroke="#e1e3e2" stroke-dasharray="100,0" stroke-width="4"/>
</svg>
<div class="absolute inset-0 flex flex-col items-center justify-center">
<span class="text-xs text-on-surface-variant text-center px-4">Sin gastos</span>
</div>`;
document.getElementById('ui-donut-legend').innerHTML = '';
}
} catch (e) {
console.error('Error cargando el dashboard:', e);
console.error('Dashboard error:', e);
}
});
function formatMoney(n) {
return '$' + parseFloat(n).toLocaleString('es-AR', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}