diff options
| author | Joe Carstairs <me@joeac.net> | 2026-08-29 15:19:52 +0100 |
|---|---|---|
| committer | Joe Carstairs <me@joeac.net> | 2026-08-29 15:19:52 +0100 |
| commit | 54159c767377dfe6384547a4517f4fe3f002caad (patch) | |
| tree | b5ad661b4505caccee8d73d22f42957055863108 /schist_desktop_gui/src | |
| parent | a511d72248d20ba2fcb436b58ee2459d422d80b7 (diff) | |
yet)
Diffstat (limited to 'schist_desktop_gui/src')
| -rw-r--r-- | schist_desktop_gui/src/gui/components/input.rs | 3 | ||||
| -rw-r--r-- | schist_desktop_gui/src/gui/components/new_transaction_form.rs | 185 | ||||
| -rw-r--r-- | schist_desktop_gui/src/gui/components/transactions_view.rs | 23 | ||||
| -rw-r--r-- | schist_desktop_gui/src/style.rs | 1 |
4 files changed, 192 insertions, 20 deletions
diff --git a/schist_desktop_gui/src/gui/components/input.rs b/schist_desktop_gui/src/gui/components/input.rs index ce4d228..8a4653a 100644 --- a/schist_desktop_gui/src/gui/components/input.rs +++ b/schist_desktop_gui/src/gui/components/input.rs @@ -4,9 +4,10 @@ use crate::style::{BORDER_WIDTH, BORDER_WIDTH_THICK, TEXT_SIZE_SM}; pub fn input<'a, Message: Clone>( placeholder: &'a str, + content: &'a str, ) -> TextInput<'a, Message> { - iced::widget::text_input(placeholder, "") + iced::widget::text_input(placeholder, content) .style(move |theme, status| match status { iced::widget::text_input::Status::Active => hovered_input_style(theme), iced::widget::text_input::Status::Disabled => disabled_input_style(theme), diff --git a/schist_desktop_gui/src/gui/components/new_transaction_form.rs b/schist_desktop_gui/src/gui/components/new_transaction_form.rs index 58509b1..7fb9b85 100644 --- a/schist_desktop_gui/src/gui/components/new_transaction_form.rs +++ b/schist_desktop_gui/src/gui/components/new_transaction_form.rs @@ -1,9 +1,16 @@ -use iced::{Element, widget::row}; +use iced::{Element, widget::{column, row}}; +use schist_models::{Account, Bucket, DateUtc, Transaction}; +use schist_traits::{Dateable, Nowlike}; -use crate::{gui::components::{Text, button, input::input}, style::SPACING_SM, traits::{Component, Viewable}}; +use crate::{gui::components::{Text, button, input::input}, style::{SPACING_SM, SPACING_V_SM}, traits::{Component, Viewable}}; #[derive(Clone, Debug)] pub struct NewTransactionForm { + active_account: Option<Account>, + buckets: Vec<Bucket>, + this_year: String, + this_month: String, + today: String, day: String, month: String, year: String, @@ -11,6 +18,7 @@ pub struct NewTransactionForm { payee: String, quantity: String, submit_text: Text, + validation_err: Option<String>, } #[derive(Clone, Debug)] @@ -22,13 +30,21 @@ pub enum Message { PayeeInputChanged(String), QuantityInputChanged(String), Submit, + SetActiveAccount(Account), + SetBuckets(Vec<Bucket>), } -pub enum Action { None } +pub enum Action { None, AddTransaction(Transaction) } impl NewTransactionForm { - pub fn new() -> Self { + pub fn new(active_account: Option<Account>, buckets: Vec<Bucket>) -> Self { + let today = DateUtc::now(); Self { + active_account, + buckets, + this_year: today.year().to_string(), + this_month: today.month().to_string(), + today: today.day().to_string(), day: String::new(), month: String::new(), year: String::new(), @@ -36,27 +52,168 @@ impl NewTransactionForm { payee: String::new(), quantity: String::new(), submit_text: Text::new("+ Add"), + validation_err: None, } } + + fn reset(&mut self) { + self.day = String::new(); + self.month = String::new(); + self.year = String::new(); + self.bucket = String::new(); + self.payee = String::new(); + self.quantity = String::new(); + self.validation_err = None; + } + + fn create_transaction_or_else_validation_error(&self) -> Result<Transaction, String> { + Ok(Transaction::new( + self.get_valid_account_id_or_else_validation_error()?, + self.get_valid_quantity_or_else_validation_error()?, + self.get_valid_bucket_id_or_else_validation_error()?, + &self.payee, + self.get_valid_date_or_else_validation_error()?, + "")) + } + + fn get_valid_account_id_or_else_validation_error(&self) -> Result<i32, String> { + match &self.active_account { + Option::Some(account) => Ok(account.id), + Option::None => Err(format!("No account selected: select an account to add a transaction.")), + } + } + + fn get_valid_bucket_id_or_else_validation_error(&self) -> Result<Option<i32>, String> { + if self.bucket.trim().len() == 0 { + return Ok(None); + }; + match self.buckets.iter().find(|b| String::eq(&b.name, &self.bucket)) { + Option::Some(bucket) => Ok(Some(bucket.id)), + Option::None => Err(format!( + "There's no bucket called '{}': check the buckets view to list existing buckets or to add a new one.", + self.bucket)), + } + } + + fn get_valid_date_or_else_validation_error(&self) -> Result<DateUtc, String> { + let day = if self.day.trim().len() == 0 { + Ok(DateUtc::now().day()) + } else { + self.day.trim().parse::<u32>() + }; + let month = self.get_valid_month_or_else_validation_error()?; + let year = self.get_valid_year_or_else_validation_error()?; + let first_day_of_month = DateUtc::from_ymd(year, month, 1) + .map_err(|err| format!("Unexpected error: {}", err))?; + let days_in_month = first_day_of_month.days_in_month(); + match day { + Err(_) => Err(format!("'{}' is not a valid day. Choose a number between 1 and {}.", self.day, days_in_month)), + Ok(day) => if day > 0 && day <= days_in_month { + DateUtc::from_ymd(year, month, day) + .map_err(|err| format!("Unexpected error: {}", err)) + } else { + Err(format!("'{}' is not a valid day: there are only {} days in {}. Choose a number between 1 and {}.", + self.day, days_in_month, first_day_of_month.format("%m %Y"), days_in_month)) + } + } + } + + fn get_valid_month_or_else_validation_error(&self) -> Result<u32, String> { + if self.month.trim().len() == 0 { + return Ok(DateUtc::now().month()) + }; + match self.month.trim().parse::<u32>() { + Err(_) => Err(format!("'{}' is not a valid month. Choose a number between 1 and 12.", self.month)), + Ok(month) => if month > 0 && month < 13 { + Ok(month) + } else { + Err(format!("'{}' is not a valid month. Choose a number between 1 and 12.", self.month)) + } + } + } + + fn get_valid_year_or_else_validation_error(&self) -> Result<i32, String> { + if self.year.trim().len() == 0 { + return Ok(DateUtc::now().year()) + }; + self.year.trim().parse::<i32>().map_err(|_| format!("'{}' is not a valid year.", self.year)) + } + + fn get_valid_quantity_or_else_validation_error(&self) -> Result<i32, String> { + if self.quantity.trim().len() == 0 { + return Err(String::from("No quantity provided: insert a quantity.")); + }; + let pounds = self.quantity.parse::<f32>() + .map_err(|_| format!("'{}' is not a valid quantity. Use a number.", self.quantity))?; + Ok((pounds * 100.0).floor() as i32) + } } impl<'a> Viewable<'a, Message> for NewTransactionForm { fn view(&'a self) -> Element<'a, Message> { - row![ - input("25").on_input(Message::DayInputChanged).width(32.0), - input("12").on_input(Message::MonthInputChanged).width(32.0), - input("2026").on_input(Message::YearInputChanged).width(48.0), - input("").on_input(Message::BucketInputChanged).width(138.0), - input("").on_input(Message::PayeeInputChanged).width(266.0), - input("").on_input(Message::QuantityInputChanged).width(104.0), - button(&self.submit_text, Message::Submit, false).width(64.0), - ].spacing(u32::from(SPACING_SM)) + column![ + Text::new(&self.validation_err.clone().unwrap_or_else(String::new)).danger().small(), + row![ + input(&self.today, &self.day).on_input(Message::DayInputChanged).width(32.0), + input(&self.this_month, &self.month).on_input(Message::MonthInputChanged).width(32.0), + input(&self.this_year, &self.year).on_input(Message::YearInputChanged).width(48.0), + input("", &self.bucket).on_input(Message::BucketInputChanged).width(138.0), + input("", &self.payee).on_input(Message::PayeeInputChanged).width(266.0), + input("", &self.quantity).on_input(Message::QuantityInputChanged).width(104.0), + button(&self.submit_text, Message::Submit, false).width(64.0), + ].spacing(u32::from(SPACING_SM)), + ].spacing(u32::from(SPACING_V_SM)) .into() } } impl<'a> Component<'a, Message, Action> for NewTransactionForm { fn update(&mut self, message: Message) -> Action { - Action::None + match message { + Message::DayInputChanged(day) => { + self.day = day; + Action::None + } + Message::MonthInputChanged(month) => { + self.month = month; + Action::None + } + Message::YearInputChanged(year) => { + self.year = year; + Action::None + } + Message::BucketInputChanged(bucket) => { + self.bucket = bucket; + Action::None + } + Message::PayeeInputChanged(payee) => { + self.payee = payee; + Action::None + } + Message::QuantityInputChanged(quantity) => { + self.quantity = quantity; + Action::None + } + Message::SetActiveAccount(account) => { + self.active_account = Some(account); + Action::None + } + Message::SetBuckets(buckets) => { + self.buckets = buckets; + Action::None + } + Message::Submit => { + match self.create_transaction_or_else_validation_error() { + Ok(transaction) => { + self.reset(); + Action::AddTransaction(transaction) + } + Err(validation_err) => { + self.validation_err = Some(validation_err); + Action::None + } + } + } + } } } diff --git a/schist_desktop_gui/src/gui/components/transactions_view.rs b/schist_desktop_gui/src/gui/components/transactions_view.rs index cc4e17c..f11b713 100644 --- a/schist_desktop_gui/src/gui/components/transactions_view.rs +++ b/schist_desktop_gui/src/gui/components/transactions_view.rs @@ -51,11 +51,12 @@ impl TransactionsView { let active_nav_entry = account_navigation_entries.first().cloned(); let navigation = Navigation::new( active_nav_entry.clone(), account_navigation_entries); + let active_account = active_nav_entry.map(|entry| entry.account); let transactions_table = - TransactionsTable::new(accounts, &active_nav_entry.map(|entry| entry.account), account_transfers, buckets, transactions); + TransactionsTable::new(accounts, &active_account, account_transfers, buckets, transactions); Self { navigation: navigation, - new_transaction_form: NewTransactionForm::new(), + new_transaction_form: NewTransactionForm::new(active_account, buckets.to_vec()), transactions_table, } } @@ -91,8 +92,13 @@ impl<'a> Component<'a, Message, Action> for TransactionsView { } Message::SetBuckets(buckets) => { - self.update_transactions_table( - transactions_table::Message::SetBuckets(buckets.clone())) + let action_1 = self.update_transactions_table( + transactions_table::Message::SetBuckets(buckets.clone())); + let action_2 = self.update_new_transaction_form( + new_transaction_form::Message::SetBuckets(buckets.clone())); + match (action_1, action_2) { + (Action::None, Action::None) => Action::None, + } } Message::SetTransactions(transactions) => { @@ -114,7 +120,13 @@ impl TransactionsView { ) -> Action { match self.navigation.update(message) { navigation::Action::ActivateOption(option) | navigation::Action::SelectOption(option) => { - self.update_transactions_table(transactions_table::Message::SetActiveAccount(option.account)) + let action_1 = self.update_transactions_table( + transactions_table::Message::SetActiveAccount(option.account.clone())); + let action_2 = self.update_new_transaction_form( + new_transaction_form::Message::SetActiveAccount(option.account)); + match (action_1, action_2) { + (Action::None, Action::None) => Action::None, + } } navigation::Action::None => Action::None, } @@ -135,6 +147,7 @@ impl TransactionsView { ) -> Action { match self.new_transaction_form.update(message) { new_transaction_form::Action::None => Action::None, + new_transaction_form::Action::AddTransaction(transaction) => Action::None, } } } diff --git a/schist_desktop_gui/src/style.rs b/schist_desktop_gui/src/style.rs index a94d540..3bceb0c 100644 --- a/schist_desktop_gui/src/style.rs +++ b/schist_desktop_gui/src/style.rs @@ -2,6 +2,7 @@ pub const BORDER_RADIUS: u16 = 2; pub const BORDER_WIDTH: u16 = 1; pub const BORDER_WIDTH_THICK: u16 = 2; +pub const SPACING_V_SM: u16 = 2; pub const SPACING_SM: u16 = 12; pub const SPACING_MD: u16 = 24; pub const SPACING_LG: u16 = 32; |
