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
+186
View File
@@ -0,0 +1,186 @@
<!DOCTYPE html>
<html class="light" lang="es"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Nuevo Movimiento - Sanctuary</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@300;400;500;600;700;800&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"surface-container-high": "#e6e9e8",
"on-secondary": "#ffffff",
"on-primary-fixed-variant": "#004f4f",
"on-primary-container": "#97e7e6",
"on-background": "#191c1c",
"secondary-fixed-dim": "#b4ccbd",
"inverse-surface": "#2e3131",
"surface-container": "#eceeed",
"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": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
},
"fontFamily": {
"headline": ["Manrope"],
"body": ["Manrope"],
"label": ["Manrope"]
}
},
},
}
</script>
<style>
body { font-family: 'Manrope', sans-serif; }
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
.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>
</head>
<body class="bg-background text-on-background min-h-screen flex flex-col">
<!-- Top AppBar - Transactional Context (Shell Suppressed per mandate) -->
<header class="bg-[#f8faf9] dark:bg-[#191c1c] w-full top-0 px-6 py-4 flex justify-between items-center bg-transparent">
<div class="flex items-center gap-4">
<button class="hover:opacity-80 transition-opacity p-2 -ml-2" onclick="window.location.href='index.html'">
<span class="material-symbols-outlined text-primary dark:text-[#a0f0f0]" data-icon="chevron_left">chevron_left</span>
</button>
<h1 class="font-['Manrope'] headline-sm text-[#191c1c] dark:text-[#f8faf9] font-bold">Nuevo Movimiento</h1>
</div>
<div class="w-10"></div> <!-- Spacer for center-ish balance -->
</header>
<main class="flex-1 px-6 pb-32 max-w-md mx-auto w-full overflow-y-auto">
<!-- Type Selector (Segmented Control) -->
<div class="mt-4 p-1.5 bg-surface-container-low rounded-xl flex items-center mb-8" 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">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>
<!-- Hero Amount Input -->
<div class="relative text-center mb-10 prosperity-glow py-8 rounded-[2rem]">
<label class="block text-xs uppercase tracking-[0.15em] font-bold text-on-surface-variant mb-2">Monto del Movimiento</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" class="text-6xl font-extrabold tracking-tight text-primary">0</span>
</div>
</div>
<!-- Modern Form Fields Grid -->
<div class="space-y-6">
<!-- Category Dropdown -->
<div class="space-y-2">
<label class="text-[11px] uppercase tracking-widest font-bold text-on-surface-variant px-1">Categoría</label>
<select id="ui-categoria" class="bg-surface-container-lowest p-4 rounded-xl w-full font-bold text-on-surface focus:outline-none">
<option value="1">Comida</option>
<option value="2">Vivienda</option>
<option value="3">Transporte</option>
<option value="4">Servicios</option>
<option value="5">Entretenimiento</option>
<option value="6">Ingresos</option>
</select>
</div>
<!-- Date & Notes Grid -->
<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 px-1">Fecha</label>
<div class="bg-surface-container-highest/40 p-4 rounded-xl flex items-center gap-3">
<span class="material-symbols-outlined text-primary text-sm" data-icon="calendar_today">calendar_today</span>
<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>
</button>
</div>
</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">
<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">
<span>Guardar Movimiento</span>
<span class="material-symbols-outlined" data-icon="check_circle">check_circle</span>
</button>
</div>
<!-- Background Decorative Image (Sanctuary Aesthetic) -->
<div class="fixed top-0 right-0 -z-10 opacity-10">
<img alt="Calm plant leaves" class="w-64 h-64 object-cover rounded-full blur-xl translate-x-20 -translate-y-20" data-alt="Close-up of minimalist green tropical leaves with soft natural light and subtle shadows on a neutral background" src="https://lh3.googleusercontent.com/aida-public/AB6AXuD2h_FlCe-zmCdWpoDHshmlKT5cXI45RMNZ5-AgxCqZYUPAm3glpQE_wwgbqJ2nxoYSqTenbalJ4bQQBYGIiBfP5wtSkQt6hOJ-Gtl_cifGTAktK43DvjfdfiHDniAMFOJUG0cJmjTrd3nISnFPn-xNsg3CSTGPMIIW_n5H7HSUQcYBeDxWje7Wu8zDxapTTpMfci3Y4IeXwn-Wq94lmbuXPYPbpNtL19_R_6JNyujOMerXEhtUQm5nUIjIEG27anBV5TGHfaZabw"/>
<script src="js/add.js"></script>
</body></html>
+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']);
}
?>
+7
View File
@@ -0,0 +1,7 @@
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
body {
font-family: 'Manrope', sans-serif;
min-height: max(884px, 100dvh);
}
+2
View File
@@ -0,0 +1,2 @@
*.sqlite
*.sqlite-*
+26
View File
@@ -0,0 +1,26 @@
CREATE TABLE IF NOT EXISTS categorias (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nombre TEXT NOT NULL,
icono TEXT NOT NULL,
color_hex TEXT DEFAULT '#005050'
);
CREATE TABLE IF NOT EXISTS movimientos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
tipo TEXT CHECK( tipo IN ('gasto','ingreso') ) NOT NULL,
monto REAL NOT NULL,
fecha TEXT NOT NULL,
notas TEXT,
id_categoria INTEGER,
fecha_creacion DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY(id_categoria) REFERENCES categorias(id)
);
-- Insertar algunas categorías por defecto
INSERT INTO categorias (nombre, icono, color_hex) VALUES
('Comida', 'restaurant', '#005050'),
('Vivienda', 'home', '#4d6357'),
('Transporte', 'directions_car', '#264b5c'),
('Servicios', 'bolt', '#3e4948'),
('Entretenimiento', 'movie', '#006a6a'),
('Salario / Ingresos', 'payments', '#006a6a');
+235
View File
@@ -0,0 +1,235 @@
<!DOCTYPE html>
<html lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"background": "#f8faf9",
"primary-container": "#006a6a",
"surface-container-lowest": "#ffffff",
"on-primary-fixed": "#002020",
"on-background": "#191c1c",
"secondary-fixed": "#d0e8d9",
"surface-container-high": "#e6e9e8",
"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": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
},
"fontFamily": {
"headline": ["Manrope"],
"body": ["Manrope"],
"label": ["Manrope"]
}
},
},
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
body { font-family: 'Manrope', sans-serif; }
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-background text-on-background min-h-screen pb-32">
<!-- TopAppBar -->
<header class="bg-[#f8faf9] dark:bg-slate-950 docked full-width top-0 sticky z-40">
<div class="flex justify-between items-center w-full px-6 py-4">
<div class="flex items-center gap-3">
<span class="material-symbols-outlined text-[#006a6a] dark:text-teal-400">account_balance_wallet</span>
<h1 class="font-['Manrope'] headline-sm tracking-tight text-xl font-bold text-[#006a6a] dark:text-teal-500">Quiet Wealth</h1>
</div>
<div class="flex items-center gap-2">
<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-[#3e4948] dark:text-slate-400">add_circle</span>
</button>
</div>
</div>
</header>
<main class="px-6 space-y-8 mt-4">
<!-- Screen Title -->
<div class="flex items-end justify-between">
<h2 class="text-3xl font-extrabold tracking-tight text-on-surface">Resumen</h2>
<p class="text-on-surface-variant label-sm tracking-widest uppercase pb-1">Octubre 2023</p>
</div>
<!-- Hero Balance Card: The Prosperity Spark -->
<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-tertiary-fixed opacity-10 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">
<p class="text-on-primary/70 font-label tracking-widest uppercase text-xs mb-2">Balance Total</p>
<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">
<span class="material-symbols-outlined text-sm" style="font-variation-settings: 'FILL' 1;">trending_up</span>
<span class="text-sm font-medium">+12.4% este mes</span>
</div>
</div>
</div>
<!-- Spending Insights: Donut Chart Section -->
<section class="space-y-4">
<h4 class="headline-sm text-on-surface-variant font-semibold px-1">Distribución de Gastos</h4>
<div class="bg-surface-container-low rounded-[2rem] p-8 flex flex-col items-center">
<!-- Visual Donut (CSS Representation) -->
<div id="ui-donut-chart" class="relative w-48 h-48 mb-8">
<svg class="w-full h-full transform -rotate-90" viewbox="0 0 36 36">
<!-- 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>
<div class="absolute inset-0 flex flex-col items-center justify-center">
<span class="text-2xl font-bold">$2,840</span>
<span class="text-[10px] uppercase tracking-widest text-on-surface-variant font-bold">Gastado</span>
</div>
</div>
<!-- Legend -->
<div id="ui-donut-legend" class="w-full grid grid-cols-1 gap-3">
<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">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>
</section>
</main>
<!-- BottomNavBar -->
<nav class="fixed bottom-0 left-0 w-full z-50 flex justify-around items-center px-8 pb-8 pt-4 bg-[#ffffff]/70 dark:bg-slate-900/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">
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 1;">home</span>
</button>
<button class="flex items-center justify-center text-[#3e4948] dark:text-slate-400 p-3 hover:text-[#006a6a] dark:hover:text-teal-300 active:scale-90 transition-transform duration-300">
<span class="material-symbols-outlined">analytics</span>
</button>
<button class="flex items-center justify-center text-[#3e4948] dark:text-slate-400 p-3 hover:text-[#006a6a] dark:hover:text-teal-300 active:scale-90 transition-transform duration-300">
<span class="material-symbols-outlined">credit_card</span>
</button>
<button class="flex items-center justify-center text-[#3e4948] dark:text-slate-400 p-3 hover:text-[#006a6a] dark:hover:text-teal-300 active:scale-90 transition-transform duration-300">
<span class="material-symbols-outlined">person</span>
</button>
<script src="js/dashboard.js"></script>
</body></html>
+94
View File
@@ -0,0 +1,94 @@
document.addEventListener('DOMContentLoaded', () => {
let currentMonto = "0";
let selectedTipo = "gasto"; // default
const montoEl = document.getElementById('ui-monto');
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');
// Default a hoy
iptFecha.valueAsDate = new Date();
// 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');
}
};
btnGasto.addEventListener('click', () => { selectedTipo = 'gasto'; updateTipoUI(); });
btnIngreso.addEventListener('click', () => { selectedTipo = 'ingreso'; updateTipoUI(); });
// Numpad logic
numpad.addEventListener('click', (e) => {
const btn = e.target.closest('button');
if (!btn) return;
const val = btn.textContent.trim() || btn.innerText.trim();
if (val === 'backspace' || btn.querySelector('[data-icon="backspace"]')) {
currentMonto = currentMonto.slice(0, -1);
if (currentMonto === '') currentMonto = "0";
} else {
if (currentMonto === "0" && val !== ".") {
currentMonto = val;
} else {
if (val === "." && currentMonto.includes(".")) return;
currentMonto += val;
}
}
montoEl.textContent = currentMonto;
});
// Guardar Movimiento
btnSave.addEventListener('click', async () => {
const payload = {
tipo: selectedTipo,
monto: parseFloat(currentMonto),
fecha: document.getElementById('ui-fecha').value,
notas: document.getElementById('ui-notas').value,
id_categoria: document.getElementById('ui-categoria').value
};
if (isNaN(payload.monto) || payload.monto <= 0) {
alert('Por favor ingrese un monto mayor a 0');
return;
}
try {
const btnOriginal = btnSave.innerHTML;
btnSave.innerHTML = 'Guardando...';
btnSave.disabled = true;
const r = await fetch('api/movimientos.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const data = await r.json();
if (data.status === 'success') {
window.location.href = 'index.html'; // Redirige al inicio tras guardar
} else {
alert('Error al guardar: ' + data.message);
btnSave.innerHTML = btnOriginal;
btnSave.disabled = false;
}
} catch (err) {
console.error(err);
alert('Fallo en la comunicación');
btnSave.disabled = false;
}
});
});
+80
View File
@@ -0,0 +1,80 @@
document.addEventListener('DOMContentLoaded', async () => {
try {
const response = await fetch('api/movimientos.php');
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 += `
<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>`;
document.getElementById('ui-donut-chart').innerHTML = svgHtml;
document.getElementById('ui-donut-legend').innerHTML = legendHtml;
} 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>`;
document.getElementById('ui-donut-legend').innerHTML = '';
}
}
} catch (e) {
console.error('Error cargando el dashboard:', e);
}
});
+67
View File
@@ -0,0 +1,67 @@
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"background": "#f8faf9",
"primary-container": "#006a6a",
"surface-container-lowest": "#ffffff",
"on-primary-fixed": "#002020",
"on-background": "#191c1c",
"secondary-fixed": "#d0e8d9",
"surface-container-high": "#e6e9e8",
"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": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
},
"fontFamily": {
"headline": ["Manrope"],
"body": ["Manrope"],
"label": ["Manrope"]
}
},
},
}