Lines
7.28 %
Functions
1.85 %
Branches
100 %
use std::sync::Arc;
use askama::Template;
use axum::extract::{Query, State};
use axum::http::{HeaderMap, header};
use axum::{Extension, Json};
use axum::{http::StatusCode, response::IntoResponse};
use num_rational::Rational64;
use serde::{Deserialize, Serialize};
use server::command::{
CmdResult, FinanceEntity, account::GetAccount, account::GetAccountCommodities,
account::GetBalance, account::ListAccounts,
};
use sqlx::types::Uuid;
use crate::pages::HtmlTemplate;
use crate::{AppState, jwt_auth::JWTAuthMiddleware};
#[derive(Template)]
#[template(path = "pages/account/list.html")]
struct AccountListPage;
pub async fn account_list_page() -> impl IntoResponse {
let template = AccountListPage {};
HtmlTemplate(template)
}
#[derive(Serialize, Clone)]
pub struct BalanceView {
balance: Rational64,
commodity: String,
#[derive(Serialize)]
struct AccountView {
id: Uuid,
name: String,
balance: Option<Vec<BalanceView>>,
#[template(path = "components/account/table.html")]
struct AccountTableTemplate {
accounts: Vec<AccountView>,
struct AccountJson {
id: String,
async fn get_balance_display(
account_id: Uuid,
user_id: Uuid,
commodities: &[server::command::CommodityInfo],
) -> Result<Option<Vec<BalanceView>>, server::command::CmdError> {
let balance_result = GetBalance::new()
.user_id(user_id)
.account_id(account_id)
.run()
.await?;
match balance_result {
Some(CmdResult::MultiCurrencyBalance(balances)) => {
if balances.is_empty() {
Ok(None)
} else if balances.len() == 1 {
// Single currency
let (commodity, balance) = &balances[0];
let commodity_info = commodities
.iter()
.find(|c| c.commodity_id == commodity.id)
.map(|c| c.symbol.clone())
.unwrap_or_else(|| "?".to_string());
Ok(Some(vec![BalanceView {
balance: *balance,
commodity: commodity_info,
}]))
} else {
// Multiple currencies - show all balances comma-separated
// TODO: rework for arrays?
let balance_strings: Vec<BalanceView> = balances
.map(|(commodity, balance)| {
let symbol = commodities
BalanceView {
commodity: symbol,
})
.collect();
Ok(Some(balance_strings))
Some(CmdResult::Rational(balance)) => {
// Single currency result (when commodity_id was specified)
if balance == Rational64::new(0, 1) {
} else if commodities.is_empty() {
balance,
commodity: "?".to_string(),
let symbol = &commodities[0].symbol;
commodity: symbol.to_string(),
None => Ok(None),
_ => Err(server::command::CmdError::Args(
"Unexpected result type from GetBalance".to_string(),
)),
pub async fn account_table(
State(_data): State<Arc<AppState>>,
Extension(jwt_auth): Extension<JWTAuthMiddleware>,
headers: HeaderMap,
) -> Result<impl IntoResponse, StatusCode> {
let result = ListAccounts::new()
.user_id(jwt_auth.user.id)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let mut accounts = Vec::new();
if let Some(CmdResult::TaggedEntities(entities)) = result {
for (entity, tags) in entities {
if let FinanceEntity::Account(account) = entity {
// Find name tag
let name = if let FinanceEntity::Tag(n) = &tags["name"] {
n.tag_value.clone()
return Err(StatusCode::INTERNAL_SERVER_ERROR);
// Get all commodities for this account
let commodities_result = GetAccountCommodities::new()
.account_id(account.id)
let commodities =
if let Some(CmdResult::CommodityInfoList(commodities)) = commodities_result {
commodities
Vec::new()
let balance_display =
get_balance_display(account.id, jwt_auth.user.id, &commodities)
accounts.push(AccountView {
id: account.id,
name,
balance: balance_display,
});
// Check if the client is requesting JSON by examining the Accept header
let wants_json = headers
.get(header::ACCEPT)
.and_then(|value| value.to_str().ok())
.is_some_and(|value| value.contains("application/json"));
if wants_json {
// Return JSON response
let accounts_json: Vec<AccountJson> = accounts
.map(|a| AccountJson {
id: a.id.to_string(),
name: a.name.clone(),
balance: a.balance.clone(),
return Ok(Json(accounts_json).into_response());
// Default to HTML response
Ok(HtmlTemplate(AccountTableTemplate { accounts }).into_response())
#[derive(Deserialize)]
pub struct AccountInfoParam {
account: Uuid,
#[template(path = "components/account/info.html")]
struct AccountInfoTemplate {
pub async fn account_info(
Query(params): Query<AccountInfoParam>,
let account_result = GetAccount::new()
.account_id(params.account)
let name = if let Some(CmdResult::TaggedEntities(entities)) = account_result
&& let Some((FinanceEntity::Account(_account), tags)) = entities.first()
&& let Some(FinanceEntity::Tag(name_tag)) = tags.get("name")
{
name_tag.tag_value.clone()
return Err(StatusCode::NOT_FOUND);
let commodities = if let Some(CmdResult::CommodityInfoList(commodities)) = commodities_result {
let balance = get_balance_display(params.account, jwt_auth.user.id, &commodities)
Ok(HtmlTemplate(AccountInfoTemplate {
account_id: params.account,
}))