diff options
| author | Joe Carstairs <me@joeac.net> | 2026-08-28 17:00:27 +0100 |
|---|---|---|
| committer | Joe Carstairs <me@joeac.net> | 2026-08-28 17:00:27 +0100 |
| commit | 3555fffb71838a1f34421255270913831adb69f0 (patch) | |
| tree | cf02ee7d9f6ab004ce0ba6c07fb60cae24c15feb /schist_desktop_gui | |
| parent | 988e4670f24994c8cab000c323a607fea284a447 (diff) | |
refactor out transactions table
Diffstat (limited to 'schist_desktop_gui')
| -rw-r--r-- | schist_desktop_gui/src/gui/components.rs | 2 | ||||
| -rw-r--r-- | schist_desktop_gui/src/gui/components/transactions_table.rs | 266 | ||||
| -rw-r--r-- | schist_desktop_gui/src/gui/components/transactions_table/view_transactions_table.rs (renamed from schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs) | 32 | ||||
| -rw-r--r-- | schist_desktop_gui/src/gui/components/transactions_view.rs | 271 |
4 files changed, 340 insertions, 231 deletions
diff --git a/schist_desktop_gui/src/gui/components.rs b/schist_desktop_gui/src/gui/components.rs index 606c418..3bb09e3 100644 --- a/schist_desktop_gui/src/gui/components.rs +++ b/schist_desktop_gui/src/gui/components.rs @@ -6,6 +6,7 @@ pub mod button; pub mod navigation; pub mod panel; pub mod text; +pub mod transactions_table; pub mod transactions_view; pub use account_navigation_entry::AccountNavigationEntry; @@ -16,4 +17,5 @@ pub use button::{button, panel_button}; pub use navigation::Navigation; pub use panel::Panel; pub use text::Text; +pub use transactions_table::TransactionsTable; pub use transactions_view::TransactionsView; diff --git a/schist_desktop_gui/src/gui/components/transactions_table.rs b/schist_desktop_gui/src/gui/components/transactions_table.rs new file mode 100644 index 0000000..d4ab398 --- /dev/null +++ b/schist_desktop_gui/src/gui/components/transactions_table.rs @@ -0,0 +1,266 @@ +mod view_transactions_table; + +use std::collections::{HashMap, VecDeque}; +use std::ops::Neg; + +use iced::font; +use itertools::Itertools; +use schist_models::{Account, AccountTransfer, Bucket, DateUtc, Transaction}; + +use crate::{ + traits::Component, +}; + +#[derive(Clone, Debug)] +pub struct TransactionsTable { + accounts: Vec<Account>, + account_transfers: Vec<AccountTransfer>, + active_account: Option<Account>, + buckets: Vec<Bucket>, + transactions: Vec<Transaction>, + transaction_rows_by_account_id: HashMap<i32, Vec<TransactionRow>>, +} + +#[derive(Clone, Debug)] +pub enum Message { + SetAccounts(Vec<Account>), + SetAccountTransfers(Vec<AccountTransfer>), + SetActiveAccount(Account), + SetBuckets(Vec<Bucket>), + SetTransactions(Vec<Transaction>), +} + +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<Self> { + 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), + Option::None => (format!("Bucket <{}>", bucket_id), font::Style::Italic), + }, + Option::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<Self> { + 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.neg(), + 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<std::cmp::Ordering> { + 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 TransactionsTable { + pub fn new( + accounts: &[Account], + active_account: &Option<Account>, + account_transfers: &[AccountTransfer], + buckets: &[Bucket], + transactions: &[Transaction], + ) -> Self { + let transaction_rows_by_account_id = + calculate_rows(accounts, account_transfers, buckets, transactions); + Self { + accounts: accounts.to_vec(), + account_transfers: account_transfers.to_vec(), + active_account: active_account.clone(), + buckets: buckets.to_vec(), + transactions: transactions.to_vec(), + transaction_rows_by_account_id, + } + } +} + +impl<'a> Component<'a, Message, Action> for TransactionsTable { + fn update(&mut self, message: Message) -> Action { + match 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, + ); + Action::None + } + + 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::SetActiveAccount(account) => { + self.active_account = Some(account); + 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.transactions = transactions; + self.transaction_rows_by_account_id = calculate_rows( + &self.accounts, + &self.account_transfers, + &self.buckets, + &self.transactions, + ); + Action::None + } + } + } +} + +fn calculate_rows( + accounts: &[Account], + account_transfers: &[AccountTransfer], + buckets: &[Bucket], + transactions: &[Transaction], +) -> HashMap<i32, Vec<TransactionRow>> { + 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()) + })) +} diff --git a/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_table/view_transactions_table.rs index 268984b..b6e8599 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_table/view_transactions_table.rs @@ -1,23 +1,19 @@ use iced::{ - alignment, widget::{ - row, scrollable::{self, Scrollbar}, table, table::column, - Container, Scrollable, + Scrollable, }, - Element, Length, + Element, }; -use crate::{gui::components::Text, style::SPACING_LG, traits::Viewable}; +use crate::{gui::components::Text, traits::Viewable}; -use super::{Message, TransactionRow, TransactionsView}; +use super::{Message, TransactionRow, TransactionsTable}; -impl<'a> Viewable<'a, Message> for TransactionsView { +impl<'a> Viewable<'a, Message> for TransactionsTable { fn view(&'a self) -> Element<'a, Message> { - let navigation = self.navigation.view().map(Message::NavigationMessage); - let columns = [ column( Text::default("Date").width(iced::Length::Fixed(128.0)), @@ -46,12 +42,11 @@ impl<'a> Viewable<'a, Message> for TransactionsView { ]; let rows = self - .navigation - .active_option - .clone() - .map_or_else(Vec::default, |option| { + .active_account + .as_ref() + .map_or_else(Vec::default, |active_account| { self.transaction_rows_by_account_id - .get(&option.account.id) + .get(&active_account.id) .cloned() .unwrap_or_else(Vec::default) }); @@ -61,14 +56,7 @@ impl<'a> Viewable<'a, Message> for TransactionsView { vertical: Scrollbar::default(), }); - row![ - Container::new(navigation).width(Length::FillPortion(1)), - Container::new(table).width(Length::FillPortion(3)), - ] - .align_y(alignment::Vertical::Center) - .height(Length::Fill) - .spacing(u32::from(SPACING_LG)) - .into() + table.into() } } diff --git a/schist_desktop_gui/src/gui/components/transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view.rs index a52c419..5627e71 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view.rs @@ -1,24 +1,27 @@ -mod view_transactions_view; - -use std::collections::{HashMap, VecDeque}; +use iced::{ + alignment, + widget::{row, Container}, + Element, Length, +}; -use iced::font; -use itertools::Itertools; -use schist_models::{Account, AccountTransfer, Bucket, DateUtc, Transaction}; +use schist_models::{Account, AccountTransfer, Bucket, Transaction}; use crate::{ - gui::components::{navigation, AccountNavigationEntry, Navigation}, - traits::Component, + gui::components::{ + AccountNavigationEntry, + Navigation, + TransactionsTable, + navigation, + transactions_table + }, + traits::{Component, Viewable}, + style::SPACING_LG, }; #[derive(Clone, Debug)] pub struct TransactionsView { - accounts: Vec<Account>, - account_transfers: Vec<AccountTransfer>, - buckets: Vec<Bucket>, navigation: Navigation<AccountNavigationEntry>, - transactions: Vec<Transaction>, - transaction_rows_by_account_id: HashMap<i32, Vec<TransactionRow>>, + transactions_table: TransactionsTable, } #[derive(Clone, Debug)] @@ -30,133 +33,13 @@ pub enum Message { SetAccountTransfers(Vec<AccountTransfer>), SetBuckets(Vec<Bucket>), SetTransactions(Vec<Transaction>), + TransactionsTableMessage(transactions_table::Message), } 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<Self> { - 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<Self> { - 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<std::cmp::Ordering> { - 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], @@ -169,19 +52,14 @@ impl TransactionsView { .cloned() .map(AccountNavigationEntry::new) .collect(); + let active_nav_entry = account_navigation_entries.first().cloned(); let navigation = Navigation::new( - account_navigation_entries.first().cloned(), - account_navigation_entries, - ); - let transaction_rows_by_account_id = - calculate_rows(accounts, account_transfers, buckets, transactions); + active_nav_entry.clone(), account_navigation_entries); + let transactions_table = + TransactionsTable::new(accounts, &active_nav_entry.map(|entry| entry.account), account_transfers, buckets, transactions); Self { - accounts: accounts.to_vec(), - account_transfers: account_transfers.to_vec(), - buckets: buckets.to_vec(), navigation: navigation, - transactions: transactions.to_vec(), - transaction_rows_by_account_id, + transactions_table, } } } @@ -191,52 +69,36 @@ impl<'a> Component<'a, Message, Action> for TransactionsView { match message { Message::NavigationMessage(message) => self.update_navigation(message), + Message::TransactionsTableMessage(message) => self.update_transactions_table(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 action_1 = self.update_transactions_table( + transactions_table::Message::SetAccounts(accounts.clone())); + let account_navigation_entries: Vec<AccountNavigationEntry> = accounts .into_iter() .map(AccountNavigationEntry::new) .collect(); - self.update_navigation(navigation::Message::SetOptions(account_navigation_entries)) + let action_2 = self.update_navigation(navigation::Message::SetOptions(account_navigation_entries)); + + match (action_1, action_2) { + (Action::None, Action::None) => Action::None + } } 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 + self.update_transactions_table( + transactions_table::Message::SetAccountTransfers(account_transfers.clone())) } 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 + self.update_transactions_table( + transactions_table::Message::SetBuckets(buckets.clone())) } Message::SetTransactions(transactions) => { - self.transactions = transactions; - self.transaction_rows_by_account_id = calculate_rows( - &self.accounts, - &self.account_transfers, - &self.buckets, - &self.transactions, - ); - Action::None + self.update_transactions_table( + transactions_table::Message::SetTransactions(transactions.clone())) } Message::SelectNextAccount => self.update_navigation(navigation::Message::SelectNext), @@ -252,44 +114,35 @@ impl TransactionsView { message: navigation::Message<AccountNavigationEntry>, ) -> Action { match self.navigation.update(message) { - navigation::Action::ActivateOption(_) | navigation::Action::SelectOption(_) => { - Action::None + navigation::Action::ActivateOption(option) | navigation::Action::SelectOption(option) => { + self.update_transactions_table(transactions_table::Message::SetActiveAccount(option.account)) } navigation::Action::None => Action::None, } } -} - -fn calculate_rows( - accounts: &[Account], - account_transfers: &[AccountTransfer], - buckets: &[Bucket], - transactions: &[Transaction], -) -> HashMap<i32, Vec<TransactionRow>> { - 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)); + fn update_transactions_table( + &mut self, + message: transactions_table::Message, + ) -> Action { + match self.transactions_table.update(message) { + transactions_table::Action::None => Action::None, } + } +} - (account.id, transaction_rows.into()) - })) +impl<'a> Viewable<'a, Message> for TransactionsView { + fn view(&'a self) -> Element<'a, Message> { + let navigation = self.navigation.view().map(Message::NavigationMessage); + let table = self.transactions_table.view().map(Message::TransactionsTableMessage); + + row![ + Container::new(navigation).width(Length::FillPortion(1)), + Container::new(table).width(Length::FillPortion(3)), + ] + .align_y(alignment::Vertical::Center) + .height(Length::Fill) + .spacing(u32::from(SPACING_LG)) + .into() + } } |
