import { useState, useEffect, useRef } from "react";
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts";
const STORAGE_KEY = "finance_tracker_v2";
const SNAPSHOTS_KEY = "finance_tracker_snapshots_v2";
const initialData = {
assets: [
{ id: "cat-1", name: "Cash & Bank", accounts: [
{ id: "a-1", name: "Checking Account", balance: 0, type: "checking" },
{ id: "a-2", name: "Savings Account", balance: 0, type: "savings" },
]},
{ id: "cat-2", name: "Investments", accounts: [
{ id: "a-3", name: "Investment Portfolio", balance: 0, type: "investment" },
]},
],
liabilities: [
{ id: "cat-3", name: "Credit Cards", accounts: [
{ id: "a-4", name: "Credit Card", balance: 0, type: "credit" },
]},
{ id: "cat-4", name: "Loans", accounts: [
{ id: "a-5", name: "Student Loan", balance: 0, type: "loan" },
]},
],
};
// ── YNAB Color Palette ────────────────────────────────────────────────────────
const C = {
bg: "#f5f5f0", // warm off-white page bg
sidebar: "#2a2f4e", // YNAB deep navy sidebar
sidebarHover: "#363c5c",
sidebarActive: "#1a1f3a",
accent: "#0fa3b1", // YNAB teal
accentLight: "#e6f7f9",
positive: "#16a34a",
positiveLight: "#dcfce7",
negative: "#dc2626",
negativeLight: "#fee2e2",
cardBg: "#ffffff",
border: "#e5e5e0",
borderStrong: "#d0d0c8",
text: "#1a1a2e",
textMid: "#555560",
textLight: "#9898a0",
catHeader: "#f0eff8", // light lavender category headers like YNAB
catHeaderText: "#3a3a5c",
inputBg: "#ffffff",
inputBorder: "#c8c8c0",
shadow: "0 1px 3px rgba(0,0,0,0.08)",
shadowMd: "0 2px 8px rgba(0,0,0,0.10)",
};
const accountIcons = { checking: "🏦", savings: "💰", investment: "📈", credit: "💳", loan: "📋", other: "◎" };
const MONTHS = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
function formatCurrency(val) {
return new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0 }).format(val || 0);
}
function formatDate(iso) {
const d = new Date(iso);
return `${MONTHS[d.getMonth()]} ${d.getFullYear()}`;
}
function uid() { return "id-" + Date.now() + "-" + Math.random().toString(36).slice(2, 7); }
function sumSection(cats) {
return cats.reduce((s, cat) => s + cat.accounts.reduce((ss, a) => ss + Number(a.balance || 0), 0), 0);
}
// ── Inline editable text ──────────────────────────────────────────────────────
function InlineEdit({ value, onCommit, style = {}, inputStyle = {} }) {
const [editing, setEditing] = useState(false);
const [val, setVal] = useState(value);
const ref = useRef();
useEffect(() => { if (editing) ref.current?.select(); }, [editing]);
useEffect(() => { setVal(value); }, [value]);
const commit = () => { setEditing(false); onCommit(val.trim() || value); };
if (editing) return (
<input ref={ref} value={val} onChange={e => setVal(e.target.value)}
onBlur={commit}
onKeyDown={e => { if (e.key === "Enter") commit(); if (e.key === "Escape") { setVal(value); setEditing(false); } }}
style={{ border: `1.5px solid ${C.accent}`, borderRadius: "4px", padding: "2px 7px", fontSize: "inherit", fontFamily: "inherit", outline: "none", background: C.inputBg, color: C.text, width: "100%", ...inputStyle }}
/>
);
return (
<span onClick={() => setEditing(true)} title="Click to rename"
style={{ cursor: "text", borderBottom: "1px dashed transparent", transition: "border-color 0.15s", ...style }}
onMouseEnter={e => e.currentTarget.style.borderBottomColor = C.accent}
onMouseLeave={e => e.currentTarget.style.borderBottomColor = "transparent"}
>{value}</span>
);
}
// ── Balance inline edit ───────────────────────────────────────────────────────
function BalanceEdit({ value, onCommit }) {
const [editing, setEditing] = useState(false);
const [val, setVal] = useState(String(value));
const ref = useRef();
useEffect(() => { if (editing) { setVal(String(value)); ref.current?.select(); } }, [editing]);
const commit = () => { setEditing(false); onCommit(parseFloat(val) || 0); };
if (editing) return (
<input ref={ref} value={val} onChange={e => setVal(e.target.value)} type="number"
onBlur={commit}
onKeyDown={e => { if (e.key === "Enter") commit(); if (e.key === "Escape") setEditing(false); }}
style={{ width: "110px", border: `1.5px solid ${C.accent}`, borderRadius: "4px", padding: "3px 8px", fontSize: "13px", textAlign: "right", outline: "none", background: C.inputBg, color: C.text, fontFamily: "inherit" }}
/>
);
const isNeg = Number(value) < 0;
return (
<span onClick={() => setEditing(true)} title="Click to edit"
style={{ fontSize: "13px", fontWeight: "600", color: isNeg ? C.negative : C.text, cursor: "pointer", minWidth: "90px", textAlign: "right", display: "inline-block", padding: "2px 4px", borderRadius: "3px", transition: "background 0.12s" }}
onMouseEnter={e => e.currentTarget.style.background = C.accentLight}
onMouseLeave={e => e.currentTarget.style.background = "transparent"}
>{formatCurrency(value)}</span>
);
}
// ── Main App ──────────────────────────────────────────────────────────────────
export default function FinanceTracker() {
const [view, setView] = useState("dashboard");
const [data, setData] = useState(() => {
try { const s = localStorage.getItem(STORAGE_KEY); return s ? JSON.parse(s) : initialData; } catch { return initialData; }
});
const [snapshots, setSnapshots] = useState(() => {
try { const s = localStorage.getItem(SNAPSHOTS_KEY); return s ? JSON.parse(s) : []; } catch { return []; }
});
const [snapshotSaved, setSnapshotSaved] = useState(false);
const drag = useRef(null);
const [dragOver, setDragOver] = useState(null);
const [draggingId, setDraggingId] = useState(null);
useEffect(() => { localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); }, [data]);
useEffect(() => { localStorage.setItem(SNAPSHOTS_KEY, JSON.stringify(snapshots)); }, [snapshots]);
const totalAssets = sumSection(data.assets);
const totalLiabilities = sumSection(data.liabilities);
const netWorth = totalAssets - totalLiabilities;
const updateData = fn => setData(prev => { const next = JSON.parse(JSON.stringify(prev)); fn(next); return next; });
const renameCat = (section, catId, name) => updateData(d => { const c = d[section].find(x => x.id === catId); if (c) c.name = name; });
const deleteCat = (section, catId) => updateData(d => {
const cats = d[section], idx = cats.findIndex(c => c.id === catId);
if (idx === -1) return;
const orphans = cats[idx].accounts;
cats.splice(idx, 1);
if (cats.length > 0 && orphans.length > 0) cats[0].accounts.push(...orphans);
});
const addCat = (section) => updateData(d => { d[section].push({ id: uid(), name: "New Category", accounts: [] }); });
const updateAccount = (section, catId, accountId, patch) => updateData(d => {
const cat = d[section].find(c => c.id === catId); if (!cat) return;
const acc = cat.accounts.find(a => a.id === accountId); if (acc) Object.assign(acc, patch);
});
const deleteAccount = (section, catId, accountId) => updateData(d => {
const cat = d[section].find(c => c.id === catId);
if (cat) cat.accounts = cat.accounts.filter(a => a.id !== accountId);
});
const addAccount = (section, catId, account) => updateData(d => {
const cat = d[section].find(c => c.id === catId);
if (cat) cat.accounts.push({ id: uid(), ...account });
});
const onDragStart = (e, accountId, section, catId) => {
drag.current = { accountId, section, catId }; setDraggingId(accountId); e.dataTransfer.effectAllowed = "move";
};
const onDragEnd = () => { drag.current = null; setDraggingId(null); setDragOver(null); };
const onDragOver = (e, catId) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; setDragOver(catId); };
const onDrop = (e, targetSection, targetCatId) => {
e.preventDefault(); setDragOver(null);
if (!drag.current) return;
const { accountId, section: fromSection, catId: fromCatId } = drag.current;
if (fromCatId === targetCatId) return;
updateData(d => {
const fromCat = d[fromSection].find(c => c.id === fromCatId);
const toCat = d[targetSection].find(c => c.id === targetCatId);
if (!fromCat || !toCat) return;
const idx = fromCat.accounts.findIndex(a => a.id === accountId);
if (idx === -1) return;
const [acc] = fromCat.accounts.splice(idx, 1);
toCat.accounts.push(acc);
});
};
const saveSnapshot = () => {
const snap = { date: new Date().toISOString(), assets: totalAssets, liabilities: totalLiabilities, netWorth, data: JSON.parse(JSON.stringify(data)) };
setSnapshots(prev => {
const filtered = prev.filter(s => formatDate(s.date) !== formatDate(snap.date));
return [...filtered, snap].sort((a, b) => new Date(a.date) - new Date(b.date));
});
setSnapshotSaved(true); setTimeout(() => setSnapshotSaved(false), 2000);
};
const chartData = snapshots.map(s => ({ date: formatDate(s.date), Assets: s.assets, Liabilities: s.liabilities, "Net Worth": s.netWorth }));
const nwPositive = netWorth >= 0;
const navItems = [["dashboard","Balance Sheet"],["reports","Reports & Trends"]];
return (
<div style={{ display: "flex", minHeight: "100vh", fontFamily: "'Verdana', Geneva, sans-serif", background: C.bg, color: C.text }}>
{/* ── Sidebar ── */}
<div style={{ width: "220px", flexShrink: 0, background: C.sidebar, display: "flex", flexDirection: "column", padding: "0" }}>
{/* Logo area */}
<div style={{ padding: "24px 20px 20px", borderBottom: "1px solid rgba(255,255,255,0.07)" }}>
<div style={{ fontSize: "10px", color: "rgba(255,255,255,0.4)", letterSpacing: "0.12em", textTransform: "uppercase", marginBottom: "4px" }}>My Finances</div>
<div style={{ fontSize: "17px", fontWeight: "700", color: "#ffffff", letterSpacing: "-0.01em" }}>Net Worth</div>
<div style={{ fontSize: "22px", fontWeight: "700", marginTop: "6px", color: nwPositive ? "#4ade80" : "#f87171" }}>{formatCurrency(netWorth)}</div>
</div>
{/* Summary */}
<div style={{ padding: "14px 20px", borderBottom: "1px solid rgba(255,255,255,0.07)" }}>
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: "8px" }}>
<span style={{ fontSize: "11px", color: "rgba(255,255,255,0.45)" }}>Assets</span>
<span style={{ fontSize: "11px", fontWeight: "600", color: "#4ade80" }}>{formatCurrency(totalAssets)}</span>
</div>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<span style={{ fontSize: "11px", color: "rgba(255,255,255,0.45)" }}>Liabilities</span>
<span style={{ fontSize: "11px", fontWeight: "600", color: "#f87171" }}>{formatCurrency(totalLiabilities)}</span>
</div>
</div>
{/* Nav */}
<nav style={{ padding: "12px 10px", flex: 1 }}>
{navItems.map(([v, label]) => (
<button key={v} onClick={() => setView(v)} style={{
display: "block", width: "100%", textAlign: "left",
padding: "9px 12px", marginBottom: "2px",
background: view === v ? C.accent : "transparent",
color: view === v ? "#ffffff" : "rgba(255,255,255,0.55)",
border: "none", borderRadius: "6px", cursor: "pointer",
fontSize: "12px", fontWeight: view === v ? "600" : "400",
fontFamily: "inherit", transition: "all 0.15s",
}}
onMouseEnter={e => { if (view !== v) e.currentTarget.style.background = C.sidebarHover; e.currentTarget.style.color = "#fff"; }}
onMouseLeave={e => { if (view !== v) { e.currentTarget.style.background = "transparent"; e.currentTarget.style.color = "rgba(255,255,255,0.55)"; } }}
>{label}</button>
))}
</nav>
{/* Snapshot button */}
<div style={{ padding: "16px 12px", borderTop: "1px solid rgba(255,255,255,0.07)" }}>
<button onClick={saveSnapshot} style={{
width: "100%", padding: "10px 12px",
background: snapshotSaved ? "#16a34a" : C.accent,
color: "#ffffff", border: "none", borderRadius: "6px",
cursor: "pointer", fontSize: "11px", fontWeight: "600",
fontFamily: "inherit", transition: "background 0.25s",
}}>
{snapshotSaved ? "✓ Snapshot Saved!" : "Save Monthly Snapshot"}
</button>
{snapshots.length > 0 && (
<div style={{ textAlign: "center", marginTop: "7px", fontSize: "10px", color: "rgba(255,255,255,0.3)" }}>
{snapshots.length} snapshot{snapshots.length > 1 ? "s" : ""} saved
</div>
)}
</div>
</div>
{/* ── Main Content ── */}
<div style={{ flex: 1, overflowY: "auto" }}>
{/* Top bar */}
<div style={{ background: C.cardBg, borderBottom: `1px solid ${C.border}`, padding: "14px 32px", display: "flex", alignItems: "center", boxShadow: C.shadow }}>
<div style={{ fontSize: "15px", fontWeight: "700", color: C.text }}>
{view === "dashboard" ? "Balance Sheet" : "Reports & Trends"}
</div>
</div>
<div style={{ padding: "24px 32px", maxWidth: "1000px" }}>
{view === "dashboard" && (
<>
{/* Net worth summary cards */}
<div style={{ display: "grid", gridTemplateColumns: "repeat(3,1fr)", gap: "14px", marginBottom: "28px" }}>
{[
{ label: "Net Worth", value: formatCurrency(netWorth), color: nwPositive ? C.positive : C.negative, bg: nwPositive ? C.positiveLight : C.negativeLight },
{ label: "Total Assets", value: formatCurrency(totalAssets), color: C.positive, bg: C.positiveLight },
{ label: "Total Liabilities", value: formatCurrency(totalLiabilities), color: C.negative, bg: C.negativeLight },
].map(s => (
<div key={s.label} style={{ background: s.bg, border: `1px solid ${C.border}`, borderRadius: "10px", padding: "16px 20px", boxShadow: C.shadow }}>
<div style={{ fontSize: "10px", fontWeight: "700", letterSpacing: "0.08em", textTransform: "uppercase", color: C.textMid, marginBottom: "6px" }}>{s.label}</div>
<div style={{ fontSize: "20px", fontWeight: "700", color: s.color }}>{s.value}</div>
</div>
))}
</div>
{/* Two-column layout */}
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "24px" }}>
<SectionPanel section="assets" title="Assets" accentColor={C.positive} categories={data.assets} totalSection={totalAssets}
renameCat={renameCat} deleteCat={deleteCat} addCat={addCat}
updateAccount={updateAccount} deleteAccount={deleteAccount} addAccount={addAccount}
onDragStart={onDragStart} onDragEnd={onDragEnd} onDragOver={onDragOver} onDrop={onDrop}
dragOver={dragOver} draggingId={draggingId}
/>
<SectionPanel section="liabilities" title="Liabilities" accentColor={C.negative} categories={data.liabilities} totalSection={totalLiabilities}
renameCat={renameCat} deleteCat={deleteCat} addCat={addCat}
updateAccount={updateAccount} deleteAccount={deleteAccount} addAccount={addAccount}
onDragStart={onDragStart} onDragEnd={onDragEnd} onDragOver={onDragOver} onDrop={onDrop}
dragOver={dragOver} draggingId={draggingId}
/>
</div>
</>
)}
{view === "reports" && <ReportsView snapshots={snapshots} chartData={chartData} />}
</div>
</div>
</div>
);
}
// ── Section Panel ─────────────────────────────────────────────────────────────
function SectionPanel({ section, title, accentColor, categories, totalSection, renameCat, deleteCat, addCat, updateAccount, deleteAccount, addAccount, onDragStart, onDragEnd, onDragOver, onDrop, dragOver, draggingId }) {
return (
<div>
{/* Section header */}
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "12px" }}>
<div style={{ fontSize: "13px", fontWeight: "700", color: C.text, textTransform: "uppercase", letterSpacing: "0.06em" }}>{title}</div>
<div style={{ fontSize: "13px", fontWeight: "700", color: accentColor }}>{formatCurrency(totalSection)}</div>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
{categories.map(cat => (
<CategoryBlock key={cat.id} cat={cat} section={section} accentColor={accentColor}
renameCat={renameCat} deleteCat={deleteCat}
updateAccount={updateAccount} deleteAccount={deleteAccount} addAccount={addAccount}
onDragStart={onDragStart} onDragEnd={onDragEnd} onDragOver={onDragOver} onDrop={onDrop}
dragOver={dragOver} draggingId={draggingId}
/>
))}
<button onClick={() => addCat(section)} style={{
background: "transparent", border: `1.5px dashed ${C.borderStrong}`,
color: C.textLight, padding: "9px 14px", borderRadius: "8px",
cursor: "pointer", fontSize: "12px", fontWeight: "600",
fontFamily: "inherit", transition: "all 0.15s", textAlign: "left",
}}
onMouseEnter={e => { e.currentTarget.style.borderColor = accentColor; e.currentTarget.style.color = accentColor; e.currentTarget.style.background = C.accentLight; }}
onMouseLeave={e => { e.currentTarget.style.borderColor = C.borderStrong; e.currentTarget.style.color = C.textLight; e.currentTarget.style.background = "transparent"; }}>
+ Add Category
</button>
</div>
</div>
);
}
// ── Category Block ────────────────────────────────────────────────────────────
function CategoryBlock({ cat, section, accentColor, renameCat, deleteCat, updateAccount, deleteAccount, addAccount, onDragStart, onDragEnd, onDragOver, onDrop, dragOver, draggingId }) {
const [addingAccount, setAddingAccount] = useState(false);
const [newAcc, setNewAcc] = useState({ name: "", balance: "", type: section === "assets" ? "checking" : "credit" });
const catTotal = cat.accounts.reduce((s, a) => s + Number(a.balance || 0), 0);
const isOver = dragOver === cat.id;
const submitAdd = () => {
if (!newAcc.name.trim()) return;
addAccount(section, cat.id, { name: newAcc.name.trim(), balance: parseFloat(newAcc.balance) || 0, type: newAcc.type });
setNewAcc({ name: "", balance: "", type: section === "assets" ? "checking" : "credit" });
setAddingAccount(false);
};
return (
<div
onDragOver={e => onDragOver(e, cat.id)}
onDrop={e => onDrop(e, section, cat.id)}
style={{
background: C.cardBg,
border: `1.5px solid ${isOver ? accentColor : C.border}`,
borderRadius: "10px",
boxShadow: isOver ? `0 0 0 3px ${accentColor}22` : C.shadow,
overflow: "hidden",
transition: "border-color 0.15s, box-shadow 0.15s",
}}
>
{/* Category header */}
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "10px 14px", background: C.catHeader, borderBottom: `1px solid ${C.border}` }}>
<div style={{ display: "flex", alignItems: "center", gap: "7px", flex: 1, minWidth: 0 }}>
<span style={{ fontSize: "10px", color: accentColor }}>▶</span>
<InlineEdit value={cat.name} onCommit={name => renameCat(section, cat.id, name)}
style={{ fontSize: "11px", fontWeight: "700", letterSpacing: "0.06em", textTransform: "uppercase", color: C.catHeaderText }} />
</div>
<div style={{ display: "flex", alignItems: "center", gap: "10px", flexShrink: 0 }}>
<span style={{ fontSize: "12px", fontWeight: "700", color: catTotal < 0 ? C.negative : C.textMid }}>{catTotal !== 0 ? formatCurrency(catTotal) : "—"}</span>
<button onClick={() => deleteCat(section, cat.id)} title="Delete category"
style={{ background: "none", border: "none", color: C.borderStrong, cursor: "pointer", fontSize: "16px", padding: "0", lineHeight: 1, transition: "color 0.15s" }}
onMouseEnter={e => e.currentTarget.style.color = C.negative}
onMouseLeave={e => e.currentTarget.style.color = C.borderStrong}>×</button>
</div>
</div>
{/* Accounts */}
<div>
{cat.accounts.length === 0 && !addingAccount && (
<div style={{ padding: "12px 14px", fontSize: "11px", color: C.textLight, fontStyle: "italic" }}>
Drop accounts here
</div>
)}
{cat.accounts.map((account, idx) => (
<AccountRow key={account.id} account={account} section={section} catId={cat.id} accentColor={accentColor}
isLast={idx === cat.accounts.length - 1 && !addingAccount}
dragging={draggingId === account.id}
onDragStart={onDragStart} onDragEnd={onDragEnd}
onUpdateName={name => updateAccount(section, cat.id, account.id, { name })}
onUpdateBalance={balance => updateAccount(section, cat.id, account.id, { balance })}
onDelete={() => deleteAccount(section, cat.id, account.id)}
/>
))}
{addingAccount ? (
<div style={{ padding: "12px 14px", borderTop: `1px solid ${C.border}`, display: "flex", flexDirection: "column", gap: "8px" }}>
<input placeholder="Account name" value={newAcc.name} onChange={e => setNewAcc(p => ({ ...p, name: e.target.value }))}
style={iStyle} autoFocus onKeyDown={e => e.key === "Escape" && setAddingAccount(false)} />
<div style={{ display: "flex", gap: "6px" }}>
<input placeholder="Balance" type="number" value={newAcc.balance} onChange={e => setNewAcc(p => ({ ...p, balance: e.target.value }))}
onKeyDown={e => e.key === "Enter" && submitAdd()}
style={{ ...iStyle, flex: 1 }} />
<select value={newAcc.type} onChange={e => setNewAcc(p => ({ ...p, type: e.target.value }))} style={{ ...iStyle, flex: 1 }}>
{(section === "assets"
? [["checking","Checking"],["savings","Savings"],["investment","Investment"],["other","Other"]]
: [["credit","Credit Card"],["loan","Loan"],["other","Other"]]
).map(([v, l]) => <option key={v} value={v}>{l}</option>)}
</select>
</div>
<div style={{ display: "flex", gap: "6px" }}>
<button onClick={submitAdd} style={{ ...bStyle, background: accentColor, color: "#fff", border: "none" }}>Add Account</button>
<button onClick={() => setAddingAccount(false)} style={bStyle}>Cancel</button>
</div>
</div>
) : (
<button onClick={() => setAddingAccount(true)}
style={{ width: "100%", background: "none", border: "none", borderTop: `1px solid ${C.border}`, color: C.textLight, cursor: "pointer", padding: "9px 14px", fontSize: "11px", fontWeight: "600", textAlign: "left", fontFamily: "inherit", transition: "all 0.13s" }}
onMouseEnter={e => { e.currentTarget.style.color = accentColor; e.currentTarget.style.background = C.accentLight; }}
onMouseLeave={e => { e.currentTarget.style.color = C.textLight; e.currentTarget.style.background = "transparent"; }}>
+ Add Account
</button>
)}
</div>
</div>
);
}
// ── Account Row ───────────────────────────────────────────────────────────────
function AccountRow({ account, section, catId, accentColor, dragging, isLast, onDragStart, onDragEnd, onUpdateName, onUpdateBalance, onDelete }) {
const [hovered, setHovered] = useState(false);
return (
<div
draggable
onDragStart={e => onDragStart(e, account.id, section, catId)}
onDragEnd={onDragEnd}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{
display: "flex", alignItems: "center", gap: "8px",
padding: "9px 14px",
borderBottom: isLast ? "none" : `1px solid ${C.border}`,
opacity: dragging ? 0.3 : 1,
cursor: "grab",
background: hovered && !dragging ? "#f8f8f5" : "transparent",
transition: "background 0.1s, opacity 0.15s",
}}
>
<span style={{ color: hovered ? C.textLight : C.border, fontSize: "12px", userSelect: "none", flexShrink: 0, transition: "color 0.12s" }}>⠿</span>
<span style={{ fontSize: "14px", flexShrink: 0, width: "18px" }}>{accountIcons[account.type] || "◎"}</span>
<span style={{ flex: 1, fontSize: "12px", color: C.text, minWidth: 0 }}>
<InlineEdit value={account.name} onCommit={onUpdateName} style={{ color: C.text }} />
</span>
<BalanceEdit value={account.balance} onCommit={onUpdateBalance} />
<button onClick={onDelete}
style={{ background: "none", border: "none", color: "transparent", cursor: "pointer", fontSize: "15px", padding: "0 2px", lineHeight: 1, flexShrink: 0, transition: "color 0.13s" }}
onMouseEnter={e => e.currentTarget.style.color = C.negative}
onMouseLeave={e => e.currentTarget.style.color = "transparent"}>×</button>
</div>
);
}
// ── Reports View ──────────────────────────────────────────────────────────────
function ReportsView({ snapshots, chartData }) {
const [metric, setMetric] = useState("Net Worth");
if (snapshots.length === 0) return (
<div style={{ textAlign: "center", padding: "80px 40px", color: C.textLight }}>
<div style={{ fontSize: "40px", marginBottom: "16px" }}>📊</div>
<div style={{ fontSize: "15px", fontWeight: "700", color: C.textMid, marginBottom: "8px" }}>No history yet</div>
<div style={{ fontSize: "12px", color: C.textLight }}>Save monthly snapshots from the Balance Sheet to track your progress over time.</div>
</div>
);
const latest = snapshots[snapshots.length - 1];
const previous = snapshots.length > 1 ? snapshots[snapshots.length - 2] : null;
const nwChange = previous ? latest.netWorth - previous.netWorth : null;
const chartColors = { "Net Worth": C.accent, Assets: C.positive, Liabilities: C.negative };
return (
<>
<div style={{ display: "grid", gridTemplateColumns: "repeat(3,1fr)", gap: "14px", marginBottom: "24px" }}>
{[
{ label: "Current Net Worth", value: formatCurrency(latest.netWorth), color: latest.netWorth >= 0 ? C.positive : C.negative, bg: latest.netWorth >= 0 ? C.positiveLight : C.negativeLight },
{ label: "Month-over-Month", value: nwChange !== null ? (nwChange >= 0 ? "+" : "") + formatCurrency(nwChange) : "—", color: nwChange !== null ? (nwChange >= 0 ? C.positive : C.negative) : C.textMid, bg: nwChange !== null ? (nwChange >= 0 ? C.positiveLight : C.negativeLight) : "#f5f5f0" },
{ label: "Snapshots Saved", value: snapshots.length, color: C.accent, bg: C.accentLight },
].map(s => (
<div key={s.label} style={{ background: s.bg, border: `1px solid ${C.border}`, borderRadius: "10px", padding: "16px 20px", boxShadow: C.shadow }}>
<div style={{ fontSize: "10px", fontWeight: "700", letterSpacing: "0.08em", textTransform: "uppercase", color: C.textMid, marginBottom: "6px" }}>{s.label}</div>
<div style={{ fontSize: "22px", fontWeight: "700", color: s.color }}>{s.value}</div>
</div>
))}
</div>
{/* Chart card */}
<div style={{ background: C.cardBg, border: `1px solid ${C.border}`, borderRadius: "10px", padding: "20px", marginBottom: "20px", boxShadow: C.shadow }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "16px" }}>
<div style={{ fontSize: "13px", fontWeight: "700", color: C.text }}>Historical Trend</div>
<div style={{ display: "flex", gap: "6px" }}>
{["Net Worth","Assets","Liabilities"].map(m => (
<button key={m} onClick={() => setMetric(m)} style={{
padding: "5px 12px", borderRadius: "20px",
background: metric === m ? chartColors[m] : "transparent",
color: metric === m ? "#fff" : C.textMid,
border: `1.5px solid ${metric === m ? chartColors[m] : C.borderStrong}`,
cursor: "pointer", fontSize: "11px", fontWeight: "600", fontFamily: "inherit", transition: "all 0.15s",
}}>{m}</button>
))}
</div>
</div>
<ResponsiveContainer width="100%" height={240}>
<AreaChart data={chartData} margin={{ top: 8, right: 8, left: 8, bottom: 0 }}>
<defs>
<linearGradient id="grad" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={chartColors[metric]} stopOpacity={0.18} />
<stop offset="95%" stopColor={chartColors[metric]} stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid stroke={C.border} strokeDasharray="3 3" />
<XAxis dataKey="date" tick={{ fill: C.textLight, fontSize: 10, fontFamily: "Verdana" }} axisLine={false} tickLine={false} />
<YAxis tick={{ fill: C.textLight, fontSize: 10, fontFamily: "Verdana" }} axisLine={false} tickLine={false}
tickFormatter={v => "$" + (Math.abs(v) >= 1000 ? (v/1000).toFixed(0) + "k" : v)} />
<Tooltip contentStyle={{ background: C.cardBg, border: `1px solid ${C.border}`, borderRadius: "8px", fontSize: "12px", boxShadow: C.shadowMd }}
labelStyle={{ color: C.textMid, fontWeight: "700" }} formatter={v => [formatCurrency(v), metric]} />
<Area type="monotone" dataKey={metric} stroke={chartColors[metric]} strokeWidth={2.5} fill="url(#grad)" dot={{ fill: chartColors[metric], r: 3, strokeWidth: 0 }} />
</AreaChart>
</ResponsiveContainer>
</div>
{/* Table */}
<div style={{ background: C.cardBg, border: `1px solid ${C.border}`, borderRadius: "10px", overflow: "hidden", boxShadow: C.shadow }}>
<div style={{ padding: "14px 20px", borderBottom: `1px solid ${C.border}`, fontSize: "13px", fontWeight: "700", color: C.text }}>Monthly Records</div>
<table style={{ width: "100%", borderCollapse: "collapse", fontSize: "12px" }}>
<thead>
<tr style={{ background: C.catHeader }}>
{["Period","Assets","Liabilities","Net Worth","Change"].map(h => (
<th key={h} style={{ textAlign: h === "Period" ? "left" : "right", padding: "10px 16px", color: C.textMid, fontSize: "10px", letterSpacing: "0.07em", textTransform: "uppercase", fontWeight: "700", borderBottom: `1px solid ${C.border}` }}>{h}</th>
))}
</tr>
</thead>
<tbody>
{[...snapshots].reverse().map((s, i, arr) => {
const prev = arr[i + 1];
const change = prev ? s.netWorth - prev.netWorth : null;
return (
<tr key={s.date} style={{ borderBottom: `1px solid ${C.border}` }}
onMouseEnter={e => e.currentTarget.style.background = "#f8f8f5"}
onMouseLeave={e => e.currentTarget.style.background = "transparent"}>
<td style={{ padding: "11px 16px", color: C.textMid, fontWeight: "600" }}>{formatDate(s.date)}</td>
<td style={{ padding: "11px 16px", textAlign: "right", color: C.positive, fontWeight: "600" }}>{formatCurrency(s.assets)}</td>
<td style={{ padding: "11px 16px", textAlign: "right", color: C.negative, fontWeight: "600" }}>{formatCurrency(s.liabilities)}</td>
<td style={{ padding: "11px 16px", textAlign: "right", color: s.netWorth >= 0 ? C.positive : C.negative, fontWeight: "700" }}>{formatCurrency(s.netWorth)}</td>
<td style={{ padding: "11px 16px", textAlign: "right", fontWeight: "600", color: change === null ? C.textLight : change >= 0 ? C.positive : C.negative }}>
{change === null ? "—" : (change >= 0 ? "▲ +" : "▼ ") + formatCurrency(change)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</>
);
}
const iStyle = {
background: C.inputBg, border: `1px solid ${C.inputBorder}`, color: C.text,
padding: "7px 10px", fontSize: "12px", width: "100%", borderRadius: "6px",
outline: "none", boxSizing: "border-box", fontFamily: "'Verdana', Geneva, sans-serif",
transition: "border-color 0.15s",
};
const bStyle = {
padding: "7px 14px", background: C.bg, border: `1px solid ${C.borderStrong}`,
color: C.textMid, borderRadius: "6px", cursor: "pointer",
fontSize: "11px", fontWeight: "600", fontFamily: "'Verdana', Geneva, sans-serif",
};