diff options
| author | Joe Carstairs <me@joeac.net> | 2026-02-22 21:34:24 +0000 |
|---|---|---|
| committer | Joe Carstairs <me@joeac.net> | 2026-02-22 21:34:24 +0000 |
| commit | 4030a63ac6fe61df87dca07e4143a6b84a9e8475 (patch) | |
| tree | b7455e7f865bf638f0ceaad7b6bbfca51b988824 /schist_desktop_gui | |
| parent | 96e1b8a6158e5ac3b56c03718de5650ff805d41b (diff) | |
task-085: include account transfers
Diffstat (limited to 'schist_desktop_gui')
5 files changed, 263 insertions, 71 deletions
diff --git a/schist_desktop_gui/src/gui.rs b/schist_desktop_gui/src/gui.rs index 9648ff0..78fea11 100644 --- a/schist_desktop_gui/src/gui.rs +++ b/schist_desktop_gui/src/gui.rs @@ -7,9 +7,10 @@ use std::{path::PathBuf, str::FromStr}; use diesel::SqliteConnection; use iced::{Element, Task}; -use schist_models::{Account, Bucket, Transaction}; +use schist_models::{Account, AccountTransfer, Bucket, Transaction}; use schist_queries::{ - accounts::get_all_accounts, buckets::get_all_buckets, transactions::get_all_transactions, + account_transfers::get_all_account_transfers, accounts::get_all_accounts, + buckets::get_all_buckets, transactions::get_all_transactions, }; use crate::{ @@ -33,6 +34,7 @@ pub enum Message { FilesScreenMessage(files_screen::Message), MainScreenMessage(main_screen::Message), RefetchedAccounts(FetchResult<Vec<Account>>), + RefetchedAccountTransfers(FetchResult<Vec<AccountTransfer>>), RefetchedBuckets(FetchResult<Vec<Bucket>>), RefetchedTransactions(FetchResult<Vec<Transaction>>), } @@ -188,6 +190,16 @@ impl Gui { } iced::Task::none() } + Message::RefetchedAccountTransfers(account_transfers) => { + match account_transfers { + Ok(account_transfers) => { + self.main_screen + .update(main_screen::Message::SetAccountTransfers(account_transfers)); + } + Err(err) => println!("Failed to get account transfers: {:?}", err.message), + } + iced::Task::none() + } Message::RefetchedTransactions(transactions) => { match transactions { Ok(transactions) => { @@ -239,6 +251,7 @@ impl Gui { ) -> Task<Message> { let buckets = get_all_buckets(&mut connection).map_err(FetchError::new); let accounts = get_all_accounts(&mut connection).map_err(FetchError::new); + let account_transfers = get_all_account_transfers(&mut connection).map_err(FetchError::new); let transactions = get_all_transactions(&mut connection).map_err(FetchError::new); self.connection = Some(connection); self.active_screen = Screen::MainScreen; @@ -250,6 +263,7 @@ impl Gui { iced::Task::batch(vec![ self.update(Message::RefetchedBuckets(buckets)), self.update(Message::RefetchedAccounts(accounts)), + self.update(Message::RefetchedAccountTransfers(account_transfers)), self.update(Message::RefetchedTransactions(transactions)), ]) } diff --git a/schist_desktop_gui/src/gui/components/text.rs b/schist_desktop_gui/src/gui/components/text.rs index 0cf9698..068e89a 100644 --- a/schist_desktop_gui/src/gui/components/text.rs +++ b/schist_desktop_gui/src/gui/components/text.rs @@ -160,6 +160,13 @@ impl Text { ..self.clone() } } + + pub fn style(&self, font_style: font::Style) -> Self { + Self { + font_style, + ..self.clone() + } + } } impl<'a, Message> From<Text> for iced::Element<'a, Message> 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<i32, i32>, + accounts: Vec<Account>, + account_transfers: Vec<AccountTransfer>, buckets: Vec<Bucket>, navigation: Navigation<AccountNavigationEntry>, - transactions_by_account_id: HashMap<i32, Vec<Transaction>>, + transactions: Vec<Transaction>, + transaction_rows_by_account_id: HashMap<i32, Vec<TransactionRow>>, } #[derive(Clone, Debug)] @@ -24,6 +27,7 @@ pub enum Message { SelectNextAccount, SelectPrevAccount, SetAccounts(Vec<Account>), + SetAccountTransfers(Vec<AccountTransfer>), SetBuckets(Vec<Bucket>), SetTransactions(Vec<Transaction>), } @@ -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<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], buckets: &[Bucket], transactions: &[Transaction]) -> Self { + pub fn new( + accounts: &[Account], + account_transfers: &[AccountTransfer], + buckets: &[Bucket], + transactions: &[Transaction], + ) -> Self { let account_navigation_entries: Vec<AccountNavigationEntry> = 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<AccountNavigationEntry> = 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<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_view/view_transactions_view.rs index 4f61493..1427830 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs @@ -1,5 +1,3 @@ -use std::collections::HashMap; - use iced::{ alignment, widget::{ @@ -11,11 +9,10 @@ use iced::{ }, Element, Length, }; -use schist_models::{Bucket, Transaction}; use crate::{gui::components::Text, style::SPACING_LG, traits::Viewable}; -use super::{Message, TransactionsView}; +use super::{Message, TransactionRow, TransactionsView}; impl<'a> Viewable<'a, Message> for TransactionsView { fn view(&'a self) -> Element<'a, Message> { @@ -28,7 +25,7 @@ impl<'a> Viewable<'a, Message> for TransactionsView { ), column( Text::default("Bucket").width(iced::Length::Fixed(128.0)), - |t: Transaction| view_bucket_cell(t, &self.buckets), + view_bucket_cell, ), column( Text::default("Payee").width(iced::Length::Fixed(256.0)), @@ -40,7 +37,7 @@ impl<'a> Viewable<'a, Message> for TransactionsView { ), column( Text::default("Balance").width(iced::Length::Fixed(96.0)), - |t: Transaction| view_balance_cell(t, &self.account_balances_by_transaction_id), + view_balance_cell, ), ]; @@ -49,7 +46,7 @@ impl<'a> Viewable<'a, Message> for TransactionsView { .active_option .clone() .map_or_else(Vec::default, |option| { - self.transactions_by_account_id + self.transaction_rows_by_account_id .get(&option.account.id) .cloned() .unwrap_or_else(Vec::default) @@ -71,54 +68,40 @@ impl<'a> Viewable<'a, Message> for TransactionsView { } } -fn view_date_cell(transaction: Transaction) -> Text { - Text::default(transaction.date.format("%e %b %Y").as_str()) +fn view_date_cell(row: TransactionRow) -> Text { + Text::default(row.date.format("%e %b %Y").as_str()) .width(iced::Length::Fixed(128.0)) .small() .clip() } -fn view_bucket_cell(t: Transaction, buckets: &[Bucket]) -> Text { - (t.bucket_id.map_or_else( - || Text::default("None").italic(), - |id| { - buckets.iter().find(|b| b.id == id).map_or_else( - || Text::default(format!("<{}>", id).as_str()).italic(), - |b| Text::default(&b.name), - ) - }, - )) - .width(iced::Length::Fixed(128.0)) - .small() - .clip() +fn view_bucket_cell(row: TransactionRow) -> Text { + Text::default(&row.bucket) + .style(row.bucket_font_style) + .width(iced::Length::Fixed(128.0)) + .small() + .clip() } -fn view_payee_cell(t: Transaction) -> Text { - Text::default(&t.counterparty) +fn view_payee_cell(row: TransactionRow) -> Text { + Text::default(&row.payee) + .style(row.payee_font_style) .width(iced::Length::Fixed(256.0)) .small() .clip() } -fn view_quantity_cell(t: Transaction) -> Text { - Text::currency(t.amount) +fn view_quantity_cell(row: TransactionRow) -> Text { + Text::currency(row.amount) .width(iced::Length::Fixed(96.0)) .small() .clip() .align_right() } -fn view_balance_cell( - transaction: Transaction, - account_balances_by_transaction_id: &HashMap<i32, i32>, -) -> Text { - (account_balances_by_transaction_id - .get(&transaction.id) - .map_or_else( - || Text::default("Error").danger(), - |&balance| Text::currency(balance), - )) - .width(iced::Length::Fixed(96.0)) - .small() - .clip() +fn view_balance_cell(row: TransactionRow) -> Text { + Text::currency(row.balance) + .width(iced::Length::Fixed(96.0)) + .small() + .clip() } diff --git a/schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs b/schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs index 038d65f..6580758 100644 --- a/schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs +++ b/schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs @@ -4,7 +4,7 @@ use iced::{ widget::{row, Container}, Element, Length, }; -use schist_models::{Account, Bucket, Transaction}; +use schist_models::{Account, AccountTransfer, Bucket, Transaction}; use crate::{ gui::components::{ @@ -35,6 +35,7 @@ pub enum Message { TransactionsViewMessage(transactions_view::Message), SelectView(View), SetAccounts(Vec<Account>), + SetAccountTransfers(Vec<AccountTransfer>), SetBuckets(Vec<Bucket>), SetTransactions(Vec<Transaction>), ViewNavigationMessage(navigation::Message<View>), @@ -121,6 +122,14 @@ impl<'a> Component<'a, Message, Action> for MainScreen { Action::None } + Message::SetAccountTransfers(account_transfers) => { + self.transactions_view + .update(transactions_view::Message::SetAccountTransfers( + account_transfers, + )); + Action::None + } + Message::SetTransactions(transactions) => { self.transactions_view .update(transactions_view::Message::SetTransactions(transactions)); @@ -206,7 +215,7 @@ impl MainScreen { balances_view: BalancesView::new(Vec::new()), buckets: Vec::new(), buckets_view: BucketsView::new(Vec::new(), "Hello, buckets!"), - transactions_view: TransactionsView::new(&[], &[], &[]), + transactions_view: TransactionsView::new(&[], &[], &[], &[]), active_view: View::Balances, view_navigation: Navigation::new(Some(View::Balances), View::views()), } |
