diff options
| author | joeac <me@joeac.net> | 2026-02-28 08:50:46 +0000 |
|---|---|---|
| committer | joeac <me@joeac.net> | 2026-02-28 08:50:46 +0000 |
| commit | 342dadaf91093c1ce9c3ad248a2952ed3cde4de6 (patch) | |
| tree | 0546aa27e558c498cbc730a6f1854f11e9fa844b /schist_desktop_gui/src/gui/components/transactions_view.rs | |
| parent | 65596363d43995bf2002d4aa8405d84484f2542b (diff) | |
| parent | f3659bd8e9d5e20528aaa1e0e7c1b206fb0eb31c (diff) | |
Merge pull request 'task-085: view transactions in desktop GUI' (#5) from task-085 into main
Reviewed-on: https://git.joeac.net/joeac/schist/pulls/5
Diffstat (limited to 'schist_desktop_gui/src/gui/components/transactions_view.rs')
| -rw-r--r-- | schist_desktop_gui/src/gui/components/transactions_view.rs | 337 |
1 files changed, 255 insertions, 82 deletions
diff --git a/schist_desktop_gui/src/gui/components/transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view.rs index 1cb569c..a52c419 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view.rs @@ -1,122 +1,295 @@ -use iced::{ - alignment, - widget::{column, row, Container}, - Element, Length, -}; -use schist_models::Account; +mod view_transactions_view; + +use std::collections::{HashMap, VecDeque}; + +use iced::font; +use itertools::Itertools; +use schist_models::{Account, AccountTransfer, Bucket, DateUtc, Transaction}; use crate::{ - gui::components::{navigation, Navigation, Text}, - style::SPACING_LG, - traits::{Component, Viewable}, + gui::components::{navigation, AccountNavigationEntry, Navigation}, + traits::Component, }; #[derive(Clone, Debug)] pub struct TransactionsView { accounts: Vec<Account>, - greeting: String, - navigation: Navigation<Text>, + account_transfers: Vec<AccountTransfer>, + buckets: Vec<Bucket>, + navigation: Navigation<AccountNavigationEntry>, + transactions: Vec<Transaction>, + transaction_rows_by_account_id: HashMap<i32, Vec<TransactionRow>>, } #[derive(Clone, Debug)] pub enum Message { - NavigationMessage(navigation::Message<Text>), - SelectNextTransaction, - SelectPrevTransaction, + NavigationMessage(navigation::Message<AccountNavigationEntry>), + SelectNextAccount, + SelectPrevAccount, SetAccounts(Vec<Account>), + SetAccountTransfers(Vec<AccountTransfer>), + SetBuckets(Vec<Bucket>), + SetTransactions(Vec<Transaction>), } pub enum Action { None, } -impl TransactionsView { - pub fn new(accounts: Vec<Account>, greeting: &str) -> Self { - let account_names = view_account_names(accounts.clone()); - let navigation = Navigation::new(account_names.clone().first().cloned(), account_names); - Self { - accounts: accounts, - greeting: greeting.to_owned(), - navigation: navigation, +#[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<'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()]; - 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 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], + account_transfers: &[AccountTransfer], + buckets: &[Bucket], + transactions: &[Transaction], + ) -> Self { + let account_navigation_entries: Vec<AccountNavigationEntry> = accounts + .iter() + .cloned() + .map(AccountNavigationEntry::new) + .collect(); + 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); + 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, + } } } 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, - }; - Action::None - } + Message::NavigationMessage(message) => self.update_navigation(message), + 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, - } + self.transaction_rows_by_account_id = calculate_rows( + &self.accounts, + &self.account_transfers, + &self.buckets, + &self.transactions, + ); + let account_navigation_entries: Vec<AccountNavigationEntry> = accounts + .into_iter() + .map(AccountNavigationEntry::new) + .collect(); + self.update_navigation(navigation::Message::SetOptions(account_navigation_entries)) } - 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, - } + + 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::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, - } + + 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 } + + Message::SelectNextAccount => self.update_navigation(navigation::Message::SelectNext), + + Message::SelectPrevAccount => self.update_navigation(navigation::Message::SelectPrev), } } } -fn make_greeting(account_name: Text) -> String { - format!("Hello, {} account!", account_name.content) +impl TransactionsView { + fn update_navigation( + &mut self, + message: navigation::Message<AccountNavigationEntry>, + ) -> Action { + match self.navigation.update(message) { + navigation::Action::ActivateOption(_) | navigation::Action::SelectOption(_) => { + Action::None + } + navigation::Action::None => Action::None, + } + } } -fn view_account_names(accounts: Vec<Account>) -> Vec<Text> { - accounts - .iter() - .map(|acc| Text::new(acc.name.as_str())) - .collect() +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()) + })) } |
