web/pages/account/
list.rs1use std::sync::Arc;
2
3use askama::Template;
4use axum::extract::{Query, State};
5use axum::http::{HeaderMap, header};
6use axum::{Extension, Json};
7use axum::{http::StatusCode, response::IntoResponse};
8use num_rational::Rational64;
9use serde::{Deserialize, Serialize};
10use server::command::{
11 CmdResult, FinanceEntity, account::GetAccount, account::GetAccountCommodities,
12 account::GetBalance, account::ListAccounts,
13};
14use sqlx::types::Uuid;
15
16use crate::pages::HtmlTemplate;
17use crate::{AppState, jwt_auth::JWTAuthMiddleware};
18
19#[derive(Template)]
20#[template(path = "pages/account/list.html")]
21struct AccountListPage;
22
23pub async fn account_list_page() -> impl IntoResponse {
24 let template = AccountListPage {};
25 HtmlTemplate(template)
26}
27
28#[derive(Serialize, Clone)]
29pub struct BalanceView {
30 balance: Rational64,
31 commodity: String,
32}
33
34#[derive(Serialize)]
35struct AccountView {
36 id: Uuid,
37 name: String,
38 currency: Option<Uuid>,
39 balance: Option<Vec<BalanceView>>,
40}
41
42#[derive(Template)]
43#[template(path = "components/account/table.html")]
44struct AccountTableTemplate {
45 accounts: Vec<AccountView>,
46}
47
48#[derive(Serialize)]
49struct AccountJson {
50 id: String,
51 name: String,
52 currency: Option<String>,
53 balance: Option<Vec<BalanceView>>,
54}
55
56async fn get_balance_display(
57 account_id: Uuid,
58 user_id: Uuid,
59 commodities: &[server::command::CommodityInfo],
60) -> Result<Option<Vec<BalanceView>>, server::command::CmdError> {
61 let balance_result = GetBalance::new()
62 .user_id(user_id)
63 .account_id(account_id)
64 .run()
65 .await?;
66
67 match balance_result {
68 Some(CmdResult::MultiCurrencyBalance(balances)) => {
69 if balances.is_empty() {
70 Ok(None)
71 } else if balances.len() == 1 {
72 let (commodity, balance) = &balances[0];
74 let commodity_info = commodities
75 .iter()
76 .find(|c| c.commodity_id == commodity.id)
77 .map_or_else(|| "?".to_string(), |c| c.symbol.clone());
78 Ok(Some(vec![BalanceView {
79 balance: *balance,
80 commodity: commodity_info,
81 }]))
82 } else {
83 let balance_strings: Vec<BalanceView> = balances
86 .iter()
87 .map(|(commodity, balance)| {
88 let symbol = commodities
89 .iter()
90 .find(|c| c.commodity_id == commodity.id)
91 .map_or_else(|| "?".to_string(), |c| c.symbol.clone());
92 BalanceView {
93 balance: *balance,
94 commodity: symbol,
95 }
96 })
97 .collect();
98 Ok(Some(balance_strings))
99 }
100 }
101 Some(CmdResult::Rational(balance)) => {
102 if balance == Rational64::new(0, 1) {
104 Ok(None)
105 } else if commodities.is_empty() {
106 Ok(Some(vec![BalanceView {
107 balance,
108 commodity: "?".to_string(),
109 }]))
110 } else {
111 let symbol = &commodities[0].symbol;
112 Ok(Some(vec![BalanceView {
113 balance,
114 commodity: symbol.clone(),
115 }]))
116 }
117 }
118 None => Ok(None),
119 _ => Err(server::command::CmdError::Args(
120 "Unexpected result type from GetBalance".to_string(),
121 )),
122 }
123}
124
125pub async fn account_table(
126 State(_data): State<Arc<AppState>>,
127 Extension(jwt_auth): Extension<JWTAuthMiddleware>,
128 headers: HeaderMap,
129) -> Result<impl IntoResponse, StatusCode> {
130 let result = ListAccounts::new()
131 .user_id(jwt_auth.user.id)
132 .run()
133 .await
134 .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
135
136 let mut accounts = Vec::new();
137 if let Some(CmdResult::TaggedEntities { entities, .. }) = result {
138 for (entity, tags) in entities {
139 if let FinanceEntity::Account(account) = entity {
140 let name = if let FinanceEntity::Tag(n) = &tags["name"] {
142 n.tag_value.clone()
143 } else {
144 return Err(StatusCode::INTERNAL_SERVER_ERROR);
145 };
146
147 let commodities_result = GetAccountCommodities::new()
149 .user_id(jwt_auth.user.id)
150 .account_id(account.id)
151 .run()
152 .await
153 .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
154
155 let commodities =
156 if let Some(CmdResult::CommodityInfoList(commodities)) = commodities_result {
157 commodities
158 } else {
159 Vec::new()
160 };
161
162 let balance_display =
163 get_balance_display(account.id, jwt_auth.user.id, &commodities)
164 .await
165 .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
166
167 let currency = commodities.first().map(|c| c.commodity_id);
168
169 accounts.push(AccountView {
170 id: account.id,
171 name,
172 currency,
173 balance: balance_display,
174 });
175 }
176 }
177 }
178
179 let wants_json = headers
181 .get(header::ACCEPT)
182 .and_then(|value| value.to_str().ok())
183 .is_some_and(|value| value.contains("application/json"));
184
185 if wants_json {
186 let accounts_json: Vec<AccountJson> = accounts
188 .iter()
189 .map(|a| AccountJson {
190 id: a.id.to_string(),
191 name: a.name.clone(),
192 currency: a.currency.map(|c| c.to_string()),
193 balance: a.balance.clone(),
194 })
195 .collect();
196
197 return Ok(Json(accounts_json).into_response());
198 }
199
200 Ok(HtmlTemplate(AccountTableTemplate { accounts }).into_response())
202}
203
204#[derive(Deserialize)]
205pub struct AccountInfoParam {
206 account: Uuid,
207}
208
209#[derive(Template)]
210#[template(path = "components/account/info.html")]
211struct AccountInfoTemplate {
212 account_id: Uuid,
213 name: String,
214 balance: Option<Vec<BalanceView>>,
215}
216
217pub async fn account_info(
218 Query(params): Query<AccountInfoParam>,
219 State(_data): State<Arc<AppState>>,
220 Extension(jwt_auth): Extension<JWTAuthMiddleware>,
221) -> Result<impl IntoResponse, StatusCode> {
222 let account_result = GetAccount::new()
223 .user_id(jwt_auth.user.id)
224 .account_id(params.account)
225 .run()
226 .await
227 .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
228
229 let name = if let Some(CmdResult::TaggedEntities { entities, .. }) = account_result
230 && let Some((FinanceEntity::Account(_account), tags)) = entities.first()
231 && let Some(FinanceEntity::Tag(name_tag)) = tags.get("name")
232 {
233 name_tag.tag_value.clone()
234 } else {
235 return Err(StatusCode::NOT_FOUND);
236 };
237
238 let commodities_result = GetAccountCommodities::new()
239 .user_id(jwt_auth.user.id)
240 .account_id(params.account)
241 .run()
242 .await
243 .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
244
245 let commodities = if let Some(CmdResult::CommodityInfoList(commodities)) = commodities_result {
246 commodities
247 } else {
248 Vec::new()
249 };
250
251 let balance = get_balance_display(params.account, jwt_auth.user.id, &commodities)
252 .await
253 .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
254
255 Ok(HtmlTemplate(AccountInfoTemplate {
256 account_id: params.account,
257 name,
258 balance,
259 }))
260}