diff --git a/Cargo.toml b/Cargo.toml index 96bd6d7..3ed57e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +chrono = "0.4.26" crossbeam-channel = "0.5.8" crossterm = "0.26.1" futures = "0.3.28" @@ -14,5 +15,6 @@ ratatui = "0.21.0" rust_decimal = "1.31.0" simplelog = "0.12.1" sqlx = { version = "0.6.3", features = ["postgres", "runtime-tokio-native-tls", "sqlite", "decimal", "time"] } +substring = "1.4.5" time = "0.3.25" tokio = { version = "1.28.2", features = ["full"] } diff --git a/src/app.rs b/src/app.rs index e3b8416..9e12f6f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -84,10 +84,28 @@ impl<'a> App<'a> { } pub fn refresh(&mut self) { - let fut = Arc::clone(&self.db); + let db_clone1 = Arc::clone(&self.db); + let db_clone2 = Arc::clone(&self.db); + let db_clone3 = Arc::clone(&self.db); tokio::spawn(async move { - let res = fut.lock().await.get_all_records().await; + let res = db_clone1.lock().await.get_all_transactions().await; + match res { + Ok(_) => {} + Err(e) => warn!("{}", e), + } + }); + + tokio::spawn(async move { + let res = db_clone2.lock().await.get_all_accounts().await; + match res { + Ok(_) => {} + Err(e) => warn!("{}", e), + } + }); + + tokio::spawn(async move { + let res = db_clone3.lock().await.get_all_buckets().await; match res { Ok(_) => {} Err(e) => warn!("{}", e), diff --git a/src/db/connection.rs b/src/db/connection.rs index 03c18d1..2db7dc8 100644 --- a/src/db/connection.rs +++ b/src/db/connection.rs @@ -4,6 +4,8 @@ use sqlx::Error; use sqlx::PgPool; use super::data_cache::DataCache; +use super::tables::Account; +use super::tables::Buckets; use super::tables::Transaction; pub struct DB { @@ -24,7 +26,7 @@ impl DB { }) } - pub async fn get_all_records(&mut self) -> Result<(), Error> { + pub async fn get_all_transactions(&mut self) -> Result<(), Error> { let mut rows = sqlx::query_as::<_, Transaction>("SELECT trns_id, trns_amount, trns_description, trns_account, trns_bucket, trns_date FROM rcnt.transactions ORDER BY trns_id DESC") .fetch(&self.conn_pool); @@ -40,6 +42,49 @@ impl DB { transactions.append(&mut temp_transactions); } + return Ok(()); + } + // + + pub async fn get_all_accounts(&mut self) -> Result<(), Error> { + let mut rows = sqlx::query_as::<_, Account>( + "SELECT acnt_id, acnt_dsply_name, acnt_description FROM rcnt.accounts;", + ) + .fetch(&self.conn_pool); + + let mut temp_transactions: Vec = Vec::new(); + + while let Some(row) = rows.try_next().await? { + temp_transactions.push(row); + } + + { + let mut transactions = self.data_cache.accounts.lock().unwrap(); + transactions.clear(); + transactions.append(&mut temp_transactions); + } + + return Ok(()); + } + + pub async fn get_all_buckets(&mut self) -> Result<(), Error> { + let mut rows = sqlx::query_as::<_, Buckets>( + "SELECT bkt_id, bkt_dsply_code, bkt_dsply_name, bkt_description FROM rcnt.buckets;", + ) + .fetch(&self.conn_pool); + + let mut temp_transactions: Vec = Vec::new(); + + while let Some(row) = rows.try_next().await? { + temp_transactions.push(row); + } + + { + let mut transactions = self.data_cache.buckets.lock().unwrap(); + transactions.clear(); + transactions.append(&mut temp_transactions); + } + return Ok(()); } } diff --git a/src/uis/mod.rs b/src/uis/mod.rs index 0727dad..75c8e4c 100644 --- a/src/uis/mod.rs +++ b/src/uis/mod.rs @@ -1,3 +1,9 @@ +use ratatui::backend::Backend; +use ratatui::layout::Rect; +use ratatui::style::{Color, Style}; +use ratatui::widgets::{Block, Borders, List, ListItem}; +use ratatui::Frame; + pub mod history; pub use self::history::render_history_tab; @@ -6,3 +12,21 @@ pub use self::new_transaction::render_new_transaction_tab; pub mod navigation_frame; pub use self::navigation_frame::render_navigation_frame; + +pub mod sub_screens; + +pub fn render_popup(f: &mut Frame, base: Rect, list_items: Vec) { + let space = Rect { + x: base.x - 1, + y: base.y + 1, + width: base.width + 1, + height: list_items.len() as u16 + 2, + }; + + let list = List::new(list_items).block( + Block::default() + .borders(Borders::ALL) + .style(Style::default().bg(Color::Black)), + ); + f.render_widget(list, space); +} diff --git a/src/uis/new_transaction.rs b/src/uis/new_transaction.rs index 74d5b26..a18d1a9 100644 --- a/src/uis/new_transaction.rs +++ b/src/uis/new_transaction.rs @@ -1,79 +1,22 @@ use crate::{ app::App, - db::tables::{Account, Buckets, PartialTransactionBreakdown}, + uis::sub_screens::transaction_manual::{handle_manual_event, render_manual_entry, ManualData}, }; use crossterm::event::{KeyCode, KeyEvent, KeyEventKind}; use ratatui::{ backend::Backend, - layout::{Alignment, Constraint, Direction, Layout, Rect}, + layout::{Constraint, Direction, Layout, Rect}, style::{Color, Style}, text::Text, widgets::{Block, Borders, Paragraph}, Frame, }; -use rust_decimal::Decimal; -use time::Date; - -use log::debug; - -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum ManualDataFocus { - Account, - Amount, - Date, - Bucket, - Description, - Breakdowns, -} - -struct ManualData { - pub focus: Option, - - pub account: Option, - pub amount: Option, - pub date: Option, - pub bucket: Option, - pub description: Option, - - pub editing_breakdown: PartialTransactionBreakdown, - pub breakdowns: Vec, -} - -impl ManualData { - pub fn new() -> ManualData { - ManualData { - focus: None, - account: None, - amount: None, - date: None, - bucket: None, - description: None, - editing_breakdown: PartialTransactionBreakdown::new_empty(), - breakdowns: Vec::new(), - } - } - pub fn send_tab(&mut self) { - let next = if self.focus.is_some() { - match self.focus.unwrap() { - ManualDataFocus::Account => ManualDataFocus::Amount, - ManualDataFocus::Amount => ManualDataFocus::Date, - ManualDataFocus::Date => ManualDataFocus::Bucket, - ManualDataFocus::Bucket => ManualDataFocus::Description, - ManualDataFocus::Description => ManualDataFocus::Breakdowns, - ManualDataFocus::Breakdowns => ManualDataFocus::Account, - } - } else { - ManualDataFocus::Account - }; - self.focus = Some(next); - } -} pub struct NewTransactionTabState<'a> { pub cur_tab_index: usize, pub tabs: Vec<&'a str>, - manual_data: ManualData, + pub manual_data: ManualData, } impl<'a> NewTransactionTabState<'a> { @@ -94,7 +37,7 @@ impl<'a> NewTransactionTabState<'a> { if event.kind == KeyEventKind::Press { if transact_state.cur_tab_index == 1 { - handle_manual_event(event, app); + handle_manual_event(event, app); } else { match event.code { KeyCode::Tab => app.states.transactions.next_tab(), @@ -146,154 +89,4 @@ pub fn render_new_transaction_tab(f: &mut Frame, body_rect: Rect, }; } -fn handle_manual_event(event: KeyEvent, app: &mut App) { - if app.states.transactions.manual_data.focus.is_some() { - match event.code { - KeyCode::Tab => { - app.states.transactions.manual_data.send_tab(); - } - KeyCode::Char(value) => { - match app.states.transactions.manual_data.focus.unwrap() { - _ => {} - } - } - _ => {} - } - } else { - match event.code { - KeyCode::Tab => { - app.states.transactions.next_tab(); - } - KeyCode::Enter => { - app.states.transactions.manual_data.focus = Some(ManualDataFocus::Account); - } - _ => {} - }; - } -} - -pub fn render_manual_entry(f: &mut Frame, area: Rect, app: &App) { - let constraints: Vec = vec![ - Constraint::Length(1), - Constraint::Length(1), // account - Constraint::Length(1), - Constraint::Length(1), // amount - Constraint::Length(1), - Constraint::Length(1), // date - Constraint::Length(1), - Constraint::Length(1), // bucket - Constraint::Length(1), - Constraint::Max(5), // description - Constraint::Length(1), - Constraint::Length(1), // Breakdown - Constraint::Length(1), - ]; - - let split_body = Layout::default() - .direction(Direction::Vertical) - .constraints(constraints) - .split(area); - - { - let manual_state = &app.states.transactions.manual_data; - - render_manual_row( - f, - split_body[1], - "Account: ", - manual_state.account.as_ref().map(|val| val.acnt_dsply_name.clone()).unwrap_or_default(), - manual_state.focus, - ManualDataFocus::Account, - ); - - render_manual_row( - f, - split_body[3], - "Amount: ", - manual_state.amount.map(|val| val.to_string()).unwrap_or_default(), - manual_state.focus, - ManualDataFocus::Amount, - ); - - render_manual_row( - f, - split_body[5], - "Date: ", - manual_state.date.as_ref().map(|val| val.to_string()).unwrap_or_default(), - manual_state.focus, - ManualDataFocus::Date, - ); - - render_manual_row( - f, - split_body[7], - "Bucket: ", - manual_state.bucket.as_ref().map(|val| val.bkt_dsply_code.clone()).unwrap_or_default(), - manual_state.focus, - ManualDataFocus::Bucket, - ); - - render_manual_row( - f, - split_body[split_body.len() - 4], - "Description: ", - manual_state.description.clone().unwrap_or("".to_string()), - manual_state.focus, - ManualDataFocus::Description, - ); - - render_manual_row( - f, - split_body[split_body.len() - 2], - "Transaction Breakdown: ", - "[ + ]".to_string(), - manual_state.focus, - ManualDataFocus::Breakdowns, - ); - } -} - -pub fn render_manual_row( - f: &mut Frame, - row_area: Rect, - left_text: &str, - right_text: String, - focus: Option, - matching: ManualDataFocus, -) { - let is_active = focus.map(|focus| focus == matching).unwrap_or(false); - - let horizontal_pieces = Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Percentage(35), - Constraint::Percentage(45), - Constraint::Percentage(20), - ]) - .split(row_area); - - f.render_widget( - Paragraph::new(Text::styled(left_text, Style::default().fg(Color::Yellow))) - .alignment(Alignment::Right), - horizontal_pieces[0], - ); - - let right_bg_color = if is_active { - Color::Yellow - } else { - Color::White - }; - - f.render_widget( - Paragraph::new(Text::styled(right_text, Style::default().fg(Color::Black))) - .alignment(Alignment::Center) - .block( - Block::default() - .borders(Borders::NONE) - .style(Style::default().bg(right_bg_color)), - ), - horizontal_pieces[1], - ); -} - pub fn render_quick_entry(f: &mut Frame, area: Rect, app: &App) {} diff --git a/src/uis/sub_screens/mod.rs b/src/uis/sub_screens/mod.rs new file mode 100644 index 0000000..ad2aee2 --- /dev/null +++ b/src/uis/sub_screens/mod.rs @@ -0,0 +1 @@ +pub mod transaction_manual; diff --git a/src/uis/sub_screens/transaction_manual.rs b/src/uis/sub_screens/transaction_manual.rs new file mode 100644 index 0000000..4115cbe --- /dev/null +++ b/src/uis/sub_screens/transaction_manual.rs @@ -0,0 +1,426 @@ +use crate::{ + app::App, + db::tables::{Account, Buckets, PartialTransactionBreakdown}, + uis::render_popup, +}; + +use crossterm::event::{KeyCode, KeyEvent, KeyEventKind}; +use chrono::prelude::Local; +use ratatui::{ + backend::Backend, + layout::{Alignment, Constraint, Direction, Layout, Rect}, + style::{Color, Style}, + text::Text, + widgets::{Block, Borders, ListItem, Paragraph}, + Frame, +}; +// use substring::Substring; + +// use log::debug; +// use rust_decimal::Decimal; +// use time::Date; + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum ManualDataFocus { + Account, + Amount, + Date, + Bucket, + Description, + Breakdowns, +} + +pub struct ManualData { + pub focus: Option, + pub show_popup: bool, + + pub account: Option, + pub account_index: usize, + pub amount: Option, + pub date: Option, + pub bucket: Option, + pub bucket_index: usize, + pub description: Option, + + pub editing_breakdown: PartialTransactionBreakdown, + pub breakdowns: Vec, +} + +impl ManualData { + pub fn new() -> ManualData { + ManualData { + focus: None, + show_popup: true, + account: None, + account_index: 0, + amount: None, + date: Some(format!("+{}", Local::now().format("%m-%d-%Y"))), + bucket: None, + bucket_index: 0, + description: None, + editing_breakdown: PartialTransactionBreakdown::new_empty(), + breakdowns: Vec::new(), + } + } + pub fn send_tab(&mut self) { + let next = if self.focus.is_some() { + match self.focus.unwrap() { + ManualDataFocus::Account => ManualDataFocus::Amount, + ManualDataFocus::Amount => ManualDataFocus::Date, + ManualDataFocus::Date => ManualDataFocus::Bucket, + ManualDataFocus::Bucket => ManualDataFocus::Description, + ManualDataFocus::Description => ManualDataFocus::Breakdowns, + ManualDataFocus::Breakdowns => ManualDataFocus::Account, + } + } else { + ManualDataFocus::Account + }; + self.focus = Some(next); + } +} + +pub fn handle_manual_event(event: KeyEvent, app: &mut App) { + if app.states.transactions.manual_data.focus.is_some() { + match event.code { + KeyCode::Tab => match app.states.transactions.manual_data.focus.unwrap() { + ManualDataFocus::Account => { + if app.states.transactions.manual_data.show_popup { + app.states.transactions.manual_data.account_index += 1; + if app.states.transactions.manual_data.account_index + > app.data_cache.accounts.lock().unwrap().len() - 1 + { + app.states.transactions.manual_data.account_index = 0; + } + } else { + app.states.transactions.manual_data.send_tab(); + } + } + ManualDataFocus::Bucket => { + if app.states.transactions.manual_data.show_popup { + app.states.transactions.manual_data.bucket_index += 1; + if app.states.transactions.manual_data.bucket_index + > app.data_cache.buckets.lock().unwrap().len() - 1 + { + app.states.transactions.manual_data.bucket_index = 0; + } + } else { + app.states.transactions.manual_data.show_popup = false; + app.states.transactions.manual_data.send_tab(); + } + } + ManualDataFocus::Date => { + if let Some(ref mut x) = app.states.transactions.manual_data.date { + if x.len() == 0 { + x.push_str(&format!("+{}", Local::now().format("%m-%d-%Y"))); + } + } + app.states.transactions.manual_data.send_tab() + } + _ => app.states.transactions.manual_data.send_tab(), + }, + KeyCode::Enter => match app.states.transactions.manual_data.focus.unwrap() { + ManualDataFocus::Account => { + app.states.transactions.manual_data.show_popup = + !app.states.transactions.manual_data.show_popup; + + if !app.states.transactions.manual_data.show_popup { + app.states.transactions.manual_data.account = app + .data_cache + .accounts + .lock() + .unwrap() + .get(app.states.transactions.manual_data.account_index) + .cloned(); + + app.states.transactions.manual_data.send_tab() + } + } + ManualDataFocus::Bucket => { + app.states.transactions.manual_data.show_popup = + !app.states.transactions.manual_data.show_popup; + + if !app.states.transactions.manual_data.show_popup { + app.states.transactions.manual_data.bucket = app + .data_cache + .buckets + .lock() + .unwrap() + .get(app.states.transactions.manual_data.bucket_index) + .cloned(); + + app.states.transactions.manual_data.send_tab() + } + } + _ => {} + }, + KeyCode::Backspace => match app.states.transactions.manual_data.focus.unwrap() { + ManualDataFocus::Amount => { + if let Some(ref mut s) = app.states.transactions.manual_data.amount { + s.pop(); + } + } + ManualDataFocus::Description => { + if let Some(ref mut s) = app.states.transactions.manual_data.description { + s.pop(); + } + } + ManualDataFocus::Date => { + if let Some(ref mut s) = app.states.transactions.manual_data.date { + s.pop(); + } + } + _ => {} + }, + KeyCode::Char(value) => match app.states.transactions.manual_data.focus.unwrap() { + ManualDataFocus::Amount => { + if value.is_digit(10) || value == '.' { + if let Some(ref mut s) = app.states.transactions.manual_data.amount { + s.push(value); + } else { + app.states.transactions.manual_data.amount = Some(value.to_string()); + } + } + } + ManualDataFocus::Description => { + if let Some(ref mut s) = app.states.transactions.manual_data.description { + s.push(value); + } else { + app.states.transactions.manual_data.description = Some(value.to_string()); + } + } + ManualDataFocus::Date => { + if value.is_digit(10) { + if let Some(ref mut s) = app.states.transactions.manual_data.date { + if s.len() < 10 { + s.push(value); + } + if s.len() == 2 || s.len() == 5 { + s.push('-'); + } + } else { + app.states.transactions.manual_data.date = Some(value.to_string()); + } + } + } + _ => {} + }, + _ => {} + } + if let Some(ManualDataFocus::Date) = app.states.transactions.manual_data.focus { + if let Some(ref mut x) = app.states.transactions.manual_data.date { + if x.starts_with("+") { + x.clear(); + } + } + } + } else { + match event.code { + KeyCode::Tab => { + app.states.transactions.next_tab(); + } + KeyCode::Enter => { + app.states.transactions.manual_data.focus = Some(ManualDataFocus::Account); + } + _ => {} + }; + } +} + +pub fn render_manual_entry(f: &mut Frame, area: Rect, app: &App) { + let constraints: Vec = vec![ + Constraint::Length(1), + Constraint::Length(1), // account + Constraint::Length(1), + Constraint::Length(1), // amount + Constraint::Length(1), + Constraint::Length(1), // date + Constraint::Length(1), + Constraint::Length(1), // bucket + Constraint::Length(1), + Constraint::Max(5), // description + Constraint::Length(1), + Constraint::Length(1), // Breakdown + Constraint::Length(1), + ]; + + let split_body = Layout::default() + .direction(Direction::Vertical) + .constraints(constraints) + .split(area); + + { + let manual_state = &app.states.transactions.manual_data; + + render_manual_row( + f, + split_body[1], + "Account: ", + manual_state + .account + .as_ref() + .map(|val| val.acnt_dsply_name.clone()) + .unwrap_or_default(), + manual_state.focus, + ManualDataFocus::Account, + ); + + render_manual_row( + f, + split_body[3], + "Amount: ", + manual_state + .amount + .as_ref() + .map(|val| val.to_string()) + .unwrap_or_default(), + manual_state.focus, + ManualDataFocus::Amount, + ); + + render_manual_row( + f, + split_body[5], + "Date: ", + manual_state.date.clone().unwrap_or_default(), + manual_state.focus, + ManualDataFocus::Date, + ); + + render_manual_row( + f, + split_body[7], + "Bucket: ", + manual_state + .bucket + .as_ref() + .map(|val| val.bkt_dsply_code.clone()) + .unwrap_or_default(), + manual_state.focus, + ManualDataFocus::Bucket, + ); + + render_manual_row( + f, + split_body[split_body.len() - 4], + "Description: ", + manual_state.description.clone().unwrap_or("".to_string()), + manual_state.focus, + ManualDataFocus::Description, + ); + + render_manual_row( + f, + split_body[split_body.len() - 2], + "Transaction Breakdown: ", + "[ + ]".to_string(), + manual_state.focus, + ManualDataFocus::Breakdowns, + ); + + if app.states.transactions.manual_data.focus == Some(ManualDataFocus::Account) + && app.states.transactions.manual_data.show_popup + { + let horizontal_pieces = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(35), + Constraint::Percentage(45), + Constraint::Percentage(20), + ]) + .split(split_body[1]); + + let list_items: Vec = app + .data_cache + .accounts + .lock() + .unwrap() + .iter() + .map(|account| ListItem::new(account.acnt_description.clone())) + .enumerate() + .map(|(i, item)| { + if app.states.transactions.manual_data.account_index == i { + item.style(Style::default().bg(Color::Yellow).fg(Color::Black)) + } else { + item + } + }) + .collect(); + + render_popup(f, horizontal_pieces[1], list_items); + } + if app.states.transactions.manual_data.focus == Some(ManualDataFocus::Bucket) + && app.states.transactions.manual_data.show_popup + { + let horizontal_pieces = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(35), + Constraint::Percentage(45), + Constraint::Percentage(20), + ]) + .split(split_body[7]); + + let list_items: Vec = app + .data_cache + .buckets + .lock() + .unwrap() + .iter() + .map(|bucket| ListItem::new(bucket.bkt_dsply_code.clone())) + .enumerate() + .map(|(i, item)| { + if app.states.transactions.manual_data.bucket_index == i { + item.style(Style::default().bg(Color::Yellow).fg(Color::Black)) + } else { + item + } + }) + .collect(); + + render_popup(f, horizontal_pieces[1], list_items); + } + } +} + +pub fn render_manual_row( + f: &mut Frame, + row_area: Rect, + left_text: &str, + right_text: String, + focus: Option, + matching: ManualDataFocus, +) { + let is_active = focus.map(|focus| focus == matching).unwrap_or(false); + + let horizontal_pieces = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Percentage(35), + Constraint::Percentage(45), + Constraint::Percentage(20), + ]) + .split(row_area); + + f.render_widget( + Paragraph::new(Text::styled(left_text, Style::default().fg(Color::Yellow))) + .alignment(Alignment::Right), + horizontal_pieces[0], + ); + + let right_bg_color = if is_active { + Color::Yellow + } else { + Color::White + }; + + f.render_widget( + Paragraph::new(Text::styled(right_text, Style::default().fg(Color::Black))) + .alignment(Alignment::Center) + .block( + Block::default() + .borders(Borders::NONE) + .style(Style::default().bg(right_bg_color)), + ), + horizontal_pieces[1], + ); +}