diff --git a/src/activity_page/activity_ui.rs b/src/activity_page/activity_ui.rs index a9195b5..111ef7b 100644 --- a/src/activity_page/activity_ui.rs +++ b/src/activity_page/activity_ui.rs @@ -180,10 +180,10 @@ pub fn activity_ui( } } - if let Some(index) = table_data.state.selected() { - if index > 10 { - *table_data.state.offset_mut() = index - 10; - } + if let Some(index) = table_data.state.selected() + && index > 10 + { + *table_data.state.offset_mut() = index - 10; } f.render_widget(year_tab, chunks[0]); diff --git a/src/home_page/home_ui.rs b/src/home_page/home_ui.rs index 9697307..bd655b9 100644 --- a/src/home_page/home_ui.rs +++ b/src/home_page/home_ui.rs @@ -223,10 +223,10 @@ pub fn home_ui( } // Always keep some items rendered on the upper side of the table - if let Some(index) = table.state.selected() { - if index > 10 { - *table.state.offset_mut() = index - 10; - } + if let Some(index) = table.state.selected() + && index > 10 + { + *table.state.offset_mut() = index - 10; } // After all data is in place, render the widgets one by one diff --git a/src/key_checker/key_handler.rs b/src/key_checker/key_handler.rs index cdda55b..8f19252 100644 --- a/src/key_checker/key_handler.rs +++ b/src/key_checker/key_handler.rs @@ -4,6 +4,7 @@ use std::collections::HashMap; use crate::activity_page::ActivityData; use crate::chart_page::ChartData; +use crate::db::MONTHS; use crate::home_page::TransactionData; use crate::outputs::TxType; use crate::outputs::{HandlingOutput, TxUpdateError, VerifyingOutput}; @@ -65,6 +66,7 @@ pub struct InputKeyHandler<'a> { popup_scroll_position: &'a mut usize, max_popup_scroll: &'a mut usize, lerp_state: &'a mut LerpState, + last_summary_data: &'a mut Option>>, conn: &'a mut Connection, } @@ -113,6 +115,7 @@ impl<'a> InputKeyHandler<'a> { popup_scroll_position: &'a mut usize, max_popup_scroll: &'a mut usize, lerp_state: &'a mut LerpState, + last_summary_data: &'a mut Option>>, conn: &'a mut Connection, ) -> InputKeyHandler<'a> { let total_tags = summary_data @@ -162,6 +165,7 @@ impl<'a> InputKeyHandler<'a> { popup_scroll_position, max_popup_scroll, lerp_state, + last_summary_data, conn, } } @@ -798,24 +802,103 @@ impl<'a> InputKeyHandler<'a> { pub fn change_summary_sort(&mut self) { *self.summary_sort = self.summary_sort.next_type(); let summary_data = self.summary_table.items.clone(); - let sorted_data = sort_table_data(summary_data, self.summary_sort); + let mut sorted_data = sort_table_data(summary_data, self.summary_sort); let selection_status = self.summary_table.state.selected(); - *self.summary_table = TableData::new(sorted_data); self.summary_table.state.select(selection_status); + + let mut summary_tags_map = HashMap::new(); + + let previous_indexes = match self.summary_modes.index { + 0 => { + if self.summary_months.index > 0 { + Some((self.summary_months.index - 1, self.summary_years.index)) + } else if self.summary_months.index == 0 && self.summary_years.index > 0 { + Some((self.summary_years.index - 1, MONTHS.len() - 1)) + } else { + None + } + } + 1 => { + if self.summary_years.index > 0 { + Some((self.summary_months.index, self.summary_years.index - 1)) + } else { + None + } + } + _ => None, + }; + + if let Some((month, year)) = previous_indexes { + let (_, _, _, method_data) = + self.summary_data + .get_tx_data(self.summary_modes, month, year, &None, self.conn); + + *self.last_summary_data = Some(method_data); + + let tag_data = self + .summary_data + .get_table_data(self.summary_modes, month, year); + + for tag in tag_data { + let tag_name = tag[0].clone(); + let tag_income = tag[1].parse::().unwrap(); + let tag_expense = tag[2].parse::().unwrap(); + + summary_tags_map.insert(tag_name, (tag_income, tag_expense)); + } + } + + for data in &mut sorted_data { + let tag_name = &data[0]; + let mut to_push = Vec::new(); + + if let Some((last_earning, last_expense)) = summary_tags_map.get(tag_name) { + let current_earning = data[1].parse::().unwrap(); + let current_expense = data[2].parse::().unwrap(); + + let earning_increased_percentage = + ((current_earning - last_earning) / last_earning) * 100.0; + + if last_earning == &0.0 { + to_push.push("∞".to_string()); + } else if earning_increased_percentage < 0.0 { + to_push.push(format!("↓{:.2}", earning_increased_percentage.abs())); + } else { + to_push.push(format!("↑{earning_increased_percentage:.2}")); + } + + let expense_increased_percentage = + ((current_expense - last_expense) / last_expense) * 100.0; + + if last_expense == &0.0 { + to_push.push("∞".to_string()); + } else if expense_increased_percentage < 0.0 { + to_push.push(format!("↓{:.2}", expense_increased_percentage.abs())); + } else { + to_push.push(format!("↑{expense_increased_percentage:.2}")); + } + } else { + to_push.push("∞".to_string()); + to_push.push("∞".to_string()); + } + data.extend(to_push); + } + + *self.summary_table = TableData::new(sorted_data); } /// If Enter is pressed on Summary page while a tag is selected /// go to search page and search for it #[cfg(not(tarpaulin_include))] pub fn search_tag(&mut self) { - if let SummaryTab::Table = self.summary_tab { - if let Some(index) = self.summary_table.state.selected() { - let tag_name = &self.summary_table.items[index][0]; - let search_param = TxData::custom("", "", "", "", "", "", tag_name, 0); - *self.search_data = search_param; - self.go_search(); - self.search_tx(); - } + if let SummaryTab::Table = self.summary_tab + && let Some(index) = self.summary_table.state.selected() + { + let tag_name = &self.summary_table.items[index][0]; + let search_param = TxData::custom("", "", "", "", "", "", tag_name, 0); + *self.search_data = search_param; + self.go_search(); + self.search_tx(); } } @@ -1012,19 +1095,19 @@ impl<'a> InputKeyHandler<'a> { #[cfg(not(tarpaulin_include))] pub fn switch_chart_tx_method_activation(&mut self) { - if !*self.chart_hidden_mode { - if let ChartTab::TxMethods = self.chart_tab { - let selected_index = self.chart_tx_methods.index; - let all_tx_methods = get_all_tx_methods_cumulative(self.conn); - - let selected_method = &all_tx_methods[selected_index]; - let activation_status = self - .chart_activated_methods - .get_mut(selected_method) - .unwrap(); - *activation_status = !*activation_status; - self.lerp_state.clear(); - } + if !*self.chart_hidden_mode + && let ChartTab::TxMethods = self.chart_tab + { + let selected_index = self.chart_tx_methods.index; + let all_tx_methods = get_all_tx_methods_cumulative(self.conn); + + let selected_method = &all_tx_methods[selected_index]; + let activation_status = self + .chart_activated_methods + .get_mut(selected_method) + .unwrap(); + *activation_status = !*activation_status; + self.lerp_state.clear(); } } @@ -1692,14 +1775,92 @@ impl InputKeyHandler<'_> { /// Reset summary table data by recreating it from gathered Summary Data #[cfg(not(tarpaulin_include))] fn reload_summary(&mut self) { - let summary_table = self.summary_data.get_table_data( + let mut summary_table = self.summary_data.get_table_data( self.summary_modes, self.summary_months.index, self.summary_years.index, ); self.total_tags = summary_table.len(); - *self.summary_table = TableData::new(summary_table); *self.summary_sort = SortingType::ByTags; + let mut summary_tags_map = HashMap::new(); + + let previous_indexes = match self.summary_modes.index { + 0 => { + if self.summary_months.index > 0 { + Some((self.summary_months.index - 1, self.summary_years.index)) + } else if self.summary_months.index == 0 && self.summary_years.index > 0 { + Some((self.summary_years.index - 1, MONTHS.len() - 1)) + } else { + None + } + } + 1 => { + if self.summary_years.index > 0 { + Some((self.summary_months.index, self.summary_years.index - 1)) + } else { + None + } + } + _ => None, + }; + + if let Some((month, year)) = previous_indexes { + let (_, _, _, method_data) = + self.summary_data + .get_tx_data(self.summary_modes, month, year, &None, self.conn); + + *self.last_summary_data = Some(method_data); + + let tag_data = self + .summary_data + .get_table_data(self.summary_modes, month, year); + + for tag in tag_data { + let tag_name = tag[0].clone(); + let tag_income = tag[1].parse::().unwrap(); + let tag_expense = tag[2].parse::().unwrap(); + + summary_tags_map.insert(tag_name, (tag_income, tag_expense)); + } + } + + for data in &mut summary_table { + let tag_name = &data[0]; + let mut to_push = Vec::new(); + + if let Some((last_earning, last_expense)) = summary_tags_map.get(tag_name) { + let current_earning = data[1].parse::().unwrap(); + let current_expense = data[2].parse::().unwrap(); + + let earning_increased_percentage = + ((current_earning - last_earning) / last_earning) * 100.0; + + if last_earning == &0.0 { + to_push.push("∞".to_string()); + } else if earning_increased_percentage < 0.0 { + to_push.push(format!("↓{:.2}", earning_increased_percentage.abs())); + } else { + to_push.push(format!("↑{earning_increased_percentage:.2}")); + } + + let expense_increased_percentage = + ((current_expense - last_expense) / last_expense) * 100.0; + + if last_expense == &0.0 { + to_push.push("∞".to_string()); + } else if expense_increased_percentage < 0.0 { + to_push.push(format!("↓{:.2}", expense_increased_percentage.abs())); + } else { + to_push.push(format!("↑{expense_increased_percentage:.2}")); + } + } else { + to_push.push("∞".to_string()); + to_push.push("∞".to_string()); + } + data.extend(to_push); + } + + *self.summary_table = TableData::new(summary_table); } /// Reload summary data by fetching from the DB diff --git a/src/page_handler/initializer.rs b/src/page_handler/initializer.rs index a32b849..fe65a10 100644 --- a/src/page_handler/initializer.rs +++ b/src/page_handler/initializer.rs @@ -27,16 +27,16 @@ pub fn initialize_app( let new_version_available = new_version.unwrap_or_default(); // If is not terminal, try to start a terminal otherwise create an error.txt file with the error message - if !atty::is(Stream::Stdout) { - if let Err(err) = start_terminal(original_dir.to_str().unwrap()) { - let mut error_location = PathBuf::from(&original_dir); - error_location.push("Error.txt"); - - let mut open = File::create(error_location)?; - let to_write = format!("{}\n{}", original_dir.to_str().unwrap(), err); - open.write_all(to_write.as_bytes())?; - process::exit(1); - } + if !atty::is(Stream::Stdout) + && let Err(err) = start_terminal(original_dir.to_str().unwrap()) + { + let mut error_location = PathBuf::from(&original_dir); + error_location.push("Error.txt"); + + let mut open = File::create(error_location)?; + let to_write = format!("{}\n{}", original_dir.to_str().unwrap(), err); + open.write_all(to_write.as_bytes())?; + process::exit(1); } // If the location was changed/json file found, change the db directory. diff --git a/src/page_handler/ui_handler.rs b/src/page_handler/ui_handler.rs index a11f8b0..4cc4c7f 100644 --- a/src/page_handler/ui_handler.rs +++ b/src/page_handler/ui_handler.rs @@ -131,6 +131,8 @@ pub fn start_app( summary_years.index, )); + let mut last_summary_data = None; + // Data for the Search Page's table let mut search_table = TableData::new(Vec::new()); @@ -239,6 +241,7 @@ pub fn start_app( summary_hidden_mode, &summary_sort, &mut lerp_state, + &last_summary_data, conn, ), CurrentUi::Search => search_ui( @@ -342,6 +345,7 @@ pub fn start_app( &mut popup_scroll_position, &mut max_popup_scroll, &mut lerp_state, + &mut last_summary_data, conn, ); diff --git a/src/search_page/search_ui.rs b/src/search_page/search_ui.rs index 809a808..52ab4f9 100644 --- a/src/search_page/search_ui.rs +++ b/src/search_page/search_ui.rs @@ -318,10 +318,10 @@ pub fn search_ui( } } - if let Some(index) = search_table.state.selected() { - if index > 10 { - *search_table.state.offset_mut() = index - 10; - } + if let Some(index) = search_table.state.selected() + && index > 10 + { + *search_table.state.offset_mut() = index - 10; } // Render the previously generated data into an interface diff --git a/src/summary_page/summary_data.rs b/src/summary_page/summary_data.rs index c8d8256..61eea78 100644 --- a/src/summary_page/summary_data.rs +++ b/src/summary_page/summary_data.rs @@ -222,8 +222,9 @@ impl SummaryData { mode: &IndexedData, month: usize, year: usize, + comparison: &Option, conn: &Connection, - ) -> (MyVec, MyVec, MyVec, MyVec, MyVec) { + ) -> (MyVec, MyVec, MyVec, MyVec) { let all_methods = get_all_tx_methods(conn); let mut total_income: f64 = 0.0; let mut total_expense: f64 = 0.0; @@ -345,7 +346,7 @@ impl SummaryData { let mut method_data = Vec::new(); - for method in &all_methods { + for (index, method) in all_methods.iter().enumerate() { let earning_percentage = if method_earning[method] == 0.0 { format!("{:.2}", 0.0) } else { @@ -369,49 +370,65 @@ impl SummaryData { } else { format!("{:.2}", method_expense[method] / total_month_checked) }; - method_data.push(vec![ + let mut to_push = vec![ method.to_string(), format!("{:.2}", method_earning[method]), format!("{:.2}", method_expense[method]), earning_percentage, expense_percentage, - average_earning, - average_expense, - ]); + ]; + + if mode.index != 0 { + to_push.push(average_earning); + to_push.push(average_expense); + } + + if let Some(comparison) = comparison.as_ref() { + let last_earning = comparison[index][1].parse::().unwrap(); + let last_expense = comparison[index][2].parse::().unwrap(); + + let current_earning = method_earning[method]; + let current_expense = method_expense[method]; + + let earning_increased_percentage = + ((current_earning - last_earning) / last_earning) * 100.0; + + if last_earning == 0.0 { + to_push.push("∞".to_string()); + } else if earning_increased_percentage < 0.0 { + to_push.push(format!("↓{:.2}", earning_increased_percentage.abs())); + } else { + to_push.push(format!("↑{earning_increased_percentage:.2}")); + } + + let expense_increased_percentage = + ((current_expense - last_expense) / last_expense) * 100.0; + + if last_expense == 0.0 { + to_push.push("∞".to_string()); + } else if expense_increased_percentage < 0.0 { + to_push.push(format!("↓{:.2}", expense_increased_percentage.abs())); + } else { + to_push.push(format!("↑{expense_increased_percentage:.2}")); + } + } + method_data.push(to_push); } - let summary_data_1 = vec![ - vec![ - String::from("Total Income"), - format!("{:.2}", total_income), - income_percentage, - ], - vec![ - String::from("Total Expense"), - format!("{:.2}", total_expense), - expense_percentage, - ], - vec![ - String::from("Net"), - format!("{:.2}", total_income - total_expense), - String::from("-"), - ], - ]; + let mut summary_data_1 = vec![vec![ + String::from("Net"), + format!("{:.2}", total_income), + format!("{:.2}", total_expense), + income_percentage, + expense_percentage, + ]]; + + if mode.index != 0 { + summary_data_1[0].push(format!("{average_income:.2}")); + summary_data_1[0].push(format!("{average_expense:.2}")); + } let summary_data_2 = vec![ - vec![ - String::from("Average Income"), - format!("{:.2}", average_income), - String::from("-"), - ], - vec![ - String::from("Average Expense"), - format!("{:.2}", average_expense), - String::from("-"), - ], - ]; - - let summary_data_3 = vec![ vec![ String::from("Largest Income"), biggest_earning.2, @@ -424,36 +441,22 @@ impl SummaryData { format!("{:.2}", biggest_expense.0), biggest_expense.1, ], - vec![ - String::from("Months Checked"), - total_month_checked.to_string(), - String::from("-"), - String::from("-"), - ], ]; - let summary_data_4 = vec![ + let summary_data_3 = vec![ vec![ String::from("Peak Earning"), peak_earning.1, format!("{:.2}", peak_earning.0), - String::from("-"), ], vec![ String::from("Peak Expense"), peak_expense.1, format!("{:.2}", peak_expense.0), - String::from("-"), ], ]; - ( - summary_data_1, - summary_data_2, - summary_data_3, - summary_data_4, - method_data, - ) + (summary_data_1, summary_data_2, summary_data_3, method_data) } /// Updates values based on the gathered data diff --git a/src/summary_page/summary_ui.rs b/src/summary_page/summary_ui.rs index 7b2b133..8966a04 100644 --- a/src/summary_page/summary_ui.rs +++ b/src/summary_page/summary_ui.rs @@ -9,7 +9,10 @@ use crate::page_handler::{ BACKGROUND, BOX, HEADER, IndexedData, SELECTED, SortingType, SummaryTab, TEXT, TableData, }; use crate::summary_page::SummaryData; -use crate::utility::{LerpState, create_tab, get_all_tx_methods, main_block, styled_block}; +use crate::utility::{ + LerpState, create_tab, get_all_tx_methods, main_block, styled_block, styled_block_no_bottom, + styled_block_no_top, +}; /// The function draws the Summary page of the interface. #[cfg(not(tarpaulin_include))] @@ -24,16 +27,16 @@ pub fn summary_ui( summary_hidden_mode: bool, summary_sort: &SortingType, lerp_state: &mut LerpState, + previous_data: &Option>>, conn: &Connection, ) { - let (summary_data_1, summary_data_2, summary_data_3, summary_data_4, method_data) = - summary_data.get_tx_data(mode_selection, months.index, years.index, conn); - - let mut summary_table_1 = TableData::new(summary_data_1); - let mut summary_table_2 = TableData::new(summary_data_2); - let mut summary_table_3 = TableData::new(summary_data_3); - let mut summary_table_4 = TableData::new(summary_data_4); - let mut method_table = TableData::new(method_data); + let (summary_data_1, summary_data_2, summary_data_3, method_data) = summary_data.get_tx_data( + mode_selection, + months.index, + years.index, + previous_data, + conn, + ); let size = f.area(); @@ -55,27 +58,50 @@ pub fn summary_ui( "Total Expense" }; - let header_cells = [ + let mut table_headers = vec![ tag_header, total_income_header, total_expense_header, "Income %", "Expense %", - ] - .into_iter() - .map(|h| Cell::from(h).style(Style::default().fg(BACKGROUND))); + ]; + + if mode_selection.index == 0 { + table_headers.push("MoM Income %"); + table_headers.push("MoM Expense %"); + } else if mode_selection.index == 1 { + table_headers.push("YoY Income %"); + table_headers.push("YoY Expense %"); + } - let method_header_cells = [ + let header_cells = table_headers + .into_iter() + .map(|h| Cell::from(h).style(Style::default().fg(BACKGROUND))); + + let mut method_headers = vec![ "Method", "Total Income", "Total Expense", "Income %", "Expense %", - "Average Income", - "Average Expense", - ] - .iter() - .map(|h| Cell::from(*h).style(Style::default().fg(BACKGROUND))); + ]; + + if mode_selection.index != 0 { + method_headers.push("Average Income"); + method_headers.push("Average Expense"); + } + + if mode_selection.index == 0 { + method_headers.push("MoM Income %"); + method_headers.push("MoM Expense %"); + } else if mode_selection.index == 1 { + method_headers.push("YoY Income %"); + method_headers.push("YoY Expense %"); + } + + let method_header_cells = method_headers + .iter() + .map(|h| Cell::from(*h).style(Style::default().fg(BACKGROUND))); let header = Row::new(header_cells) .style(Style::default().bg(HEADER)) @@ -94,8 +120,9 @@ pub fn summary_ui( if summary_hidden_mode { main_layout = main_layout.constraints([ - Constraint::Length(method_len + 3), - Constraint::Length(9), + Constraint::Length(method_len + 2), + Constraint::Length(3), + Constraint::Length(4), Constraint::Min(0), ]); summary_layout = @@ -107,8 +134,9 @@ pub fn summary_ui( Constraint::Length(3), Constraint::Length(3), Constraint::Length(3), - Constraint::Length(method_len + 3), - Constraint::Length(9), + Constraint::Length(method_len + 2), + Constraint::Length(3), + Constraint::Length(4), Constraint::Min(0), ]); summary_layout = summary_layout @@ -118,8 +146,9 @@ pub fn summary_ui( main_layout = main_layout.constraints([ Constraint::Length(3), Constraint::Length(3), - Constraint::Length(method_len + 3), - Constraint::Length(9), + Constraint::Length(method_len + 2), + Constraint::Length(3), + Constraint::Length(4), Constraint::Min(0), ]); summary_layout = summary_layout @@ -128,12 +157,13 @@ pub fn summary_ui( 2 => { main_layout = main_layout.constraints([ Constraint::Length(3), - Constraint::Length(method_len + 3), - Constraint::Length(9), + Constraint::Length(method_len + 2), + Constraint::Length(3), + Constraint::Length(4), Constraint::Min(0), ]); summary_layout = summary_layout - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]); + .constraints([Constraint::Percentage(100), Constraint::Percentage(50)]); } _ => {} } @@ -141,21 +171,11 @@ pub fn summary_ui( let chunks = main_layout.split(size); let summary_chunk = if summary_hidden_mode { - summary_layout.split(chunks[1]) + summary_layout.split(chunks[2]) } else { - summary_layout.split(chunks[4 - mode_selection.index]) + summary_layout.split(chunks[5 - mode_selection.index]) }; - let left_summary = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Percentage(60), Constraint::Percentage(40)]) - .split(summary_chunk[0]); - - let right_summary = Layout::default() - .direction(Direction::Vertical) - .constraints([Constraint::Percentage(60), Constraint::Percentage(40)]) - .split(summary_chunk[1]); - f.render_widget(main_block(), size); let mut month_tab = create_tab(months, "Months"); @@ -172,6 +192,26 @@ pub fn summary_ui( .map(|(row_index, item)| { let cells = item.iter().enumerate().map(|(index, c)| { let Ok(parsed_num) = c.parse::() else { + if c == "∞" { + let lerp_id = format!("summary_table_main:{index}:{row_index}"); + lerp_state.lerp(&lerp_id, 0.0); + } + + let symbol = if c.contains('↑') || c.contains('↓') { + c.chars().next() + } else { + None + }; + + if let Some(sym) = symbol { + let c = c.replace(sym, ""); + if let Ok(parsed_num) = c.parse::() { + let lerp_id = format!("summary_table_main:{index}:{row_index}"); + let new_c = lerp_state.lerp(&lerp_id, parsed_num); + + return Cell::from(format!("{sym}{new_c:.2}").separate_with_commas()); + } + } return Cell::from(c.separate_with_commas()); }; @@ -186,227 +226,213 @@ pub fn summary_ui( .style(Style::default().fg(TEXT)) }); - let mut table_area = Table::new( - rows, - [ + let table_width = if mode_selection.index == 2 { + vec![ Constraint::Percentage(20), Constraint::Percentage(20), Constraint::Percentage(20), Constraint::Percentage(20), Constraint::Percentage(20), - ], - ) - .header(header) - .block(styled_block("Tags")) - .style(Style::default().fg(BOX)); - - let summary_rows_1 = summary_table_1 - .items - .iter() - .enumerate() - .map(|(row_index, item)| { - let cells = item.iter().enumerate().map(|(index, c)| { - let mut cell = if let Ok(parsed_num) = c.parse::() { - let lerp_id = format!("summary_table_1:{index}:{row_index}"); - let new_c = lerp_state.lerp(&lerp_id, parsed_num); - - let text = if index == 2 { - // Total income/expense % column. Add % char manually - format!("{new_c:.2}%").separate_with_commas() - } else { - format!("{new_c:.2}").separate_with_commas() - }; - - Cell::from(text) - } else { - Cell::from(c.separate_with_commas()) - }; - - if index == 0 { - cell = cell.style(Style::default().fg(TEXT).add_modifier(Modifier::BOLD)); - } - cell - }); - Row::new(cells) - .height(1) - .bottom_margin(0) - .style(Style::default().fg(TEXT)) - }); + ] + } else { + vec![ + Constraint::Percentage(14), + Constraint::Percentage(14), + Constraint::Percentage(14), + Constraint::Percentage(14), + Constraint::Percentage(14), + Constraint::Percentage(14), + Constraint::Percentage(15), + ] + }; - let summary_area_1 = Table::new( - summary_rows_1, - [ - Constraint::Percentage(33), - Constraint::Percentage(33), - Constraint::Percentage(33), - ], - ) - .block(styled_block("")) - .style(Style::default().fg(BOX)); + let mut table_area = Table::new(rows, table_width) + .header(header) + .block(styled_block("Tags")) + .style(Style::default().fg(BOX)); - let summary_rows_2 = summary_table_2 - .items - .iter() - .enumerate() - .map(|(row_index, item)| { - let cells = item.iter().enumerate().map(|(index, c)| { - let mut cell = if let Ok(parsed_num) = c.parse::() { - let lerp_id = format!("summary_table_2:{index}:{row_index}"); - let new_c = lerp_state.lerp(&lerp_id, parsed_num); + let summary_rows_2 = summary_data_2.iter().enumerate().map(|(row_index, item)| { + let cells = item.iter().enumerate().map(|(index, c)| { + let mut cell = if let Ok(parsed_num) = c.parse::() { + let lerp_id = format!("summary_table_2:{index}:{row_index}"); + let new_c = lerp_state.lerp(&lerp_id, parsed_num); - Cell::from(format!("{new_c:.2}").separate_with_commas()) - } else { - Cell::from(c.separate_with_commas()) - }; + Cell::from(format!("{new_c:.2}").separate_with_commas()) + } else { + Cell::from(c.separate_with_commas()) + }; - if index == 0 { - cell = cell.style(Style::default().fg(TEXT).add_modifier(Modifier::BOLD)); - } - cell - }); - Row::new(cells) - .height(1) - .bottom_margin(0) - .style(Style::default().fg(TEXT)) + if index == 0 { + cell = cell.style(Style::default().fg(TEXT).add_modifier(Modifier::BOLD)); + } + cell }); + Row::new(cells) + .height(1) + .bottom_margin(0) + .style(Style::default().fg(TEXT)) + }); let summary_area_2 = Table::new( summary_rows_2, [ - Constraint::Percentage(33), - Constraint::Percentage(33), - Constraint::Percentage(33), + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), + Constraint::Percentage(25), ], ) .block(styled_block("")) .style(Style::default().fg(BOX)); - let summary_rows_3 = summary_table_3 - .items - .iter() - .enumerate() - .map(|(row_index, item)| { - let height = 1; - let cells = item.iter().enumerate().map(|(index, c)| { - let mut cell = if let Ok(parsed_num) = c.parse::() { - let lerp_id = format!("summary_table_3:{index}:{row_index}"); - let new_c = lerp_state.lerp(&lerp_id, parsed_num); - - let text = if index == 1 && row_index == 2 { - // Month checked value. No need float for this - let new_c = new_c as i64; - format!("{new_c}").separate_with_commas() - } else { - format!("{new_c:.2}").separate_with_commas() - }; + let summary_rows_3 = summary_data_3.iter().enumerate().map(|(row_index, item)| { + let height = 1; + let cells = item.iter().enumerate().map(|(index, c)| { + let mut cell = if let Ok(parsed_num) = c.parse::() { + let lerp_id = format!("summary_table_3:{index}:{row_index}"); + let new_c = lerp_state.lerp(&lerp_id, parsed_num); - Cell::from(text) + let text = if index == 1 && row_index == 2 { + // Month checked value. No need float for this + let new_c = new_c as i64; + format!("{new_c}").separate_with_commas() } else { - Cell::from(c.separate_with_commas()) + format!("{new_c:.2}").separate_with_commas() }; - if index == 0 { - cell = cell.style(Style::default().fg(TEXT).add_modifier(Modifier::BOLD)); - } - cell - }); - Row::new(cells) - .height(height as u16) - .bottom_margin(0) - .style(Style::default().fg(TEXT)) + Cell::from(text) + } else { + Cell::from(c.separate_with_commas()) + }; + + if index == 0 { + cell = cell.style(Style::default().fg(TEXT).add_modifier(Modifier::BOLD)); + } + cell }); + Row::new(cells) + .height(height as u16) + .bottom_margin(0) + .style(Style::default().fg(TEXT)) + }); let summary_area_3 = Table::new( summary_rows_3, [ - Constraint::Percentage(25), - Constraint::Percentage(25), - Constraint::Percentage(25), - Constraint::Percentage(25), + Constraint::Percentage(33), + Constraint::Percentage(33), + Constraint::Percentage(33), ], ) .block(styled_block("")) .style(Style::default().fg(BOX)); - let summary_rows_4 = summary_table_4 - .items - .iter() - .enumerate() - .map(|(row_index, item)| { - let cells = item.iter().enumerate().map(|(index, c)| { - let mut cell = if let Ok(parsed_num) = c.parse::() { - let lerp_id = format!("summary_table_4:{index}:{row_index}"); - let new_c = lerp_state.lerp(&lerp_id, parsed_num); + let method_rows = method_data.iter().enumerate().map(|(row_index, item)| { + let cells = item.iter().enumerate().map(|(index, c)| { + let mut cell = if let Ok(parsed_num) = c.parse::() { + let lerp_id = format!("method_table:{index}:{row_index}"); + let new_c = lerp_state.lerp(&lerp_id, parsed_num); - Cell::from(format!("{new_c:.2}").separate_with_commas()) + Cell::from(format!("{new_c:.2}").separate_with_commas()) + } else { + let symbol = if c.contains('↑') || c.contains('↓') { + c.chars().next() } else { - Cell::from(c.separate_with_commas()) + None }; - if index == 0 { - cell = cell.style(Style::default().fg(TEXT).add_modifier(Modifier::BOLD)); - } - cell - }); - Row::new(cells) - .height(1) - .bottom_margin(0) - .style(Style::default().fg(TEXT)) - }); + if let Some(sym) = symbol { + let c = c.replace(sym, ""); + if let Ok(parsed_num) = c.parse::() { + let lerp_id = format!("method_table:{index}:{row_index}"); + let new_c = lerp_state.lerp(&lerp_id, parsed_num); - let summary_area_4 = Table::new( - summary_rows_4, - [ - Constraint::Percentage(25), - Constraint::Percentage(25), - Constraint::Percentage(25), - Constraint::Percentage(25), - ], - ) - .block(styled_block("")) - .style(Style::default().fg(BOX)); + return Cell::from(format!("{sym}{new_c:.2}").separate_with_commas()); + } + } - let method_rows = method_table - .items - .iter() - .enumerate() - .map(|(row_index, item)| { - let cells = item.iter().enumerate().map(|(index, c)| { - let mut cell = if let Ok(parsed_num) = c.parse::() { + if c == "∞" { let lerp_id = format!("method_table:{index}:{row_index}"); - let new_c = lerp_state.lerp(&lerp_id, parsed_num); - - Cell::from(format!("{new_c:.2}").separate_with_commas()) - } else { - Cell::from(c.separate_with_commas()) - }; - - if index == 0 { - cell = cell.style(Style::default().fg(TEXT).add_modifier(Modifier::BOLD)); + lerp_state.lerp(&lerp_id, 0.0); } - cell - }); - Row::new(cells) - .height(1) - .bottom_margin(0) - .style(Style::default().fg(TEXT)) - }); + Cell::from(c.separate_with_commas()) + }; - let method_area = Table::new( - method_rows, - [ + if index == 0 { + cell = cell.style(Style::default().fg(TEXT).add_modifier(Modifier::BOLD)); + } + cell + }); + Row::new(cells) + .height(1) + .bottom_margin(0) + .style(Style::default().fg(TEXT)) + }); + + let method_widths = if mode_selection.index == 2 { + vec![ + Constraint::Percentage(10), Constraint::Percentage(14), Constraint::Percentage(14), Constraint::Percentage(14), Constraint::Percentage(14), + Constraint::Percentage(16), + Constraint::Percentage(16), + ] + } else if mode_selection.index == 1 { + vec![ + Constraint::Percentage(10), + Constraint::Percentage(12), + Constraint::Percentage(12), + Constraint::Percentage(10), + Constraint::Percentage(10), + Constraint::Percentage(12), + Constraint::Percentage(12), + Constraint::Percentage(10), + Constraint::Percentage(10), + ] + } else { + vec![ + Constraint::Percentage(10), Constraint::Percentage(14), Constraint::Percentage(14), Constraint::Percentage(14), - ], - ) - .header(method_header) - .block(styled_block("")) - .style(Style::default().fg(BOX)); + Constraint::Percentage(14), + Constraint::Percentage(16), + Constraint::Percentage(16), + ] + }; + + let method_area = Table::new(method_rows, &method_widths) + .header(method_header) + .block(styled_block_no_bottom("")) + .style(Style::default().fg(BOX)); + + let net_row = summary_data_1.iter().enumerate().map(|(row_index, item)| { + let cells = item.iter().enumerate().map(|(index, c)| { + let mut cell = if let Ok(parsed_num) = c.parse::() { + let lerp_id = format!("summary_rows_1:{index}:{row_index}"); + let new_c = lerp_state.lerp(&lerp_id, parsed_num); + + Cell::from(format!("{new_c:.2}").separate_with_commas()) + } else { + Cell::from(c.separate_with_commas()) + }; + + if index == 0 { + cell = cell.style(Style::default().fg(TEXT).add_modifier(Modifier::BOLD)); + } + cell + }); + Row::new(cells) + .height(1) + .bottom_margin(0) + .style(Style::default().fg(TEXT)) + }); + + let net_area = Table::new(net_row, &method_widths) + .block(styled_block_no_top("")) + .style(Style::default().fg(BOX)); match current_page { // Previously added a black block to year and month widget if a value is not selected @@ -432,41 +458,41 @@ pub fn summary_ui( } // Always keep some items rendered on the upper side of the table - if let Some(index) = table_data.state.selected() { - if index > 7 { - *table_data.state.offset_mut() = index - 7; - } + if let Some(index) = table_data.state.selected() + && index > 7 + { + *table_data.state.offset_mut() = index - 7; } if summary_hidden_mode { - f.render_stateful_widget(summary_area_1, left_summary[0], &mut summary_table_1.state); - f.render_stateful_widget(summary_area_2, left_summary[1], &mut summary_table_2.state); - f.render_stateful_widget(summary_area_3, right_summary[0], &mut summary_table_3.state); - f.render_stateful_widget(summary_area_4, right_summary[1], &mut summary_table_4.state); - f.render_stateful_widget(table_area, chunks[2], &mut table_data.state); - f.render_stateful_widget(method_area, chunks[0], &mut method_table.state); + f.render_widget(summary_area_2, summary_chunk[1]); + f.render_widget(summary_area_3, summary_chunk[0]); + f.render_widget(table_area, chunks[3]); + f.render_widget(net_area, chunks[1]); + f.render_widget(method_area, chunks[0]); } else { f.render_widget(mode_selection_tab, chunks[0]); - f.render_stateful_widget(summary_area_1, left_summary[0], &mut summary_table_1.state); - f.render_stateful_widget(summary_area_2, left_summary[1], &mut summary_table_2.state); - f.render_stateful_widget(summary_area_3, right_summary[0], &mut summary_table_3.state); - f.render_stateful_widget(summary_area_4, right_summary[1], &mut summary_table_4.state); + f.render_widget(summary_area_2, summary_chunk[1]); + f.render_widget(summary_area_3, summary_chunk[0]); match mode_selection.index { 0 => { f.render_widget(year_tab, chunks[1]); f.render_widget(month_tab, chunks[2]); - f.render_stateful_widget(table_area, chunks[5], &mut table_data.state); - f.render_stateful_widget(method_area, chunks[3], &mut method_table.state); + f.render_stateful_widget(table_area, chunks[6], &mut table_data.state); + f.render_widget(net_area, chunks[4]); + f.render_widget(method_area, chunks[3]); } 1 => { f.render_widget(year_tab, chunks[1]); - f.render_stateful_widget(table_area, chunks[4], &mut table_data.state); - f.render_stateful_widget(method_area, chunks[2], &mut method_table.state); + f.render_stateful_widget(table_area, chunks[5], &mut table_data.state); + f.render_widget(net_area, chunks[3]); + f.render_widget(method_area, chunks[2]); } 2 => { - f.render_stateful_widget(table_area, chunks[3], &mut table_data.state); - f.render_stateful_widget(method_area, chunks[1], &mut method_table.state); + f.render_stateful_widget(table_area, chunks[4], &mut table_data.state); + f.render_widget(net_area, chunks[2]); + f.render_widget(method_area, chunks[1]); } _ => {} } diff --git a/src/tx_handler/add_tx.rs b/src/tx_handler/add_tx.rs index 986d65b..09240ff 100644 --- a/src/tx_handler/add_tx.rs +++ b/src/tx_handler/add_tx.rs @@ -1,4 +1,5 @@ use rusqlite::{Connection, Result as sqlResult}; + use std::collections::HashMap; use crate::utility::{ diff --git a/src/utility/traits/autofiller.rs b/src/utility/traits/autofiller.rs index 52728a7..3e8671f 100644 --- a/src/utility/traits/autofiller.rs +++ b/src/utility/traits/autofiller.rs @@ -1,6 +1,7 @@ -use crate::utility::{get_all_details, get_all_tags, get_all_tx_methods, get_best_match}; use rusqlite::Connection; +use crate::utility::{get_all_details, get_all_tags, get_all_tx_methods, get_best_match}; + pub trait AutoFiller { fn autofill_tx_method(&self, user_input: &str, conn: &Connection) -> String { let all_tx_methods = get_all_tx_methods(conn); diff --git a/src/utility/traits/stepper.rs b/src/utility/traits/stepper.rs index 6a8559b..14737af 100644 --- a/src/utility/traits/stepper.rs +++ b/src/utility/traits/stepper.rs @@ -1,9 +1,10 @@ +use chrono::{Duration, NaiveDate}; +use rusqlite::Connection; + use crate::outputs::{NAType, StepType, SteppingError, VerifyingOutput}; use crate::page_handler::DateType; use crate::utility::traits::DataVerifier; use crate::utility::{get_all_tags, get_all_tx_methods}; -use chrono::{Duration, NaiveDate}; -use rusqlite::Connection; pub trait FieldStepper: DataVerifier { fn step_date( diff --git a/src/utility/traits/verifier.rs b/src/utility/traits/verifier.rs index ac1b458..a834674 100644 --- a/src/utility/traits/verifier.rs +++ b/src/utility/traits/verifier.rs @@ -1,11 +1,12 @@ -use crate::outputs::{AType, NAType, VerifyingOutput}; -use crate::page_handler::DateType; -use crate::utility::{get_all_tags, get_all_tx_methods, get_best_match}; use chrono::naive::NaiveDate; use rusqlite::Connection; use std::cmp::Ordering; use std::collections::HashSet; +use crate::outputs::{AType, NAType, VerifyingOutput}; +use crate::page_handler::DateType; +use crate::utility::{get_all_tags, get_all_tx_methods, get_best_match}; + pub trait DataVerifier { /// Checks if: /// diff --git a/src/utility/utils.rs b/src/utility/utils.rs index b192322..e7306c9 100644 --- a/src/utility/utils.rs +++ b/src/utility/utils.rs @@ -323,7 +323,7 @@ pub fn check_n_create_db(verifying_path: &PathBuf) -> Result<(), Box> /// Returns a styled block for UI to use #[cfg(not(tarpaulin_include))] #[must_use] -pub fn styled_block(title: &str) -> Block { +pub fn styled_block(title: &str) -> Block<'_> { Block::default() .borders(Borders::ALL) .border_type(BorderType::Rounded) @@ -334,6 +334,30 @@ pub fn styled_block(title: &str) -> Block { )) } +#[must_use] +pub fn styled_block_no_top(title: &str) -> Block<'_> { + Block::default() + .borders(Borders::LEFT | Borders::RIGHT | Borders::BOTTOM) + .border_type(BorderType::Rounded) + .style(Style::default().bg(BACKGROUND).fg(BOX)) + .title(Span::styled( + title, + Style::default().add_modifier(Modifier::BOLD), + )) +} + +#[must_use] +pub fn styled_block_no_bottom(title: &str) -> Block<'_> { + Block::default() + .borders(Borders::LEFT | Borders::RIGHT | Borders::TOP) + .border_type(BorderType::Rounded) + .style(Style::default().bg(BACKGROUND).fg(BOX)) + .title(Span::styled( + title, + Style::default().add_modifier(Modifier::BOLD), + )) +} + #[cfg(not(tarpaulin_include))] #[must_use] pub fn main_block<'a>() -> Block<'a> { @@ -344,7 +368,7 @@ pub fn main_block<'a>() -> Block<'a> { /// Used for rendering #[cfg(not(tarpaulin_include))] #[must_use] -pub fn create_bolded_text(text: &str) -> Vec { +pub fn create_bolded_text(text: &str) -> Vec> { let mut text_data = Vec::new(); for line in text.split('\n') { diff --git a/tests/common.rs b/tests/common.rs index d054af6..8b950c5 100644 --- a/tests/common.rs +++ b/tests/common.rs @@ -2,11 +2,12 @@ use rex_tui::db::create_db; use rusqlite::Connection; use std::fs; +#[must_use] pub fn create_test_db(file_name: &str) -> Connection { - if let Ok(metadata) = fs::metadata(file_name) { - if metadata.is_file() { - fs::remove_file(file_name).expect("Failed to delete existing file"); - } + if let Ok(metadata) = fs::metadata(file_name) + && metadata.is_file() + { + fs::remove_file(file_name).expect("Failed to delete existing file"); } let mut conn = Connection::open(file_name).unwrap(); diff --git a/tests/db_updates.rs b/tests/db_updates.rs index f7c80f5..256a53b 100644 --- a/tests/db_updates.rs +++ b/tests/db_updates.rs @@ -5,10 +5,10 @@ use rusqlite::Connection; use std::fs; fn check_test_db(file_name: &str) { - if let Ok(metadata) = fs::metadata(file_name) { - if metadata.is_file() { - fs::remove_file(file_name).expect("Failed to delete existing file"); - } + if let Ok(metadata) = fs::metadata(file_name) + && metadata.is_file() + { + fs::remove_file(file_name).expect("Failed to delete existing file"); } } diff --git a/tests/stepper.rs b/tests/stepper.rs index e0cdb47..d8544e1 100644 --- a/tests/stepper.rs +++ b/tests/stepper.rs @@ -16,10 +16,10 @@ impl FieldStepper for Testing {} impl DataVerifier for Testing {} fn create_test_db(file_name: &str) -> Connection { - if let Ok(metadata) = fs::metadata(file_name) { - if metadata.is_file() { - fs::remove_file(file_name).expect("Failed to delete existing file"); - } + if let Ok(metadata) = fs::metadata(file_name) + && metadata.is_file() + { + fs::remove_file(file_name).expect("Failed to delete existing file"); } let mut conn = Connection::open(file_name).unwrap(); diff --git a/tests/summary.rs b/tests/summary.rs index faa7a60..b31a1fa 100644 --- a/tests/summary.rs +++ b/tests/summary.rs @@ -54,36 +54,18 @@ fn check_summary_data_1() { let my_summary = SummaryData::new(&conn); let my_summary_text = my_summary.get_table_data(&summary_modes, 6, 1); - let my_summary_text_2 = my_summary.get_tx_data(&summary_modes, 6, 1, &conn); + let my_summary_text_2 = my_summary.get_tx_data(&summary_modes, 6, 1, &None, &conn); let expected_data_1 = vec![vec!["Food", "200.00", "100.00", "100.00", "100.00"]]; let expected_data_2 = ( - vec![ - vec![ - "Total Income".to_string(), - "200.00".to_string(), - "66.67".to_string(), - ], - vec![ - "Total Expense".to_string(), - "100.00".to_string(), - "33.33".to_string(), - ], - vec!["Net".to_string(), "100.00".to_string(), "-".to_string()], - ], - vec![ - vec![ - "Average Income".to_string(), - "200.00".to_string(), - "-".to_string(), - ], - vec![ - "Average Expense".to_string(), - "100.00".to_string(), - "-".to_string(), - ], - ], + vec![vec![ + "Net".to_string(), + "200.00".to_string(), + "100.00".to_string(), + "66.67".to_string(), + "33.33".to_string(), + ]], vec![ vec![ "Largest Income".to_string(), @@ -97,25 +79,17 @@ fn check_summary_data_1() { "100.00".to_string(), "Cash Cow".to_string(), ], - vec![ - "Months Checked".to_string(), - "1".to_string(), - "-".to_string(), - "-".to_string(), - ], ], vec![ vec![ "Peak Earning".to_string(), "07-2023".to_string(), "200.00".to_string(), - "-".to_string(), ], vec![ "Peak Expense".to_string(), "07-2023".to_string(), "100.00".to_string(), - "-".to_string(), ], ], vec![ @@ -125,8 +99,6 @@ fn check_summary_data_1() { "0.00".to_string(), "100.00".to_string(), "0.00".to_string(), - "200.00".to_string(), - "0.00".to_string(), ], vec![ "Cash Cow".to_string(), @@ -134,8 +106,6 @@ fn check_summary_data_1() { "100.00".to_string(), "0.00".to_string(), "100.00".to_string(), - "0.00".to_string(), - "100.00".to_string(), ], ], ); @@ -205,7 +175,7 @@ fn check_summary_data_2() { let my_summary = SummaryData::new(&conn); let my_summary_text = my_summary.get_table_data(&summary_modes, 0, 0); - let my_summary_text_2 = my_summary.get_tx_data(&summary_modes, 0, 0, &conn); + let my_summary_text_2 = my_summary.get_tx_data(&summary_modes, 0, 0, &None, &conn); let expected_data_1 = vec![ vec![ @@ -225,31 +195,15 @@ fn check_summary_data_2() { ]; let expected_data_2 = ( - vec![ - vec![ - "Total Income".to_string(), - "1700.00".to_string(), - "62.96".to_string(), - ], - vec![ - "Total Expense".to_string(), - "1000.00".to_string(), - "37.04".to_string(), - ], - vec!["Net".to_string(), "700.00".to_string(), "-".to_string()], - ], - vec![ - vec![ - "Average Income".to_string(), - "425.00".to_string(), - "-".to_string(), - ], - vec![ - "Average Expense".to_string(), - "250.00".to_string(), - "-".to_string(), - ], - ], + vec![vec![ + "Net".to_string(), + "1700.00".to_string(), + "1000.00".to_string(), + "62.96".to_string(), + "37.04".to_string(), + "425.00".to_string(), + "250.00".to_string(), + ]], vec![ vec![ "Largest Income".to_string(), @@ -263,25 +217,17 @@ fn check_summary_data_2() { "500.00".to_string(), "Super Special Bank".to_string(), ], - vec![ - "Months Checked".to_string(), - "4".to_string(), - "-".to_string(), - "-".to_string(), - ], ], vec![ vec![ "Peak Earning".to_string(), "05-2022".to_string(), "1000.00".to_string(), - "-".to_string(), ], vec![ "Peak Expense".to_string(), "01-2022".to_string(), "500.00".to_string(), - "-".to_string(), ], ], vec![ @@ -360,7 +306,7 @@ fn check_summary_data_3() { let my_summary = SummaryData::new(&conn); let my_summary_text = my_summary.get_table_data(&summary_modes, 0, 1); - let my_summary_text_2 = my_summary.get_tx_data(&summary_modes, 0, 1, &conn); + let my_summary_text_2 = my_summary.get_tx_data(&summary_modes, 0, 1, &None, &conn); let expected_data_1 = vec![ vec![ @@ -380,31 +326,15 @@ fn check_summary_data_3() { ]; let expected_data_2 = ( - vec![ - vec![ - "Total Income".to_string(), - "200.00".to_string(), - "66.67".to_string(), - ], - vec![ - "Total Expense".to_string(), - "100.00".to_string(), - "33.33".to_string(), - ], - vec!["Net".to_string(), "100.00".to_string(), "-".to_string()], - ], - vec![ - vec![ - "Average Income".to_string(), - "66.67".to_string(), - "-".to_string(), - ], - vec![ - "Average Expense".to_string(), - "33.33".to_string(), - "-".to_string(), - ], - ], + vec![vec![ + "Net".to_string(), + "200.00".to_string(), + "100.00".to_string(), + "66.67".to_string(), + "33.33".to_string(), + "66.67".to_string(), + "33.33".to_string(), + ]], vec![ vec![ "Largest Income".to_string(), @@ -418,25 +348,17 @@ fn check_summary_data_3() { "100.00".to_string(), "Super Special Bank".to_string(), ], - vec![ - "Months Checked".to_string(), - "3".to_string(), - "-".to_string(), - "-".to_string(), - ], ], vec![ vec![ "Peak Earning".to_string(), "07-2023".to_string(), "100.00".to_string(), - "-".to_string(), ], vec![ "Peak Expense".to_string(), "08-2022".to_string(), "100.00".to_string(), - "-".to_string(), ], ], vec![