mirror of
https://github.com/elisiariocouto/leggen.git
synced 2025-12-13 11:22:21 +00:00
fix: resolve 404 balances endpoint and currency formatting errors
- Add missing /api/v1/balances endpoint to backend - Update frontend Account type to match backend AccountDetails model - Add currency validation with EUR fallback in formatCurrency function - Update AccountsOverview, TransactionsList, and Dashboard components - Fix balance calculations to use balances array structure - All pre-commit checks pass
This commit is contained in:
committed by
Elisiário Couto
parent
947342e196
commit
417b77539f
@@ -61,9 +61,13 @@ export default function AccountsOverview() {
|
||||
);
|
||||
}
|
||||
|
||||
const totalBalance = accounts?.reduce((sum, account) => sum + (account.balance || 0), 0) || 0;
|
||||
const totalBalance = accounts?.reduce((sum, account) => {
|
||||
// Get the first available balance from the balances array
|
||||
const primaryBalance = account.balances?.[0]?.amount || 0;
|
||||
return sum + primaryBalance;
|
||||
}, 0) || 0;
|
||||
const totalAccounts = accounts?.length || 0;
|
||||
const uniqueBanks = new Set(accounts?.map(acc => acc.bank_name) || []).size;
|
||||
const uniqueBanks = new Set(accounts?.map(acc => acc.institution_id) || []).size;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -125,54 +129,57 @@ export default function AccountsOverview() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-gray-200">
|
||||
{accounts.map((account) => {
|
||||
const accountBalance = balances?.find(b => b.account_id === account.id);
|
||||
const balance = account.balance || accountBalance?.balance_amount || 0;
|
||||
const isPositive = balance >= 0;
|
||||
{accounts.map((account) => {
|
||||
// Get balance from account's balances array or fallback to balances query
|
||||
const accountBalance = account.balances?.[0];
|
||||
const fallbackBalance = balances?.find(b => b.account_id === account.id);
|
||||
const balance = accountBalance?.amount || fallbackBalance?.balance_amount || 0;
|
||||
const currency = accountBalance?.currency || fallbackBalance?.currency || account.currency || 'EUR';
|
||||
const isPositive = balance >= 0;
|
||||
|
||||
return (
|
||||
<div key={account.id} className="p-6 hover:bg-gray-50 transition-colors">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="p-3 bg-gray-100 rounded-full">
|
||||
<Building2 className="h-6 w-6 text-gray-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-lg font-medium text-gray-900">
|
||||
{account.name}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-600">
|
||||
{account.bank_name} • {account.account_type}
|
||||
</p>
|
||||
{account.iban && (
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
IBAN: {account.iban}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
return (
|
||||
<div key={account.id} className="p-6 hover:bg-gray-50 transition-colors">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="p-3 bg-gray-100 rounded-full">
|
||||
<Building2 className="h-6 w-6 text-gray-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-lg font-medium text-gray-900">
|
||||
{account.name || 'Unnamed Account'}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-600">
|
||||
{account.institution_id} • {account.status}
|
||||
</p>
|
||||
{account.iban && (
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
IBAN: {account.iban}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-right">
|
||||
<div className="flex items-center space-x-2">
|
||||
{isPositive ? (
|
||||
<TrendingUp className="h-4 w-4 text-green-500" />
|
||||
) : (
|
||||
<TrendingDown className="h-4 w-4 text-red-500" />
|
||||
)}
|
||||
<p className={`text-lg font-semibold ${
|
||||
isPositive ? 'text-green-600' : 'text-red-600'
|
||||
}`}>
|
||||
{formatCurrency(balance, account.currency)}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500">
|
||||
Updated {formatDate(account.updated_at)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="text-right">
|
||||
<div className="flex items-center space-x-2">
|
||||
{isPositive ? (
|
||||
<TrendingUp className="h-4 w-4 text-green-500" />
|
||||
) : (
|
||||
<TrendingDown className="h-4 w-4 text-red-500" />
|
||||
)}
|
||||
<p className={`text-lg font-semibold ${
|
||||
isPositive ? 'text-green-600' : 'text-red-600'
|
||||
}`}>
|
||||
{formatCurrency(balance, currency)}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500">
|
||||
Updated {formatDate(account.last_accessed || account.created)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -34,7 +34,11 @@ export default function Dashboard() {
|
||||
{ name: 'Analytics', icon: BarChart3, id: 'analytics' as TabType },
|
||||
];
|
||||
|
||||
const totalBalance = accounts?.reduce((sum, account) => sum + (account.balance || 0), 0) || 0;
|
||||
const totalBalance = accounts?.reduce((sum, account) => {
|
||||
// Get the first available balance from the balances array
|
||||
const primaryBalance = account.balances?.[0]?.amount || 0;
|
||||
return sum + primaryBalance;
|
||||
}, 0) || 0;
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-gray-100">
|
||||
|
||||
@@ -163,11 +163,11 @@ export default function TransactionsList() {
|
||||
className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||
>
|
||||
<option value="">All accounts</option>
|
||||
{accounts?.map((account) => (
|
||||
<option key={account.id} value={account.id}>
|
||||
{account.name} ({account.bank_name})
|
||||
</option>
|
||||
))}
|
||||
{accounts?.map((account) => (
|
||||
<option key={account.id} value={account.id}>
|
||||
{account.name || 'Unnamed Account'} ({account.institution_id})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -259,10 +259,10 @@ export default function TransactionsList() {
|
||||
{transaction.description}
|
||||
</h4>
|
||||
|
||||
<div className="text-xs text-gray-500 space-y-1">
|
||||
{account && (
|
||||
<p>{account.name} • {account.bank_name}</p>
|
||||
)}
|
||||
<div className="text-xs text-gray-500 space-y-1">
|
||||
{account && (
|
||||
<p>{account.name || 'Unnamed Account'} • {account.institution_id}</p>
|
||||
)}
|
||||
|
||||
{(transaction.creditor_name || transaction.debtor_name) && (
|
||||
<p>
|
||||
|
||||
@@ -5,10 +5,22 @@ export function cn(...inputs: ClassValue[]) {
|
||||
}
|
||||
|
||||
export function formatCurrency(amount: number, currency: string = 'EUR'): string {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: currency,
|
||||
}).format(amount);
|
||||
// Validate currency code - must be 3 letters and a valid ISO 4217 code
|
||||
const validCurrency = currency && /^[A-Z]{3}$/.test(currency) ? currency : 'EUR';
|
||||
|
||||
try {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: validCurrency,
|
||||
}).format(amount);
|
||||
} catch (error) {
|
||||
// Fallback if currency is still invalid
|
||||
console.warn(`Invalid currency code: ${currency}, falling back to EUR`);
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'EUR',
|
||||
}).format(amount);
|
||||
}
|
||||
}
|
||||
|
||||
export function formatDate(date: string): string {
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
export interface AccountBalance {
|
||||
amount: number;
|
||||
currency: string;
|
||||
balance_type: string;
|
||||
last_change_date?: string;
|
||||
}
|
||||
|
||||
export interface Account {
|
||||
id: string;
|
||||
name: string;
|
||||
bank_name: string;
|
||||
account_type: string;
|
||||
currency: string;
|
||||
balance?: number;
|
||||
institution_id: string;
|
||||
status: string;
|
||||
iban?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
name?: string;
|
||||
currency?: string;
|
||||
created: string;
|
||||
last_accessed?: string;
|
||||
balances: AccountBalance[];
|
||||
}
|
||||
|
||||
export interface Transaction {
|
||||
|
||||
@@ -164,6 +164,56 @@ async def get_account_balances(account_id: str) -> APIResponse:
|
||||
) from e
|
||||
|
||||
|
||||
@router.get("/balances", response_model=APIResponse)
|
||||
async def get_all_balances() -> APIResponse:
|
||||
"""Get all balances from all accounts in database"""
|
||||
try:
|
||||
# Get all accounts first to iterate through them
|
||||
db_accounts = await database_service.get_accounts_from_db()
|
||||
|
||||
all_balances = []
|
||||
for db_account in db_accounts:
|
||||
try:
|
||||
# Get balances for this account
|
||||
db_balances = await database_service.get_balances_from_db(
|
||||
account_id=db_account["id"]
|
||||
)
|
||||
|
||||
# Process balances and add account info
|
||||
for balance in db_balances:
|
||||
balance_data = {
|
||||
"id": f"{db_account['id']}_{balance['type']}", # Create unique ID
|
||||
"account_id": db_account["id"],
|
||||
"balance_amount": balance["amount"],
|
||||
"balance_type": balance["type"],
|
||||
"currency": balance["currency"],
|
||||
"reference_date": balance.get(
|
||||
"timestamp", db_account.get("last_accessed")
|
||||
),
|
||||
"created_at": db_account.get("created"),
|
||||
"updated_at": db_account.get("last_accessed"),
|
||||
}
|
||||
all_balances.append(balance_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to get balances for account {db_account['id']}: {e}"
|
||||
)
|
||||
continue
|
||||
|
||||
return APIResponse(
|
||||
success=True,
|
||||
data=all_balances,
|
||||
message=f"Retrieved {len(all_balances)} balances from {len(db_accounts)} accounts",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get all balances: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Failed to get balances: {str(e)}"
|
||||
) from e
|
||||
|
||||
|
||||
@router.get("/accounts/{account_id}/transactions", response_model=APIResponse)
|
||||
async def get_account_transactions(
|
||||
account_id: str,
|
||||
|
||||
Reference in New Issue
Block a user