Lines
0 %
Functions
Branches
100 %
use std::sync::Arc;
use askama::Template;
use axum::Json;
use axum::extract::Query;
use axum::http::HeaderMap;
use axum::{Extension, extract::State, http::StatusCode, response::IntoResponse};
use num_rational::Rational64;
use serde::Deserialize;
use server::command::{
CmdResult, FinanceEntity, ReportData, ReportNode, commodity::ListCommodities,
report::BalanceReport,
};
use sqlx::types::Uuid;
use sqlx::types::chrono::NaiveDate;
use crate::{AppState, jwt_auth::JWTAuthMiddleware, pages::HtmlTemplate};
use super::{build_report_filter, empty_string_as_none};
#[derive(Template)]
#[template(path = "pages/report/balance.html")]
struct BalanceReportPage;
pub async fn balance_report_page() -> impl IntoResponse {
HtmlTemplate(BalanceReportPage)
}
pub(super) struct CommodityOption {
pub(super) id: Uuid,
pub(super) symbol: String,
pub(super) name: String,
struct ReportRowView {
account_name: String,
depth: usize,
amounts: Vec<AmountView>,
struct AmountView {
commodity_symbol: String,
amount: Rational64,
fn flatten_nodes(nodes: &[ReportNode]) -> Vec<ReportRowView> {
let mut rows = Vec::new();
for node in nodes {
rows.push(ReportRowView {
account_name: node.account_name.clone(),
depth: node.depth,
amounts: node
.amounts
.iter()
.map(|a| AmountView {
commodity_symbol: a.commodity_symbol.clone(),
amount: a.amount,
})
.collect(),
});
rows.extend(flatten_nodes(&node.children));
rows
pub(super) fn fetch_commodity_list(
entities: Vec<(
FinanceEntity,
std::collections::HashMap<String, FinanceEntity>,
)>,
) -> Vec<CommodityOption> {
let mut commodities = Vec::new();
for (entity, tags) in entities {
if let FinanceEntity::Commodity(commodity) = entity
&& let (FinanceEntity::Tag(s), FinanceEntity::Tag(n)) = (&tags["symbol"], &tags["name"])
{
commodities.push(CommodityOption {
id: commodity.id,
symbol: s.tag_value.clone(),
name: n.tag_value.clone(),
commodities
#[template(path = "components/report/balance_table.html")]
struct BalanceTableTemplate {
commodities: Vec<CommodityOption>,
rows: Vec<ReportRowView>,
as_of: Option<String>,
target_commodity_id: Option<String>,
tag_filters: String,
tag_filter_mode: String,
scripting_enabled: bool,
#[derive(Deserialize)]
pub struct BalanceParams {
#[serde(default, deserialize_with = "empty_string_as_none")]
tag_filters: Option<String>,
tag_filter_mode: Option<String>,
pub async fn balance_report_table(
Query(params): Query<BalanceParams>,
State(_data): State<Arc<AppState>>,
Extension(jwt_auth): Extension<JWTAuthMiddleware>,
headers: HeaderMap,
) -> Result<impl IntoResponse, StatusCode> {
let commodity_entities = ListCommodities::new()
.user_id(jwt_auth.user.id)
.run()
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let commodities = commodity_entities
.and_then(|r| {
if let CmdResult::TaggedEntities { entities, .. } = r {
Some(fetch_commodity_list(entities))
} else {
None
.unwrap_or_default();
let mut cmd = BalanceReport::new().user_id(jwt_auth.user.id);
if let Some(ref tid_str) = params.target_commodity_id
&& let Ok(tid) = tid_str.parse::<Uuid>()
cmd = cmd.target_commodity_id(tid);
if let Some(ref as_of_str) = params.as_of
&& let Ok(date) = NaiveDate::parse_from_str(as_of_str, "%Y-%m-%d")
cmd = cmd.as_of(date.and_hms_opt(23, 59, 59).unwrap().and_utc());
if let Some(filter) = build_report_filter(
params.tag_filters.as_deref(),
params.tag_filter_mode.as_deref(),
) {
cmd = cmd.report_filter(filter);
let result = cmd
let report_data = match result {
Some(CmdResult::Report(data)) => data,
_ => ReportData {
meta: server::command::ReportMeta {
date_from: None,
date_to: None,
target_commodity_id: None,
},
periods: vec![],
if headers
.get("accept")
.and_then(|v| v.to_str().ok())
.is_some_and(|v| v.contains("application/json"))
return Ok(Json(report_data).into_response());
let rows: Vec<ReportRowView> = report_data
.periods
.flat_map(|p| flatten_nodes(&p.roots))
.collect();
let tag_filter_mode = params
.tag_filter_mode
.clone()
.unwrap_or_else(|| "visual".to_string());
Ok(HtmlTemplate(BalanceTableTemplate {
commodities,
rows,
as_of: params.as_of,
target_commodity_id: params.target_commodity_id,
tag_filters: params.tag_filters.unwrap_or_default(),
tag_filter_mode,
scripting_enabled: cfg!(feature = "scripting"),
.into_response())