diff --git a/src/models/transaction.rs b/src/models/transaction.rs index 2effc67..1d7c534 100644 --- a/src/models/transaction.rs +++ b/src/models/transaction.rs @@ -15,6 +15,12 @@ pub struct Transaction { accumulated: i32, } +#[derive(FromRow, Serialize, Deserialize, Debug)] +pub struct TransactionAggregated { + tx_date: DateTime, + accumulated: i32, +} + impl Transaction { pub async fn new( pool: &SqlitePool, @@ -151,6 +157,39 @@ impl Transaction { Ok(res) } + pub async fn group_by_date( + pool: &SqlitePool, + account: i32, + after: Option>, + before: Option>, + asc: bool, + ) -> Result> { + let mut query = + sqlx::QueryBuilder::new("SELECT accumulated, tx_date FROM transactions WHERE account="); + query.push_bind(account); + + if let Some(a) = after { + query.push(" AND tx_date >= "); + query.push_bind(a); + } + + if let Some(b) = before { + query.push(" AND tx_date <= "); + query.push_bind(b); + } + + query.push(" GROUP BY tx_date HAVING max(tx_order)"); + + let rows = query.build().fetch_all(pool).await?; + + let mut res = Vec::new(); + for r in &rows { + res.push(TransactionAggregated::from_row(r)?); + } + + Ok(res) + } + pub fn get_id(&self) -> i32 { self.transaction_id } diff --git a/static/styles.css b/static/styles.css index 19a77f8..dd6641a 100644 --- a/static/styles.css +++ b/static/styles.css @@ -588,6 +588,10 @@ video { } } +.relative { + position: relative; +} + .mb-2 { margin-bottom: 0.5rem; } diff --git a/templates/accounts.html b/templates/accounts.html index 3ed3e1b..920ddca 100644 --- a/templates/accounts.html +++ b/templates/accounts.html @@ -7,7 +7,20 @@ + -
+
+

Net amount

+
+ +
+
+ +
+
+
+

Last transactions

@@ -58,12 +71,49 @@ border-spacing: 0.2rem; } + + {% endblock body %} diff --git a/webserver/src/routes/ui/account.rs b/webserver/src/routes/ui/account.rs index d2bfe68..4bd2254 100644 --- a/webserver/src/routes/ui/account.rs +++ b/webserver/src/routes/ui/account.rs @@ -5,7 +5,7 @@ use axum::{ response::IntoResponse, Json, }; -use chrono::{DateTime, Utc}; +use chrono::{Date, DateTime, Duration, DurationRound, TimeZone, Utc}; use hyper::{header::CONTENT_TYPE, StatusCode}; use serde::Deserialize; use sqlx::SqlitePool; @@ -16,16 +16,31 @@ use accounters::models::{account::Account, categories::Category, transaction::Tr #[derive(Deserialize)] pub struct AccountViewParams { + from: Option, + to: Option, entries: Option, page: Option, } +fn parse_date(s: &str) -> Option> { + let mut iter = s.split('-'); + let year = iter.next()?.parse::().ok()?; + let month = iter.next()?.parse::().ok()?; + let day = iter.next()?.parse::().ok()?; + Utc.with_ymd_and_hms(year, month, day, 0, 0, 0).single() +} + pub async fn list( State(db): State>, State(tmpls): State>, uid: UserToken, Path(account_id): Path, - Query(AccountViewParams { entries, page }): Query, + Query(AccountViewParams { + from, + to, + entries, + page, + }): Query, ) -> impl IntoResponse { let mut ctx = Context::new(); @@ -48,6 +63,22 @@ pub async fn list( ); } + let from = from + .and_then(|x| parse_date(&x)) + .unwrap_or(Utc::now().duration_trunc(Duration::days(1)).unwrap() - Duration::days(30)); + let to = to + .and_then(|x| parse_date(&x)) + .unwrap_or(Utc::now().duration_trunc(Duration::days(1)).unwrap()); + + ctx.insert("date_from", &from); + ctx.insert("date_to", &to); + + let tx_agg = Transaction::group_by_date(db.as_ref(), account_id, Some(from), Some(to), false) + .await + .unwrap(); + + ctx.insert("tx_agg", &tx_agg); + let categories: HashMap = Category::list(db.as_ref()) .await .unwrap()