Lines
16.95 %
Functions
8.33 %
Branches
100 %
use std::sync::Arc;
use askama::Template;
use axum::extract::Query;
use axum::{Extension, extract::State, http::StatusCode, response::IntoResponse};
use num_rational::Rational64;
use serde::Deserialize;
use server::command::{
CmdResult, FinanceEntity, commodity::GetCommodity, transaction::ListTransactions,
};
use sqlx::types::Uuid;
use sqlx::types::chrono::{DateTime, Utc};
use crate::{AppState, jwt_auth::JWTAuthMiddleware, pages::HtmlTemplate};
#[derive(Template)]
#[template(path = "pages/transaction/list.html")]
struct TransactionListPage {
account: Option<Uuid>,
}
pub async fn transaction_list_page(Query(params): Query<TransactionParam>) -> impl IntoResponse {
let template = TransactionListPage {
account: params.account,
HtmlTemplate(template)
struct SplitView {
account_id: Uuid,
account_name: String,
amount: String,
currency: String,
tags: Vec<finance::tag::Tag>,
struct TransactionView {
id: Uuid,
date: DateTime<Utc>,
description: String,
splits: Vec<SplitView>,
#[template(path = "components/transaction/table.html")]
struct TransactionTableTemplate {
transactions: Vec<TransactionView>,
#[derive(Deserialize)]
pub struct TransactionParam {
pub async fn transaction_table(
Query(transactionparam): Query<TransactionParam>,
State(_data): State<Arc<AppState>>,
Extension(jwt_auth): Extension<JWTAuthMiddleware>,
) -> Result<impl IntoResponse, StatusCode> {
let mut cmd = ListTransactions::new().user_id(jwt_auth.user.id);
if let Some(id) = transactionparam.account {
cmd = cmd.account(id);
let result = cmd
.run()
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let mut transactions = Vec::new();
if let Some(CmdResult::TaggedEntities(entities)) = result {
for (entity, tags) in entities {
if let FinanceEntity::Transaction(tx) = entity {
// Get description from note tag or use default
let description = if let Some(FinanceEntity::Tag(note)) = tags.get("note") {
note.tag_value.clone()
} else {
format!("Transaction {}", tx.id)
// Get splits for this transaction
let split_result = server::command::split::ListSplits::new()
.user_id(jwt_auth.user.id)
.transaction(tx.id)
// Calculate total amount and get currency
let mut total = Rational64::new(0, 1);
let mut currency = String::new();
let mut splits = Vec::new();
if let Some(CmdResult::TaggedEntities(split_entities)) = split_result {
for (split_entity, split_tags) in split_entities {
if let FinanceEntity::Split(split) = split_entity {
let split_amount = Rational64::new(split.value_num, split.value_denom);
let mut account_name = format!("Account {}", split.account_id);
let mut split_currency = String::new();
// For filtered view, show amount from the filtered account's split
// For unfiltered view, sum positive values to get transaction total
let is_filtered_account =
transactionparam.account == Some(split.account_id);
let should_count = if transactionparam.account.is_some() {
is_filtered_account
split.value_num > 0
if should_count {
total += split_amount;
// Get account name and currency for this split
if let Ok(Some(CmdResult::TaggedEntities(account_entities))) =
server::command::account::GetAccount::new()
.account_id(split.account_id)
&& let Some((FinanceEntity::Account(_account), account_tags)) =
account_entities.first()
{
if let Some(FinanceEntity::Tag(name)) = account_tags.get("name") {
account_name = name.tag_value.clone();
// Get commodity for this split
if let Ok(Some(CmdResult::TaggedEntities(commodity_entities))) =
GetCommodity::new()
.commodity_id(split.commodity_id)
&& let Some((_, commodity_tags)) = commodity_entities.first()
&& let Some(FinanceEntity::Tag(symbol)) =
commodity_tags.get("symbol")
split_currency = symbol.tag_value.clone();
// Set transaction currency based on what we're counting
if currency.is_empty() && should_count {
currency = split_currency.clone();
// Format the split amount - preserve the sign for template logic
let split_amount_str = if *split_amount.denom() == 1 {
split_amount.numer().to_string()
let value =
*split_amount.numer() as f64 / *split_amount.denom() as f64;
format!("{value:.2}")
// Extract tags from the split_tags HashMap by taking ownership
let mut tags: Vec<finance::tag::Tag> = split_tags
.into_values()
.filter_map(|entity| {
if let FinanceEntity::Tag(tag) = entity {
Some(tag)
None
})
.collect();
// Sort tags by name, then by value for stable display
tags.sort_by(|a, b| {
a.tag_name
.cmp(&b.tag_name)
.then_with(|| a.tag_value.cmp(&b.tag_value))
});
// Add the split with its account name
splits.push(SplitView {
account_id: split.account_id,
account_name,
amount: split_amount_str,
currency: split_currency,
tags,
// Format the amount with currency
let amount_str = if *total.denom() == 1 {
total.numer().to_string()
format!("{:.2}", *total.numer() as f64 / *total.denom() as f64)
transactions.push(TransactionView {
id: tx.id,
date: tx.post_date,
description,
amount: amount_str,
currency,
splits,
transactions.sort_by(|a, b| b.date.cmp(&a.date));
Ok(HtmlTemplate(TransactionTableTemplate { transactions }))