From d5f1554949321f2ebfda89efe39b0477a6ff2e55 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sun, 8 Feb 2026 15:15:24 +0000 Subject: task-085: transactions view has transactions --- .../src/gui/components/transactions_view.rs | 106 +++++++++------------ 1 file changed, 46 insertions(+), 60 deletions(-) (limited to 'schist_desktop_gui/src/gui/components/transactions_view.rs') diff --git a/schist_desktop_gui/src/gui/components/transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view.rs index 1cb569c..59d7ae3 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view.rs @@ -1,26 +1,28 @@ +use std::collections::HashMap; + use iced::{ alignment, widget::{column, row, Container}, Element, Length, }; -use schist_models::Account; +use itertools::Itertools; +use schist_models::{Account, Transaction}; use crate::{ - gui::components::{navigation, Navigation, Text}, + gui::components::{navigation, AccountNavigationEntry, Navigation, Text}, style::SPACING_LG, traits::{Component, Viewable}, }; #[derive(Clone, Debug)] pub struct TransactionsView { - accounts: Vec, - greeting: String, - navigation: Navigation, + navigation: Navigation, + transactions_by_account_id: HashMap>, } #[derive(Clone, Debug)] pub enum Message { - NavigationMessage(navigation::Message), + NavigationMessage(navigation::Message), SelectNextTransaction, SelectPrevTransaction, SetAccounts(Vec), @@ -31,13 +33,20 @@ pub enum Action { } impl TransactionsView { - pub fn new(accounts: Vec, greeting: &str) -> Self { - let account_names = view_account_names(accounts.clone()); - let navigation = Navigation::new(account_names.clone().first().cloned(), account_names); + pub fn new(accounts: Vec, transactions: Vec) -> Self { + let account_navigation_entries: Vec = accounts + .into_iter() + .map(AccountNavigationEntry::new) + .collect(); + let navigation = Navigation::new( + account_navigation_entries.first().cloned(), + account_navigation_entries, + ); Self { - accounts: accounts, - greeting: greeting.to_owned(), navigation: navigation, + transactions_by_account_id: transactions + .into_iter() + .into_group_map_by(|t| t.account_id), } } } @@ -45,7 +54,20 @@ impl TransactionsView { impl<'a> Viewable<'a, Message> for TransactionsView { fn view(&'a self) -> Element<'a, Message> { let navigation = self.navigation.view().map(Message::NavigationMessage); - let main_content = column![Text::default(&self.greeting).as_element()]; + let main_content = column![ + Text::default("Hello, transactions!").as_element(), + Text::default(&format!( + "{} transactions in this account.", + self.navigation.active_option.clone().map_or( + 0, + |AccountNavigationEntry { account, .. }| self + .transactions_by_account_id + .get(&account.id) + .map_or(0, |transactions| transactions.len()) + ) + )) + .as_element(), + ]; row![ Container::new(navigation).width(Length::FillPortion(1)), Container::new(main_content).width(Length::FillPortion(3)), @@ -61,62 +83,26 @@ impl<'a> Component<'a, Message, Action> for TransactionsView { fn update(&mut self, message: Message) -> Action { match message { Message::NavigationMessage(message) => { - match self.navigation.update(message) { - navigation::Action::ActivateOption(group_name) - | navigation::Action::SelectOption(group_name) => { - self.greeting = make_greeting(group_name); - Action::None - } - navigation::Action::None => Action::None, - }; + self.navigation.update(message); Action::None } Message::SetAccounts(accounts) => { - self.accounts = accounts.clone(); - match self - .navigation - .update(navigation::Message::SetOptions(view_account_names( - accounts, - ))) { - navigation::Action::ActivateOption(option) - | navigation::Action::SelectOption(option) => { - self.greeting = make_greeting(option); - Action::None - } - navigation::Action::None => Action::None, - } + let account_navigation_entries: Vec = accounts + .into_iter() + .map(AccountNavigationEntry::new) + .collect(); + self.navigation + .update(navigation::Message::SetOptions(account_navigation_entries)); + Action::None } Message::SelectNextTransaction => { - match self.navigation.update(navigation::Message::SelectNext) { - navigation::Action::ActivateOption(option) - | navigation::Action::SelectOption(option) => { - self.greeting = make_greeting(option); - Action::None - } - navigation::Action::None => Action::None, - } + self.navigation.update(navigation::Message::SelectNext); + Action::None } Message::SelectPrevTransaction => { - match self.navigation.update(navigation::Message::SelectPrev) { - navigation::Action::ActivateOption(option) - | navigation::Action::SelectOption(option) => { - self.greeting = make_greeting(option); - Action::None - } - navigation::Action::None => Action::None, - } + self.navigation.update(navigation::Message::SelectPrev); + Action::None } } } } - -fn make_greeting(account_name: Text) -> String { - format!("Hello, {} account!", account_name.content) -} - -fn view_account_names(accounts: Vec) -> Vec { - accounts - .iter() - .map(|acc| Text::new(acc.name.as_str())) - .collect() -} -- cgit v1.2.3 From 5b592ca899ad5a9090c1acd5776221f79e44d121 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sun, 8 Feb 2026 15:16:13 +0000 Subject: task-085: rename SelectNextTransaction -> SelectNext Account as this was always a mis-nomer --- schist_desktop_gui/src/gui/components/transactions_view.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'schist_desktop_gui/src/gui/components/transactions_view.rs') diff --git a/schist_desktop_gui/src/gui/components/transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view.rs index 59d7ae3..d28629d 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view.rs @@ -23,8 +23,8 @@ pub struct TransactionsView { #[derive(Clone, Debug)] pub enum Message { NavigationMessage(navigation::Message), - SelectNextTransaction, - SelectPrevTransaction, + SelectNextAccount, + SelectPrevAccount, SetAccounts(Vec), } @@ -95,11 +95,11 @@ impl<'a> Component<'a, Message, Action> for TransactionsView { .update(navigation::Message::SetOptions(account_navigation_entries)); Action::None } - Message::SelectNextTransaction => { + Message::SelectNextAccount => { self.navigation.update(navigation::Message::SelectNext); Action::None } - Message::SelectPrevTransaction => { + Message::SelectPrevAccount => { self.navigation.update(navigation::Message::SelectPrev); Action::None } -- cgit v1.2.3 From b9244f6049c685c2b983632359676521506f8772 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sun, 8 Feb 2026 15:22:50 +0000 Subject: task-085: fetches transactions from database --- schist_desktop_gui/src/gui/components/transactions_view.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'schist_desktop_gui/src/gui/components/transactions_view.rs') diff --git a/schist_desktop_gui/src/gui/components/transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view.rs index d28629d..6f2ad44 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view.rs @@ -26,6 +26,7 @@ pub enum Message { SelectNextAccount, SelectPrevAccount, SetAccounts(Vec), + SetTransactions(Vec), } pub enum Action { @@ -86,6 +87,7 @@ impl<'a> Component<'a, Message, Action> for TransactionsView { self.navigation.update(message); Action::None } + Message::SetAccounts(accounts) => { let account_navigation_entries: Vec = accounts .into_iter() @@ -95,10 +97,18 @@ impl<'a> Component<'a, Message, Action> for TransactionsView { .update(navigation::Message::SetOptions(account_navigation_entries)); Action::None } + + Message::SetTransactions(transactions) => { + self.transactions_by_account_id = + transactions.into_iter().into_group_map_by(|t| t.account_id); + Action::None + } + Message::SelectNextAccount => { self.navigation.update(navigation::Message::SelectNext); Action::None } + Message::SelectPrevAccount => { self.navigation.update(navigation::Message::SelectPrev); Action::None -- cgit v1.2.3 From 170c1a58ebd8b6ff1dceb5f46146f76b601c91ad Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sun, 8 Feb 2026 15:49:01 +0000 Subject: task-085: factor out Viewable impl for TransactionsView --- .../src/gui/components/transactions_view.rs | 40 +++------------------- 1 file changed, 4 insertions(+), 36 deletions(-) (limited to 'schist_desktop_gui/src/gui/components/transactions_view.rs') diff --git a/schist_desktop_gui/src/gui/components/transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view.rs index 6f2ad44..f39b522 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view.rs @@ -1,17 +1,13 @@ +mod view_transactions_view; + use std::collections::HashMap; -use iced::{ - alignment, - widget::{column, row, Container}, - Element, Length, -}; use itertools::Itertools; use schist_models::{Account, Transaction}; use crate::{ - gui::components::{navigation, AccountNavigationEntry, Navigation, Text}, - style::SPACING_LG, - traits::{Component, Viewable}, + gui::components::{navigation, AccountNavigationEntry, Navigation}, + traits::Component, }; #[derive(Clone, Debug)] @@ -52,34 +48,6 @@ impl TransactionsView { } } -impl<'a> Viewable<'a, Message> for TransactionsView { - fn view(&'a self) -> Element<'a, Message> { - let navigation = self.navigation.view().map(Message::NavigationMessage); - let main_content = column![ - Text::default("Hello, transactions!").as_element(), - Text::default(&format!( - "{} transactions in this account.", - self.navigation.active_option.clone().map_or( - 0, - |AccountNavigationEntry { account, .. }| self - .transactions_by_account_id - .get(&account.id) - .map_or(0, |transactions| transactions.len()) - ) - )) - .as_element(), - ]; - row![ - Container::new(navigation).width(Length::FillPortion(1)), - Container::new(main_content).width(Length::FillPortion(3)), - ] - .align_y(alignment::Vertical::Center) - .height(Length::Fill) - .spacing(SPACING_LG) - .into() - } -} - impl<'a> Component<'a, Message, Action> for TransactionsView { fn update(&mut self, message: Message) -> Action { match message { -- cgit v1.2.3 From 97b413aa5962bded37dedb380034fa7b97bdc06c Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sun, 8 Feb 2026 20:26:30 +0000 Subject: task-085: display transaction data in tables --- .../src/gui/components/transactions_view.rs | 87 +++++++++++++++++----- 1 file changed, 69 insertions(+), 18 deletions(-) (limited to 'schist_desktop_gui/src/gui/components/transactions_view.rs') diff --git a/schist_desktop_gui/src/gui/components/transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view.rs index f39b522..8e076f1 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view.rs @@ -6,13 +6,14 @@ use itertools::Itertools; use schist_models::{Account, Transaction}; use crate::{ - gui::components::{navigation, AccountNavigationEntry, Navigation}, + gui::components::{navigation, table, AccountNavigationEntry, Navigation, Table}, traits::Component, }; #[derive(Clone, Debug)] pub struct TransactionsView { navigation: Navigation, + table: Table, transactions_by_account_id: HashMap>, } @@ -23,6 +24,7 @@ pub enum Message { SelectPrevAccount, SetAccounts(Vec), SetTransactions(Vec), + TableMessage(table::Message), } pub enum Action { @@ -30,57 +32,106 @@ pub enum Action { } impl TransactionsView { - pub fn new(accounts: Vec, transactions: Vec) -> Self { + pub fn new(accounts: &[Account], transactions: &[Transaction]) -> Self { let account_navigation_entries: Vec = accounts - .into_iter() + .iter() + .cloned() .map(AccountNavigationEntry::new) .collect(); let navigation = Navigation::new( account_navigation_entries.first().cloned(), account_navigation_entries, ); - Self { + let mut transactions_view = Self { navigation: navigation, + table: Table::default(), transactions_by_account_id: transactions .into_iter() + .cloned() .into_group_map_by(|t| t.account_id), - } + }; + transactions_view.refresh_table(); + transactions_view + } + + fn refresh_table(&mut self) { + self.table = + Table { + cols: vec![ + table::Column::new(0, "Date"), + table::Column::new(1, "Bucket"), + table::Column::new(2, "Payee"), + table::Column::new(3, "Quantity"), + table::Column::new(4, "Balance"), + ], + rows: self.navigation.active_option.clone().map_or_else( + Vec::new, + |AccountNavigationEntry { account, .. }| { + self.transactions_by_account_id + .get(&account.id) + .cloned() + .unwrap_or_else(Vec::new) + .iter() + .map(|t| { + HashMap::from([ + (0, table::Value::String(t.date.to_string())), + ( + 1, + table::Value::String(t.bucket_id.map_or_else( + || String::from("None"), + |id| id.to_string(), + )), + ), + (2, table::Value::String(t.counterparty.clone())), + (3, table::Value::Currency(t.amount)), + (4, table::Value::String(String::from("TODO"))), + ]) + }) + .collect() + }, + ), + }; } } impl<'a> Component<'a, Message, Action> for TransactionsView { fn update(&mut self, message: Message) -> Action { match message { - Message::NavigationMessage(message) => { - self.navigation.update(message); - Action::None - } + Message::NavigationMessage(message) => self.update_navigation(message), Message::SetAccounts(accounts) => { let account_navigation_entries: Vec = accounts .into_iter() .map(AccountNavigationEntry::new) .collect(); - self.navigation - .update(navigation::Message::SetOptions(account_navigation_entries)); - Action::None + self.update_navigation(navigation::Message::SetOptions(account_navigation_entries)) } Message::SetTransactions(transactions) => { self.transactions_by_account_id = transactions.into_iter().into_group_map_by(|t| t.account_id); + self.refresh_table(); Action::None } - Message::SelectNextAccount => { - self.navigation.update(navigation::Message::SelectNext); - Action::None - } + Message::SelectNextAccount => self.update_navigation(navigation::Message::SelectNext), + + Message::SelectPrevAccount => self.update_navigation(navigation::Message::SelectPrev), + } + } +} - Message::SelectPrevAccount => { - self.navigation.update(navigation::Message::SelectPrev); +impl TransactionsView { + fn update_navigation( + &mut self, + message: navigation::Message, + ) -> Action { + match self.navigation.update(message) { + navigation::Action::ActivateOption(_) | navigation::Action::SelectOption(_) => { + self.refresh_table(); Action::None } + navigation::Action::None => Action::None, } } } -- cgit v1.2.3 From 8f57c4a25e6eb109259a082cd4f1e1007ad4044c Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Thu, 12 Feb 2026 10:10:27 +0000 Subject: task-085: each column has its own width --- schist_desktop_gui/src/gui/components/transactions_view.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'schist_desktop_gui/src/gui/components/transactions_view.rs') diff --git a/schist_desktop_gui/src/gui/components/transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view.rs index 8e076f1..e78a142 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view.rs @@ -60,9 +60,9 @@ impl TransactionsView { cols: vec![ table::Column::new(0, "Date"), table::Column::new(1, "Bucket"), - table::Column::new(2, "Payee"), - table::Column::new(3, "Quantity"), - table::Column::new(4, "Balance"), + table::Column::new(2, "Payee").mul_width(1.5), + table::Column::new(3, "Quantity").mul_width(0.67), + table::Column::new(4, "Balance").mul_width(0.67), ], rows: self.navigation.active_option.clone().map_or_else( Vec::new, -- cgit v1.2.3 From b3f91aef7c9673c180690f6e22984d50e0dbeecf Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Thu, 12 Feb 2026 19:11:22 +0000 Subject: task-085: clips overflowing table cells --- schist_desktop_gui/src/gui/components/transactions_view.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'schist_desktop_gui/src/gui/components/transactions_view.rs') diff --git a/schist_desktop_gui/src/gui/components/transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view.rs index e78a142..51877cf 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view.rs @@ -60,7 +60,7 @@ impl TransactionsView { cols: vec![ table::Column::new(0, "Date"), table::Column::new(1, "Bucket"), - table::Column::new(2, "Payee").mul_width(1.5), + table::Column::new(2, "Payee").mul_width(2.0), table::Column::new(3, "Quantity").mul_width(0.67), table::Column::new(4, "Balance").mul_width(0.67), ], -- cgit v1.2.3 From 413afad1fdf1d15fefc1b709f2eba1332a110608 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Thu, 12 Feb 2026 20:00:26 +0000 Subject: task-085: uses new iced::widget::Table instead of home-made widget --- .../src/gui/components/transactions_view.rs | 52 ++-------------------- 1 file changed, 3 insertions(+), 49 deletions(-) (limited to 'schist_desktop_gui/src/gui/components/transactions_view.rs') diff --git a/schist_desktop_gui/src/gui/components/transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view.rs index 51877cf..491049d 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view.rs @@ -6,14 +6,13 @@ use itertools::Itertools; use schist_models::{Account, Transaction}; use crate::{ - gui::components::{navigation, table, AccountNavigationEntry, Navigation, Table}, + gui::components::{navigation, AccountNavigationEntry, Navigation}, traits::Component, }; #[derive(Clone, Debug)] pub struct TransactionsView { navigation: Navigation, - table: Table, transactions_by_account_id: HashMap>, } @@ -24,7 +23,6 @@ pub enum Message { SelectPrevAccount, SetAccounts(Vec), SetTransactions(Vec), - TableMessage(table::Message), } pub enum Action { @@ -42,55 +40,13 @@ impl TransactionsView { account_navigation_entries.first().cloned(), account_navigation_entries, ); - let mut transactions_view = Self { + Self { navigation: navigation, - table: Table::default(), transactions_by_account_id: transactions .into_iter() .cloned() .into_group_map_by(|t| t.account_id), - }; - transactions_view.refresh_table(); - transactions_view - } - - fn refresh_table(&mut self) { - self.table = - Table { - cols: vec![ - table::Column::new(0, "Date"), - table::Column::new(1, "Bucket"), - table::Column::new(2, "Payee").mul_width(2.0), - table::Column::new(3, "Quantity").mul_width(0.67), - table::Column::new(4, "Balance").mul_width(0.67), - ], - rows: self.navigation.active_option.clone().map_or_else( - Vec::new, - |AccountNavigationEntry { account, .. }| { - self.transactions_by_account_id - .get(&account.id) - .cloned() - .unwrap_or_else(Vec::new) - .iter() - .map(|t| { - HashMap::from([ - (0, table::Value::String(t.date.to_string())), - ( - 1, - table::Value::String(t.bucket_id.map_or_else( - || String::from("None"), - |id| id.to_string(), - )), - ), - (2, table::Value::String(t.counterparty.clone())), - (3, table::Value::Currency(t.amount)), - (4, table::Value::String(String::from("TODO"))), - ]) - }) - .collect() - }, - ), - }; + } } } @@ -110,7 +66,6 @@ impl<'a> Component<'a, Message, Action> for TransactionsView { Message::SetTransactions(transactions) => { self.transactions_by_account_id = transactions.into_iter().into_group_map_by(|t| t.account_id); - self.refresh_table(); Action::None } @@ -128,7 +83,6 @@ impl TransactionsView { ) -> Action { match self.navigation.update(message) { navigation::Action::ActivateOption(_) | navigation::Action::SelectOption(_) => { - self.refresh_table(); Action::None } navigation::Action::None => Action::None, -- cgit v1.2.3 From 6137e4bec5fd7f34c66162b32424343a30b3c5f2 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sun, 22 Feb 2026 08:49:25 +0000 Subject: task-085: formats bucket cells --- schist_desktop_gui/src/gui/components/transactions_view.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) (limited to 'schist_desktop_gui/src/gui/components/transactions_view.rs') diff --git a/schist_desktop_gui/src/gui/components/transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view.rs index 491049d..11249a1 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view.rs @@ -3,7 +3,7 @@ mod view_transactions_view; use std::collections::HashMap; use itertools::Itertools; -use schist_models::{Account, Transaction}; +use schist_models::{Account, Bucket, Transaction}; use crate::{ gui::components::{navigation, AccountNavigationEntry, Navigation}, @@ -12,6 +12,7 @@ use crate::{ #[derive(Clone, Debug)] pub struct TransactionsView { + buckets: Vec, navigation: Navigation, transactions_by_account_id: HashMap>, } @@ -22,6 +23,7 @@ pub enum Message { SelectNextAccount, SelectPrevAccount, SetAccounts(Vec), + SetBuckets(Vec), SetTransactions(Vec), } @@ -30,7 +32,7 @@ pub enum Action { } impl TransactionsView { - pub fn new(accounts: &[Account], transactions: &[Transaction]) -> Self { + pub fn new(accounts: &[Account], buckets: &[Bucket], transactions: &[Transaction]) -> Self { let account_navigation_entries: Vec = accounts .iter() .cloned() @@ -41,6 +43,7 @@ impl TransactionsView { account_navigation_entries, ); Self { + buckets: buckets.to_vec(), navigation: navigation, transactions_by_account_id: transactions .into_iter() @@ -63,6 +66,11 @@ impl<'a> Component<'a, Message, Action> for TransactionsView { self.update_navigation(navigation::Message::SetOptions(account_navigation_entries)) } + Message::SetBuckets(buckets) => { + self.buckets = buckets; + Action::None + } + Message::SetTransactions(transactions) => { self.transactions_by_account_id = transactions.into_iter().into_group_map_by(|t| t.account_id); -- cgit v1.2.3 From 96e1b8a6158e5ac3b56c03718de5650ff805d41b Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sun, 22 Feb 2026 09:32:21 +0000 Subject: task-085: displays running balances --- .../src/gui/components/transactions_view.rs | 33 ++++++++++++++++------ 1 file changed, 25 insertions(+), 8 deletions(-) (limited to 'schist_desktop_gui/src/gui/components/transactions_view.rs') diff --git a/schist_desktop_gui/src/gui/components/transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view.rs index 11249a1..27e18cd 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view.rs @@ -1,6 +1,6 @@ mod view_transactions_view; -use std::collections::HashMap; +use std::{collections::HashMap, mem::transmute}; use itertools::Itertools; use schist_models::{Account, Bucket, Transaction}; @@ -12,6 +12,7 @@ use crate::{ #[derive(Clone, Debug)] pub struct TransactionsView { + account_balances_by_transaction_id: HashMap, buckets: Vec, navigation: Navigation, transactions_by_account_id: HashMap>, @@ -42,13 +43,30 @@ impl TransactionsView { account_navigation_entries.first().cloned(), account_navigation_entries, ); - Self { + let mut transactions_view = Self { + account_balances_by_transaction_id: HashMap::with_capacity(transactions.len()), buckets: buckets.to_vec(), navigation: navigation, - transactions_by_account_id: transactions - .into_iter() - .cloned() - .into_group_map_by(|t| t.account_id), + transactions_by_account_id: HashMap::with_capacity(accounts.len()), + }; + transactions_view.set_transactions(transactions); + transactions_view + } + + fn set_transactions(&mut self, transactions: &[Transaction]) { + self.transactions_by_account_id = transactions + .into_iter() + .cloned() + .into_group_map_by(|t| t.account_id); + self.account_balances_by_transaction_id = HashMap::new(); + for entry in self.transactions_by_account_id.iter_mut() { + entry.1.sort_by(|t1, t2| t2.date.cmp(&t1.date)); + let mut running_balance = 0; + for transaction in entry.1.iter().rev() { + running_balance += transaction.amount; + self.account_balances_by_transaction_id + .insert(transaction.id, running_balance); + } } } } @@ -72,8 +90,7 @@ impl<'a> Component<'a, Message, Action> for TransactionsView { } Message::SetTransactions(transactions) => { - self.transactions_by_account_id = - transactions.into_iter().into_group_map_by(|t| t.account_id); + self.set_transactions(&transactions); Action::None } -- cgit v1.2.3 From 4030a63ac6fe61df87dca07e4143a6b84a9e8475 Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sun, 22 Feb 2026 21:34:24 +0000 Subject: task-085: include account transfers --- .../src/gui/components/transactions_view.rs | 235 ++++++++++++++++++--- 1 file changed, 207 insertions(+), 28 deletions(-) (limited to 'schist_desktop_gui/src/gui/components/transactions_view.rs') diff --git a/schist_desktop_gui/src/gui/components/transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view.rs index 27e18cd..a52c419 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view.rs @@ -1,9 +1,10 @@ mod view_transactions_view; -use std::{collections::HashMap, mem::transmute}; +use std::collections::{HashMap, VecDeque}; +use iced::font; use itertools::Itertools; -use schist_models::{Account, Bucket, Transaction}; +use schist_models::{Account, AccountTransfer, Bucket, DateUtc, Transaction}; use crate::{ gui::components::{navigation, AccountNavigationEntry, Navigation}, @@ -12,10 +13,12 @@ use crate::{ #[derive(Clone, Debug)] pub struct TransactionsView { - account_balances_by_transaction_id: HashMap, + accounts: Vec, + account_transfers: Vec, buckets: Vec, navigation: Navigation, - transactions_by_account_id: HashMap>, + transactions: Vec, + transaction_rows_by_account_id: HashMap>, } #[derive(Clone, Debug)] @@ -24,6 +27,7 @@ pub enum Message { SelectNextAccount, SelectPrevAccount, SetAccounts(Vec), + SetAccountTransfers(Vec), SetBuckets(Vec), SetTransactions(Vec), } @@ -32,8 +36,134 @@ pub enum Action { None, } +#[derive(Debug, PartialEq, Eq)] +struct TransactionRowWithoutAggregations { + amount: i32, + bucket: String, + bucket_font_style: font::Style, + date: DateUtc, + payee: String, + payee_font_style: font::Style, +} + +impl TransactionRowWithoutAggregations { + fn from_transaction( + account_id: i32, + transaction: &Transaction, + buckets: &[Bucket], + ) -> Option { + if transaction.account_id == account_id { + let (bucket, bucket_font_style) = match transaction.bucket_id { + Some(bucket_id) => match buckets.iter().find(|b| b.id == bucket_id) { + Some(bucket) => (bucket.name.clone(), font::Style::Normal), + None => (format!("Bucket <{}>", bucket_id), font::Style::Italic), + }, + None => (String::from("None"), font::Style::Italic), + }; + Some(Self { + amount: transaction.amount, + bucket, + bucket_font_style, + date: transaction.date, + payee: transaction.counterparty.clone(), + payee_font_style: font::Style::Normal, + }) + } else { + None + } + } + + fn from_account_transfer( + account_id: i32, + account_transfer: &AccountTransfer, + accounts: &[Account], + ) -> Option { + if account_transfer.from_account_id == account_id { + let (payee, payee_font_style) = if let Some(account) = accounts + .iter() + .find(|a| a.id == account_transfer.to_account_id) + { + (account.name.clone(), font::Style::Normal) + } else { + (format!("<{}>", account_id), font::Style::Italic) + }; + Some(Self { + amount: -account_transfer.amount, + bucket: String::from("Account transfer"), + bucket_font_style: font::Style::Italic, + date: account_transfer.date, + payee, + payee_font_style, + }) + } else if account_transfer.to_account_id == account_id { + let (payee, payee_font_style) = if let Some(account) = accounts + .iter() + .find(|a| a.id == account_transfer.from_account_id) + { + (account.name.clone(), font::Style::Normal) + } else { + (format!("Account <{}>", account_id), font::Style::Italic) + }; + Some(Self { + amount: account_transfer.amount, + bucket: String::from("Account transfer"), + bucket_font_style: font::Style::Italic, + date: account_transfer.date, + payee, + payee_font_style, + }) + } else { + None + } + } + + fn with_aggregations(self, balance: i32) -> TransactionRow { + TransactionRow { + amount: self.amount, + balance, + bucket: self.bucket, + bucket_font_style: self.bucket_font_style, + date: self.date, + payee: self.payee, + payee_font_style: self.payee_font_style, + } + } + + fn compare_date_desc(&self, other: &Self) -> std::cmp::Ordering { + other.date.cmp(&self.date) + } +} + +impl std::cmp::PartialOrd for TransactionRowWithoutAggregations { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.compare_date_desc(other)) + } +} + +impl std::cmp::Ord for TransactionRowWithoutAggregations { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.compare_date_desc(other) + } +} + +#[derive(Clone, Debug, Hash)] +struct TransactionRow { + amount: i32, + balance: i32, + bucket: String, + bucket_font_style: font::Style, + date: DateUtc, + payee: String, + payee_font_style: font::Style, +} + impl TransactionsView { - pub fn new(accounts: &[Account], buckets: &[Bucket], transactions: &[Transaction]) -> Self { + pub fn new( + accounts: &[Account], + account_transfers: &[AccountTransfer], + buckets: &[Bucket], + transactions: &[Transaction], + ) -> Self { let account_navigation_entries: Vec = accounts .iter() .cloned() @@ -43,30 +173,15 @@ impl TransactionsView { account_navigation_entries.first().cloned(), account_navigation_entries, ); - let mut transactions_view = Self { - account_balances_by_transaction_id: HashMap::with_capacity(transactions.len()), + let transaction_rows_by_account_id = + calculate_rows(accounts, account_transfers, buckets, transactions); + Self { + accounts: accounts.to_vec(), + account_transfers: account_transfers.to_vec(), buckets: buckets.to_vec(), navigation: navigation, - transactions_by_account_id: HashMap::with_capacity(accounts.len()), - }; - transactions_view.set_transactions(transactions); - transactions_view - } - - fn set_transactions(&mut self, transactions: &[Transaction]) { - self.transactions_by_account_id = transactions - .into_iter() - .cloned() - .into_group_map_by(|t| t.account_id); - self.account_balances_by_transaction_id = HashMap::new(); - for entry in self.transactions_by_account_id.iter_mut() { - entry.1.sort_by(|t1, t2| t2.date.cmp(&t1.date)); - let mut running_balance = 0; - for transaction in entry.1.iter().rev() { - running_balance += transaction.amount; - self.account_balances_by_transaction_id - .insert(transaction.id, running_balance); - } + transactions: transactions.to_vec(), + transaction_rows_by_account_id, } } } @@ -77,6 +192,13 @@ impl<'a> Component<'a, Message, Action> for TransactionsView { Message::NavigationMessage(message) => self.update_navigation(message), Message::SetAccounts(accounts) => { + self.accounts = accounts.clone(); + self.transaction_rows_by_account_id = calculate_rows( + &self.accounts, + &self.account_transfers, + &self.buckets, + &self.transactions, + ); let account_navigation_entries: Vec = accounts .into_iter() .map(AccountNavigationEntry::new) @@ -84,13 +206,36 @@ impl<'a> Component<'a, Message, Action> for TransactionsView { self.update_navigation(navigation::Message::SetOptions(account_navigation_entries)) } + Message::SetAccountTransfers(account_transfers) => { + self.account_transfers = account_transfers; + self.transaction_rows_by_account_id = calculate_rows( + &self.accounts, + &self.account_transfers, + &self.buckets, + &self.transactions, + ); + Action::None + } + Message::SetBuckets(buckets) => { self.buckets = buckets; + self.transaction_rows_by_account_id = calculate_rows( + &self.accounts, + &self.account_transfers, + &self.buckets, + &self.transactions, + ); Action::None } Message::SetTransactions(transactions) => { - self.set_transactions(&transactions); + self.transactions = transactions; + self.transaction_rows_by_account_id = calculate_rows( + &self.accounts, + &self.account_transfers, + &self.buckets, + &self.transactions, + ); Action::None } @@ -114,3 +259,37 @@ impl TransactionsView { } } } + +fn calculate_rows( + accounts: &[Account], + account_transfers: &[AccountTransfer], + buckets: &[Bucket], + transactions: &[Transaction], +) -> HashMap> { + HashMap::from_iter(accounts.iter().map(|account| { + let transaction_rows_without_aggs = transactions + .iter() + .filter_map(|t| { + TransactionRowWithoutAggregations::from_transaction(account.id, t, buckets) + }) + .chain(account_transfers.iter().filter_map(|at| { + TransactionRowWithoutAggregations::from_account_transfer(account.id, at, accounts) + })) + .sorted(); + + let mut running_balance: i32 = 0; + let mut transaction_rows = VecDeque::with_capacity(transaction_rows_without_aggs.len()); + for row in transaction_rows_without_aggs.rev() { + if running_balance.checked_add(row.amount).is_none() { + panic!( + "can't add {} + {}. row: {:?}", + running_balance, row.amount, row + ); + } + running_balance += row.amount; + transaction_rows.push_front(row.with_aggregations(running_balance)); + } + + (account.id, transaction_rows.into()) + })) +} -- cgit v1.2.3