Feature: CRUD de categorías + sesiones persistentes

- 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 <noreply@anthropic.com>
This commit is contained in:
Ricardo Monla
2026-06-03 16:49:27 -03:00
co-authored by Claude Sonnet 4.6
parent 7cb6107cb4
commit e98bfa0afb
9 changed files with 431 additions and 0 deletions
+56
View File
@@ -0,0 +1,56 @@
<?php
require_once 'auth.php';
header('Content-Type: application/json');
require_once 'db.php';
$pdo = getDB();
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
$stmt = $pdo->query("
SELECT c.*, COUNT(m.id) as total_movimientos
FROM categorias c
LEFT JOIN movimientos m ON m.id_categoria = c.id
GROUP BY c.id ORDER BY c.nombre
");
echo json_encode(['status' => 'success', 'categorias' => $stmt->fetchAll(PDO::FETCH_ASSOC)]);
} elseif ($method === 'POST') {
$d = json_decode(file_get_contents('php://input'), true);
if (empty($d['nombre']) || empty($d['icono'])) {
http_response_code(400);
die(json_encode(['status' => 'error', 'message' => 'Nombre e ícono son obligatorios']));
}
$stmt = $pdo->prepare("INSERT INTO categorias (nombre, icono, color_hex) VALUES (?, ?, ?)");
$stmt->execute([trim($d['nombre']), trim($d['icono']), $d['color_hex'] ?? '#005050']);
echo json_encode(['status' => 'success', 'id' => $pdo->lastInsertId()]);
} elseif ($method === 'PUT') {
$d = json_decode(file_get_contents('php://input'), true);
if (empty($d['id']) || empty($d['nombre']) || empty($d['icono'])) {
http_response_code(400);
die(json_encode(['status' => 'error', 'message' => 'id, nombre e ícono son obligatorios']));
}
$stmt = $pdo->prepare("UPDATE categorias SET nombre=?, icono=?, color_hex=? WHERE id=?");
$stmt->execute([trim($d['nombre']), trim($d['icono']), $d['color_hex'] ?? '#005050', (int)$d['id']]);
echo json_encode(['status' => 'success']);
} elseif ($method === 'DELETE') {
$d = json_decode(file_get_contents('php://input'), true);
if (empty($d['id'])) {
http_response_code(400);
die(json_encode(['status' => 'error', 'message' => 'Falta el ID']));
}
$count = $pdo->prepare("SELECT COUNT(*) FROM movimientos WHERE id_categoria = ?");
$count->execute([(int)$d['id']]);
if ($count->fetchColumn() > 0) {
http_response_code(409);
die(json_encode(['status' => 'error', 'message' => 'No se puede eliminar: tiene movimientos asociados']));
}
$pdo->prepare("DELETE FROM categorias WHERE id = ?")->execute([(int)$d['id']]);
echo json_encode(['status' => 'success']);
} else {
http_response_code(405);
echo json_encode(['status' => 'error', 'message' => 'Método no permitido']);
}