mirror of
https://github.com/elisiariocouto/leggen.git
synced 2025-12-14 09:42:21 +00:00
Fix Balance Progress Over Time chart by adding historical balance endpoint
Co-authored-by: elisiariocouto <818914+elisiariocouto@users.noreply.github.com>
This commit is contained in:
committed by
Elisiário Couto
parent
35b6d98e6a
commit
b7e4ec4a1b
@@ -54,6 +54,18 @@ export const apiClient = {
|
|||||||
return response.data.data;
|
return response.data.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Get historical balances for balance progression chart
|
||||||
|
getHistoricalBalances: async (days?: number, accountId?: string): Promise<Balance[]> => {
|
||||||
|
const queryParams = new URLSearchParams();
|
||||||
|
if (days) queryParams.append("days", days.toString());
|
||||||
|
if (accountId) queryParams.append("account_id", accountId);
|
||||||
|
|
||||||
|
const response = await api.get<ApiResponse<Balance[]>>(
|
||||||
|
`/balances/history?${queryParams.toString()}`
|
||||||
|
);
|
||||||
|
return response.data.data;
|
||||||
|
},
|
||||||
|
|
||||||
// Get balances for specific account
|
// Get balances for specific account
|
||||||
getAccountBalances: async (accountId: string): Promise<Balance[]> => {
|
getAccountBalances: async (accountId: string): Promise<Balance[]> => {
|
||||||
const response = await api.get<ApiResponse<Balance[]>>(
|
const response = await api.get<ApiResponse<Balance[]>>(
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ function AnalyticsDashboard() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { data: balances, isLoading: balancesLoading } = useQuery({
|
const { data: balances, isLoading: balancesLoading } = useQuery({
|
||||||
queryKey: ["balances"],
|
queryKey: ["historical-balances"],
|
||||||
queryFn: () => apiClient.getBalances(),
|
queryFn: () => apiClient.getHistoricalBalances(365), // Get 1 year of history
|
||||||
});
|
});
|
||||||
|
|
||||||
const isLoading = statsLoading || accountsLoading || balancesLoading;
|
const isLoading = statsLoading || accountsLoading || balancesLoading;
|
||||||
|
|||||||
@@ -520,3 +520,131 @@ def get_account(account_id: str):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
conn.close()
|
conn.close()
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
||||||
|
def get_historical_balances(account_id=None, days=365):
|
||||||
|
"""Get historical balance progression based on transaction history"""
|
||||||
|
db_path = path_manager.get_database_path()
|
||||||
|
if not db_path.exists():
|
||||||
|
return []
|
||||||
|
|
||||||
|
conn = sqlite3.connect(str(db_path))
|
||||||
|
conn.row_factory = sqlite3.Row
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get current balance for each account/type to use as the final balance
|
||||||
|
current_balances_query = """
|
||||||
|
SELECT account_id, type, amount, currency
|
||||||
|
FROM balances b1
|
||||||
|
WHERE b1.timestamp = (
|
||||||
|
SELECT MAX(b2.timestamp)
|
||||||
|
FROM balances b2
|
||||||
|
WHERE b2.account_id = b1.account_id AND b2.type = b1.type
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
params = []
|
||||||
|
|
||||||
|
if account_id:
|
||||||
|
current_balances_query += " AND b1.account_id = ?"
|
||||||
|
params.append(account_id)
|
||||||
|
|
||||||
|
cursor.execute(current_balances_query, params)
|
||||||
|
current_balances = {
|
||||||
|
(row['account_id'], row['type']): {
|
||||||
|
'amount': row['amount'],
|
||||||
|
'currency': row['currency']
|
||||||
|
}
|
||||||
|
for row in cursor.fetchall()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get transactions for the specified period, ordered by date descending
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
cutoff_date = (datetime.now() - timedelta(days=days)).isoformat()
|
||||||
|
|
||||||
|
transactions_query = """
|
||||||
|
SELECT accountId, transactionDate, transactionValue
|
||||||
|
FROM transactions
|
||||||
|
WHERE transactionDate >= ?
|
||||||
|
"""
|
||||||
|
|
||||||
|
if account_id:
|
||||||
|
transactions_query += " AND accountId = ?"
|
||||||
|
params = [cutoff_date, account_id]
|
||||||
|
else:
|
||||||
|
params = [cutoff_date]
|
||||||
|
|
||||||
|
transactions_query += " ORDER BY transactionDate DESC"
|
||||||
|
|
||||||
|
cursor.execute(transactions_query, params)
|
||||||
|
transactions = cursor.fetchall()
|
||||||
|
|
||||||
|
# Calculate historical balances by working backwards from current balance
|
||||||
|
historical_balances = []
|
||||||
|
account_running_balances = {}
|
||||||
|
|
||||||
|
# Initialize running balances with current balances
|
||||||
|
for (acc_id, balance_type), balance_info in current_balances.items():
|
||||||
|
if acc_id not in account_running_balances:
|
||||||
|
account_running_balances[acc_id] = {}
|
||||||
|
account_running_balances[acc_id][balance_type] = balance_info['amount']
|
||||||
|
|
||||||
|
# Group transactions by date
|
||||||
|
from collections import defaultdict
|
||||||
|
transactions_by_date = defaultdict(list)
|
||||||
|
|
||||||
|
for txn in transactions:
|
||||||
|
date_str = txn['transactionDate'][:10] # Extract just the date part
|
||||||
|
transactions_by_date[date_str].append(txn)
|
||||||
|
|
||||||
|
# Generate historical balance points
|
||||||
|
# Start from today and work backwards
|
||||||
|
current_date = datetime.now().date()
|
||||||
|
|
||||||
|
for day_offset in range(0, days, 7): # Sample every 7 days for performance
|
||||||
|
target_date = current_date - timedelta(days=day_offset)
|
||||||
|
target_date_str = target_date.isoformat()
|
||||||
|
|
||||||
|
# For each account, create balance entries
|
||||||
|
for acc_id in account_running_balances:
|
||||||
|
for balance_type in ['closingBooked']: # Focus on closingBooked for the chart
|
||||||
|
if balance_type in account_running_balances[acc_id]:
|
||||||
|
balance_amount = account_running_balances[acc_id][balance_type]
|
||||||
|
currency = current_balances.get((acc_id, balance_type), {}).get('currency', 'EUR')
|
||||||
|
|
||||||
|
historical_balances.append({
|
||||||
|
'id': f"{acc_id}_{balance_type}_{target_date_str}",
|
||||||
|
'account_id': acc_id,
|
||||||
|
'balance_amount': balance_amount,
|
||||||
|
'balance_type': balance_type,
|
||||||
|
'currency': currency,
|
||||||
|
'reference_date': target_date_str,
|
||||||
|
'created_at': None,
|
||||||
|
'updated_at': None
|
||||||
|
})
|
||||||
|
|
||||||
|
# Subtract transactions that occurred on this date and later dates
|
||||||
|
# to simulate going back in time
|
||||||
|
for date_str in list(transactions_by_date.keys()):
|
||||||
|
if date_str >= target_date_str:
|
||||||
|
for txn in transactions_by_date[date_str]:
|
||||||
|
acc_id = txn['accountId']
|
||||||
|
amount = txn['transactionValue']
|
||||||
|
|
||||||
|
if acc_id in account_running_balances:
|
||||||
|
for balance_type in account_running_balances[acc_id]:
|
||||||
|
account_running_balances[acc_id][balance_type] -= amount
|
||||||
|
|
||||||
|
# Remove processed transactions to avoid double-processing
|
||||||
|
del transactions_by_date[date_str]
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Sort by date for proper chronological order
|
||||||
|
historical_balances.sort(key=lambda x: x['reference_date'])
|
||||||
|
|
||||||
|
return historical_balances
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
conn.close()
|
||||||
|
raise e
|
||||||
|
|||||||
@@ -215,6 +215,31 @@ async def get_all_balances() -> APIResponse:
|
|||||||
) from e
|
) from e
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/balances/history", response_model=APIResponse)
|
||||||
|
async def get_historical_balances(
|
||||||
|
days: Optional[int] = Query(default=365, le=1095, ge=1, description="Number of days of history to retrieve"),
|
||||||
|
account_id: Optional[str] = Query(default=None, description="Filter by specific account ID")
|
||||||
|
) -> APIResponse:
|
||||||
|
"""Get historical balance progression calculated from transaction history"""
|
||||||
|
try:
|
||||||
|
# Get historical balances from database
|
||||||
|
historical_balances = await database_service.get_historical_balances_from_db(
|
||||||
|
account_id=account_id, days=days
|
||||||
|
)
|
||||||
|
|
||||||
|
return APIResponse(
|
||||||
|
success=True,
|
||||||
|
data=historical_balances,
|
||||||
|
message=f"Retrieved {len(historical_balances)} historical balance points over {days} days",
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get historical balances: {e}")
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500, detail=f"Failed to get historical 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,
|
||||||
|
|||||||
@@ -195,6 +195,22 @@ class DatabaseService:
|
|||||||
logger.error(f"Failed to get balances from database: {e}")
|
logger.error(f"Failed to get balances from database: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
async def get_historical_balances_from_db(
|
||||||
|
self, account_id: Optional[str] = None, days: int = 365
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""Get historical balance progression from SQLite database"""
|
||||||
|
if not self.sqlite_enabled:
|
||||||
|
logger.warning("SQLite database disabled, cannot read historical balances")
|
||||||
|
return []
|
||||||
|
|
||||||
|
try:
|
||||||
|
balances = sqlite_db.get_historical_balances(account_id=account_id, days=days)
|
||||||
|
logger.debug(f"Retrieved {len(balances)} historical balance points from database")
|
||||||
|
return balances
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Failed to get historical balances from database: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
async def get_account_summary_from_db(
|
async def get_account_summary_from_db(
|
||||||
self, account_id: str
|
self, account_id: str
|
||||||
) -> Optional[Dict[str, Any]]:
|
) -> Optional[Dict[str, Any]]:
|
||||||
|
|||||||
Reference in New Issue
Block a user