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, ReportData, ReportNode, commodity::ListCommodities, report::TrialBalance,
};
use sqlx::types::Uuid;
use sqlx::types::chrono::NaiveDate;
use crate::{AppState, jwt_auth::JWTAuthMiddleware, pages::HtmlTemplate};
use super::balance::{self, CommodityOption};
use super::{build_report_filter, empty_string_as_none};
#[derive(Template)]
#[template(path = "pages/report/trial_balance.html")]
struct TrialBalanceReportPage;
pub async fn trial_balance_report_page() -> impl IntoResponse {
HtmlTemplate(TrialBalanceReportPage)
}
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
#[template(path = "components/report/trial_balance_table.html")]
struct TrialBalanceTableTemplate {
commodities: Vec<CommodityOption>,
rows: Vec<ReportRowView>,
date_from: Option<String>,
date_to: Option<String>,
target_commodity_id: Option<String>,
tag_filters: String,
tag_filter_mode: String,
scripting_enabled: bool,
#[derive(Deserialize)]
pub struct TrialBalanceParams {
#[serde(default, deserialize_with = "empty_string_as_none")]
tag_filters: Option<String>,
tag_filter_mode: Option<String>,
pub async fn trial_balance_report_table(
Query(params): Query<TrialBalanceParams>,
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(balance::fetch_commodity_list(entities))
} else {
None
.unwrap_or_default();
let date_from = params
.date_from
.as_deref()
.and_then(|s| NaiveDate::parse_from_str(s, "%Y-%m-%d").ok())
.map(|d| d.and_hms_opt(0, 0, 0).unwrap().and_utc());
let date_to = params
.date_to
.map(|d| d.and_hms_opt(23, 59, 59).unwrap().and_utc());
let tag_filter_mode = params
.tag_filter_mode
.clone()
.unwrap_or_else(|| "visual".to_string());
let (Some(df), Some(dt)) = (date_from, date_to) else {
return Ok(HtmlTemplate(TrialBalanceTableTemplate {
commodities,
rows: vec![],
date_from: params.date_from,
date_to: params.date_to,
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());
let mut cmd = TrialBalance::new()
.date_from(df)
.date_to(dt);
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(filter) = build_report_filter(
params.tag_filters.as_deref(),
Some(tag_filter_mode.as_str()),
) {
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();
Ok(HtmlTemplate(TrialBalanceTableTemplate {
rows,
.into_response())