From 4a8edf6eb8547d48316f24c3985130f02bb68e1a Mon Sep 17 00:00:00 2001 From: ribardej Date: Thu, 30 Oct 2025 12:30:35 +0100 Subject: [PATCH 1/2] feat(frontend): added CNB API and moved management into a new tab --- 7project/frontend/src/pages/Dashboard.tsx | 185 +++++++++++++++------- 7project/frontend/src/ui.css | 4 +- 7project/frontend/vite.config.ts | 19 ++- 3 files changed, 146 insertions(+), 62 deletions(-) diff --git a/7project/frontend/src/pages/Dashboard.tsx b/7project/frontend/src/pages/Dashboard.tsx index c4cf021..0caa584 100644 --- a/7project/frontend/src/pages/Dashboard.tsx +++ b/7project/frontend/src/pages/Dashboard.tsx @@ -1,8 +1,9 @@ import { useEffect, useMemo, useState } from 'react'; -import { type Category, type Transaction, type BalancePoint, createTransaction, getCategories, getTransactions, createCategory, updateTransaction, getBalanceSeries } from '../api'; +import { type Category, type Transaction, type BalancePoint, getCategories, getTransactions, createTransaction, updateTransaction, getBalanceSeries } from '../api'; import AccountPage from './AccountPage'; import AppearancePage from './AppearancePage'; import BalanceChart from './BalanceChart'; +import ManualManagement from './ManualManagement'; import CategoryPieChart from './CategoryPieChart'; import MockBankModal, { type MockGenerationOptions } from './MockBankModal'; import { BACKEND_URL } from '../config'; @@ -11,8 +12,108 @@ function formatAmount(n: number) { return new Intl.NumberFormat(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(n); } +// Add this new component to your Dashboard.tsx file, above the Dashboard component + +// Define the structure for the rate data we care about +type CnbRate = { + currencyCode: string; + rate: number; +}; + +// The part of the API response structure we need +type CnbApiResponse = { + rates: Array<{ + amount: number; + currencyCode: string; + rate: number; + }>; +}; + +// The currencies you want to display +const TARGET_CURRENCIES = ['EUR', 'USD', 'NOK']; + +function CurrencyRates() { + const [rates, setRates] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + async function fetchRates() { + setLoading(true); + setError(null); + + // Get today's date in YYYY-MM-DD format for the API + const today = new Date().toISOString().split('T')[0]; + const CNB_API_URL = `/api-cnb/cnbapi/exrates/daily?date=${today}&lang=EN`; + + try { + const res = await fetch(CNB_API_URL); + if (!res.ok) { + // This can happen on weekends/holidays or if rates aren't posted yet + throw new Error(`Rates unavailable (Status: ${res.status})`); + } + const data: CnbApiResponse = await res.json(); + + if (!data.rates) { + throw new Error("Invalid API response"); + } + + const filteredRates = data.rates + .filter(rate => TARGET_CURRENCIES.includes(rate.currencyCode)) + .map(rate => ({ + currencyCode: rate.currencyCode, + // Handle 'amount' field (e.g., JPY is per 100) + rate: rate.rate / rate.amount + })); + + setRates(filteredRates); + } catch (err: any) { + setError(err.message || 'Could not load rates'); + } finally { + setLoading(false); + } + } + + fetchRates(); + }, []); // Runs once on component mount + + return ( + // This component will push itself to the bottom of the sidebar +
+

+ Rates (vs CZK) +

+ {loading &&
Loading...
} + {error &&
{error}
} + {!loading && !error && ( +
    + {rates.length > 0 ? rates.map(rate => ( +
  • + {rate.currencyCode} + {rate.rate.toFixed(3)} +
  • + )) :
  • No rates found.
  • } +
+ )} +
+ ); +} + export default function Dashboard({ onLogout }: { onLogout: () => void }) { - const [current, setCurrent] = useState<'home' | 'account' | 'appearance'>('home'); + const [current, setCurrent] = useState<'home' | 'manual' | 'account' | 'appearance'>('home'); const [transactions, setTransactions] = useState([]); const [categories, setCategories] = useState([]); const [loading, setLoading] = useState(true); @@ -41,11 +142,6 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) { } } - // 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(''); @@ -63,12 +159,7 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) { // 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(''); + // Manual forms moved to ManualManagement page // Inline edit state for transaction categories const [editingTxId, setEditingTxId] = useState(null); @@ -167,23 +258,6 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) { 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); @@ -206,17 +280,23 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) { return (
-