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_transfers: Vec, active_account: Option, buckets: Vec, transactions: Vec, transaction_rows_by_account_id: HashMap>, } #[derive(Clone, Debug)] pub enum Message { SetAccounts(Vec), SetAccountTransfers(Vec), SetActiveAccount(Account), SetBuckets(Vec), SetTransactions(Vec), } 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), 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 { 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 { 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_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> { 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()) })) }