Init: MVP de Gestión de Pagos (Stitch UI + SQLite/PHP)

This commit is contained in:
Ricardo Monla
2026-04-08 23:39:24 -03:00
commit aeceba9095
12 changed files with 870 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
<?php
// pagos/api/auth.php
// Protección súper rápida con Basic Auth.
// Cámbiale la contraseña o envuélvelo en las variables de entorno de tu contenedor.
$valido_usuario = 'root';
$valido_pass = 'pagos123';
if (!isset($_SERVER['PHP_AUTH_USER']) || $_SERVER['PHP_AUTH_USER'] !== $valido_usuario || $_SERVER['PHP_AUTH_PW'] !== $valido_pass) {
header('WWW-Authenticate: Basic realm="Sanctuary Finanzas"');
header('HTTP/1.0 401 Unauthorized');
die("Acceso denegado.");
}
?>
+31
View File
@@ -0,0 +1,31 @@
<?php
// pagos/api/db.php
$db_file = __DIR__ . '/../db/finanzas.sqlite';
$schema_file = __DIR__ . '/../db/schema.sql';
// Función para obtener conexión PDO a SQLite
function getDB() {
global $db_file, $schema_file;
$is_new = !file_exists($db_file);
try {
$pdo = new PDO("sqlite:" . $db_file);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// Si no existía, crear la BD desde schema y establecer permisos
if ($is_new && file_exists($schema_file)) {
$schema = file_get_contents($schema_file);
$pdo->exec($schema);
// Intentar dar permisos para www-data en el servidor
@chmod($db_file, 0666);
@chmod(dirname($db_file), 0777);
}
return $pdo;
} catch (PDOException $e) {
http_response_code(500);
die(json_encode(['error' => 'Connection failed: ' . $e->getMessage()]));
}
}
?>
+80
View File
@@ -0,0 +1,80 @@
<?php
// pagos/api/movimientos.php
require_once 'auth.php'; // Protección de acceso
header('Content-Type: application/json');
require_once 'db.php';
$pdo = getDB();
$method = $_SERVER['REQUEST_METHOD'];
if ($method === 'GET') {
// Retornar balance, datos para gráfico y últimos movimientos
try {
// 1. Balance total
$stmtBalance = $pdo->query("SELECT
SUM(CASE WHEN tipo = 'ingreso' THEN monto ELSE -monto END) as balance
FROM movimientos");
$balance = $stmtBalance->fetch(PDO::FETCH_ASSOC)['balance'] ?? 0;
// 2. Últimos movimientos
$stmtMovi = $pdo->query("SELECT m.*, c.nombre as categoria_nombre, c.icono as categoria_icono
FROM movimientos m
LEFT JOIN categorias c ON m.id_categoria = c.id
ORDER BY m.fecha DESC, m.id DESC LIMIT 5");
$movimientos = $stmtMovi->fetchAll(PDO::FETCH_ASSOC);
// 3. Agrupación por categoría para gráfico donut (sólo gastos de este mes/año)
// Por simplicidad traemos todos los gastos
$stmtGraph = $pdo->query("SELECT c.nombre, c.color_hex, SUM(m.monto) as total
FROM movimientos m
JOIN categorias c ON m.id_categoria = c.id
WHERE m.tipo = 'gasto'
GROUP BY c.id
ORDER BY total DESC");
$grafico = $stmtGraph->fetchAll(PDO::FETCH_ASSOC);
// 4. Categorías para el modal de nuevo registro
$stmtCat = $pdo->query("SELECT * FROM categorias");
$categorias = $stmtCat->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
'status' => 'success',
'balance' => (float)$balance,
'movimientos' => $movimientos,
'grafico' => $grafico,
'categorias' => $categorias
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
}
} elseif ($method === 'POST') {
// Insertar nuevo movimiento
$data = json_decode(file_get_contents('php://input'), true);
// Validación mínima
if (!isset($data['tipo'], $data['monto'], $data['id_categoria'], $data['fecha'])) {
http_response_code(400);
die(json_encode(['status' => 'error', 'message' => 'Faltan campos obligatorios']));
}
try {
$stmt = $pdo->prepare("INSERT INTO movimientos (tipo, monto, fecha, notas, id_categoria) VALUES (?, ?, ?, ?, ?)");
$stmt->execute([
$data['tipo'],
(float) $data['monto'],
$data['fecha'],
$data['notas'] ?? '',
(int) $data['id_categoria']
]);
echo json_encode(['status' => 'success', 'id' => $pdo->lastInsertId()]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['status' => 'error', 'message' => $e->getMessage()]);
}
} else {
http_response_code(405);
echo json_encode(['status' => 'error', 'message' => 'Método no permitido']);
}
?>