diff --git a/7project/backend/app/services/bank_scraper.py b/7project/backend/app/services/bank_scraper.py index a0c8207..732277c 100644 --- a/7project/backend/app/services/bank_scraper.py +++ b/7project/backend/app/services/bank_scraper.py @@ -1,17 +1,18 @@ import json import logging from os.path import dirname, join +from time import strptime from uuid import UUID import httpx from sqlalchemy import select from app.core.db import async_session_maker +from app.models.transaction import Transaction from app.models.user import User logger = logging.getLogger(__name__) -# Reuse CSAS mTLS certs used by OAuth profile calls OAUTH_DIR = join(dirname(__file__), "..", "oauth") CERTS = ( join(OAUTH_DIR, "public_key.pem"), @@ -20,10 +21,6 @@ CERTS = ( async def aload_ceska_sporitelna_transactions(user_id: str) -> None: - """ - Async entry point to load Česká spořitelna transactions for a single user. - Validates the user_id and performs a minimal placeholder action. - """ try: uid = UUID(str(user_id)) except Exception: @@ -34,9 +31,6 @@ async def aload_ceska_sporitelna_transactions(user_id: str) -> None: async def aload_all_ceska_sporitelna_transactions() -> None: - """ - Async entry point to load Česká spořitelna transactions for all users. - """ async with async_session_maker() as session: result = await session.execute(select(User)) users = result.unique().scalars().all() @@ -54,7 +48,7 @@ async def aload_all_ceska_sporitelna_transactions() -> None: async def _aload_ceska_sporitelna_transactions(user_id: UUID) -> None: - async with async_session_maker() as session: + async with (async_session_maker() as session): result = await session.execute(select(User).where(User.id == user_id)) user: User = result.unique().scalar_one_or_none() if user is None: @@ -106,16 +100,25 @@ async def _aload_ceska_sporitelna_transactions(user_id: UUID) -> None: if response.status_code != httpx.codes.OK: continue - # Placeholder: just print the account transactions - transactions = response.json()["transactions"] - pass for transaction in transactions: - #parse and store transaction to database - #create Transaction object and save to DB - #obj = + description = transaction.get("entryDetails", {}).get("transactionDetails", {}).get( + "additionalRemittanceInformation") + date_str = transaction.get("bookingDate", {}).get("date") + date = strptime(date_str, "%Y-%m-%d") if date_str else None + amount = transaction.get("amount", {}).get("value") + if transaction.get("creditDebitIndicator") == "DBIT": + amount = -abs(amount) + obj = Transaction( + amount=amount, + description=description, + date=date, + user_id=user_id, + ) + session.add(obj) + await session.commit() pass pass 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 (
-