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
+72 -37
View File
@@ -1,48 +1,83 @@
# Plan de Desarrollo: Sistema de Gestión de Gastos y Pagos (rm-PAGOs) # Plan de Desarrollo — rm-PAGOs (Quiet Wealth)
## Contexto y Arquitectura ## Stack
- **Frontend**: Diseños generados por Stitch (HTML, CSS Vainilla, JS). Diseño moderno "Quiet Wealth / Financial Sanctuary". | Capa | Tecnología |
- **Backend**: PHP 8.2 (procesamiento ligero, para máxima compatibilidad con el servidor de destino). |------|-----------|
- **Base de Datos**: SQLite3 (liviana, portátil y soportada nativamente por PHP, sin necesidad de un servidor de BD separado). | Frontend | HTML + Tailwind CSS (CDN local) + Vanilla JS |
- **Despliegue**: Servidor Proxmox LXC `srvv-nginx-rm` (Debian 12, NGINX 1.22.1, HTTPS activo). | Backend | PHP 8.2 (FastCGI) |
| Base de datos | SQLite3 (PDO) |
| Auth | Sesión PHP nativa |
| Servidor local | Docker Compose: NGINX + PHP-FPM |
--- ---
## Fases de Ejecución ## Estado actual por pantalla
### Fase 1: Estructuración y Maquetación (Frontend Static) ### ✅ login.html — Completa
**Objetivo:** Tener todo el diseño web unificado y navegable estáticamente. - Formulario propio con diseño Quiet Wealth (sin popup del browser)
1. Crear la estructura de la aplicación DENTRO de la carpeta `pagos/` (que es la que se usará para el despliegue). `pagos/index.html`, `pagos/css/`, `pagos/api/`, `pagos/db/`. - Validación de credenciales contra `config.php` (gitignoreado)
2. Exportar las pantallas generadas en Stitch (Dashboard, Nuevo Gasto, Historial) y acomodarlas como plantillas base (HTML). - Redirección automática si ya hay sesión activa
3. Configurar el archivo CSS general con las variables del sistema de diseño (Lumina Finance).
4. Enlazar la navegación básica con Javascript o anclas entre pantallas.
### Fase 2: Motor de Base de Datos y Backend (PHP/SQLite) ### ✅ index.html — Dashboard
**Objetivo:** Persistir la información y crear la lógica detrás de las pantallas. - Balance total dinámico desde SQLite
1. Diseñar el esquema de base de datos SQLite (tablas básicas como `movimientos` y `categorias`). - Gráfico donut de distribución de gastos por categoría (SVG puro)
2. Crear un archivo clase base en PHP (`db.php`) para conexión y operaciones CRUD. - Últimos 5 movimientos con ícono, descripción y monto
3. Crear los endpoints/controladores PHP (`api/movimientos.php`) que manejen operaciones GET, POST, PUT y DELETE. - Navegación inferior funcional
4. Ajustar los scripts de inicialización de la BD.
### Fase 3: Integración Frontend ↔ Backend (Dinámica) ### ✅ add.html — Nuevo movimiento
**Objetivo:** Darle vida a las pantallas usando Javascript interactuando con PHP. - Selector Gasto / Ingreso
1. **Nuevo Movimiento**: Enlazar el formulario "Agregar" usando Fetch/AJAX (Vanilla JS) para enviarle los datos al backend de forma transparente sin refrescar. - Numpad táctil + soporte teclado físico (desktop)
2. **Dashboard**: - Categorías cargadas dinámicamente desde la API
- Calcular saldo dinámico, sumatorias de gastos del mes, etc., usando SQL. - Campo fecha (default: hoy) y notas opcionales
- Enlazar librería de gráficos (por ejemplo Chart.js) para pintar la dona de categorías leyendo desde la base de datos. - POST a la API con redirect al dashboard al guardar
3. **Historial**: Renderizar la lista de últimos movimientos desde SQLite y añadir sistema de borrado.
### Fase 4: Despliegue Configurado (Producción) ### ✅ historial.html — Historial completo
**Objetivo:** Llevar el sistema al servidor `srvv-nginx-rm` para uso real. - Lista de todos los movimientos agrupados por mes
1. **Seguridad Básica:** Añadir una pantalla de Login o protección sencilla (ya que son datos financieros personales) utilizando las sesiones nativas de PHP o Basic Auth en NGINX. - Botón eliminar con modal de confirmación
2. **Configuración del Servidor:** - Toast de confirmación tras eliminar
- Crear un subdirectorio o alias en NGINX (ejemplo `rmonla.duckdns.org/pagos`).
- Clonar o actualizar por deploy key en `/var/www/pagos`. ### ✅ API (movimientos.php)
- Asegurar permisos de escritura a la carpeta `db/` para que PHP (usuario `www-data`) pueda modifciar el archivo `.sqlite`. - GET dashboard: balance + últimos 5 + donut + categorías
3. Pruebas End-to-End directas desde el teléfono. - GET ?view=historial: todos los movimientos
- POST: nuevo movimiento
- DELETE: eliminar por ID
### ✅ Infraestructura Docker
- NGINX + PHP-FPM en contenedores separados
- Tailwind CDN descargado en build y servido localmente
- Auth por sesión PHP (sin Basic Auth del browser)
- Script `_app --status / --start / --stop`
--- ---
## Roadmap de Próximos Pasos (De Inmediato) ## Pendientes / En progreso
1. Iniciar con la **Fase 1**, configurando estructura base (`index.html`, `style.css`) volcando el HTML de Stitch en un index.
2. Crear un script rápido de inicialización de SQLite. ### ✅ UI — Responsive desktop
- [x] Sidebar de navegación fijo en pantallas ≥768px (con logout)
- [x] Dashboard en 2 columnas en desktop (balance+movimientos | donut)
- [x] add.html: formulario + numpad en 2 columnas en desktop
- [x] historial.html: ancho extendido en desktop, header con botón Nuevo
- [x] Soporte teclado físico en numpad (0-9, punto, Backspace, Enter)
### 🔶 UI — Polish
- [ ] Fecha en formato legible en movimientos recientes del dashboard
- [ ] Indicador de mes actual dinámico (hoy hardcodeado como "Octubre 2023")
- [ ] El "+12.4% este mes" del balance card → calculado real
- [ ] Estado vacío más visual en historial (primer uso)
### 🔶 Funcionalidades faltantes
- [ ] Editar movimiento existente
- [ ] Filtro por categoría / mes en historial
- [ ] Resumen mensual (ingresos vs gastos del mes)
### ⏳ Fase futura — CLI
- Interfaz de línea de comandos que replique las operaciones de la GUI
- Comandos: `pagos add`, `pagos list`, `pagos delete <id>`, `pagos balance`
- Mismo SQLite como backend → datos compartidos con la web
---
## Notas de arquitectura
- Credenciales en `pagos/api/config.php` (gitignoreado, ver `config.example.php`)
- SQLite en `pagos/db/finanzas.sqlite` (gitignoreado)
- Tailwind CDN se descarga en `docker build` y se copia al volumen en el entrypoint
+148 -144
View File
@@ -1,180 +1,184 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="es">
<html class="light" lang="es"><head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/> <meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Nuevo Movimiento - Sanctuary</title> <title>Nuevo Movimiento — Quiet Wealth</title>
<script src="js/tailwind.cdn.js"></script> <script src="js/tailwind.cdn.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@300;400;500;600;700;800&display=swap" rel="stylesheet"/> <link href="https://fonts.googleapis.com/css2?family=Manrope:wght@300;400;500;600;700;800&display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/> <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
<script id="tailwind-config"> <script>
tailwind.config = { tailwind.config = {
darkMode: "class",
theme: { theme: {
extend: { extend: {
"colors": { colors: {
"surface-container-high": "#e6e9e8", "background":"#f8faf9","primary-container":"#006a6a","surface-container-lowest":"#ffffff",
"on-secondary": "#ffffff", "on-background":"#191c1c","surface-container-high":"#e6e9e8","secondary-container":"#cde6d6",
"on-primary-fixed-variant": "#004f4f", "surface-container-highest":"#e1e3e2","surface-container-low":"#f2f4f3","on-primary":"#ffffff",
"on-primary-container": "#97e7e6", "tertiary":"#264b5c","on-surface":"#191c1c","on-surface-variant":"#3e4948",
"on-background": "#191c1c", "outline-variant":"#bec9c8","on-error":"#ffffff","secondary-fixed-dim":"#b4ccbd",
"secondary-fixed-dim": "#b4ccbd", "surface-variant":"#e1e3e2","outline":"#6e7979","surface-container":"#eceeed",
"inverse-surface": "#2e3131", "error":"#ba1a1a","secondary":"#4d6357","surface":"#f8faf9","error-container":"#ffdad6",
"surface-container": "#eceeed", "primary":"#005050","on-secondary":"#ffffff","primary-fixed":"#a0f0f0"
"surface-tint": "#006a6a",
"outline-variant": "#bec9c8",
"on-surface": "#191c1c",
"on-primary": "#ffffff",
"tertiary": "#264b5c",
"primary-fixed-dim": "#84d4d3",
"error-container": "#ffdad6",
"surface-bright": "#f8faf9",
"tertiary-fixed-dim": "#a7cce1",
"on-error": "#ffffff",
"outline": "#6e7979",
"background": "#f8faf9",
"secondary-fixed": "#d0e8d9",
"primary-fixed": "#a0f0f0",
"on-secondary-container": "#51685b",
"primary": "#005050",
"on-primary-fixed": "#002020",
"surface-container-highest": "#e1e3e2",
"surface": "#f8faf9",
"on-tertiary-container": "#b9def3",
"inverse-primary": "#84d4d3",
"on-tertiary-fixed": "#001e2b",
"on-surface-variant": "#3e4948",
"error": "#ba1a1a",
"surface-variant": "#e1e3e2",
"on-error-container": "#93000a",
"inverse-on-surface": "#eff1f0",
"tertiary-fixed": "#c2e8fd",
"tertiary-container": "#3f6375",
"secondary": "#4d6357",
"on-secondary-fixed-variant": "#364b40",
"on-secondary-fixed": "#0a1f16",
"secondary-container": "#cde6d6",
"on-tertiary": "#ffffff",
"surface-dim": "#d8dada",
"primary-container": "#006a6a",
"on-tertiary-fixed-variant": "#264b5c",
"surface-container-lowest": "#ffffff",
"surface-container-low": "#f2f4f3"
}, },
"borderRadius": { fontFamily: { headline:["Manrope"], body:["Manrope"] }
"DEFAULT": "0.25rem", }
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
},
"fontFamily": {
"headline": ["Manrope"],
"body": ["Manrope"],
"label": ["Manrope"]
} }
},
},
} }
</script> </script>
<style> <style>
body { font-family:'Manrope',sans-serif; } body { font-family:'Manrope',sans-serif; }
.material-symbols-outlined { .material-symbols-outlined { font-variation-settings:'FILL' 0,'wght' 400,'GRAD' 0,'opsz' 24; }
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; .numpad-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:0.75rem; }
}
.numpad-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
}
.prosperity-glow {
background: radial-gradient(circle at center, rgba(160, 240, 240, 0.15) 0%, transparent 70%);
}
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style> </style>
</head> </head>
<body class="bg-background text-on-background min-h-screen flex flex-col"> <body class="bg-background text-on-background min-h-screen">
<div class="max-w-md mx-auto w-full flex flex-col flex-1 relative">
<!-- Top AppBar - Transactional Context (Shell Suppressed per mandate) --> <!-- ═══════════ SIDEBAR (desktop) ═══════════ -->
<header class="bg-[#f8faf9] dark:bg-[#191c1c] w-full top-0 px-6 py-4 flex justify-between items-center bg-transparent"> <aside class="hidden md:flex flex-col fixed left-0 top-0 h-screen w-56 bg-surface-container-lowest border-r border-outline-variant z-40 py-7 px-4 gap-1">
<div class="flex items-center gap-4"> <div class="flex items-center gap-2 px-2 mb-7">
<button class="hover:opacity-80 transition-opacity p-2 -ml-2" onclick="window.location.href='index.html'"> <span class="material-symbols-outlined text-[#006a6a] text-2xl" style="font-variation-settings:'FILL' 1;">account_balance_wallet</span>
<span class="material-symbols-outlined text-primary dark:text-[#a0f0f0]" data-icon="chevron_left">chevron_left</span> <span class="font-extrabold text-[#006a6a] tracking-tight text-lg">Quiet Wealth</span>
</div>
<a href="index.html" class="flex items-center gap-3 px-3 py-2.5 rounded-xl text-on-surface-variant hover:bg-surface-container text-sm font-medium transition-colors">
<span class="material-symbols-outlined text-[20px]">home</span>Dashboard
</a>
<a href="historial.html" class="flex items-center gap-3 px-3 py-2.5 rounded-xl text-on-surface-variant hover:bg-surface-container text-sm font-medium transition-colors">
<span class="material-symbols-outlined text-[20px]">history</span>Historial
</a>
<a href="add.html" class="flex items-center gap-3 px-3 py-2.5 rounded-xl bg-[#005050]/10 text-primary font-bold text-sm">
<span class="material-symbols-outlined text-[20px]" style="font-variation-settings:'FILL' 1;">add_circle</span>Nuevo movimiento
</a>
<div class="mt-auto">
<button onclick="fetch('api/login.php?action=logout').then(()=>location.href='login.html')"
class="flex items-center gap-3 px-3 py-2.5 rounded-xl text-on-surface-variant hover:bg-surface-container text-sm font-medium transition-colors w-full">
<span class="material-symbols-outlined text-[20px]">logout</span>Salir
</button> </button>
<h1 class="font-['Manrope'] headline-sm text-[#191c1c] dark:text-[#f8faf9] font-bold">Nuevo Movimiento</h1>
</div> </div>
<div class="w-10"></div> <!-- Spacer for center-ish balance --> </aside>
<!-- ═══════════ CONTENIDO ═══════════ -->
<div class="md:ml-56 min-h-screen flex flex-col">
<!-- Header -->
<header class="bg-background sticky top-0 z-30 border-b border-outline-variant/30 px-6 md:px-8 py-4 flex items-center gap-4">
<button onclick="window.location.href='index.html'" class="p-2 -ml-2 hover:bg-surface-container rounded-full transition-colors">
<span class="material-symbols-outlined text-primary">arrow_back</span>
</button>
<h1 class="text-xl font-bold text-on-surface">Nuevo Movimiento</h1>
</header> </header>
<main class="flex-1 px-6 pb-32 max-w-md mx-auto w-full overflow-y-auto">
<!-- Type Selector (Segmented Control) --> <!-- Form: 2 columnas en desktop -->
<div class="mt-4 p-1.5 bg-surface-container-low rounded-xl flex items-center mb-8" id="ui-tipo-selector"> <main class="flex-1 px-6 md:px-8 py-6 pb-28 md:pb-10">
<button id="btn-gasto" data-tipo="gasto" class="flex-1 py-2.5 text-sm font-bold bg-surface-container-lowest text-primary rounded-lg shadow-sm">Gasto</button> <div class="max-w-4xl mx-auto md:grid md:grid-cols-2 md:gap-10 md:items-start space-y-6 md:space-y-0">
<button id="btn-ingreso" data-tipo="ingreso" class="flex-1 py-2.5 text-sm font-semibold text-on-surface-variant hover:opacity-80 transition-opacity">Ingreso</button>
<!-- COLUMNA IZQUIERDA: campos del formulario -->
<div class="space-y-6">
<!-- Tipo selector -->
<div class="p-1.5 bg-surface-container-low rounded-xl flex items-center" id="ui-tipo-selector">
<button id="btn-gasto" data-tipo="gasto"
class="flex-1 py-2.5 text-sm font-bold bg-surface-container-lowest text-primary rounded-lg shadow-sm transition-all">
Gasto
</button>
<button id="btn-ingreso" data-tipo="ingreso"
class="flex-1 py-2.5 text-sm font-semibold text-on-surface-variant hover:opacity-80 transition-opacity">
Ingreso
</button>
</div> </div>
<!-- Hero Amount Input -->
<div class="relative text-center mb-10 prosperity-glow py-8 rounded-[2rem]"> <!-- Monto (visible en mobile aquí; en desktop muestra en col derecha) -->
<label class="block text-xs uppercase tracking-[0.15em] font-bold text-on-surface-variant mb-2">Monto del Movimiento</label> <div class="md:hidden relative text-center py-8 bg-surface-container-low rounded-[2rem]">
<label class="block text-xs uppercase tracking-[0.15em] font-bold text-on-surface-variant mb-2">Monto</label>
<div class="flex items-baseline justify-center gap-1">
<span class="text-3xl font-light text-on-surface-variant">$</span>
<span id="ui-monto-mobile" class="text-6xl font-extrabold tracking-tight text-primary">0</span>
</div>
</div>
<!-- Categoría -->
<div class="space-y-2">
<label class="text-[11px] uppercase tracking-widest font-bold text-on-surface-variant">Categoría</label>
<div class="relative">
<select id="ui-categoria" class="appearance-none bg-surface-container-lowest px-4 py-3.5 pr-10 rounded-xl w-full font-semibold text-on-surface focus:outline-none focus:ring-2 focus:ring-primary/30 text-sm">
<option value="">Cargando…</option>
</select>
<span class="material-symbols-outlined absolute right-3 top-1/2 -translate-y-1/2 text-outline pointer-events-none text-[18px]">expand_more</span>
</div>
</div>
<!-- Fecha y Notas -->
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<label class="text-[11px] uppercase tracking-widest font-bold text-on-surface-variant">Fecha</label>
<div class="bg-surface-container-lowest px-4 py-3 rounded-xl flex items-center gap-2">
<span class="material-symbols-outlined text-primary text-[18px]">calendar_today</span>
<input type="date" id="ui-fecha" class="bg-transparent text-sm font-semibold w-full focus:outline-none"/>
</div>
</div>
<div class="space-y-2">
<label class="text-[11px] uppercase tracking-widest font-bold text-on-surface-variant">Notas</label>
<div class="bg-surface-container-lowest px-4 py-3 rounded-xl flex items-center gap-2">
<span class="material-symbols-outlined text-outline text-[18px]">edit_note</span>
<input type="text" id="ui-notas" placeholder="Opcional…" class="bg-transparent text-sm font-medium w-full focus:outline-none placeholder-outline"/>
</div>
</div>
</div>
<!-- Guardar (desktop: dentro de la columna izquierda) -->
<button id="btn-save"
class="hidden md:flex w-full h-14 bg-gradient-to-br from-primary to-primary-container text-white rounded-xl font-bold text-base shadow-lg shadow-primary/20 active:scale-[0.98] transition-transform items-center justify-center gap-2">
<span>Guardar Movimiento</span>
<span class="material-symbols-outlined text-[20px]" style="font-variation-settings:'FILL' 1;">check_circle</span>
</button>
</div>
<!-- COLUMNA DERECHA: monto + numpad -->
<div class="space-y-5">
<!-- Monto display (desktop) -->
<div class="hidden md:block text-center py-8 bg-surface-container-low rounded-[2rem]">
<label class="block text-xs uppercase tracking-[0.15em] font-bold text-on-surface-variant mb-2">Monto</label>
<div class="flex items-baseline justify-center gap-1"> <div class="flex items-baseline justify-center gap-1">
<span class="text-3xl font-light text-on-surface-variant">$</span> <span class="text-3xl font-light text-on-surface-variant">$</span>
<span id="ui-monto" class="text-6xl font-extrabold tracking-tight text-primary">0</span> <span id="ui-monto" class="text-6xl font-extrabold tracking-tight text-primary">0</span>
</div> </div>
</div> </div>
<!-- Modern Form Fields Grid -->
<div class="space-y-6"> <!-- Numpad -->
<!-- Category Dropdown --> <div id="ui-numpad" class="numpad-grid bg-surface-container-lowest rounded-[2rem] p-4">
<div class="space-y-2"> <button class="h-14 rounded-xl flex items-center justify-center text-2xl font-semibold text-on-surface hover:bg-surface-container-high transition-colors">1</button>
<label class="text-[11px] uppercase tracking-widest font-bold text-on-surface-variant px-1">Categoría</label> <button class="h-14 rounded-xl flex items-center justify-center text-2xl font-semibold text-on-surface hover:bg-surface-container-high transition-colors">2</button>
<select id="ui-categoria" class="bg-surface-container-lowest p-4 rounded-xl w-full font-bold text-on-surface focus:outline-none"> <button class="h-14 rounded-xl flex items-center justify-center text-2xl font-semibold text-on-surface hover:bg-surface-container-high transition-colors">3</button>
<option value="">Cargando...</option> <button class="h-14 rounded-xl flex items-center justify-center text-2xl font-semibold text-on-surface hover:bg-surface-container-high transition-colors">4</button>
</select> <button class="h-14 rounded-xl flex items-center justify-center text-2xl font-semibold text-on-surface hover:bg-surface-container-high transition-colors">5</button>
</div> <button class="h-14 rounded-xl flex items-center justify-center text-2xl font-semibold text-on-surface hover:bg-surface-container-high transition-colors">6</button>
<!-- Date & Notes Grid --> <button class="h-14 rounded-xl flex items-center justify-center text-2xl font-semibold text-on-surface hover:bg-surface-container-high transition-colors">7</button>
<div class="grid grid-cols-2 gap-4"> <button class="h-14 rounded-xl flex items-center justify-center text-2xl font-semibold text-on-surface hover:bg-surface-container-high transition-colors">8</button>
<div class="space-y-2"> <button class="h-14 rounded-xl flex items-center justify-center text-2xl font-semibold text-on-surface hover:bg-surface-container-high transition-colors">9</button>
<label class="text-[11px] uppercase tracking-widest font-bold text-on-surface-variant px-1">Fecha</label> <button class="h-14 rounded-xl flex items-center justify-center text-2xl font-semibold text-on-surface hover:bg-surface-container-high transition-colors">.</button>
<div class="bg-surface-container-highest/40 p-4 rounded-xl flex items-center gap-3"> <button class="h-14 rounded-xl flex items-center justify-center text-2xl font-semibold text-on-surface hover:bg-surface-container-high transition-colors">0</button>
<span class="material-symbols-outlined text-primary text-sm" data-icon="calendar_today">calendar_today</span> <button class="h-14 rounded-xl flex items-center justify-center text-on-surface hover:bg-surface-container-high transition-colors">
<input type="date" id="ui-fecha" class="bg-transparent text-sm font-bold w-full focus:outline-none"/>
</div>
</div>
<div class="space-y-2">
<label class="text-[11px] uppercase tracking-widest font-bold text-on-surface-variant px-1">Notas</label>
<div class="bg-surface-container-highest/40 p-4 rounded-xl flex items-center gap-3">
<span class="material-symbols-outlined text-outline text-sm" data-icon="edit_note">edit_note</span>
<input type="text" id="ui-notas" placeholder="Opcional..." class="bg-transparent text-sm font-medium w-full focus:outline-none placeholder-outline"/>
</div>
</div>
</div>
</div>
<!-- Integrated Custom Numpad -->
<div id="ui-numpad" class="mt-12 numpad-grid">
<button class="h-16 rounded-xl flex items-center justify-center text-2xl font-semibold text-on-surface hover:bg-surface-container-high transition-colors">1</button>
<button class="h-16 rounded-xl flex items-center justify-center text-2xl font-semibold text-on-surface hover:bg-surface-container-high transition-colors">2</button>
<button class="h-16 rounded-xl flex items-center justify-center text-2xl font-semibold text-on-surface hover:bg-surface-container-high transition-colors">3</button>
<button class="h-16 rounded-xl flex items-center justify-center text-2xl font-semibold text-on-surface hover:bg-surface-container-high transition-colors">4</button>
<button class="h-16 rounded-xl flex items-center justify-center text-2xl font-semibold text-on-surface hover:bg-surface-container-high transition-colors">5</button>
<button class="h-16 rounded-xl flex items-center justify-center text-2xl font-semibold text-on-surface hover:bg-surface-container-high transition-colors">6</button>
<button class="h-16 rounded-xl flex items-center justify-center text-2xl font-semibold text-on-surface hover:bg-surface-container-high transition-colors">7</button>
<button class="h-16 rounded-xl flex items-center justify-center text-2xl font-semibold text-on-surface hover:bg-surface-container-high transition-colors">8</button>
<button class="h-16 rounded-xl flex items-center justify-center text-2xl font-semibold text-on-surface hover:bg-surface-container-high transition-colors">9</button>
<button class="h-16 rounded-xl flex items-center justify-center text-2xl font-semibold text-on-surface hover:bg-surface-container-high transition-colors">.</button>
<button class="h-16 rounded-xl flex items-center justify-center text-2xl font-semibold text-on-surface hover:bg-surface-container-high transition-colors">0</button>
<button class="h-16 rounded-xl flex items-center justify-center text-on-surface hover:bg-surface-container-high transition-colors">
<span class="material-symbols-outlined" data-icon="backspace">backspace</span> <span class="material-symbols-outlined" data-icon="backspace">backspace</span>
</button> </button>
</div> </div>
</div>
</div>
</main> </main>
<!-- Bottom Action Area -->
<div class="fixed bottom-0 left-0 w-full p-6 bg-gradient-to-t from-background via-background/95 to-transparent"> <!-- Guardar (mobile: fixed bottom) -->
<button id="btn-save" class="w-full h-16 bg-gradient-to-br from-[#005050] to-[#006a6a] text-white rounded-xl font-bold text-lg shadow-[0px_12px_32px_rgba(0,80,80,0.2)] active:scale-[0.98] transition-transform flex items-center justify-center gap-3"> <div class="md:hidden fixed bottom-0 inset-x-0 p-4 bg-gradient-to-t from-background via-background/95 to-transparent">
<button id="btn-save-mobile"
class="w-full h-14 bg-gradient-to-br from-primary to-primary-container text-white rounded-xl font-bold text-base shadow-lg shadow-primary/20 active:scale-[0.98] transition-transform flex items-center justify-center gap-2">
<span>Guardar Movimiento</span> <span>Guardar Movimiento</span>
<span class="material-symbols-outlined" data-icon="check_circle">check_circle</span> <span class="material-symbols-outlined text-[20px]" style="font-variation-settings:'FILL' 1;">check_circle</span>
</button> </button>
</div> </div>
</div> </div>
<script src="js/ui.js"></script> <script src="js/ui.js"></script>
<script src="js/add.js"></script> <script src="js/add.js"></script>
</body></html> </body>
</html>
+2
View File
@@ -1,6 +1,8 @@
<?php <?php
require_once __DIR__ . '/config.php'; require_once __DIR__ . '/config.php';
ini_set('session.cookie_httponly', '1');
ini_set('session.cookie_samesite', 'Lax');
session_start(); session_start();
if (!isset($_SESSION['qw_auth'])) { if (!isset($_SESSION['qw_auth'])) {
+3
View File
@@ -1,5 +1,8 @@
<?php <?php
require_once __DIR__ . '/config.php'; require_once __DIR__ . '/config.php';
ini_set('session.cookie_httponly', '1');
ini_set('session.cookie_samesite', 'Lax');
session_start(); session_start();
header('Content-Type: application/json'); header('Content-Type: application/json');
+69 -85
View File
@@ -3,120 +3,104 @@
<head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/> <meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Historial - Quiet Wealth</title> <title>Historial Quiet Wealth</title>
<script src="js/tailwind.cdn.js"></script> <script src="js/tailwind.cdn.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet"/> <link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/> <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
<script id="tailwind-config"> <script>
tailwind.config = { tailwind.config = {
darkMode: "class",
theme: { theme: {
extend: { extend: {
"colors": { colors: {
"background": "#f8faf9", "background":"#f8faf9","primary-container":"#006a6a","surface-container-lowest":"#ffffff",
"primary-container": "#006a6a", "on-background":"#191c1c","surface-container-high":"#e6e9e8","surface-container-highest":"#e1e3e2",
"surface-container-lowest": "#ffffff", "surface-container-low":"#f2f4f3","on-primary":"#ffffff","on-surface":"#191c1c",
"on-primary-fixed": "#002020", "on-surface-variant":"#3e4948","outline-variant":"#bec9c8","on-error":"#ffffff",
"on-background": "#191c1c", "surface-variant":"#e1e3e2","outline":"#6e7979","surface-container":"#eceeed",
"secondary-fixed": "#d0e8d9", "error":"#ba1a1a","secondary":"#4d6357","surface":"#f8faf9","error-container":"#ffdad6",
"surface-container-high": "#e6e9e8", "primary":"#005050","on-secondary":"#ffffff"
"primary-fixed-dim": "#84d4d3",
"on-error-container": "#93000a",
"secondary-container": "#cde6d6",
"surface-container-highest": "#e1e3e2",
"inverse-surface": "#2e3131",
"surface-container-low": "#f2f4f3",
"on-tertiary-fixed": "#001e2b",
"tertiary-fixed": "#c2e8fd",
"on-primary-fixed-variant": "#004f4f",
"on-secondary-container": "#51685b",
"on-primary": "#ffffff",
"tertiary-fixed-dim": "#a7cce1",
"inverse-primary": "#84d4d3",
"tertiary": "#264b5c",
"on-surface": "#191c1c",
"on-tertiary-container": "#b9def3",
"on-surface-variant": "#3e4948",
"surface-dim": "#d8dada",
"outline-variant": "#bec9c8",
"inverse-on-surface": "#eff1f0",
"on-error": "#ffffff",
"on-tertiary-fixed-variant": "#264b5c",
"on-tertiary": "#ffffff",
"tertiary-container": "#3f6375",
"on-secondary-fixed-variant": "#364b40",
"secondary-fixed-dim": "#b4ccbd",
"surface-variant": "#e1e3e2",
"outline": "#6e7979",
"surface-container": "#eceeed",
"surface-bright": "#f8faf9",
"error": "#ba1a1a",
"surface-tint": "#006a6a",
"secondary": "#4d6357",
"surface": "#f8faf9",
"on-secondary-fixed": "#0a1f16",
"error-container": "#ffdad6",
"primary": "#005050",
"on-primary-container": "#97e7e6",
"on-secondary": "#ffffff",
"primary-fixed": "#a0f0f0"
}, },
"borderRadius": { fontFamily: { headline:["Manrope"], body:["Manrope"] }
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
},
"fontFamily": {
"headline": ["Manrope"],
"body": ["Manrope"],
"label": ["Manrope"]
}
} }
} }
} }
</script> </script>
<style> <style>
body { font-family: 'Manrope', sans-serif; min-height: max(884px, 100dvh); } body { font-family:'Manrope',sans-serif; }
.material-symbols-outlined { font-variation-settings:'FILL' 0,'wght' 400,'GRAD' 0,'opsz' 24; } .material-symbols-outlined { font-variation-settings:'FILL' 0,'wght' 400,'GRAD' 0,'opsz' 24; }
</style> </style>
</head> </head>
<body class="bg-background text-on-background min-h-screen pb-24"> <body class="bg-background text-on-background min-h-screen">
<div class="max-w-md mx-auto relative">
<!-- ═══════════ SIDEBAR (desktop) ═══════════ -->
<aside class="hidden md:flex flex-col fixed left-0 top-0 h-screen w-56 bg-surface-container-lowest border-r border-outline-variant z-40 py-7 px-4 gap-1">
<div class="flex items-center gap-2 px-2 mb-7">
<span class="material-symbols-outlined text-[#006a6a] text-2xl" style="font-variation-settings:'FILL' 1;">account_balance_wallet</span>
<span class="font-extrabold text-[#006a6a] tracking-tight text-lg">Quiet Wealth</span>
</div>
<a href="index.html" class="flex items-center gap-3 px-3 py-2.5 rounded-xl text-on-surface-variant hover:bg-surface-container text-sm font-medium transition-colors">
<span class="material-symbols-outlined text-[20px]">home</span>Dashboard
</a>
<a href="historial.html" class="flex items-center gap-3 px-3 py-2.5 rounded-xl bg-[#005050]/10 text-primary font-bold text-sm">
<span class="material-symbols-outlined text-[20px]" style="font-variation-settings:'FILL' 1;">history</span>Historial
</a>
<a href="add.html" class="flex items-center gap-3 px-3 py-2.5 rounded-xl text-on-surface-variant hover:bg-surface-container text-sm font-medium transition-colors">
<span class="material-symbols-outlined text-[20px]">add_circle</span>Nuevo movimiento
</a>
<div class="mt-auto">
<button onclick="fetch('api/login.php?action=logout').then(()=>location.href='login.html')"
class="flex items-center gap-3 px-3 py-2.5 rounded-xl text-on-surface-variant hover:bg-surface-container text-sm font-medium transition-colors w-full">
<span class="material-symbols-outlined text-[20px]">logout</span>Salir
</button>
</div>
</aside>
<!-- ═══════════ CONTENIDO ═══════════ -->
<div class="md:ml-56 min-h-screen flex flex-col">
<!-- Header --> <!-- Header -->
<header class="bg-[#f8faf9] sticky top-0 z-40 px-6 py-4 flex items-center gap-4"> <header class="bg-background sticky top-0 z-30 border-b border-outline-variant/30 px-6 md:px-8 py-4 flex items-center justify-between">
<button class="p-2 -ml-2 hover:bg-surface-container rounded-full transition-colors" onclick="window.location.href='index.html'"> <div class="flex items-center gap-3">
<button onclick="window.location.href='index.html'" class="md:hidden p-2 -ml-2 hover:bg-surface-container rounded-full transition-colors">
<span class="material-symbols-outlined text-primary">arrow_back</span> <span class="material-symbols-outlined text-primary">arrow_back</span>
</button> </button>
<h1 class="text-xl font-bold text-[#006a6a]">Historial</h1> <h1 class="text-xl font-bold text-on-surface">Historial</h1>
</div>
<a href="add.html" class="flex items-center gap-1.5 px-4 py-2 bg-primary text-white rounded-xl font-bold text-sm hover:bg-primary-container transition-colors shadow-sm">
<span class="material-symbols-outlined text-[18px]">add</span>
<span class="hidden sm:inline">Nuevo</span>
</a>
</header> </header>
<main class="px-6 mt-4 space-y-2"> <!-- Lista -->
<!-- Skeleton / estado inicial --> <main class="flex-1 px-6 md:px-8 py-6 pb-24 md:pb-8">
<div class="max-w-3xl mx-auto md:mx-0">
<!-- Loading -->
<div id="ui-loading" class="py-16 flex flex-col items-center gap-3 text-on-surface-variant"> <div id="ui-loading" class="py-16 flex flex-col items-center gap-3 text-on-surface-variant">
<span class="material-symbols-outlined text-4xl opacity-40">hourglass_top</span> <span class="material-symbols-outlined text-4xl opacity-40">hourglass_top</span>
<p class="text-sm">Cargando movimientos...</p> <p class="text-sm">Cargando movimientos</p>
</div> </div>
<div id="ui-empty" class="hidden py-16 flex flex-col items-center gap-3 text-on-surface-variant">
<!-- Vacío -->
<div id="ui-empty" class="hidden py-16 flex flex-col items-center gap-4 text-on-surface-variant">
<span class="material-symbols-outlined text-5xl opacity-30">receipt_long</span> <span class="material-symbols-outlined text-5xl opacity-30">receipt_long</span>
<p class="text-sm font-medium">Sin movimientos registrados</p> <div class="text-center">
<button onclick="window.location.href='add.html'" class="mt-2 px-5 py-2.5 bg-primary text-white rounded-xl text-sm font-bold"> <p class="font-semibold text-on-surface">Sin movimientos aún</p>
Agregar primero <p class="text-sm mt-1">Registrá tu primer movimiento para empezar</p>
</button> </div>
<a href="add.html" class="mt-2 px-6 py-2.5 bg-primary text-white rounded-xl text-sm font-bold shadow-sm">
Agregar ahora
</a>
</div>
<!-- Lista de movimientos -->
<div id="ui-lista" class="hidden space-y-6"></div>
</div> </div>
<div id="ui-lista" class="space-y-1 hidden"></div>
</main> </main>
<!-- FAB agregar -->
<div class="fixed bottom-6 right-6 z-50 md:right-[calc(50%-176px)]">
<button onclick="window.location.href='add.html'"
class="w-14 h-14 bg-primary text-white rounded-2xl shadow-lg shadow-primary/30 flex items-center justify-center active:scale-90 transition-transform">
<span class="material-symbols-outlined">add</span>
</button>
</div> </div>
</div>
<script src="js/ui.js"></script> <script src="js/ui.js"></script>
<script src="js/historial.js"></script> <script src="js/historial.js"></script>
</body> </body>
+137 -189
View File
@@ -1,235 +1,183 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="es">
<html lang="en"><head> <head>
<meta charset="utf-8"/> <meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/> <meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Quiet Wealth — Dashboard</title>
<script src="js/tailwind.cdn.js"></script> <script src="js/tailwind.cdn.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet"/> <link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/> <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
<script id="tailwind-config"> <script>
tailwind.config = { tailwind.config = {
darkMode: "class",
theme: { theme: {
extend: { extend: {
"colors": { colors: {
"background": "#f8faf9", "background":"#f8faf9","primary-container":"#006a6a","surface-container-lowest":"#ffffff",
"primary-container": "#006a6a", "on-background":"#191c1c","surface-container-high":"#e6e9e8","secondary-container":"#cde6d6",
"surface-container-lowest": "#ffffff", "surface-container-highest":"#e1e3e2","surface-container-low":"#f2f4f3","on-primary":"#ffffff",
"on-primary-fixed": "#002020", "tertiary":"#264b5c","on-surface":"#191c1c","on-surface-variant":"#3e4948","surface-dim":"#d8dada",
"on-background": "#191c1c", "outline-variant":"#bec9c8","on-error":"#ffffff","tertiary-container":"#3f6375",
"secondary-fixed": "#d0e8d9", "secondary-fixed-dim":"#b4ccbd","surface-variant":"#e1e3e2","outline":"#6e7979",
"surface-container-high": "#e6e9e8", "surface-container":"#eceeed","surface-bright":"#f8faf9","error":"#ba1a1a",
"primary-fixed-dim": "#84d4d3", "secondary":"#4d6357","surface":"#f8faf9","error-container":"#ffdad6",
"on-error-container": "#93000a", "primary":"#005050","on-primary-container":"#97e7e6","on-secondary":"#ffffff","primary-fixed":"#a0f0f0"
"secondary-container": "#cde6d6",
"surface-container-highest": "#e1e3e2",
"inverse-surface": "#2e3131",
"surface-container-low": "#f2f4f3",
"on-tertiary-fixed": "#001e2b",
"tertiary-fixed": "#c2e8fd",
"on-primary-fixed-variant": "#004f4f",
"on-secondary-container": "#51685b",
"on-primary": "#ffffff",
"tertiary-fixed-dim": "#a7cce1",
"inverse-primary": "#84d4d3",
"tertiary": "#264b5c",
"on-surface": "#191c1c",
"on-tertiary-container": "#b9def3",
"on-surface-variant": "#3e4948",
"surface-dim": "#d8dada",
"outline-variant": "#bec9c8",
"inverse-on-surface": "#eff1f0",
"on-error": "#ffffff",
"on-tertiary-fixed-variant": "#264b5c",
"on-tertiary": "#ffffff",
"tertiary-container": "#3f6375",
"on-secondary-fixed-variant": "#364b40",
"secondary-fixed-dim": "#b4ccbd",
"surface-variant": "#e1e3e2",
"outline": "#6e7979",
"surface-container": "#eceeed",
"surface-bright": "#f8faf9",
"error": "#ba1a1a",
"surface-tint": "#006a6a",
"secondary": "#4d6357",
"surface": "#f8faf9",
"on-secondary-fixed": "#0a1f16",
"error-container": "#ffdad6",
"primary": "#005050",
"on-primary-container": "#97e7e6",
"on-secondary": "#ffffff",
"primary-fixed": "#a0f0f0"
}, },
"borderRadius": { fontFamily: { headline:["Manrope"], body:["Manrope"] }
"DEFAULT": "0.25rem", }
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
},
"fontFamily": {
"headline": ["Manrope"],
"body": ["Manrope"],
"label": ["Manrope"]
} }
},
},
} }
</script> </script>
<style> <style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
body { font-family:'Manrope',sans-serif; } body { font-family:'Manrope',sans-serif; }
</style> .material-symbols-outlined { font-variation-settings:'FILL' 0,'wght' 400,'GRAD' 0,'opsz' 24; }
<style>
body {
min-height: max(884px, 100dvh);
}
</style> </style>
</head> </head>
<body class="bg-background text-on-background min-h-screen pb-32"> <body class="bg-background text-on-background min-h-screen">
<div class="max-w-md mx-auto relative">
<!-- TopAppBar --> <!-- ═══════════ SIDEBAR (desktop) ═══════════ -->
<header class="bg-[#f8faf9] dark:bg-slate-950 docked full-width top-0 sticky z-40"> <aside class="hidden md:flex flex-col fixed left-0 top-0 h-screen w-56 bg-surface-container-lowest border-r border-outline-variant z-40 py-7 px-4 gap-1">
<div class="flex justify-between items-center w-full px-6 py-4"> <div class="flex items-center gap-2 px-2 mb-7">
<div class="flex items-center gap-3"> <span class="material-symbols-outlined text-[#006a6a] text-2xl" style="font-variation-settings:'FILL' 1;">account_balance_wallet</span>
<span class="material-symbols-outlined text-[#006a6a] dark:text-teal-400">account_balance_wallet</span> <span class="font-extrabold text-[#006a6a] tracking-tight text-lg">Quiet Wealth</span>
<h1 class="font-['Manrope'] headline-sm tracking-tight text-xl font-bold text-[#006a6a] dark:text-teal-500">Quiet Wealth</h1>
</div> </div>
<div class="flex items-center gap-2"> <a href="index.html" class="flex items-center gap-3 px-3 py-2.5 rounded-xl bg-[#005050]/10 text-primary font-bold text-sm">
<button class="p-2 hover:bg-[#f2f4f3] dark:hover:bg-slate-800 transition-colors rounded-full" onclick="window.location.href='add.html'"> <span class="material-symbols-outlined text-[20px]" style="font-variation-settings:'FILL' 1;">home</span>Dashboard
<span class="material-symbols-outlined text-[#3e4948] dark:text-slate-400">add_circle</span> </a>
<a href="historial.html" class="flex items-center gap-3 px-3 py-2.5 rounded-xl text-on-surface-variant hover:bg-surface-container text-sm font-medium transition-colors">
<span class="material-symbols-outlined text-[20px]">history</span>Historial
</a>
<a href="add.html" class="flex items-center gap-3 px-3 py-2.5 rounded-xl text-on-surface-variant hover:bg-surface-container text-sm font-medium transition-colors">
<span class="material-symbols-outlined text-[20px]">add_circle</span>Nuevo movimiento
</a>
<div class="mt-auto">
<button onclick="logout()" class="flex items-center gap-3 px-3 py-2.5 rounded-xl text-on-surface-variant hover:bg-surface-container text-sm font-medium transition-colors w-full">
<span class="material-symbols-outlined text-[20px]">logout</span>Salir
</button> </button>
</div> </div>
</aside>
<!-- ═══════════ CONTENIDO PRINCIPAL ═══════════ -->
<div class="md:ml-56 min-h-screen flex flex-col">
<!-- Header mobile -->
<header class="md:hidden bg-background sticky top-0 z-30 border-b border-outline-variant/30">
<div class="flex justify-between items-center px-6 py-4">
<div class="flex items-center gap-3">
<span class="material-symbols-outlined text-[#006a6a]" style="font-variation-settings:'FILL' 1;">account_balance_wallet</span>
<h1 class="text-xl font-extrabold text-[#006a6a] tracking-tight">Quiet Wealth</h1>
</div>
<button onclick="window.location.href='add.html'" class="p-2 hover:bg-surface-container-low rounded-full transition-colors">
<span class="material-symbols-outlined text-on-surface-variant">add_circle</span>
</button>
</div> </div>
</header> </header>
<main class="px-6 space-y-8 mt-4">
<!-- Screen Title --> <!-- Header desktop -->
<div class="flex items-end justify-between"> <div class="hidden md:flex items-center justify-between px-8 pt-8 pb-2">
<h2 class="text-3xl font-extrabold tracking-tight text-on-surface">Resumen</h2> <div>
<p class="text-on-surface-variant label-sm tracking-widest uppercase pb-1">Octubre 2023</p> <h2 class="text-3xl font-extrabold tracking-tight text-on-surface">Dashboard</h2>
<p id="ui-mes" class="text-sm text-on-surface-variant mt-0.5 capitalize"></p>
</div> </div>
<!-- Hero Balance Card: The Prosperity Spark --> <a href="add.html" class="flex items-center gap-2 px-5 py-2.5 bg-primary text-white rounded-xl font-bold text-sm hover:bg-primary-container transition-colors shadow-sm">
<span class="material-symbols-outlined text-[18px]">add</span>Nuevo movimiento
</a>
</div>
<!-- Contenido principal: grid en desktop -->
<main class="px-6 md:px-8 py-6 pb-28 md:pb-8 flex-1">
<div class="md:grid md:grid-cols-5 md:gap-7 md:items-start space-y-6 md:space-y-0">
<!-- COLUMNA IZQUIERDA (3/5) -->
<div class="md:col-span-3 space-y-6">
<!-- Balance card -->
<div class="relative overflow-hidden rounded-[2rem] p-8 bg-gradient-to-br from-primary to-primary-container text-on-primary shadow-2xl shadow-primary/20"> <div class="relative overflow-hidden rounded-[2rem] p-8 bg-gradient-to-br from-primary to-primary-container text-on-primary shadow-2xl shadow-primary/20">
<!-- Subtle Animated Sparkle Background --> <div class="absolute -right-10 -top-10 w-40 h-40 bg-white opacity-5 blur-3xl rounded-full"></div>
<div class="absolute -right-10 -top-10 w-40 h-40 bg-tertiary-fixed opacity-10 blur-3xl rounded-full"></div> <div class="absolute -left-10 -bottom-10 w-40 h-40 bg-white opacity-5 blur-3xl rounded-full"></div>
<div class="absolute -left-10 -bottom-10 w-40 h-40 bg-primary-fixed opacity-10 blur-3xl rounded-full"></div>
<div class="relative z-10"> <div class="relative z-10">
<p class="text-on-primary/70 font-label tracking-widest uppercase text-xs mb-2">Balance Total</p> <p class="text-white/70 tracking-widest uppercase text-xs mb-2 font-semibold">Balance Total</p>
<h3 id="ui-balance" class="text-5xl font-extrabold tracking-tighter mb-6">$0.00</h3> <h3 id="ui-balance" class="text-5xl font-extrabold tracking-tighter mb-6">$0.00</h3>
<div class="flex items-center gap-2 bg-on-primary/10 w-fit px-4 py-2 rounded-full backdrop-blur-md"> <div id="ui-balance-badge" class="flex items-center gap-2 bg-white/10 w-fit px-4 py-2 rounded-full">
<span class="material-symbols-outlined text-sm" style="font-variation-settings: 'FILL' 1;">trending_up</span> <span class="material-symbols-outlined text-sm" style="font-variation-settings:'FILL' 1;">payments</span>
<span class="text-sm font-medium">+12.4% este mes</span> <span id="ui-balance-resumen" class="text-sm font-medium">Cargando...</span>
</div> </div>
</div> </div>
</div> </div>
<!-- Spending Insights: Donut Chart Section -->
<!-- Movimientos recientes -->
<div class="space-y-4">
<div class="flex justify-between items-center">
<h4 class="text-on-surface-variant font-semibold text-sm uppercase tracking-wider">Movimientos Recientes</h4>
<button onclick="window.location.href='historial.html'" class="text-primary text-sm font-bold hover:underline">Ver todo</button>
</div>
<div id="ui-movimientos" class="bg-surface-container-lowest rounded-[1.5rem] overflow-hidden divide-y divide-surface-container-low">
<!-- cargado por JS -->
<div class="flex items-center gap-4 px-5 py-4 animate-pulse">
<div class="w-11 h-11 rounded-xl bg-surface-container-high"></div>
<div class="flex-1 space-y-2">
<div class="h-3 bg-surface-container-high rounded w-2/3"></div>
<div class="h-2 bg-surface-container-high rounded w-1/3"></div>
</div>
</div>
</div>
</div>
</div>
<!-- COLUMNA DERECHA (2/5) -->
<div class="md:col-span-2 space-y-6">
<!-- Donut chart -->
<section class="space-y-4"> <section class="space-y-4">
<h4 class="headline-sm text-on-surface-variant font-semibold px-1">Distribución de Gastos</h4> <h4 class="text-on-surface-variant font-semibold text-sm uppercase tracking-wider">Distribución de Gastos</h4>
<div class="bg-surface-container-low rounded-[2rem] p-8 flex flex-col items-center"> <div class="bg-surface-container-low rounded-[2rem] p-7 flex flex-col items-center">
<!-- Visual Donut (CSS Representation) --> <div id="ui-donut-chart" class="relative w-44 h-44 mb-6">
<div id="ui-donut-chart" class="relative w-48 h-48 mb-8"> <svg class="w-full h-full -rotate-90" viewBox="0 0 36 36">
<svg class="w-full h-full transform -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"/>
<!-- Comida (45%) -->
<circle class="stroke-primary" cx="18" cy="18" fill="none" r="16" stroke-dasharray="45, 100" stroke-linecap="round" stroke-width="4"></circle>
<!-- Vivienda (30%) -->
<circle class="stroke-secondary" cx="18" cy="18" fill="none" r="16" stroke-dasharray="30, 100" stroke-dashoffset="-45" stroke-linecap="round" stroke-width="4"></circle>
<!-- Transporte (15%) -->
<circle class="stroke-tertiary" cx="18" cy="18" fill="none" r="16" stroke-dasharray="15, 100" stroke-dashoffset="-75" stroke-linecap="round" stroke-width="4"></circle>
</svg> </svg>
<div class="absolute inset-0 flex flex-col items-center justify-center"> <div class="absolute inset-0 flex flex-col items-center justify-center">
<span class="text-2xl font-bold">$2,840</span> <span class="text-xl font-bold text-on-surface-variant"></span>
<span class="text-[10px] uppercase tracking-widest text-on-surface-variant font-bold">Gastado</span> <span class="text-[9px] uppercase tracking-widest text-on-surface-variant font-bold">Gastos</span>
</div> </div>
</div> </div>
<!-- Legend --> <div id="ui-donut-legend" class="w-full space-y-2">
<div id="ui-donut-legend" class="w-full grid grid-cols-1 gap-3"> <p class="text-xs text-on-surface-variant text-center">Sin datos aún</p>
<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 bg-primary"></div>
<span class="text-sm font-semibold">Comida</span>
</div>
<span class="text-sm text-on-surface-variant">45%</span>
</div>
<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 bg-secondary"></div>
<span class="text-sm font-semibold">Vivienda</span>
</div>
<span class="text-sm text-on-surface-variant">30%</span>
</div>
<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 bg-tertiary"></div>
<span class="text-sm font-semibold">Transporte</span>
</div>
<span class="text-sm text-on-surface-variant">15%</span>
</div>
</div>
</div>
</section>
<!-- Recent Movements -->
<section class="space-y-4">
<div class="flex justify-between items-center px-1">
<h4 class="headline-sm text-on-surface-variant font-semibold">Movimientos Recientes</h4>
<button class="text-primary text-sm font-bold" onclick="window.location.href='historial.html'">Ver todo</button>
</div>
<div id="ui-movimientos" class="space-y-6">
<!-- Transaction 1 -->
<div class="flex items-center justify-between">
<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">restaurant</span>
</div>
<div>
<p class="font-bold text-on-surface">Restaurante La Paz</p>
<p class="text-xs text-on-surface-variant">24 Oct 2023</p>
</div>
</div>
<p class="font-bold text-error">-$45.50</p>
</div>
<!-- Transaction 2 -->
<div class="flex items-center justify-between">
<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">shopping_bag</span>
</div>
<div>
<p class="font-bold text-on-surface">Tienda Moderna</p>
<p class="text-xs text-on-surface-variant">22 Oct 2023</p>
</div>
</div>
<p class="font-bold text-error">-$120.00</p>
</div>
<!-- Transaction 3 -->
<div class="flex items-center justify-between">
<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">payments</span>
</div>
<div>
<p class="font-bold text-on-surface">Depósito de Nómina</p>
<p class="text-xs text-on-surface-variant">20 Oct 2023</p>
</div>
</div>
<p class="font-bold text-primary-container">+$2,100.00</p>
</div> </div>
</div> </div>
</section> </section>
</div>
</div>
</main> </main>
<!-- BottomNavBar --> </div>
<nav class="fixed bottom-0 left-0 right-0 max-w-md mx-auto z-50 flex justify-around items-center px-8 pb-8 pt-4 bg-[#ffffff]/70 backdrop-blur-xl shadow-[0px_-12px_32px_rgba(0,80,80,0.06)] rounded-t-[2rem]">
<button class="flex items-center justify-center bg-[#006a6a] text-white rounded-2xl p-3 shadow-lg shadow-[#006a6a]/20 active:scale-90 transition-transform duration-300"> <!-- ═══════════ BOTTOM NAV (mobile) ═══════════ -->
<nav class="md:hidden fixed bottom-0 inset-x-0 z-50 flex justify-around items-center px-4 pb-6 pt-3 bg-white/80 backdrop-blur-xl shadow-[0_-8px_24px_rgba(0,80,80,0.08)] border-t border-outline-variant/30">
<button class="flex flex-col items-center gap-0.5 text-primary px-4 py-1">
<span class="material-symbols-outlined" style="font-variation-settings:'FILL' 1;">home</span> <span class="material-symbols-outlined" style="font-variation-settings:'FILL' 1;">home</span>
<span class="text-[10px] font-bold">Inicio</span>
</button> </button>
<button class="flex items-center justify-center text-[#3e4948] p-3 hover:text-[#006a6a] active:scale-90 transition-transform duration-300" onclick="window.location.href='historial.html'"> <button onclick="window.location.href='historial.html'" class="flex flex-col items-center gap-0.5 text-on-surface-variant px-4 py-1">
<span class="material-symbols-outlined">history</span> <span class="material-symbols-outlined">history</span>
<span class="text-[10px] font-medium">Historial</span>
</button> </button>
<button class="flex items-center justify-center text-[#3e4948] p-3 hover:text-[#006a6a] active:scale-90 transition-transform duration-300" onclick="window.location.href='add.html'"> <button onclick="window.location.href='add.html'" class="flex flex-col items-center gap-0.5 px-4 py-1">
<span class="material-symbols-outlined">add_circle</span> <span class="w-12 h-12 -mt-6 rounded-2xl bg-primary text-white flex items-center justify-center shadow-lg shadow-primary/30">
<span class="material-symbols-outlined">add</span>
</span>
</button> </button>
</nav> </nav>
</div>
<script src="js/ui.js"></script> <script src="js/ui.js"></script>
<script src="js/dashboard.js"></script> <script src="js/dashboard.js"></script>
</body></html> <script>
// Mes actual en header
document.getElementById('ui-mes').textContent =
new Date().toLocaleString('es-AR', { month:'long', year:'numeric' });
function logout() {
fetch('api/login.php?action=logout').then(() => window.location.href = 'login.html');
}
</script>
</body>
</html>
+34 -45
View File
@@ -1,89 +1,78 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
let currentMonto = "0"; let currentMonto = "0";
let selectedTipo = "gasto"; // default let selectedTipo = "gasto";
const montoEl = document.getElementById('ui-monto'); // 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 btnGasto = document.getElementById('btn-gasto');
const btnIngreso = document.getElementById('btn-ingreso'); const btnIngreso = document.getElementById('btn-ingreso');
const numpad = document.getElementById('ui-numpad'); const numpad = document.getElementById('ui-numpad');
const btnSave = document.getElementById('btn-save');
const iptFecha = document.getElementById('ui-fecha'); const iptFecha = document.getElementById('ui-fecha');
const saveButtons = ['btn-save', 'btn-save-mobile'].map(id => document.getElementById(id)).filter(Boolean);
// Default a hoy // Default a hoy
iptFecha.valueAsDate = new Date(); iptFecha.valueAsDate = new Date();
// Cargar categorías desde la API // Cargar categorías desde la API
apiFetch('api/movimientos.php') apiFetch('api/movimientos.php')
.then(r => r.json()) .then(r => r ? r.json() : Promise.reject('no-auth'))
.then(data => { .then(data => {
if (data.categorias) { if (!data || !data.categorias) return;
const sel = document.getElementById('ui-categoria'); const sel = document.getElementById('ui-categoria');
sel.innerHTML = data.categorias.map(c => sel.innerHTML = data.categorias.map(c =>
`<option value="${c.id}">${c.nombre}</option>` `<option value="${c.id}">${c.nombre}</option>`
).join(''); ).join('');
}
}) })
.catch(() => {}); .catch(() => {});
// Tipo selector // Tipo selector
const updateTipoUI = () => { const updateTipoUI = () => {
if (selectedTipo === 'gasto') { const isGasto = selectedTipo === 'gasto';
btnGasto.classList.replace('text-on-surface-variant', 'text-primary'); btnGasto.classList.toggle('bg-surface-container-lowest', isGasto);
btnGasto.classList.add('bg-surface-container-lowest'); btnGasto.classList.toggle('text-primary', isGasto);
btnIngreso.classList.replace('bg-surface-container-lowest', 'bg-transparent'); btnGasto.classList.toggle('shadow-sm', isGasto);
btnIngreso.classList.replace('text-primary', 'text-on-surface-variant'); btnGasto.classList.toggle('text-on-surface-variant', !isGasto);
} else { btnIngreso.classList.toggle('bg-surface-container-lowest', !isGasto);
btnIngreso.classList.replace('text-on-surface-variant', 'text-primary'); btnIngreso.classList.toggle('text-primary', !isGasto);
btnIngreso.classList.add('bg-surface-container-lowest'); btnIngreso.classList.toggle('shadow-sm', !isGasto);
btnGasto.classList.replace('bg-surface-container-lowest', 'bg-transparent'); btnIngreso.classList.toggle('text-on-surface-variant', isGasto);
btnGasto.classList.replace('text-primary', 'text-on-surface-variant');
}
}; };
btnGasto.addEventListener('click', () => { selectedTipo = 'gasto'; updateTipoUI(); }); btnGasto.addEventListener('click', () => { selectedTipo = 'gasto'; updateTipoUI(); });
btnIngreso.addEventListener('click', () => { selectedTipo = 'ingreso'; updateTipoUI(); }); btnIngreso.addEventListener('click', () => { selectedTipo = 'ingreso'; updateTipoUI(); });
// Numpad
function applyInput(val) { function applyInput(val) {
if (val === 'backspace') { if (val === 'backspace') {
currentMonto = currentMonto.slice(0, -1); currentMonto = currentMonto.slice(0, -1) || "0";
if (currentMonto === '') currentMonto = "0";
} else if (/^[0-9]$/.test(val)) { } else if (/^[0-9]$/.test(val)) {
if (currentMonto === "0") { currentMonto = currentMonto === "0" ? val : currentMonto + val;
currentMonto = val;
} else {
currentMonto += val;
}
} else if (val === '.') { } else if (val === '.') {
if (!currentMonto.includes('.')) currentMonto += '.'; if (!currentMonto.includes('.')) currentMonto += '.';
} }
montoEl.textContent = currentMonto; montoEls.forEach(el => el.textContent = currentMonto);
} }
// Numpad táctil
numpad.addEventListener('click', (e) => { numpad.addEventListener('click', (e) => {
const btn = e.target.closest('button'); const btn = e.target.closest('button');
if (!btn) return; if (!btn) return;
if (btn.querySelector('[data-icon="backspace"]')) { applyInput(btn.querySelector('[data-icon="backspace"]') ? 'backspace' : btn.textContent.trim());
applyInput('backspace');
} else {
applyInput(btn.textContent.trim());
}
}); });
// Teclado físico (desktop) // Teclado físico
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
if (document.activeElement.tagName === 'INPUT') return;
if (e.key >= '0' && e.key <= '9') applyInput(e.key); if (e.key >= '0' && e.key <= '9') applyInput(e.key);
else if (e.key === '.' || e.key === ',') applyInput('.'); else if (e.key === '.' || e.key === ',') applyInput('.');
else if (e.key === 'Backspace') applyInput('backspace'); else if (e.key === 'Backspace') applyInput('backspace');
else if (e.key === 'Enter') btnSave.click(); else if (e.key === 'Enter') doSave();
}); });
// Guardar Movimiento // Guardar
btnSave.addEventListener('click', async () => { async function doSave() {
const payload = { const payload = {
tipo: selectedTipo, tipo: selectedTipo,
monto: parseFloat(currentMonto), monto: parseFloat(currentMonto),
fecha: document.getElementById('ui-fecha').value, fecha: iptFecha.value,
notas: document.getElementById('ui-notas').value, notas: document.getElementById('ui-notas').value,
id_categoria: document.getElementById('ui-categoria').value id_categoria: document.getElementById('ui-categoria').value
}; };
@@ -93,29 +82,29 @@ document.addEventListener('DOMContentLoaded', () => {
return; return;
} }
try { saveButtons.forEach(b => { b.disabled = true; b.textContent = 'Guardando…'; });
const btnOriginal = btnSave.innerHTML;
btnSave.innerHTML = 'Guardando...';
btnSave.disabled = true;
try {
const r = await apiFetch('api/movimientos.php', { const r = await apiFetch('api/movimientos.php', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload) body: JSON.stringify(payload)
}); });
if (!r) return;
const data = await r.json(); const data = await r.json();
if (data.status === 'success') { if (data.status === 'success') {
window.location.href = 'index.html'; window.location.href = 'index.html';
} else { } else {
await rmModal.alert(data.message, 'Error al guardar'); await rmModal.alert(data.message, 'Error al guardar');
btnSave.innerHTML = btnOriginal; saveButtons.forEach(b => { b.disabled = false; b.textContent = 'Guardar Movimiento'; });
btnSave.disabled = false;
} }
} catch (err) { } catch (err) {
console.error(err); console.error(err);
await rmModal.alert('No se pudo conectar con el servidor.', 'Error de conexión'); await rmModal.alert('No se pudo conectar con el servidor.', 'Error de conexión');
btnSave.disabled = false; saveButtons.forEach(b => { b.disabled = false; b.textContent = 'Guardar Movimiento'; });
} }
}); }
saveButtons.forEach(btn => btn.addEventListener('click', doSave));
}); });
+75 -67
View File
@@ -1,80 +1,88 @@
document.addEventListener('DOMContentLoaded', async () => { document.addEventListener('DOMContentLoaded', async () => {
try { try {
const response = await apiFetch('api/movimientos.php'); const response = await apiFetch('api/movimientos.php');
if (!response) return;
const data = await response.json(); const data = await response.json();
if (data.status !== 'success') return;
if (data.status === 'success') { // Balance
// Update balance const balance = data.balance ?? 0;
document.getElementById('ui-balance').textContent = `$${data.balance.toFixed(2)}`; document.getElementById('ui-balance').textContent = formatMoney(balance);
document.getElementById('ui-balance-resumen').textContent =
balance >= 0 ? 'Saldo disponible' : 'Saldo negativo';
// Render movements // Movimientos recientes
const uiMovimientos = document.getElementById('ui-movimientos'); const container = document.getElementById('ui-movimientos');
uiMovimientos.innerHTML = ''; if (!data.movimientos.length) {
container.innerHTML = `
data.movimientos.forEach(mov => { <div class="flex flex-col items-center py-10 gap-2 text-on-surface-variant">
const isExpense = mov.tipo === 'gasto'; <span class="material-symbols-outlined text-4xl opacity-30">receipt_long</span>
const sign = isExpense ? '-' : '+'; <p class="text-sm">Sin movimientos aún</p>
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>
`;
});
// 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;
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>
</div>`; </div>`;
document.getElementById('ui-donut-chart').innerHTML = svgHtml;
document.getElementById('ui-donut-legend').innerHTML = legendHtml;
} else { } 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>`; 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('');
}
// 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 = ''; document.getElementById('ui-donut-legend').innerHTML = '';
} }
}
} catch (e) { } 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 });
}