import { useEffect, useMemo, useState } from 'react'; import { type Category, type Transaction, type BalancePoint, createTransaction, getCategories, getTransactions, createCategory, updateTransaction, getBalanceSeries } from '../api'; import AccountPage from './AccountPage'; import AppearancePage from './AppearancePage'; import BalanceChart from './BalanceChart'; import CategoryPieChart from './CategoryPieChart'; import MockBankModal, { type MockGenerationOptions } from './MockBankModal'; import { BACKEND_URL } from '../config'; function formatAmount(n: number) { return new Intl.NumberFormat(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(n); } export default function Dashboard({ onLogout }: { onLogout: () => void }) { const [current, setCurrent] = useState<'home' | 'account' | 'appearance'>('home'); const [transactions, setTransactions] = useState([]); const [categories, setCategories] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [isMockModalOpen, setMockModalOpen] = useState(false); const [isGenerating, setIsGenerating] = useState(false); // Start CSAS (George) OAuth after login async function startOauthCsas() { const base = BACKEND_URL.replace(/\/$/, ''); const url = `${base}/auth/csas/authorize`; try { const token = localStorage.getItem('token'); const res = await fetch(url, { credentials: 'include', headers: token ? { Authorization: `Bearer ${token}` } : undefined, }); const data = await res.json(); if (data && typeof data.authorization_url === 'string') { window.location.assign(data.authorization_url); } else { alert('Cannot start CSAS OAuth.'); } } catch (e) { alert('Cannot start CSAS OAuth.'); } } // New transaction form state const [amount, setAmount] = useState(''); const [description, setDescription] = useState(''); const [selectedCategoryId, setSelectedCategoryId] = useState(''); // Filters const [minAmount, setMinAmount] = useState(''); const [maxAmount, setMaxAmount] = useState(''); const [filterCategoryId, setFilterCategoryId] = useState(''); const [searchText, setSearchText] = useState(''); // Date-range filter const [startDate, setStartDate] = useState(''); // YYYY-MM-DD const [endDate, setEndDate] = useState(''); // Pagination over filtered transactions (20 per page), 0 = latest (most recent) const pageSize = 20; const [page, setPage] = useState(0); // Balance chart series for current date filter const [balanceSeries, setBalanceSeries] = useState([]); // Category creation form const [newCatName, setNewCatName] = useState(''); const [newCatDesc, setNewCatDesc] = useState(''); // New transaction date const [txDate, setTxDate] = useState(''); // Inline edit state for transaction categories const [editingTxId, setEditingTxId] = useState(null); const [editingCategoryIds, setEditingCategoryIds] = useState([]); async function loadAll() { setLoading(true); setError(null); try { const [txs, cats, series] = await Promise.all([ getTransactions(startDate || undefined, endDate || undefined), getCategories(), getBalanceSeries(startDate || undefined, endDate || undefined), ]); setTransactions(txs); setCategories(cats); setBalanceSeries(series); // reset paging to most recent setPage(0); } catch (err: any) { setError(err?.message || 'Failed to load data'); } finally { setLoading(false); } } async function handleGenerateMockTransactions(options: MockGenerationOptions) { setIsGenerating(true); setMockModalOpen(false); const { count, minAmount, maxAmount, startDate, endDate, categoryIds } = options; const newTransactions: Transaction[] = []; const startDateTime = new Date(startDate).getTime(); const endDateTime = new Date(endDate).getTime(); for (let i = 0; i < count; i++) { // Generate random data based on user input const amount = parseFloat((Math.random() * (maxAmount - minAmount) + minAmount).toFixed(2)); const randomTime = Math.random() * (endDateTime - startDateTime) + startDateTime; const date = new Date(randomTime); const dateString = date.toISOString().split('T')[0]; const randomCategory = categoryIds.length > 0 ? [categoryIds[Math.floor(Math.random() * categoryIds.length)]] : []; const payload = { amount, date: dateString, category_ids: randomCategory, }; try { const created = await createTransaction(payload); newTransactions.push(created); } catch (err) { console.error("Failed to create mock transaction:", err); alert('An error occurred while generating transactions. Check the console.'); break; } } setIsGenerating(false); alert(`${newTransactions.length} mock transactions were successfully generated!`); await loadAll(); } useEffect(() => { loadAll(); }, [startDate, endDate]); const filtered = useMemo(() => { let arr = [...transactions]; const min = minAmount !== '' ? Number(minAmount) : undefined; const max = maxAmount !== '' ? Number(maxAmount) : undefined; if (min !== undefined) arr = arr.filter(t => t.amount >= min); if (max !== undefined) arr = arr.filter(t => t.amount <= max); if (filterCategoryId !== '') arr = arr.filter(t => t.category_ids.includes(filterCategoryId as number)); if (searchText.trim()) arr = arr.filter(t => (t.description || '').toLowerCase().includes(searchText.toLowerCase())); return arr; }, [transactions, minAmount, maxAmount, filterCategoryId, searchText]); const sortedDesc = useMemo(() => { return [...filtered].sort((a, b) => { const ad = (a.date || '') > (b.date || '') ? 1 : (a.date || '') < (b.date || '') ? -1 : 0; if (ad !== 0) return -ad; // date desc return b.id - a.id; // fallback id desc }); }, [filtered]); const totalPages = Math.ceil(sortedDesc.length / pageSize); const pageStart = page * pageSize; const pageEnd = pageStart + pageSize; const visible = sortedDesc.slice(pageStart, pageEnd); function categoryNameById(id: number) { return categories.find(c => c.id === id)?.name || `#${id}`; } async function handleCreate(e: React.FormEvent) { e.preventDefault(); if (!amount) return; const payload = { amount: Number(amount), description: description || undefined, category_ids: selectedCategoryId !== '' ? [Number(selectedCategoryId)] : undefined, date: txDate || undefined, }; try { const created = await createTransaction(payload); setTransactions(prev => [created, ...prev]); setAmount(''); setDescription(''); setSelectedCategoryId(''); setTxDate(''); } catch (err: any) { alert(err?.message || 'Failed to create transaction'); } } function beginEditCategories(t: Transaction) { setEditingTxId(t.id); setEditingCategoryIds([...(t.category_ids || [])]); } function cancelEditCategories() { setEditingTxId(null); setEditingCategoryIds([]); } async function saveEditCategories() { if (editingTxId == null) return; try { const updated = await updateTransaction(editingTxId, { category_ids: editingCategoryIds }); setTransactions(prev => prev.map(p => (p.id === updated.id ? updated : p))); cancelEditCategories(); } catch (err: any) { alert(err?.message || 'Failed to update transaction categories'); } } return (

{current === 'home' ? 'Dashboard' : current === 'account' ? 'Account' : 'Appearance'}

Signed in
{current === 'home' && ( <>

Bank connections

Connect your CSAS (George) account.

Generate data from a mock bank.

Add Transaction

setAmount(e.target.value)} required /> setTxDate(e.target.value)} /> setDescription(e.target.value)} />

Categories

{ e.preventDefault(); if (!newCatName.trim()) return; try { const cat = await createCategory({ name: newCatName.trim(), description: newCatDesc || undefined }); setCategories(prev => [...prev, cat]); setNewCatName(''); setNewCatDesc(''); } catch (err: any) { alert(err?.message || 'Failed to create category'); } }}> setNewCatName(e.target.value)} /> setNewCatDesc(e.target.value)} />

Filters

setStartDate(e.target.value)} /> setEndDate(e.target.value)} /> setMinAmount(e.target.value)} /> setMaxAmount(e.target.value)} /> setSearchText(e.target.value)} />

Balance over time

{loading ? (
Loading…
) : error ? (
{error}
) : ( )}
{/* 3. Add the new section for the Category Pie Chart */}
{loading ? (
Loading…
) : error ? (
{error}
) : ( // Pass the filtered transactions to see the breakdown for the current view )}

Transactions

{loading ? (
Loading…
) : error ? (
{error}
) : filtered.length === 0 ? (
No transactions
) : ( <>
Showing {visible.length} of {filtered.length} (page {Math.min(page + 1, Math.max(1, totalPages))}/{Math.max(1, totalPages)})
{visible.map(t => ( ))}
Date Amount Description Categories
{t.date || ''} {formatAmount(t.amount)} {t.description || ''} {editingTxId === t.id ? (
) : (
{t.category_ids.map(id => categoryNameById(id)).join(', ') || '—'}
)}
)}
)} {current === 'account' && ( // lazy import avoided for simplicity )} {current === 'appearance' && ( )}
setMockModalOpen(false)} onGenerate={handleGenerateMockTransactions} />
); }