Finança caotica
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>\ud83d\udcb8 Finan\u00e7a Ca\u00f3tica</title>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300;400;500;600;700&family=Bebas+Neue&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet" />
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background: #080810; color: #f0eeff; font-family: 'Space Grotesk', sans-serif; }
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button { -webkit-appearance: none; }
input::placeholder { color: #333; }
select option { background: #12121f; color: #ccc; }
optgroup { color: #666; }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: #0d0d1c; }
::-webkit-scrollbar-thumb { background: #2a2a4a; border-radius: 3px; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useMemo, useRef, useEffect } = React;
const MONTHS = ["Jan","Fev","Mar","Abr","Mai","Jun","Jul","Ago","Set","Out","Nov","Dez"];
const MONTH_NAMES = ["Janeiro","Fevereiro","Mar\u00e7o","Abril","Maio","Junho","Julho","Agosto","Setembro","Outubro","Novembro","Dezembro"];
const CATS_REC = ["Sal\u00e1rio","Freelance","Investimentos","Aluguel","Outros"];
const CATS_DESP = ["Moradia","Alimenta\u00e7\u00e3o","Transporte","Sa\u00fade","Educa\u00e7\u00e3o","Lazer","Vestu\u00e1rio","Contas","Outros"];
const fmt = (v) => Number(v || 0).toLocaleString("pt-BR", { style: "currency", currency: "BRL" });
const parseVal = (s) => {
if (!s) return 0;
const clean = String(s).replace(/[^\d,.-]/g, "").replace(",", ".");
return parseFloat(clean) || 0;
};
const emptyRow = () => ({ id: Date.now() + Math.random(), descricao: "", categoria: "", valor: "", tipo: "despesa" });
const initRows = () => [emptyRow(), emptyRow(), emptyRow(), emptyRow(), emptyRow()];
const initData = () => {
const d = {};
for (let m = 0; m < 12; m++) d[m] = { saldoInicial: "", _rows: null };
return d;
};
const STORAGE_KEY = "financa-caotica-2024";
function loadFromStorage() {
try { const r = localStorage.getItem(STORAGE_KEY); if (r) return JSON.parse(r); } catch(e) {}
return null;
}
function saveToStorage(data) {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); } catch(e) {}
}
const navBtn = { background: "#10101f", border: "1px solid #2a2a4a", color: "#e8e6f0", width: 36, height: 36, borderRadius: 8, cursor: "pointer", fontSize: 18, fontWeight: 700 };
const cellRow = { background: "transparent", border: "none", color: "#ccc", padding: "10px 8px", fontFamily: "'Space Grotesk'", fontSize: 14, outline: "none", width: "100%", borderRight: "1px solid #1a1a2e" };
function App() {
const [view, setView] = useState("planilha");
const [currentMonth, setCurrentMonth] = useState(new Date().getMonth());
const [data, setData] = useState(() => loadFromStorage() || initData());
const [rows, setRows] = useState(() => {
const saved = loadFromStorage();
return (saved && saved[new Date().getMonth()]?._rows) || initRows();
});
const lastRowRef = useRef(null);
const [savedMsg, setSavedMsg] = useState(false);
useEffect(() => {
saveToStorage(data);
setSavedMsg(true);
const t = setTimeout(() => setSavedMsg(false), 1500);
return () => clearTimeout(t);
}, [data]);
useEffect(() => {
const mRows = data[currentMonth]?._rows;
setRows(mRows || initRows());
}, [currentMonth]);
const saldoInicial = parseVal(data[currentMonth]?.saldoInicial);
const totals = useMemo(() => {
let rec = 0, desp = 0;
rows.forEach(r => {
const v = parseVal(r.valor);
if (v <= 0) return;
if (r.tipo === "receita") rec += v; else desp += v;
});
return { rec, desp, saldo: saldoInicial + rec - desp };
}, [rows, saldoInicial]);
const saveRows = (newRows) => {
setRows(newRows);
setData(prev => ({ ...prev, [currentMonth]: { ...prev[currentMonth], _rows: newRows } }));
};
const updateRow = (id, field, value) => saveRows(rows.map(r => r.id === id ? { ...r, [field]: value } : r));
const addRow = () => {
saveRows([...rows, emptyRow()]);
setTimeout(() => lastRowRef.current?.focus(), 50);
};
const removeRow = (id) => { if (rows.length > 1) saveRows(rows.filter(r => r.id !== id)); };
const handleKeyDown = (e, id, field) => {
if ((e.key === "Enter" || e.key === "Tab") && field === "valor" && rows[rows.length-1].id === id) {
e.preventDefault(); addRow();
}
};
const resetMonth = () => {
if (window.confirm(`Apagar todos os lan\u00e7amentos de ${MONTH_NAMES[currentMonth]}?`)) {
const nr = initRows(); setRows(nr);
setData(prev => ({ ...prev, [currentMonth]: { saldoInicial: "", _rows: nr } }));
}
};
const annualStats = useMemo(() => MONTHS.map((_, m) => {
const mRows = data[m]?._rows || [];
const si = parseVal(data[m]?.saldoInicial);
let rec = 0, desp = 0;
mRows.forEach(r => { const v = parseVal(r.valor); if (v <= 0) return; if (r.tipo === "receita") rec += v; else desp += v; });
return { rec, desp, saldo: si + rec - desp, si };
}), [data]);
const totalRec = annualStats.reduce((a,s) => a+s.rec, 0);
const totalDesp = annualStats.reduce((a,s) => a+s.desp, 0);
const maxBar = Math.max(...annualStats.map(s => Math.max(s.rec, s.desp)), 1);
const saldoColor = totals.saldo >= 0 ? "#00e5a0" : "#ff4d6d";
const pct = totals.rec > 0 ? ((totals.desp / totals.rec) * 100).toFixed(0) : 0;
return (
<div style={{ fontFamily: "'Space Grotesk', sans-serif", minHeight: "100vh", background: "#080810", color: "#f0eeff" }}>
{/* HEADER */}
<div style={{ background: "linear-gradient(180deg,#12001a,#0a0a18)", borderBottom: "2px solid #ff2d7830", padding: "14px 20px", display: "flex", alignItems: "center", justifyContent: "space-between", flexWrap: "wrap", gap: 12 }}>
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
<div style={{ width: 44, height: 44, borderRadius: 12, background: "linear-gradient(135deg,#ff2d78,#7c2dff)", display: "flex", alignItems: "center", justifyContent: "center", fontSize: 20, boxShadow: "0 0 20px #ff2d7850", flexShrink: 0 }}>\ud83d\udcb8</div>
<div>
<div style={{ fontFamily: "'Bebas Neue'", fontSize: 28, letterSpacing: 3, lineHeight: 1, background: "linear-gradient(90deg,#ff2d78,#a855f7,#00e5a0)", WebkitBackgroundClip: "text", WebkitTextFillColor: "transparent" }}>Finan\u00e7a Ca\u00f3tica</div>
<div style={{ fontSize: 9, color: "#444", letterSpacing: 2, fontFamily: "Space Mono" }}>
CONTROLE DO CAOS FINANCEIRO
{savedMsg && <span style={{ color: "#00e5a080", marginLeft: 8 }}>\u2713 salvo</span>}
</div>
</div>
</div>
{/* Caixa */}
<div style={{ background: "linear-gradient(135deg,#1a0030,#0d001f)", border: `2px solid ${saldoColor}50`, borderRadius: 14, padding: "10px 20px", boxShadow: `0 0 30px ${saldoColor}20`, textAlign: "center" }}>
<div style={{ fontSize: 9, color: "#666", letterSpacing: 3, fontFamily: "Space Mono", marginBottom: 2 }}>CAIXA ATUAL</div>
<div style={{ fontFamily: "'Space Mono'", fontWeight: 700, fontSize: 22, color: saldoColor, transition: "color .3s" }}>{fmt(totals.saldo)}</div>
<div style={{ fontSize: 10, color: "#444", marginTop: 2 }}>\u2191 {fmt(totals.rec)} | \u2193 {fmt(totals.desp)}</div>
</div>
<div style={{ display: "flex", gap: 8 }}>
{[["planilha","\ud83d\udccb Planilha"],["anual","\ud83d\udcca Anual"]].map(([v, label]) => (
<button key={v} onClick={() => setView(v)} style={{ padding: "7px 14px", borderRadius: 8, cursor: "pointer", fontFamily: "'Space Grotesk'", fontWeight: 600, fontSize: 12, border: view===v ? "2px solid #a855f7" : "2px solid #2a2a3a", background: view===v ? "#a855f720" : "transparent", color: view===v ? "#a855f7" : "#666" }}>{label}</button>
))}
</div>
</div>
{/* PLANILHA */}
{view === "planilha" && (
<div style={{ padding: "20px", maxWidth: 960, margin: "0 auto" }}>
{/* Nav m\u00eas */}
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 16 }}>
<button onClick={() => setCurrentMonth(m => Math.max(0,m-1))} style={navBtn}>\u2039</button>
<div style={{ fontFamily: "Bebas Neue", fontSize: 26, letterSpacing: 3, minWidth: 150, textAlign: "center", color: "#a855f7" }}>{MONTH_NAMES[currentMonth]}</div>
<button onClick={() => setCurrentMonth(m => Math.min(11,m+1))} style={navBtn}>\u203a</button>
<button onClick={resetMonth} style={{ marginLeft: "auto", background: "none", border: "1px solid #ff4d6d30", color: "#ff4d6d60", borderRadius: 8, padding: "5px 10px", cursor: "pointer", fontSize: 11, fontFamily: "Space Mono" }}>\ud83d\uddd1 limpar m\u00eas</button>
</div>
{/* Saldo inicial */}
<div style={{ background: "#10101f", border: "1px solid #2a2a4a", borderRadius: 12, padding: "12px 16px", marginBottom: 14, display: "flex", alignItems: "center", gap: 14, flexWrap: "wrap" }}>
<div style={{ fontSize: 10, color: "#666", fontFamily: "Space Mono", whiteSpace: "nowrap" }}>\ud83d\udcb0 SALDO INICIAL DO M\u00caS</div>
<input type="number" step="0.01" placeholder="0,00"
value={data[currentMonth]?.saldoInicial || ""}
onChange={e => setData(prev => ({ ...prev, [currentMonth]: { ...prev[currentMonth], saldoInicial: e.target.value } }))}
style={{ background: "#0a0a18", border: "1px solid #2a2a4a", color: "#00e5a0", borderRadius: 8, padding: "7px 12px", fontFamily: "Space Mono", fontSize: 15, fontWeight: 700, outline: "none", width: 160 }}
/>
<div style={{ fontSize: 11, color: "#333" }}>\u2190 quanto voc\u00ea tinha no in\u00edcio do m\u00eas</div>
</div>
{/* Cards */}
<div style={{ display: "grid", gridTemplateColumns: "repeat(3,1fr)", gap: 10, marginBottom: 14 }}>
{[
{ label: "ENTRADAS", v: totals.rec, color: "#00e5a0", icon: "\u2191" },
{ label: "SA\u00cdDAS", v: totals.desp, color: "#ff4d6d", icon: "\u2193" },
{ label: "NO CAIXA", v: totals.saldo, color: saldoColor, icon: "\u2261" },
].map(c => (
<div key={c.label} style={{ background: "#10101f", border: `1px solid ${c.color}25`, borderRadius: 12, padding: "12px 16px" }}>
<div style={{ fontSize: 9, color: "#555", letterSpacing: 2, fontFamily: "Space Mono" }}>{c.icon} {c.label}</div>
<div style={{ fontFamily: "Space Mono", fontWeight: 700, fontSize: 18, color: c.color, marginTop: 4 }}>{fmt(c.v)}</div>
</div>
))}
</div>
{/* Tabela */}
<div style={{ background: "#0d0d1c", border: "1px solid #2a2a4a", borderRadius: 14, overflow: "hidden" }}>
{/* Header */}
<div style={{ display: "grid", gridTemplateColumns: "2fr 1.2fr 100px 90px 36px", background: "#16162a", borderBottom: "2px solid #2a2a4a", padding: "9px 12px" }}>
{["Descri\u00e7\u00e3o / Lan\u00e7amento","Categoria","Tipo","Valor (R$)",""].map(h => (
<div key={h} style={{ fontSize: 9, color: "#555", letterSpacing: 2, fontFamily: "Space Mono" }}>{h}</div>
))}
</div>
{/* Linhas */}
{rows.map((row, idx) => {
const v = parseVal(row.valor);
const isRec = row.tipo === "receita";
const hasVal = v > 0;
return (
<div key={row.id} style={{ display: "grid", gridTemplateColumns: "2fr 1.2fr 100px 90px 36px", borderBottom: "1px solid #1a1a2e", background: idx%2===0 ? "#0d0d1c" : "#0f0f20", alignItems: "center", padding: "0 12px" }}>
<input
placeholder={`Lan\u00e7amento ${idx+1}...`}
value={row.descricao}
onChange={e => updateRow(row.id,"descricao",e.target.value)}
onKeyDown={e => handleKeyDown(e,row.id,"descricao")}
ref={idx === rows.length-1 ? lastRowRef : null}
style={{ ...cellRow, fontWeight: hasVal ? 500 : 400 }}
/>
<select value={row.categoria} onChange={e => updateRow(row.id,"categoria",e.target.value)}
style={{ ...cellRow, color: row.categoria ? "#ccc" : "#444", fontSize: 12 }}>
<option value="">-- categoria --</option>
<optgroup label="Receitas">{CATS_REC.map(c => <option key={c}>{c}</option>)}</optgroup>
<optgroup label="Despesas">{CATS_DESP.map(c => <option key={c}>{c}</option>)}</optgroup>
</select>
<select value={row.tipo} onChange={e => updateRow(row.id,"tipo",e.target.value)}
style={{ ...cellRow, color: isRec ? "#00e5a0" : "#ff4d6d", fontWeight: 600, fontSize: 12 }}>
<option value="despesa">\u2193 Sa\u00edda</option>
<option value="receita">\u2191 Entrada</option>
</select>
<input type="number" step="0.01" min="0" placeholder="0,00" value={row.valor}
onChange={e => updateRow(row.id,"valor",e.target.value)}
onKeyDown={e => handleKeyDown(e,row.id,"valor")}
style={{ ...cellRow, fontFamily: "Space Mono", fontWeight: 700, color: hasVal ? (isRec ? "#00e5a0" : "#ff4d6d") : "#333", textAlign: "right" }}
/>
<button onClick={() => removeRow(row.id)}
style={{ background: "none", border: "none", color: "#2a2a4a", cursor: "pointer", fontSize: 16, padding: 0, lineHeight: 1 }}
onMouseEnter={e => e.target.style.color="#ff4d6d"}
onMouseLeave={e => e.target.style.color="#2a2a4a"}>\u2715</button>
</div>
);
})}
{/* Rodap\u00e9 */}
<div style={{ display: "grid", gridTemplateColumns: "2fr 1.2fr 100px 90px 36px", background: "#16162a", borderTop: "2px solid #2a2a4a", padding: "10px 12px", alignItems: "center" }}>
<div style={{ fontSize: 10, color: "#555", fontFamily: "Space Mono" }}>TOTAL DO M\u00caS</div>
<div />
<div style={{ fontSize: 10, color: "#00e5a0", fontFamily: "Space Mono" }}>\u2191 {fmt(totals.rec)}</div>
<div style={{ fontSize: 14, fontFamily: "Space Mono", fontWeight: 700, color: saldoColor, textAlign: "right" }}>{fmt(totals.saldo)}</div>
<div />
</div>
</div>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginTop: 10 }}>
<button onClick={addRow} style={{ padding: "8px 16px", background: "#a855f715", border: "1px dashed #a855f740", color: "#a855f7", borderRadius: 8, cursor: "pointer", fontFamily: "'Space Grotesk'", fontWeight: 600, fontSize: 13 }}>+ Adicionar linha</button>
<div style={{ fontSize: 9, color: "#2a2a4a", fontFamily: "Space Mono" }}>dados salv
Comentários
Postar um comentário