summaryrefslogtreecommitdiff
path: root/schist_desktop_gui
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
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')
-rw-r--r--schist_desktop_gui/src/gui.rs33
-rw-r--r--schist_desktop_gui/src/gui/components.rs2
-rw-r--r--schist_desktop_gui/src/gui/components/account_navigation_entry.rs34
-rw-r--r--schist_desktop_gui/src/gui/components/balances_view/view_balances_view.rs2
-rw-r--r--schist_desktop_gui/src/gui/components/buckets_view.rs2
-rw-r--r--schist_desktop_gui/src/gui/components/panel.rs4
-rw-r--r--schist_desktop_gui/src/gui/components/text.rs132
-rw-r--r--schist_desktop_gui/src/gui/components/transactions_view.rs337
-rw-r--r--schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs112
-rw-r--r--schist_desktop_gui/src/gui/screens/main_screen/main_screen.rs30
-rw-r--r--schist_desktop_gui/src/main.rs4
-rw-r--r--schist_desktop_gui/src/style.rs3
-rw-r--r--schist_desktop_gui/src/theme.rs11
13 files changed, 597 insertions, 109 deletions
diff --git a/schist_desktop_gui/src/gui.rs b/schist_desktop_gui/src/gui.rs
index 26f147b..78fea11 100644
--- a/schist_desktop_gui/src/gui.rs
+++ b/schist_desktop_gui/src/gui.rs
@@ -7,8 +7,11 @@ use std::{path::PathBuf, str::FromStr};
use diesel::SqliteConnection;
use iced::{Element, Task};
-use schist_models::{Account, Bucket};
-use schist_queries::{accounts::get_all_accounts, buckets::get_all_buckets};
+use schist_models::{Account, AccountTransfer, Bucket, Transaction};
+use schist_queries::{
+ account_transfers::get_all_account_transfers, accounts::get_all_accounts,
+ buckets::get_all_buckets, transactions::get_all_transactions,
+};
use crate::{
cache::{put_to_cache, remove_from_cache, CACHE_KEYS},
@@ -31,7 +34,9 @@ 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>>),
}
pub type FetchResult<T> = std::result::Result<T, FetchError>;
@@ -185,6 +190,26 @@ 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) => {
+ self.main_screen
+ .update(main_screen::Message::SetTransactions(transactions));
+ }
+ Err(err) => println!("Failed to get transactions: {:?}", err.message),
+ };
+ Task::none()
+ }
Message::FilesScreenMessage(message) => self.update_files_screen(message),
Message::FailedToCreateFile(err) => {
self.update_files_screen(files_screen::Message::ReportFailedToCreateFile(err))
@@ -226,6 +251,8 @@ 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;
if let Some(path) = path.to_str() {
@@ -236,6 +263,8 @@ 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.rs b/schist_desktop_gui/src/gui/components.rs
index bb5590f..606c418 100644
--- a/schist_desktop_gui/src/gui/components.rs
+++ b/schist_desktop_gui/src/gui/components.rs
@@ -1,3 +1,4 @@
+mod account_navigation_entry;
pub mod balances_view;
pub mod bucket_navigation_entry;
pub mod buckets_view;
@@ -7,6 +8,7 @@ pub mod panel;
pub mod text;
pub mod transactions_view;
+pub use account_navigation_entry::AccountNavigationEntry;
pub use balances_view::BalancesView;
pub use bucket_navigation_entry::BucketNavigationEntry;
pub use buckets_view::BucketsView;
diff --git a/schist_desktop_gui/src/gui/components/account_navigation_entry.rs b/schist_desktop_gui/src/gui/components/account_navigation_entry.rs
new file mode 100644
index 0000000..6a582a7
--- /dev/null
+++ b/schist_desktop_gui/src/gui/components/account_navigation_entry.rs
@@ -0,0 +1,34 @@
+use iced::Element;
+use schist_models::Account;
+
+use crate::{
+ gui::components::{navigation, Text},
+ impl_focusable,
+};
+
+#[derive(Clone, Debug, PartialEq)]
+pub struct AccountNavigationEntry {
+ pub account: Account,
+ is_focused: bool,
+}
+
+impl AccountNavigationEntry {
+ pub fn new(account: Account) -> Self {
+ Self {
+ account,
+ is_focused: false,
+ }
+ }
+}
+
+impl<'a> From<&'a AccountNavigationEntry>
+ for Element<'a, navigation::Message<AccountNavigationEntry>>
+{
+ fn from(account_navigation_entry: &'a AccountNavigationEntry) -> Self {
+ Text::default(&account_navigation_entry.account.name)
+ .highlight_maybe(account_navigation_entry.is_focused)
+ .into()
+ }
+}
+
+impl_focusable!(AccountNavigationEntry);
diff --git a/schist_desktop_gui/src/gui/components/balances_view/view_balances_view.rs b/schist_desktop_gui/src/gui/components/balances_view/view_balances_view.rs
index 22f22b6..013a6fb 100644
--- a/schist_desktop_gui/src/gui/components/balances_view/view_balances_view.rs
+++ b/schist_desktop_gui/src/gui/components/balances_view/view_balances_view.rs
@@ -24,7 +24,7 @@ impl<'a> Viewable<'a, Message> for BalancesView {
]
.align_y(alignment::Vertical::Center)
.height(Length::Fill)
- .spacing(SPACING_LG)
+ .spacing(u32::from(SPACING_LG))
.into()
}
}
diff --git a/schist_desktop_gui/src/gui/components/buckets_view.rs b/schist_desktop_gui/src/gui/components/buckets_view.rs
index 7f5b218..8f12019 100644
--- a/schist_desktop_gui/src/gui/components/buckets_view.rs
+++ b/schist_desktop_gui/src/gui/components/buckets_view.rs
@@ -115,7 +115,7 @@ impl<'a> Viewable<'a, Message> for BucketsView {
]
.align_y(alignment::Vertical::Center)
.height(Length::Fill)
- .spacing(SPACING_LG)
+ .spacing(u32::from(SPACING_LG))
.into()
}
}
diff --git a/schist_desktop_gui/src/gui/components/panel.rs b/schist_desktop_gui/src/gui/components/panel.rs
index 364659b..85bfb97 100644
--- a/schist_desktop_gui/src/gui/components/panel.rs
+++ b/schist_desktop_gui/src/gui/components/panel.rs
@@ -39,7 +39,7 @@ where
cols.push(
Text::default(err)
.highlight_maybe(value.is_focused)
- .small()
+ .v_small()
.danger()
.into(),
);
@@ -48,7 +48,7 @@ where
cols.push(
Text::default(small)
.highlight_maybe(value.is_focused)
- .small()
+ .v_small()
.weak()
.into(),
);
diff --git a/schist_desktop_gui/src/gui/components/text.rs b/schist_desktop_gui/src/gui/components/text.rs
index d171043..068e89a 100644
--- a/schist_desktop_gui/src/gui/components/text.rs
+++ b/schist_desktop_gui/src/gui/components/text.rs
@@ -1,17 +1,29 @@
use std::borrow::Borrow;
+use iced::{font, widget::container, Font};
+
use crate::{impl_focusable, style::*};
#[derive(Clone, Debug, PartialEq)]
pub struct Text {
pub content: String,
+ align: Alignment,
colour: Option<Colour>,
+ font_style: font::Style,
+ overflow_strategy: OverflowStrategy,
size: Option<Size>,
strength: Option<Strength>,
+ width: iced::Length,
is_focused: bool,
}
#[derive(Clone, Debug, PartialEq)]
+enum Alignment {
+ Left,
+ Right,
+}
+
+#[derive(Clone, Debug, PartialEq)]
enum Colour {
Danger,
Primary,
@@ -19,6 +31,12 @@ enum Colour {
}
#[derive(Clone, Debug, PartialEq)]
+enum OverflowStrategy {
+ Grow,
+ Clip,
+}
+
+#[derive(Clone, Debug, PartialEq)]
enum Strength {
Base,
Weak,
@@ -26,6 +44,7 @@ enum Strength {
#[derive(Clone, Debug, PartialEq)]
enum Size {
+ VSmall,
Small,
Base,
}
@@ -33,28 +52,55 @@ enum Size {
impl Text {
pub fn default(content: &str) -> Self {
Self {
+ align: Alignment::Left,
content: String::from(content),
colour: Some(Colour::Primary),
+ font_style: font::Style::Normal,
+ overflow_strategy: OverflowStrategy::Grow,
size: Some(Size::Base),
strength: Some(Strength::Base),
is_focused: false,
+ width: iced::Length::Shrink,
}
}
pub fn new(content: &str) -> Self {
Self {
+ align: Alignment::Left,
content: String::from(content),
colour: None,
+ font_style: font::Style::Normal,
+ overflow_strategy: OverflowStrategy::Grow,
size: None,
strength: None,
+ width: iced::Length::Shrink,
is_focused: false,
}
}
- pub fn as_element<'a, Message>(self) -> iced::Element<'a, Message> {
+ pub fn currency(amount: i32) -> Self {
+ let formatted_string = if amount < 0 {
+ format!("({} · {})", -amount / 100, -amount % 100)
+ } else {
+ format!(" {} · {} ", amount / 100, amount % 100)
+ };
+ Self::default(&formatted_string)
+ }
+
+ pub fn as_element<'a, Message>(self) -> iced::Element<'a, Message>
+ where
+ Message: 'a,
+ {
<Self as Into<iced::Element<'a, Message>>>::into(self)
}
+ pub fn v_small(&self) -> Self {
+ Self {
+ size: Some(Size::VSmall),
+ ..self.clone()
+ }
+ }
+
pub fn small(&self) -> Self {
Self {
size: Some(Size::Small),
@@ -86,15 +132,56 @@ impl Text {
self.clone()
}
}
+
+ pub fn align_right(&self) -> Self {
+ Self {
+ align: Alignment::Right,
+ ..self.clone()
+ }
+ }
+
+ pub fn width(&self, width: iced::Length) -> Self {
+ Self {
+ width,
+ ..self.clone()
+ }
+ }
+
+ pub fn clip(&self) -> Self {
+ Self {
+ overflow_strategy: OverflowStrategy::Clip,
+ ..self.clone()
+ }
+ }
+
+ pub fn italic(&self) -> Self {
+ Self {
+ font_style: font::Style::Italic,
+ ..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> {
+impl<'a, Message> From<Text> for iced::Element<'a, Message>
+where
+ Message: 'a,
+{
fn from(text: Text) -> Self {
text_as_element(text)
}
}
-impl<'a, Message> From<&'a Text> for iced::Element<'a, Message> {
+impl<'a, Message> From<&'a Text> for iced::Element<'a, Message>
+where
+ Message: 'a,
+{
fn from(text: &'a Text) -> Self {
text_as_element(text)
}
@@ -103,17 +190,31 @@ impl<'a, Message> From<&'a Text> for iced::Element<'a, Message> {
fn text_as_element<'a, T, Message>(text: T) -> iced::Element<'a, Message>
where
T: Borrow<Text> + 'a,
+ Message: 'a,
{
- iced::widget::text(text.borrow().content.clone())
+ let overflow_strategy = &text.borrow().overflow_strategy.clone();
+
+ let text_elem: iced::widget::Text = iced::widget::text(text.borrow().content.clone())
+ .font(Font {
+ style: text.borrow().font_style,
+ ..Default::default()
+ })
.size(match text.borrow().size {
- Some(Size::Small) => TEXT_SIZE_SM,
- Some(Size::Base) | None => TEXT_SIZE_BASE,
+ Some(Size::VSmall) => u32::from(TEXT_SIZE_V_SM),
+ Some(Size::Small) => u32::from(TEXT_SIZE_SM),
+ Some(Size::Base) | None => u32::from(TEXT_SIZE_BASE),
+ })
+ .align_x(match &text.borrow().align {
+ Alignment::Left => iced::alignment::Horizontal::Left,
+ Alignment::Right => iced::alignment::Horizontal::Right,
})
+ .wrapping(match overflow_strategy {
+ OverflowStrategy::Grow => iced::widget::text::Wrapping::default(),
+ OverflowStrategy::Clip => iced::widget::text::Wrapping::None,
+ })
+ .width(text.borrow().width)
.style(move |theme: &iced::Theme| iced::widget::text::Style {
- color: match (
- &text.borrow().borrow().colour,
- &text.borrow().borrow().strength,
- ) {
+ color: match (&text.borrow().colour, &text.borrow().strength) {
(Some(Colour::Danger), Some(Strength::Base) | None) => {
Some(theme.extended_palette().danger.base.text)
}
@@ -134,8 +235,15 @@ where
}
(None, _) => None,
},
- })
- .into()
+ });
+
+ match overflow_strategy {
+ OverflowStrategy::Grow => text_elem.into(),
+ OverflowStrategy::Clip => container::<'a, Message, _, _>(text_elem)
+ .height(iced::Length::Shrink)
+ .clip(true)
+ .into(),
+ }
}
impl_focusable!(Text);
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())
+ }))
}
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
new file mode 100644
index 0000000..268984b
--- /dev/null
+++ b/schist_desktop_gui/src/gui/components/transactions_view/view_transactions_view.rs
@@ -0,0 +1,112 @@
+use iced::{
+ alignment,
+ widget::{
+ row,
+ scrollable::{self, Scrollbar},
+ table,
+ table::column,
+ Container, Scrollable,
+ },
+ Element, Length,
+};
+
+use crate::{gui::components::Text, style::SPACING_LG, traits::Viewable};
+
+use super::{Message, TransactionRow, TransactionsView};
+
+impl<'a> Viewable<'a, Message> for TransactionsView {
+ fn view(&'a self) -> Element<'a, Message> {
+ let navigation = self.navigation.view().map(Message::NavigationMessage);
+
+ let columns = [
+ column(
+ Text::default("Date").width(iced::Length::Fixed(128.0)),
+ view_date_cell,
+ ),
+ column(
+ Text::default("Bucket").width(iced::Length::Fixed(128.0)),
+ view_bucket_cell,
+ ),
+ column(
+ Text::default("Payee").width(iced::Length::Fixed(256.0)),
+ view_payee_cell,
+ ),
+ column(
+ Text::default("Quantity")
+ .width(iced::Length::Fixed(96.0))
+ .align_right(),
+ view_quantity_cell,
+ ),
+ column(
+ Text::default("Balance")
+ .width(iced::Length::Fixed(96.0))
+ .align_right(),
+ view_balance_cell,
+ ),
+ ];
+
+ let rows = self
+ .navigation
+ .active_option
+ .clone()
+ .map_or_else(Vec::default, |option| {
+ self.transaction_rows_by_account_id
+ .get(&option.account.id)
+ .cloned()
+ .unwrap_or_else(Vec::default)
+ });
+
+ let table = Scrollable::new(table(columns, rows)).direction(scrollable::Direction::Both {
+ horizontal: Scrollbar::default(),
+ vertical: Scrollbar::default(),
+ });
+
+ row![
+ Container::new(navigation).width(Length::FillPortion(1)),
+ Container::new(table).width(Length::FillPortion(3)),
+ ]
+ .align_y(alignment::Vertical::Center)
+ .height(Length::Fill)
+ .spacing(u32::from(SPACING_LG))
+ .into()
+ }
+}
+
+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(row: TransactionRow) -> Text {
+ Text::default(&row.bucket)
+ .style(row.bucket_font_style)
+ .width(iced::Length::Fixed(128.0))
+ .small()
+ .clip()
+}
+
+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(row: TransactionRow) -> Text {
+ Text::currency(row.amount)
+ .width(iced::Length::Fixed(96.0))
+ .small()
+ .clip()
+ .align_right()
+}
+
+fn view_balance_cell(row: TransactionRow) -> Text {
+ Text::currency(row.balance)
+ .width(iced::Length::Fixed(96.0))
+ .small()
+ .clip()
+ .align_right()
+}
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 5ed4b30..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};
+use schist_models::{Account, AccountTransfer, Bucket, Transaction};
use crate::{
gui::components::{
@@ -35,7 +35,9 @@ 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>),
}
@@ -64,7 +66,7 @@ impl<'a> Viewable<'a, Message> for MainScreen {
]
.align_y(alignment::Vertical::Center)
.height(Length::Fill)
- .spacing(SPACING_LG)
+ .spacing(u32::from(SPACING_LG))
.into()
}
}
@@ -108,7 +110,9 @@ impl<'a> Component<'a, Message, Action> for MainScreen {
self.balances_view
.update(balances_view::Message::SetBuckets(buckets.clone()));
self.buckets_view
- .update(buckets_view::Message::SetBuckets(buckets));
+ .update(buckets_view::Message::SetBuckets(buckets.clone()));
+ self.transactions_view
+ .update(transactions_view::Message::SetBuckets(buckets));
Action::None
}
@@ -118,6 +122,20 @@ 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));
+ Action::None
+ }
+
Message::NextItem => match self.active_view {
View::Balances => {
self.balances_view
@@ -130,7 +148,7 @@ impl<'a> Component<'a, Message, Action> for MainScreen {
}
View::Transactions => {
self.transactions_view
- .update(transactions_view::Message::SelectNextTransaction);
+ .update(transactions_view::Message::SelectNextAccount);
Action::None
}
},
@@ -147,7 +165,7 @@ impl<'a> Component<'a, Message, Action> for MainScreen {
}
View::Transactions => {
self.transactions_view
- .update(transactions_view::Message::SelectPrevTransaction);
+ .update(transactions_view::Message::SelectPrevAccount);
Action::None
}
},
@@ -197,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(Vec::new(), "Hello, transactions!"),
+ transactions_view: TransactionsView::new(&[], &[], &[], &[]),
active_view: View::Balances,
view_navigation: Navigation::new(Some(View::Balances), View::views()),
}
diff --git a/schist_desktop_gui/src/main.rs b/schist_desktop_gui/src/main.rs
index a5749d3..3f18452 100644
--- a/schist_desktop_gui/src/main.rs
+++ b/schist_desktop_gui/src/main.rs
@@ -24,11 +24,11 @@ use crate::{
fn main() -> anyhow::Result<()> {
let cache = get_cache().unwrap_or_else(|_| Cache::default());
let config = Config::default();
- iced::application("Schist", Gui::update, Gui::view)
+ iced::application(move || Gui::new(&cache), Gui::update, Gui::view)
.settings(make_settings(&config.clone()))
.subscription(gui::subscription)
.theme(make_theme)
.window(make_window_settings(&config.clone()))
- .run_with(move || Gui::new(&cache))
+ .run()
.context("Failed to run Schist GUI")
}
diff --git a/schist_desktop_gui/src/style.rs b/schist_desktop_gui/src/style.rs
index c2ecb02..fd7c858 100644
--- a/schist_desktop_gui/src/style.rs
+++ b/schist_desktop_gui/src/style.rs
@@ -1,5 +1,6 @@
pub const SPACING_MD: u16 = 24;
pub const SPACING_LG: u16 = 32;
-pub const TEXT_SIZE_SM: u16 = 12;
+pub const TEXT_SIZE_V_SM: u16 = 12;
+pub const TEXT_SIZE_SM: u16 = 14;
pub const TEXT_SIZE_BASE: u16 = 16;
diff --git a/schist_desktop_gui/src/theme.rs b/schist_desktop_gui/src/theme.rs
index 98df36f..0e71689 100644
--- a/schist_desktop_gui/src/theme.rs
+++ b/schist_desktop_gui/src/theme.rs
@@ -14,12 +14,18 @@ pub fn make_theme(_state: &Gui) -> Theme {
primary: accent_8(),
success: accent_8(),
danger: red_4(),
+ warning: red_4(),
},
|palette| palette::Extended {
background: palette::Background {
base: palette::Pair::new(black(), palette.text),
+ neutral: palette::Pair::new(black(), palette.text),
weak: palette::Pair::new(grey_2(), palette.text),
+ weaker: palette::Pair::new(grey_2(), palette.text),
+ weakest: palette::Pair::new(grey_2(), palette.text),
strong: palette::Pair::new(grey_2(), palette.text),
+ stronger: palette::Pair::new(grey_2(), palette.text),
+ strongest: palette::Pair::new(grey_2(), palette.text),
},
primary: palette::Primary {
base: palette::Pair::new(palette.background, palette.text),
@@ -41,6 +47,11 @@ pub fn make_theme(_state: &Gui) -> Theme {
weak: palette::Pair::new(palette.background, red_7()),
strong: palette::Pair::new(palette.background, red_4()),
},
+ warning: palette::Warning {
+ base: palette::Pair::new(palette.background, red_7()),
+ weak: palette::Pair::new(palette.background, red_7()),
+ strong: palette::Pair::new(palette.background, red_4()),
+ },
is_dark: true,
},
)