Init: MVP de Gestión de Pagos (Stitch UI + SQLite/PHP)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -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"]
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user