summaryrefslogtreecommitdiff
path: root/schist_desktop_gui/src/gui/components/transactions_view.rs
diff options
context:
space:
mode:
authorjoeac <me@joeac.net>2026-02-28 08:50:46 +0000
committerjoeac <me@joeac.net>2026-02-28 08:50:46 +0000
commit342dadaf91093c1ce9c3ad248a2952ed3cde4de6 (patch)
tree0546aa27e558c498cbc730a6f1854f11e9fa844b /schist_desktop_gui/src/gui/components/transactions_view.rs
parent65596363d43995bf2002d4aa8405d84484f2542b (diff)
parentf3659bd8e9d5e20528aaa1e0e7c1b206fb0eb31c (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.rs337
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())
+ }))
}