mirror of
https://github.com/elisiariocouto/leggen.git
synced 2025-12-13 13:42:19 +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 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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -125,54 +129,57 @@ export default function AccountsOverview() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="divide-y divide-gray-200">
|
<div className="divide-y divide-gray-200">
|
||||||
{accounts.map((account) => {
|
{accounts.map((account) => {
|
||||||
const accountBalance = balances?.find(b => b.account_id === account.id);
|
// Get balance from account's balances array or fallback to balances query
|
||||||
const balance = account.balance || accountBalance?.balance_amount || 0;
|
const accountBalance = account.balances?.[0];
|
||||||
const isPositive = balance >= 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 (
|
return (
|
||||||
<div key={account.id} className="p-6 hover:bg-gray-50 transition-colors">
|
<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 justify-between">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<div className="p-3 bg-gray-100 rounded-full">
|
<div className="p-3 bg-gray-100 rounded-full">
|
||||||
<Building2 className="h-6 w-6 text-gray-600" />
|
<Building2 className="h-6 w-6 text-gray-600" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-lg font-medium text-gray-900">
|
<h4 className="text-lg font-medium text-gray-900">
|
||||||
{account.name}
|
{account.name || 'Unnamed Account'}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
{account.bank_name} • {account.account_type}
|
{account.institution_id} • {account.status}
|
||||||
</p>
|
</p>
|
||||||
{account.iban && (
|
{account.iban && (
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
IBAN: {account.iban}
|
IBAN: {account.iban}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
{isPositive ? (
|
{isPositive ? (
|
||||||
<TrendingUp className="h-4 w-4 text-green-500" />
|
<TrendingUp className="h-4 w-4 text-green-500" />
|
||||||
) : (
|
) : (
|
||||||
<TrendingDown className="h-4 w-4 text-red-500" />
|
<TrendingDown className="h-4 w-4 text-red-500" />
|
||||||
)}
|
)}
|
||||||
<p className={`text-lg font-semibold ${
|
<p className={`text-lg font-semibold ${
|
||||||
isPositive ? 'text-green-600' : 'text-red-600'
|
isPositive ? 'text-green-600' : 'text-red-600'
|
||||||
}`}>
|
}`}>
|
||||||
{formatCurrency(balance, account.currency)}
|
{formatCurrency(balance, currency)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
Updated {formatDate(account.updated_at)}
|
Updated {formatDate(account.last_accessed || account.created)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -34,7 +34,11 @@ export default function Dashboard() {
|
|||||||
{ name: 'Analytics', icon: BarChart3, id: 'analytics' as TabType },
|
{ 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 (
|
return (
|
||||||
<div className="flex h-screen bg-gray-100">
|
<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"
|
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>
|
<option value="">All accounts</option>
|
||||||
{accounts?.map((account) => (
|
{accounts?.map((account) => (
|
||||||
<option key={account.id} value={account.id}>
|
<option key={account.id} value={account.id}>
|
||||||
{account.name} ({account.bank_name})
|
{account.name || 'Unnamed Account'} ({account.institution_id})
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -259,10 +259,10 @@ export default function TransactionsList() {
|
|||||||
{transaction.description}
|
{transaction.description}
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<div className="text-xs text-gray-500 space-y-1">
|
<div className="text-xs text-gray-500 space-y-1">
|
||||||
{account && (
|
{account && (
|
||||||
<p>{account.name} • {account.bank_name}</p>
|
<p>{account.name || 'Unnamed Account'} • {account.institution_id}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(transaction.creditor_name || transaction.debtor_name) && (
|
{(transaction.creditor_name || transaction.debtor_name) && (
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -5,10 +5,22 @@ export function cn(...inputs: ClassValue[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function formatCurrency(amount: number, currency: string = 'EUR'): string {
|
export function formatCurrency(amount: number, currency: string = 'EUR'): string {
|
||||||
return new Intl.NumberFormat('en-US', {
|
// Validate currency code - must be 3 letters and a valid ISO 4217 code
|
||||||
style: 'currency',
|
const validCurrency = currency && /^[A-Z]{3}$/.test(currency) ? currency : 'EUR';
|
||||||
currency: currency,
|
|
||||||
}).format(amount);
|
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 {
|
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 {
|
export interface Account {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
institution_id: string;
|
||||||
bank_name: string;
|
status: string;
|
||||||
account_type: string;
|
|
||||||
currency: string;
|
|
||||||
balance?: number;
|
|
||||||
iban?: string;
|
iban?: string;
|
||||||
created_at: string;
|
name?: string;
|
||||||
updated_at: string;
|
currency?: string;
|
||||||
|
created: string;
|
||||||
|
last_accessed?: string;
|
||||||
|
balances: AccountBalance[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Transaction {
|
export interface Transaction {
|
||||||
|
|||||||
@@ -164,6 +164,56 @@ async def get_account_balances(account_id: str) -> APIResponse:
|
|||||||
) from e
|
) 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)
|
@router.get("/accounts/{account_id}/transactions", response_model=APIResponse)
|
||||||
async def get_account_transactions(
|
async def get_account_transactions(
|
||||||
account_id: str,
|
account_id: str,
|
||||||
|
|||||||
Reference in New Issue
Block a user