summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--schist_core/schist_models/src/account_transfer.rs2
-rw-r--r--schist_core/schist_models/src/date_utc.rs1
-rw-r--r--schist_core/schist_models/src/transaction.rs11
-rw-r--r--schist_desktop_gui/src/gui.rs18
-rw-r--r--schist_desktop_gui/src/gui/components/text.rs7
-rw-r--r--schist_desktop_gui/src/gui/components/transactions_view.rs235
-rw-r--r--schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs61
-rw-r--r--schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs13
8 files changed, 275 insertions, 73 deletions
diff --git a/schist_core/schist_models/src/account_transfer.rs b/schist_core/schist_models/src/account_transfer.rs
index ba33dee..11a14a1 100644
--- a/schist_core/schist_models/src/account_transfer.rs
+++ b/schist_core/schist_models/src/account_transfer.rs
@@ -3,7 +3,7 @@ use diesel::prelude::{Identifiable, Insertable, Queryable, Selectable};
use crate::date_utc::DateUtc;
-#[derive(Builder, Queryable, Identifiable, Selectable, Debug, PartialEq, Insertable)]
+#[derive(Builder, Clone, Queryable, Identifiable, Selectable, Debug, PartialEq, Eq, Insertable)]
#[diesel(table_name = crate::schema::account_transfers)]
pub struct AccountTransfer {
pub id: i32,
diff --git a/schist_core/schist_models/src/date_utc.rs b/schist_core/schist_models/src/date_utc.rs
index 658a8dd..e4363a7 100644
--- a/schist_core/schist_models/src/date_utc.rs
+++ b/schist_core/schist_models/src/date_utc.rs
@@ -26,6 +26,7 @@ use crate::datetime_utc::DatetimeUtc;
Clone,
Copy,
Debug,
+ Hash,
AsExpression,
FromSqlRow,
)]
diff --git a/schist_core/schist_models/src/transaction.rs b/schist_core/schist_models/src/transaction.rs
index 7e754be..ef0a0ab 100644
--- a/schist_core/schist_models/src/transaction.rs
+++ b/schist_core/schist_models/src/transaction.rs
@@ -4,7 +4,16 @@ use diesel::prelude::{Associations, Identifiable, Insertable, Queryable, Selecta
use crate::{account::Account, date_utc::DateUtc};
#[derive(
- Builder, Clone, Queryable, Identifiable, Selectable, Insertable, Associations, Debug, PartialEq,
+ Builder,
+ Clone,
+ Queryable,
+ Identifiable,
+ Selectable,
+ Insertable,
+ Associations,
+ Debug,
+ PartialEq,
+ Eq,
)]
#[diesel(table_name = crate::schema::transactions)]
#[diesel(belongs_to(Account))]
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()),
}