Lines
23.75 %
Functions
10 %
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, PaginationInfo, commodity::GetCommodity,
transaction::ListTransactions,
};
use sqlx::types::Uuid;
use sqlx::types::chrono::{DateTime, NaiveDate, 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 PaginationView {
current_page: i64,
total_pages: i64,
has_prev: bool,
has_next: bool,
total_count: i64,
limit: i64,
impl PaginationView {
fn from_info(info: &PaginationInfo) -> Self {
let total_pages = (info.total_count + info.limit - 1) / info.limit;
let current_page = info.offset / info.limit + 1;
Self {
current_page,
total_pages,
has_prev: current_page > 1,
has_next: info.has_more,
total_count: info.total_count,
limit: info.limit,
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>,
pagination: Option<PaginationView>,
date_from: Option<String>,
date_to: Option<String>,
#[derive(Deserialize)]
pub struct TransactionParam {
limit: Option<i64>,
page: Option<i64>,
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 limit = transactionparam.limit.unwrap_or(20);
cmd = cmd.limit(limit);
if let Some(page) = transactionparam.page {
let offset = (page - 1) * limit;
cmd = cmd.offset(offset);
if let Some(ref date_from_str) = transactionparam.date_from
&& let Ok(date) = NaiveDate::parse_from_str(date_from_str, "%Y-%m-%d")
{
let datetime = date.and_hms_opt(0, 0, 0).unwrap().and_utc();
cmd = cmd.date_from(datetime);
if let Some(ref date_to_str) = transactionparam.date_to
&& let Ok(date) = NaiveDate::parse_from_str(date_to_str, "%Y-%m-%d")
let datetime = date.and_hms_opt(23, 59, 59).unwrap().and_utc();
cmd = cmd.date_to(datetime);
let result = cmd
.run()
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let mut transactions = Vec::new();
let mut pagination_view = None;
if let Some(CmdResult::TaggedEntities {
entities,
pagination,
}) = result
if let Some(ref pag_info) = pagination {
pagination_view = Some(PaginationView::from_info(pag_info));
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();
entities: 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 {
entities: 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
entities: 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,
Ok(HtmlTemplate(TransactionTableTemplate {
transactions,
pagination: pagination_view,
account: transactionparam.account,
date_from: transactionparam.date_from,
date_to: transactionparam.date_to,
}))