use super::state::App;
use crate::app::settings_types::{SettingType, SettingsState};
use crate::config::{save_settings, AppSettings};
use crate::persistence::{load_categories, load_transactions, save_transactions};
use chrono::Duration;
use std::path::PathBuf;
impl App {
pub(crate) fn enter_settings_mode(&mut self) {
self.mode = crate::app::state::AppMode::Settings;
self.settings_state = SettingsState::new();
let loaded_settings = crate::config::load_settings().unwrap_or_default();
self.settings_state.add_setting(
"header_data",
"Data Management",
"".to_string(),
SettingType::SectionHeader,
"",
);
let path_str = self.data_file_path.to_string_lossy().to_string();
let path_val = crate::validation::strip_path_quotes(&path_str);
self.settings_state.add_setting(
"data_file_path",
"Data File Path",
path_val,
SettingType::Path,
"Absolute path to your transactions CSV file.",
);
let database_path_str = self.database_path.to_string_lossy().to_string();
let database_path_val = crate::validation::strip_path_quotes(&database_path_str);
self.settings_state.add_setting(
"database_path",
"Database Path",
database_path_val,
SettingType::Path,
"Absolute path to your categories SQLite database.",
);
self.settings_state.add_setting(
"manage_categories",
"Manage Categories",
"Open Category Catalog".to_string(),
SettingType::Action,
"Open the category catalog to add, edit, or delete categories.",
);
self.settings_state.add_setting(
"header_monthly",
"Monthly Summary View",
"".to_string(),
SettingType::SectionHeader,
"",
);
let budget_val = loaded_settings
.target_budget
.map(|v| v.to_string())
.unwrap_or_default();
self.settings_state.add_setting(
"target_budget",
"Target Budget",
budget_val,
SettingType::Number,
"Monthly spending goal. Displayed in Monthly Summary view only when cumulative mode.",
);
self.settings_state.add_setting(
"header_transactions",
"Transaction View",
"".to_string(),
SettingType::SectionHeader,
"",
);
let hourly_rate_val = loaded_settings
.hourly_rate
.map(|v| v.to_string())
.unwrap_or_default();
self.settings_state.add_setting(
"hourly_rate",
"Hourly Rate ($)",
hourly_rate_val.clone(),
SettingType::Number,
"Optional. Enter your hourly earning rate to see costs in hours.",
);
if !hourly_rate_val.is_empty() {
let show_hours_val = if loaded_settings.show_hours.unwrap_or(false) {
"◀ Yes "
} else {
" No ▶"
};
self.settings_state.add_setting(
"show_hours",
"Show Costs in Hours",
show_hours_val.to_string(),
SettingType::Toggle,
"Toggle to display transaction amounts as hours worked.",
);
}
self.settings_state.add_setting(
"header_input",
"Input Preferences",
"".to_string(),
SettingType::SectionHeader,
"",
);
let fuzzy_search_val = if loaded_settings.fuzzy_search_mode.unwrap_or(false) {
"◀ Yes "
} else {
" No ▶"
};
self.settings_state.add_setting(
"fuzzy_search_mode",
"Fuzzy Search Categories",
fuzzy_search_val.to_string(),
SettingType::Toggle,
"Toggle to enable fuzzy searching for categories/subcategories.",
);
self.settings_state.add_setting(
"header_general",
"General Preferences",
"".to_string(),
SettingType::SectionHeader,
"",
);
let hide_help_bar_val = if loaded_settings.hide_help_bar.unwrap_or(false) {
"◀ Yes "
} else {
" No ▶"
};
self.settings_state.add_setting(
"hide_help_bar",
"Hide Help Bar (NOT RECOMMENDED)",
hide_help_bar_val.to_string(),
SettingType::Toggle,
"Toggle to hide the bottom help bar (Ctrl+H will still work).",
);
self.settings_state.selected_index = 0;
while self.settings_state.selected_index < self.settings_state.items.len() {
if self.settings_state.items[self.settings_state.selected_index].setting_type
!= SettingType::SectionHeader
{
break;
}
self.settings_state.selected_index += 1;
}
if let Some(item) = self
.settings_state
.items
.get(self.settings_state.selected_index)
{
self.settings_state.edit_cursor = item.value.len();
}
self.clear_status_message();
}
pub(crate) fn exit_settings_mode(&mut self) {
self.mode = crate::app::state::AppMode::Normal;
self.settings_state = SettingsState::default();
self.clear_status_message();
}
pub(crate) fn save_settings(&mut self) {
let mut new_path_str = String::new();
let mut new_database_path_str = String::new();
let mut target_budget_str = String::new();
let mut hourly_rate_str = String::new();
let mut show_hours_val = None;
let mut fuzzy_search_val = None;
let mut hide_help_bar_val = None;
if let Some(val) = self.settings_state.get_value("data_file_path") {
new_path_str = crate::validation::strip_path_quotes(val);
}
if let Some(val) = self.settings_state.get_value("database_path") {
new_database_path_str = crate::validation::strip_path_quotes(val);
}
if let Some(val) = self.settings_state.get_value("target_budget") {
target_budget_str = val.trim().to_string();
}
if let Some(val) = self.settings_state.get_value("hourly_rate") {
hourly_rate_str = val.trim().to_string();
}
if let Some(val) = self.settings_state.get_value("show_hours") {
show_hours_val = Some(val.to_lowercase().contains("yes"));
}
if let Some(val) = self.settings_state.get_value("fuzzy_search_mode") {
fuzzy_search_val = Some(val.to_lowercase().contains("yes"));
}
if let Some(val) = self.settings_state.get_value("hide_help_bar") {
hide_help_bar_val = Some(val.to_lowercase().contains("yes"));
}
let target_budget = if target_budget_str.is_empty() {
None
} else {
match crate::validation::validate_amount_string(&target_budget_str) {
Ok(val) => Some(val),
Err(msg) => {
self.set_status_message(format!("Error: Target budget - {}", msg), None);
return;
}
}
};
let hourly_rate = if hourly_rate_str.is_empty() {
None
} else {
match crate::validation::validate_amount_string(&hourly_rate_str) {
Ok(val) => Some(val),
Err(msg) => {
self.set_status_message(format!("Error: Hourly rate - {}", msg), None);
return;
}
}
};
if new_path_str.is_empty() {
self.set_status_message("Error: Path cannot be empty.", None);
return;
}
if new_database_path_str.is_empty() {
self.set_status_message("Error: Database path cannot be empty.", None);
return;
}
let new_path = PathBuf::from(&new_path_str);
let new_database_path = PathBuf::from(&new_database_path_str);
if !new_path.exists() {
if let Err(e) = save_transactions(&self.transactions, &new_path) {
self.set_status_message(
format!(
"Error creating transactions file '{}': {}. Check path and permissions.",
new_path.display(),
e
),
None,
);
return;
}
}
let seed_categories = if self.categories.is_empty() {
load_categories().unwrap_or_default()
} else {
self.categories.clone()
};
if let Err(e) = Self::prepare_category_database_for_path_change(
&self.database_path,
&new_database_path,
&seed_categories,
) {
self.set_status_message(
format!(
"Error preparing database '{}': {}. Check path and permissions.",
new_database_path.display(),
e
),
None,
);
return;
}
let settings = AppSettings {
data_file_path: Some(new_path_str.clone()),
database_path: Some(new_database_path_str.clone()),
target_budget,
hourly_rate,
show_hours: show_hours_val,
fuzzy_search_mode: fuzzy_search_val,
hide_help_bar: hide_help_bar_val,
};
if let Err(e) = save_settings(&settings) {
self.set_status_message(format!("Error saving config file: {}", e), None);
return;
}
self.data_file_path = new_path.clone();
self.database_path = new_database_path.clone();
let txs = match load_transactions(&self.data_file_path) {
Ok(tx) => tx,
Err(e) => {
self.set_status_message(
format!(
"Error loading transactions from '{}': {}. Check file format and permissions.",
self.data_file_path.display(),
e
),
None,
);
return;
}
};
self.transactions = txs;
if let Err(e) = self.reload_categories_from_store() {
self.set_status_message(
format!(
"Error loading categories from '{}': {}. Check database path and permissions.",
self.database_path.display(),
e
),
None,
);
return;
}
self.exit_settings_mode();
self.sort_transactions();
self.filtered_indices = (0..self.transactions.len()).collect();
if !self.filtered_indices.is_empty() {
self.table_state.select(Some(0));
} else {
self.table_state.select(None);
}
self.calculate_monthly_summaries();
self.calculate_category_summaries();
self.set_status_message(
format!(
"Settings saved. Data: {} | Database: {}",
self.data_file_path.display(),
self.database_path.display()
),
Some(Duration::seconds(3)),
);
self.target_budget = target_budget;
self.hourly_rate = hourly_rate;
self.show_hours = show_hours_val.unwrap_or(false);
self.fuzzy_search_mode = fuzzy_search_val.unwrap_or(false);
self.hide_help_bar = hide_help_bar_val.unwrap_or(false);
}
pub(crate) fn reset_settings_path_to_default(&mut self) {
match Self::get_default_data_file_path() {
Ok(default_path) => {
let path_str = default_path.to_string_lossy().to_string();
let clean_path = crate::validation::strip_path_quotes(&path_str);
if let Some(idx) = self
.settings_state
.items
.iter()
.position(|i| i.key == "data_file_path")
{
self.settings_state.items[idx].value = clean_path;
if self.settings_state.selected_index == idx {
self.settings_state.edit_cursor =
self.settings_state.items[idx].value.len();
}
}
self.set_status_message("Path reset to default. Press Enter to save.", None);
}
Err(e) => {
let fallback_path = "transactions.csv";
if let Some(idx) = self
.settings_state
.items
.iter()
.position(|i| i.key == "data_file_path")
{
self.settings_state.items[idx].value = fallback_path.to_string();
if self.settings_state.selected_index == idx {
self.settings_state.edit_cursor =
self.settings_state.items[idx].value.len();
}
}
self.set_status_message(
format!(
"Error getting default path ({}). Reset to local '{}'. Press Enter to save.",
e, fallback_path
),
None,
);
}
}
}
pub(crate) fn reset_settings_database_path_to_default(&mut self) {
let data_path_value = self
.settings_state
.get_value("data_file_path")
.map(|value| crate::validation::strip_path_quotes(value))
.filter(|value| !value.trim().is_empty());
let default_path = match data_path_value {
Some(path_str) => {
Self::default_database_path_for_data_path(PathBuf::from(path_str).as_path())
}
None => Self::get_default_database_file_path()
.unwrap_or_else(|_| PathBuf::from("budget.db")),
};
let clean_path =
crate::validation::strip_path_quotes(default_path.to_string_lossy().as_ref());
if let Some(idx) = self
.settings_state
.items
.iter()
.position(|i| i.key == "database_path")
{
self.settings_state.items[idx].value = clean_path;
if self.settings_state.selected_index == idx {
self.settings_state.edit_cursor = self.settings_state.items[idx].value.len();
}
}
self.set_status_message(
"Database path reset to default for the current data file. Press Enter to save.",
None,
);
}
pub(crate) fn activate_selected_setting(&mut self) {
let selected_key = self
.settings_state
.items
.get(self.settings_state.selected_index)
.map(|item| item.key.clone());
match selected_key.as_deref() {
Some("manage_categories") => self.open_category_catalog(),
_ => self.save_settings(),
}
}
fn ensure_setting_visibility<F>(
&mut self,
target_key: &str,
should_be_visible: bool,
insert_after_key: &str,
item_creator: F,
) where
F: FnOnce() -> crate::app::settings_types::SettingItem,
{
let target_idx = self
.settings_state
.items
.iter()
.position(|i| i.key == target_key);
if should_be_visible {
if target_idx.is_none() {
if let Some(after_idx) = self
.settings_state
.items
.iter()
.position(|i| i.key == insert_after_key)
{
let item = item_creator();
self.settings_state.items.insert(after_idx + 1, item);
if self.settings_state.selected_index > after_idx {
self.settings_state.selected_index += 1;
}
}
}
} else if let Some(idx) = target_idx {
self.settings_state.items.remove(idx);
if self.settings_state.selected_index >= idx {
self.settings_state.selected_index =
self.settings_state.selected_index.saturating_sub(1);
}
}
}
pub(crate) fn update_settings_visibility(&mut self) {
let hourly_rate_has_value = self
.settings_state
.get_value("hourly_rate")
.map(|v| !v.trim().is_empty())
.unwrap_or(false);
self.ensure_setting_visibility("show_hours", hourly_rate_has_value, "hourly_rate", || {
crate::app::settings_types::SettingItem {
key: "show_hours".to_string(),
label: "Show Costs in Hours".to_string(),
value: " No ▶".to_string(), setting_type: crate::app::settings_types::SettingType::Toggle,
help: "Toggle to display transaction amounts as hours worked.".to_string(),
}
});
}
}