diff --git a/docs/plan/00_PLAN.md b/docs/plan/00_PLAN.md index 3b6fdf8..031a50d 100644 --- a/docs/plan/00_PLAN.md +++ b/docs/plan/00_PLAN.md @@ -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 -- **Frontend**: Diseños generados por Stitch (HTML, CSS Vainilla, JS). Diseño moderno "Quiet Wealth / Financial Sanctuary". -- **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). -- **Despliegue**: Servidor Proxmox LXC `srvv-nginx-rm` (Debian 12, NGINX 1.22.1, HTTPS activo). +## Stack +| Capa | Tecnología | +|------|-----------| +| Frontend | HTML + Tailwind CSS (CDN local) + Vanilla JS | +| 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) -**Objetivo:** Tener todo el diseño web unificado y navegable estáticamente. -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/`. -2. Exportar las pantallas generadas en Stitch (Dashboard, Nuevo Gasto, Historial) y acomodarlas como plantillas base (HTML). -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. +### ✅ login.html — Completa +- Formulario propio con diseño Quiet Wealth (sin popup del browser) +- Validación de credenciales contra `config.php` (gitignoreado) +- Redirección automática si ya hay sesión activa -### Fase 2: Motor de Base de Datos y Backend (PHP/SQLite) -**Objetivo:** Persistir la información y crear la lógica detrás de las pantallas. -1. Diseñar el esquema de base de datos SQLite (tablas básicas como `movimientos` y `categorias`). -2. Crear un archivo clase base en PHP (`db.php`) para conexión y operaciones CRUD. -3. Crear los endpoints/controladores PHP (`api/movimientos.php`) que manejen operaciones GET, POST, PUT y DELETE. -4. Ajustar los scripts de inicialización de la BD. +### ✅ index.html — Dashboard +- Balance total dinámico desde SQLite +- Gráfico donut de distribución de gastos por categoría (SVG puro) +- Últimos 5 movimientos con ícono, descripción y monto +- Navegación inferior funcional -### Fase 3: Integración Frontend ↔ Backend (Dinámica) -**Objetivo:** Darle vida a las pantallas usando Javascript interactuando con PHP. -1. **Nuevo Movimiento**: Enlazar el formulario "Agregar" usando Fetch/AJAX (Vanilla JS) para enviarle los datos al backend de forma transparente sin refrescar. -2. **Dashboard**: - - Calcular saldo dinámico, sumatorias de gastos del mes, etc., usando SQL. - - Enlazar librería de gráficos (por ejemplo Chart.js) para pintar la dona de categorías leyendo desde la base de datos. -3. **Historial**: Renderizar la lista de últimos movimientos desde SQLite y añadir sistema de borrado. +### ✅ add.html — Nuevo movimiento +- Selector Gasto / Ingreso +- Numpad táctil + soporte teclado físico (desktop) +- Categorías cargadas dinámicamente desde la API +- Campo fecha (default: hoy) y notas opcionales +- POST a la API con redirect al dashboard al guardar -### Fase 4: Despliegue Configurado (Producción) -**Objetivo:** Llevar el sistema al servidor `srvv-nginx-rm` para uso real. -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. -2. **Configuración del Servidor:** - - Crear un subdirectorio o alias en NGINX (ejemplo `rmonla.duckdns.org/pagos`). - - Clonar o actualizar por deploy key en `/var/www/pagos`. - - Asegurar permisos de escritura a la carpeta `db/` para que PHP (usuario `www-data`) pueda modifciar el archivo `.sqlite`. -3. Pruebas End-to-End directas desde el teléfono. +### ✅ historial.html — Historial completo +- Lista de todos los movimientos agrupados por mes +- Botón eliminar con modal de confirmación +- Toast de confirmación tras eliminar + +### ✅ API (movimientos.php) +- GET dashboard: balance + últimos 5 + donut + categorías +- 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) -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. +## Pendientes / En progreso + +### ✅ 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 `, `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 diff --git a/pagos/add.html b/pagos/add.html index 5311090..7927776 100644 --- a/pagos/add.html +++ b/pagos/add.html @@ -1,180 +1,184 @@ - - + + -Nuevo Movimiento - Sanctuary +Nuevo Movimiento — Quiet Wealth - - - - - -
- -
-
- -

Nuevo Movimiento

-
-
-
-
- -
- - -
- -
- -
-$ -0 -
-
- -
- -
- - -
- -
-
- -
-calendar_today - -
-
-
- -
-edit_note - -
-
-
-
- -
- - - - - - - - - - - - -
-
- -
- -
+ } +} + + + + + + + + + +
+ + +
+ +

Nuevo Movimiento

+
+ + +
+
+ + +
+ + +
+ + +
+ + +
+ +
+ $ + 0 +
+
+ + +
+ +
+ + expand_more +
+
+ + +
+
+ +
+ calendar_today + +
+
+
+ +
+ edit_note + +
+
+
+ + + +
+ + +
+ + + + + +
+ + + + + + + + + + + + +
+
+ +
+
+ + +
+ +
+
+ - \ No newline at end of file + + diff --git a/pagos/api/auth.php b/pagos/api/auth.php index 456552a..5b7c26d 100644 --- a/pagos/api/auth.php +++ b/pagos/api/auth.php @@ -1,6 +1,8 @@ -Historial - Quiet Wealth +Historial — Quiet Wealth - - -
+ - -
- +
+ + + +
+ + +
+
+ -

Historial

-
+ +

Historial

+
+ + add + + + -
- -
+ +
+
+ + +
hourglass_top -

Cargando movimientos...

-
- + + + + + + +
- -
- - -
- +
- diff --git a/pagos/index.html b/pagos/index.html index 209e75d..202096d 100644 --- a/pagos/index.html +++ b/pagos/index.html @@ -1,235 +1,183 @@ - - + + +Quiet Wealth — Dashboard - - - - - -
- -
-
-
-account_balance_wallet -

Quiet Wealth

+ } +} + + + + + + + + + +
+ + +
+
+
+ account_balance_wallet +

Quiet Wealth

+
+ +
+
+ + + + + +
+
+ + +
+ + +
+
+
+
+

Balance Total

+

$0.00

+
+ payments + Cargando... +
+
+
+ + +
+
+

Movimientos Recientes

+ +
+
+ +
+
+
+
+
+
+
+
+
+ +
+ + +
+ + +
+

Distribución de Gastos

+
+
+ + + +
+ + Gastos +
+
+
+

Sin datos aún

+
+
+
+ +
+
+
-
- -
-
-
-
- -
-

Resumen

-

Octubre 2023

-
- -
- -
-
-
-

Balance Total

-

$0.00

-
-trending_up -+12.4% este mes -
-
-
- -
-

Distribución de Gastos

-
- -
- - - - - - - - -
-$2,840 -Gastado -
-
- -
-
-
-
-Comida -
-45% -
-
-
-
-Vivienda -
-30% -
-
-
-
-Transporte -
-15% -
-
-
-
- -
-
-

Movimientos Recientes

- -
-
- -
-
-
-restaurant -
-
-

Restaurante La Paz

-

24 Oct 2023

-
-
-

-$45.50

-
- -
-
-
-shopping_bag -
-
-

Tienda Moderna

-

22 Oct 2023

-
-
-

-$120.00

-
- -
-
-
-payments -
-
-

Depósito de Nómina

-

20 Oct 2023

-
-
-

+$2,100.00

-
-
-
-
- -
+ - \ No newline at end of file + + + diff --git a/pagos/js/add.js b/pagos/js/add.js index 4c0f7f9..f7cb3cf 100644 --- a/pagos/js/add.js +++ b/pagos/js/add.js @@ -1,89 +1,78 @@ document.addEventListener('DOMContentLoaded', () => { let currentMonto = "0"; - let selectedTipo = "gasto"; // default + let selectedTipo = "gasto"; - const montoEl = document.getElementById('ui-monto'); - const btnGasto = document.getElementById('btn-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 btnSave = document.getElementById('btn-save'); - const iptFecha = document.getElementById('ui-fecha'); + 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.json()) + .then(r => r ? r.json() : Promise.reject('no-auth')) .then(data => { - if (data.categorias) { - const sel = document.getElementById('ui-categoria'); - sel.innerHTML = data.categorias.map(c => - `` - ).join(''); - } + if (!data || !data.categorias) return; + const sel = document.getElementById('ui-categoria'); + sel.innerHTML = data.categorias.map(c => + `` + ).join(''); }) .catch(() => {}); // Tipo selector const updateTipoUI = () => { - if (selectedTipo === 'gasto') { - btnGasto.classList.replace('text-on-surface-variant', 'text-primary'); - btnGasto.classList.add('bg-surface-container-lowest'); - btnIngreso.classList.replace('bg-surface-container-lowest', 'bg-transparent'); - btnIngreso.classList.replace('text-primary', 'text-on-surface-variant'); - } else { - btnIngreso.classList.replace('text-on-surface-variant', 'text-primary'); - btnIngreso.classList.add('bg-surface-container-lowest'); - btnGasto.classList.replace('bg-surface-container-lowest', 'bg-transparent'); - btnGasto.classList.replace('text-primary', 'text-on-surface-variant'); - } + 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(); }); + btnGasto.addEventListener('click', () => { selectedTipo = 'gasto'; updateTipoUI(); }); btnIngreso.addEventListener('click', () => { selectedTipo = 'ingreso'; updateTipoUI(); }); + // Numpad function applyInput(val) { if (val === 'backspace') { - currentMonto = currentMonto.slice(0, -1); - if (currentMonto === '') currentMonto = "0"; + currentMonto = currentMonto.slice(0, -1) || "0"; } else if (/^[0-9]$/.test(val)) { - if (currentMonto === "0") { - currentMonto = val; - } else { - currentMonto += val; - } + currentMonto = currentMonto === "0" ? val : currentMonto + val; } else if (val === '.') { if (!currentMonto.includes('.')) currentMonto += '.'; } - montoEl.textContent = currentMonto; + montoEls.forEach(el => el.textContent = currentMonto); } - // Numpad táctil numpad.addEventListener('click', (e) => { const btn = e.target.closest('button'); if (!btn) return; - if (btn.querySelector('[data-icon="backspace"]')) { - applyInput('backspace'); - } else { - applyInput(btn.textContent.trim()); - } + applyInput(btn.querySelector('[data-icon="backspace"]') ? 'backspace' : btn.textContent.trim()); }); - // Teclado físico (desktop) + // 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') btnSave.click(); + else if (e.key === 'Enter') doSave(); }); - // Guardar Movimiento - btnSave.addEventListener('click', async () => { + // Guardar + async function doSave() { const payload = { tipo: selectedTipo, monto: parseFloat(currentMonto), - fecha: document.getElementById('ui-fecha').value, + fecha: iptFecha.value, notas: document.getElementById('ui-notas').value, id_categoria: document.getElementById('ui-categoria').value }; @@ -93,29 +82,29 @@ document.addEventListener('DOMContentLoaded', () => { return; } - try { - const btnOriginal = btnSave.innerHTML; - btnSave.innerHTML = 'Guardando...'; - btnSave.disabled = true; + 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'); - btnSave.innerHTML = btnOriginal; - btnSave.disabled = false; + 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'); - btnSave.disabled = false; + saveButtons.forEach(b => { b.disabled = false; b.textContent = 'Guardar Movimiento'; }); } - }); + } + + saveButtons.forEach(btn => btn.addEventListener('click', doSave)); }); diff --git a/pagos/js/dashboard.js b/pagos/js/dashboard.js index e498614..534d295 100644 --- a/pagos/js/dashboard.js +++ b/pagos/js/dashboard.js @@ -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 += ` -
-
-
- ${mov.categoria_icono || 'payments'} -
-
-

${mov.notas || mov.categoria_nombre}

-

${mov.fecha}

-
-
-

${sign}$${parseFloat(mov.monto).toFixed(2)}

-
- `; - }); + 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 = ``; - 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 += ``; - - // Add Legend entry - legendHtml += ` -
-
-
- ${cat.nombre} -
- ${pct.toFixed(1)}% -
- `; - - currentOffset += pct; - }); - - svgHtml += ` -
- $${totalGastos.toFixed(0)} - Gastos + // Movimientos recientes + const container = document.getElementById('ui-movimientos'); + if (!data.movimientos.length) { + container.innerHTML = ` +
+ receipt_long +

Sin movimientos aún

+
`; + } 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 ` +
+
+ ${icono} +
+
+

${titulo}

+

${fecha} · ${mov.categoria_nombre || '—'}

+
+

${signo}${formatMoney(mov.monto)}

`; + }).join(''); + } - document.getElementById('ui-donut-chart').innerHTML = svgHtml; - document.getElementById('ui-donut-legend').innerHTML = legendHtml; - } else { - document.getElementById('ui-donut-chart').innerHTML = `
Sin gastos registrados
`; - 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 += ``; + legend += ` +
+
+
+ ${cat.nombre} +
+ ${pct.toFixed(1)}% +
`; + offset += pct; + }); + document.getElementById('ui-donut-chart').innerHTML = ` + ${svgCircles} +
+ ${formatMoney(total)} + Gastos +
`; + document.getElementById('ui-donut-legend').innerHTML = `
${legend}
`; + } else { + document.getElementById('ui-donut-chart').innerHTML = ` + + + +
+ Sin gastos +
`; + 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 }); +}