From e98bfa0afb04970f289538785da5a3e4dfc14cff Mon Sep 17 00:00:00 2001 From: Ricardo Monla Date: Wed, 3 Jun 2026 16:49:27 -0300 Subject: [PATCH] =?UTF-8?q?Feature:=20CRUD=20de=20categor=C3=ADas=20+=20se?= =?UTF-8?q?siones=20persistentes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - categorias.html: lista con conteo de movimientos, drawer para agregar/editar - api/categorias.php: GET/POST/PUT/DELETE (DELETE bloqueado si tiene movimientos) - categorias.js: grid de 30 íconos Material Symbols + paleta de 12 colores, preview en vivo - Sidebar actualizado en todas las páginas con link a Categorías - Sesiones PHP guardadas en pagos/db/sessions/ (bind mount) → sobreviven reinicios Docker - Fix selector de categorías en add.html: sesiones persistentes resuelven el Cargando… Co-Authored-By: Claude Sonnet 4.6 --- pagos/add.html | 3 + pagos/api/auth.php | 1 + pagos/api/categorias.php | 56 +++++++++++ pagos/api/login.php | 1 + pagos/categorias.html | 152 ++++++++++++++++++++++++++++ pagos/db/.gitignore | 1 + pagos/historial.html | 3 + pagos/index.html | 3 + pagos/js/categorias.js | 211 +++++++++++++++++++++++++++++++++++++++ 9 files changed, 431 insertions(+) create mode 100644 pagos/api/categorias.php create mode 100644 pagos/categorias.html create mode 100644 pagos/js/categorias.js diff --git a/pagos/add.html b/pagos/add.html index 7927776..93ffb42 100644 --- a/pagos/add.html +++ b/pagos/add.html @@ -49,6 +49,9 @@ tailwind.config = { add_circleNuevo movimiento + + categoryCategorías +
+
+ + + +
+ +
+
+ +

Categorías

+
+ +
+ +
+
+
+ hourglass_top +

Cargando categorías…

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

Nueva categoría

+ +
+ +
+ + +
+ + +
+ + +
+
+ category +
+
+

Vista previa

+

Así se verá en los movimientos

+
+
+ + +
+ +
+ +
+
+ + +
+ +
+ +
+
+ +
+ +
+ + +
+
+ + + + + diff --git a/pagos/db/.gitignore b/pagos/db/.gitignore index 4928ca6..dc2fb7c 100644 --- a/pagos/db/.gitignore +++ b/pagos/db/.gitignore @@ -1,2 +1,3 @@ *.sqlite *.sqlite-* +sessions/ diff --git a/pagos/historial.html b/pagos/historial.html index d5c58ff..fe3f240 100644 --- a/pagos/historial.html +++ b/pagos/historial.html @@ -47,6 +47,9 @@ tailwind.config = { add_circleNuevo movimiento + + categoryCategorías +
+ +
+ `; + }).join(''); +} + +// ─── Drawer ─────────────────────────────────────────────────────── +function abrirDrawer(cat) { + editandoId = cat ? cat.id : null; + iconoSelecto = cat ? cat.icono : ICONOS[0]; + colorSelecto = cat ? cat.color_hex : COLORES[0]; + + document.getElementById('drawer-title').textContent = cat ? 'Editar categoría' : 'Nueva categoría'; + document.getElementById('drawer-nombre').value = cat ? cat.nombre : ''; + + actualizarSeleccionIcono(); + actualizarSeleccionColor(); + actualizarPreview(); + + document.getElementById('drawer-overlay').classList.remove('hidden'); + const drawer = document.getElementById('drawer'); + drawer.classList.remove('translate-y-full','md:translate-x-full'); + setTimeout(() => document.getElementById('drawer-nombre').focus(), 300); +} + +function cerrarDrawer() { + document.getElementById('drawer-overlay').classList.add('hidden'); + const drawer = document.getElementById('drawer'); + drawer.classList.add('translate-y-full','md:translate-x-full'); +} + +// ─── Íconos ─────────────────────────────────────────────────────── +function renderIconGrid() { + document.getElementById('icon-grid').innerHTML = ICONOS.map(ic => ` + `).join(''); +} + +function seleccionarIcono(ic) { + iconoSelecto = ic; + actualizarSeleccionIcono(); + actualizarPreview(); +} + +function actualizarSeleccionIcono() { + document.querySelectorAll('#icon-grid button').forEach(b => { + const ic = b.id.replace('ico-', ''); + b.classList.toggle('bg-primary/10', ic === iconoSelecto); + b.classList.toggle('border-primary', ic === iconoSelecto); + b.querySelector('span').style.color = ic === iconoSelecto ? '#005050' : ''; + }); +} + +// ─── Colores ────────────────────────────────────────────────────── +function renderColorGrid() { + document.getElementById('color-grid').innerHTML = COLORES.map(col => ` + `).join(''); +} + +function seleccionarColor(col) { + colorSelecto = col; + actualizarSeleccionColor(); + actualizarPreview(); +} + +function actualizarSeleccionColor() { + document.querySelectorAll('#color-grid button').forEach(b => { + const col = '#' + b.id.replace('col-', ''); + b.style.outline = col === colorSelecto ? `3px solid ${col}` : 'none'; + b.style.outlineOffset = '2px'; + }); +} + +// ─── Preview ────────────────────────────────────────────────────── +function actualizarPreview() { + const nombre = document.getElementById('drawer-nombre').value.trim() || 'Vista previa'; + document.getElementById('preview-nombre').textContent = nombre; + document.getElementById('preview-icono').style.background = colorSelecto; + document.getElementById('preview-icono-sym').textContent = iconoSelecto; +} + +// ─── Guardar ────────────────────────────────────────────────────── +async function guardar() { + const nombre = document.getElementById('drawer-nombre').value.trim(); + if (!nombre) { + rmModal.alert('El nombre de la categoría no puede estar vacío.', 'Campo requerido'); + return; + } + + const btn = document.getElementById('drawer-save'); + btn.disabled = true; btn.textContent = 'Guardando…'; + + const payload = { nombre, icono: iconoSelecto, color_hex: colorSelecto }; + const method = editandoId ? 'PUT' : 'POST'; + if (editandoId) payload.id = editandoId; + + try { + const r = await apiFetch('api/categorias.php', { + method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + if (!r) return; + const data = await r.json(); + + if (data.status === 'success') { + cerrarDrawer(); + rmModal.toast(editandoId ? 'Categoría actualizada' : 'Categoría creada'); + await cargarCategorias(); + } else { + await rmModal.alert(data.message, 'Error'); + } + } catch { + await rmModal.alert('Error de conexión', 'Error'); + } finally { + btn.disabled = false; btn.textContent = 'Guardar'; + } +} + +// ─── Eliminar ───────────────────────────────────────────────────── +async function eliminar(id, nombre) { + const ok = await rmModal.confirm(`¿Eliminar la categoría "${nombre}"?`, 'Eliminar categoría'); + if (!ok) return; + + const r = await apiFetch('api/categorias.php', { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ id }) + }); + if (!r) return; + const data = await r.json(); + + if (data.status === 'success') { + rmModal.toast('Categoría eliminada'); + await cargarCategorias(); + } else { + rmModal.toast(data.message, 'error'); + } +}