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 serde::Deserialize;
use server::command::{CmdResult, ReportData, ReportMeta, report::BalanceReport};
use sqlx::types::Uuid;
use crate::{AppState, jwt_auth::JWTAuthMiddleware, pages::HtmlTemplate};
use super::{
ChartParams, CommodityOption, SummaryCard, TableControlParams, build_report_filter,
commodity_symbols_in_rows, empty_string_as_none, load_commodities, parse_date_bound,
parse_sort_order_shared, sort_top_level_rows, sum_top_level_amounts, today_string, wants_json,
};
use plotting::adapters::{BalanceChartOpts, balance_chart};
use server::command::report::view::{AmountView, ReportRowView, flatten_report_data};
#[derive(Template)]
#[template(path = "pages/report/balance.html")]
struct BalanceReportPage;
pub async fn balance_report_page() -> impl IntoResponse {
HtmlTemplate(BalanceReportPage)
}
#[template(path = "components/report/balance_table.html")]
struct BalanceTableTemplate {
commodities: Vec<CommodityOption>,
summary: Vec<SummaryCard>,
rows: Vec<ReportRowView>,
commodity_symbols: Vec<String>,
commodity_columns: bool,
sortable: bool,
collapsible: bool,
period_mode: bool,
date_from: Option<String>,
as_of: Option<String>,
target_commodity_id: Option<String>,
collapsed_depth: Option<String>,
sort_order: String,
tag_filters: String,
tag_filter_mode: String,
scripting_enabled: bool,
chart_kind: String,
chart_query: String,
renderer: String,
#[derive(Deserialize)]
pub struct BalanceParams {
#[serde(default, deserialize_with = "empty_string_as_none")]
tag_filters: Option<String>,
tag_filter_mode: Option<String>,
sort_order: Option<String>,
#[serde(flatten)]
table: TableControlParams,
chart: ChartParams,
fn balance_summary(rows: &[ReportRowView]) -> Vec<SummaryCard> {
sum_top_level_amounts(rows)
.into_iter()
.map(|a| SummaryCard {
label: format!("Total ({})", a.commodity_symbol),
amounts: vec![AmountView {
commodity_symbol: a.commodity_symbol,
amount: a.amount,
}],
is_net: false,
highlight: false,
})
.collect()
/// Build the `BalanceReport` command from shared filter params, run it,
/// and flatten to `ReportRowView`. Both the table handler and the chart
/// handlers reuse this so the chart is always consistent with the table.
async fn run_balance(
user_id: Uuid,
target_commodity_id: Option<&str>,
date_from: Option<&str>,
as_of_str: &str,
tag_filters: Option<&str>,
tag_filter_mode: Option<&str>,
) -> Result<ReportData, StatusCode> {
let mut cmd = BalanceReport::new().user_id(user_id);
if let Some(tid_str) = target_commodity_id
&& let Ok(tid) = tid_str.parse::<Uuid>()
{
cmd = cmd.target_commodity_id(tid);
if let Some(df_str) = date_from
&& let Some(df) = parse_date_bound(df_str, false)
cmd = cmd.date_from(df);
if let Some(as_of) = parse_date_bound(as_of_str, true) {
cmd = cmd.as_of(as_of);
if let Some(filter) = build_report_filter(tag_filters, tag_filter_mode) {
cmd = cmd.report_filter(filter);
let result = cmd
.run()
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(match result {
Some(CmdResult::Report(data)) => data,
_ => ReportData {
meta: ReportMeta {
date_from: None,
date_to: None,
target_commodity_id: None,
},
periods: vec![],
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 commodities = load_commodities(jwt_auth.user.id).await;
let as_of_str = params.as_of.clone().unwrap_or_else(today_string);
let period_mode = params.date_from.is_some();
let report_data = run_balance(
jwt_auth.user.id,
params.target_commodity_id.as_deref(),
params.date_from.as_deref(),
&as_of_str,
params.tag_filters.as_deref(),
params.tag_filter_mode.as_deref(),
)
.await?;
if wants_json(&headers) {
return Ok(Json(report_data).into_response());
let mut rows = flatten_report_data(&report_data);
let sort_order = parse_sort_order_shared(params.sort_order.as_deref());
sort_top_level_rows(&mut rows, sort_order);
let summary = balance_summary(&rows);
let commodity_symbols = commodity_symbols_in_rows(&rows);
let commodity_columns = params.table.commodity_columns_enabled();
let tag_filter_mode = params
.tag_filter_mode
.clone()
.unwrap_or_else(|| "visual".to_string());
let chart_kind = params.chart.chart_kind_str();
let renderer = params.chart.renderer_str();
let sort_order_str = sort_order.to_str().to_string();
let chart_query = super::encode_query(&[
(
"target_commodity_id",
params.target_commodity_id.as_deref().unwrap_or_default(),
),
("date_from", params.date_from.as_deref().unwrap_or_default()),
("as_of", as_of_str.as_str()),
"tag_filters",
params.tag_filters.as_deref().unwrap_or_default(),
("tag_filter_mode", tag_filter_mode.as_str()),
("chart_kind", chart_kind.as_str()),
("renderer", renderer.as_str()),
("sort_order", sort_order_str.as_str()),
]);
Ok(HtmlTemplate(BalanceTableTemplate {
commodities,
summary,
rows,
commodity_symbols,
commodity_columns,
sortable: true,
collapsible: true,
period_mode,
date_from: params.date_from,
as_of: Some(as_of_str),
target_commodity_id: params.target_commodity_id,
collapsed_depth: params.table.collapsed_depth,
sort_order: sort_order_str,
tag_filters: params.tag_filters.unwrap_or_default(),
tag_filter_mode,
scripting_enabled: cfg!(feature = "scripting"),
chart_kind,
chart_query,
renderer,
.into_response())
/// The balance chart shows top-level accounts by magnitude. Both the
/// SVG and JSON chart endpoints take the same params as the table
/// endpoint plus `ChartParams`.
pub struct BalanceChartQuery {
async fn balance_chart_spec(
params: &BalanceChartQuery,
) -> Result<plotting::ChartSpec, StatusCode> {
user_id,
let rows = flatten_report_data(&report_data);
Ok(balance_chart(
&rows,
BalanceChartOpts {
kind: params.chart.chart_kind_or_default(),
top_n: 10,
sort_order: sort_order.into_plotting_balance(),
))
pub async fn balance_report_chart_svg(
Query(params): Query<BalanceChartQuery>,
let spec = balance_chart_spec(jwt_auth.user.id, ¶ms).await?;
let svg = plotting::svg::render_svg(&spec, 720, 360);
Ok((
StatusCode::OK,
[(axum::http::header::CONTENT_TYPE, "image/svg+xml")],
svg,
pub async fn balance_report_chart_json(
Ok(Json(spec))