From 97b413aa5962bded37dedb380034fa7b97bdc06c Mon Sep 17 00:00:00 2001 From: Joe Carstairs Date: Sun, 8 Feb 2026 20:26:30 +0000 Subject: task-085: display transaction data in tables --- schist_desktop_gui/src/gui/components.rs | 2 + schist_desktop_gui/src/gui/components/table.rs | 42 +++++++++++ .../src/gui/components/table/view_table.rs | 48 ++++++++++++ schist_desktop_gui/src/gui/components/text.rs | 31 ++++++++ .../src/gui/components/transactions_view.rs | 87 +++++++++++++++++----- .../transactions_view/view_transactions_view.rs | 27 ++----- .../src/gui/screens/main_screen/main_screen.rs | 2 +- 7 files changed, 199 insertions(+), 40 deletions(-) create mode 100644 schist_desktop_gui/src/gui/components/table.rs create mode 100644 schist_desktop_gui/src/gui/components/table/view_table.rs diff --git a/schist_desktop_gui/src/gui/components.rs b/schist_desktop_gui/src/gui/components.rs index 606c418..ec91951 100644 --- a/schist_desktop_gui/src/gui/components.rs +++ b/schist_desktop_gui/src/gui/components.rs @@ -5,6 +5,7 @@ pub mod buckets_view; pub mod button; pub mod navigation; pub mod panel; +pub mod table; pub mod text; pub mod transactions_view; @@ -15,5 +16,6 @@ pub use buckets_view::BucketsView; pub use button::{button, panel_button}; pub use navigation::Navigation; pub use panel::Panel; +pub use table::Table; pub use text::Text; pub use transactions_view::TransactionsView; diff --git a/schist_desktop_gui/src/gui/components/table.rs b/schist_desktop_gui/src/gui/components/table.rs new file mode 100644 index 0000000..4c6d810 --- /dev/null +++ b/schist_desktop_gui/src/gui/components/table.rs @@ -0,0 +1,42 @@ +use std::collections::HashMap; + +mod view_table; + +#[derive(Clone, Debug, Default)] +pub struct Table +where + Ix: Clone + std::fmt::Debug, +{ + pub cols: Vec>, + pub rows: Vec>, +} + +#[derive(Clone, Debug)] +pub enum Value { + String(String), + Currency(i32), +} + +#[derive(Clone, Debug)] +pub struct Column +where + Ix: Clone + std::fmt::Debug, +{ + pub index: Ix, + pub name: String, +} + +impl Column +where + Ix: Clone + std::fmt::Debug, +{ + pub fn new(index: Ix, name: &str) -> Self { + Self { + index, + name: name.to_string(), + } + } +} + +#[derive(Clone, Debug)] +pub enum Message {} diff --git a/schist_desktop_gui/src/gui/components/table/view_table.rs b/schist_desktop_gui/src/gui/components/table/view_table.rs new file mode 100644 index 0000000..1d5f10b --- /dev/null +++ b/schist_desktop_gui/src/gui/components/table/view_table.rs @@ -0,0 +1,48 @@ +use std::{fmt::Debug, hash::Hash}; + +use iced::widget::{column, container, row}; + +use crate::{gui::components::Text, style::SPACING_MD, traits::Viewable}; + +use super::{Message, Table, Value}; + +impl<'a, Ix> Viewable<'a, Message> for Table +where + Ix: Clone + Debug + Eq + Hash, +{ + fn view(&'a self) -> iced::Element<'a, Message> { + let headers = row(self.cols.iter().map(|col| { + container(Text::default(&col.name)) + .width(iced::Length::FillPortion(1)) + .into() + })) + .padding(SPACING_MD); + let rows = column(self.rows.iter().map(|row_data| { + row(self.cols.iter().map(|col| { + container( + row_data + .get(&col.index) + .map(|datum| match datum { + Value::String(text) => Text::default(text).weak(), + Value::Currency(amount) => format_currency(*amount), + }) + .unwrap_or_else(|| Text::default("")), + ) + .width(iced::Length::FillPortion(1)) + .into() + })) + .spacing(SPACING_MD) + .into() + })); + column![headers, rows].into() + } +} + +fn format_currency(amount: i32) -> Text { + let text = if amount >= 0 { + format!(" {} · {} ", amount / 100, amount % 100) + } else { + format!("({} · {})", -amount / 100, -amount % 100) + }; + Text::default(&text).align_right().width(iced::Length::Fill) +} diff --git a/schist_desktop_gui/src/gui/components/text.rs b/schist_desktop_gui/src/gui/components/text.rs index d171043..0d0c8c4 100644 --- a/schist_desktop_gui/src/gui/components/text.rs +++ b/schist_desktop_gui/src/gui/components/text.rs @@ -5,12 +5,20 @@ use crate::{impl_focusable, style::*}; #[derive(Clone, Debug, PartialEq)] pub struct Text { pub content: String, + align: Alignment, colour: Option, size: Option, strength: Option, + width: iced::Length, is_focused: bool, } +#[derive(Clone, Debug, PartialEq)] +enum Alignment { + Left, + Right, +} + #[derive(Clone, Debug, PartialEq)] enum Colour { Danger, @@ -33,20 +41,24 @@ enum Size { impl Text { pub fn default(content: &str) -> Self { Self { + align: Alignment::Left, content: String::from(content), colour: Some(Colour::Primary), 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, size: None, strength: None, + width: iced::Length::Shrink, is_focused: false, } } @@ -86,6 +98,20 @@ 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() + } + } } impl<'a, Message> From for iced::Element<'a, Message> { @@ -109,6 +135,11 @@ where Some(Size::Small) => TEXT_SIZE_SM, Some(Size::Base) | None => TEXT_SIZE_BASE, }) + .align_x(match &text.borrow().align { + Alignment::Left => iced::alignment::Horizontal::Left, + Alignment::Right => iced::alignment::Horizontal::Right, + }) + .width(text.borrow().width) .style(move |theme: &iced::Theme| iced::widget::text::Style { color: match ( &text.borrow().borrow().colour, diff --git a/schist_desktop_gui/src/gui/components/transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view.rs index f39b522..8e076f1 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view.rs @@ -6,13 +6,14 @@ use itertools::Itertools; use schist_models::{Account, Transaction}; use crate::{ - gui::components::{navigation, AccountNavigationEntry, Navigation}, + gui::components::{navigation, table, AccountNavigationEntry, Navigation, Table}, traits::Component, }; #[derive(Clone, Debug)] pub struct TransactionsView { navigation: Navigation, + table: Table, transactions_by_account_id: HashMap>, } @@ -23,6 +24,7 @@ pub enum Message { SelectPrevAccount, SetAccounts(Vec), SetTransactions(Vec), + TableMessage(table::Message), } pub enum Action { @@ -30,57 +32,106 @@ pub enum Action { } impl TransactionsView { - pub fn new(accounts: Vec, transactions: Vec) -> Self { + pub fn new(accounts: &[Account], transactions: &[Transaction]) -> Self { let account_navigation_entries: Vec = accounts - .into_iter() + .iter() + .cloned() .map(AccountNavigationEntry::new) .collect(); let navigation = Navigation::new( account_navigation_entries.first().cloned(), account_navigation_entries, ); - Self { + let mut transactions_view = Self { navigation: navigation, + table: Table::default(), transactions_by_account_id: transactions .into_iter() + .cloned() .into_group_map_by(|t| t.account_id), - } + }; + transactions_view.refresh_table(); + transactions_view + } + + fn refresh_table(&mut self) { + self.table = + Table { + cols: vec![ + table::Column::new(0, "Date"), + table::Column::new(1, "Bucket"), + table::Column::new(2, "Payee"), + table::Column::new(3, "Quantity"), + table::Column::new(4, "Balance"), + ], + rows: self.navigation.active_option.clone().map_or_else( + Vec::new, + |AccountNavigationEntry { account, .. }| { + self.transactions_by_account_id + .get(&account.id) + .cloned() + .unwrap_or_else(Vec::new) + .iter() + .map(|t| { + HashMap::from([ + (0, table::Value::String(t.date.to_string())), + ( + 1, + table::Value::String(t.bucket_id.map_or_else( + || String::from("None"), + |id| id.to_string(), + )), + ), + (2, table::Value::String(t.counterparty.clone())), + (3, table::Value::Currency(t.amount)), + (4, table::Value::String(String::from("TODO"))), + ]) + }) + .collect() + }, + ), + }; } } impl<'a> Component<'a, Message, Action> for TransactionsView { fn update(&mut self, message: Message) -> Action { match message { - Message::NavigationMessage(message) => { - self.navigation.update(message); - Action::None - } + Message::NavigationMessage(message) => self.update_navigation(message), Message::SetAccounts(accounts) => { let account_navigation_entries: Vec = accounts .into_iter() .map(AccountNavigationEntry::new) .collect(); - self.navigation - .update(navigation::Message::SetOptions(account_navigation_entries)); - Action::None + self.update_navigation(navigation::Message::SetOptions(account_navigation_entries)) } Message::SetTransactions(transactions) => { self.transactions_by_account_id = transactions.into_iter().into_group_map_by(|t| t.account_id); + self.refresh_table(); Action::None } - Message::SelectNextAccount => { - self.navigation.update(navigation::Message::SelectNext); - Action::None - } + Message::SelectNextAccount => self.update_navigation(navigation::Message::SelectNext), + + Message::SelectPrevAccount => self.update_navigation(navigation::Message::SelectPrev), + } + } +} - Message::SelectPrevAccount => { - self.navigation.update(navigation::Message::SelectPrev); +impl TransactionsView { + fn update_navigation( + &mut self, + message: navigation::Message, + ) -> Action { + match self.navigation.update(message) { + navigation::Action::ActivateOption(_) | navigation::Action::SelectOption(_) => { + self.refresh_table(); Action::None } + navigation::Action::None => Action::None, } } } 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 37b6655..d1f6af2 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,37 +1,22 @@ use iced::{ alignment, - widget::{column, row, Container}, + widget::{row, Container}, Element, Length, }; -use crate::{ - gui::components::{AccountNavigationEntry, Text}, - style::SPACING_LG, - traits::Viewable, -}; +use crate::{style::SPACING_LG, traits::Viewable}; use super::{Message, TransactionsView}; 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("Hello, transactions!").as_element(), - Text::default(&format!( - "{} transactions in this account.", - self.navigation.active_option.clone().map_or( - 0, - |AccountNavigationEntry { account, .. }| self - .transactions_by_account_id - .get(&account.id) - .map_or(0, |transactions| transactions.len()) - ) - )) - .as_element(), - ]; + + let table = self.table.view().map(Message::TableMessage); + row![ Container::new(navigation).width(Length::FillPortion(1)), - Container::new(main_content).width(Length::FillPortion(3)), + Container::new(table).width(Length::FillPortion(3)), ] .align_y(alignment::Vertical::Center) .height(Length::Fill) 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 0a2c355..9852b53 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 @@ -204,7 +204,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(), Vec::new()), + transactions_view: TransactionsView::new(&[], &[]), active_view: View::Balances, view_navigation: Navigation::new(Some(View::Balances), View::views()), } -- cgit v1.2.3