envelope_cli/setup/
wizard.rs1use std::io::{self, Write};
6
7use crate::config::{paths::EnvelopePaths, settings::Settings};
8use crate::error::{EnvelopeError, EnvelopeResult};
9use crate::models::{Account, Money, TransactionStatus};
10use crate::services::account::AccountService;
11use crate::services::transaction::{CreateTransactionInput, TransactionService};
12use crate::storage::Storage;
13
14use super::steps::{
15 account::AccountSetupStep,
16 categories::{CategoriesSetupStep, CategoryChoice},
17 period::PeriodSetupStep,
18};
19
20pub struct SetupResult {
22 pub completed: bool,
24 pub account: Option<Account>,
26 pub starting_balance: Money,
28}
29
30pub struct SetupWizard {
32 paths: EnvelopePaths,
33}
34
35impl SetupWizard {
36 pub fn new(paths: EnvelopePaths) -> Self {
38 Self { paths }
39 }
40
41 pub fn needs_setup(&self, settings: &Settings) -> bool {
43 !settings.setup_completed && !self.paths.settings_file().exists()
44 }
45
46 pub fn run(&self, storage: &Storage, settings: &mut Settings) -> EnvelopeResult<SetupResult> {
48 println!();
49 println!("===========================================");
50 println!(" Welcome to EnvelopeCLI Setup Wizard!");
51 println!("===========================================");
52 println!();
53 println!("This wizard will help you set up your budget.");
54 println!("Press Ctrl+C at any time to cancel.");
55 println!();
56
57 let confirm = prompt_string("Ready to begin? (yes/no) [yes]: ")?;
59 if !confirm.is_empty() && confirm.to_lowercase() != "yes" && confirm.to_lowercase() != "y" {
60 println!("Setup cancelled.");
61 return Ok(SetupResult {
62 completed: false,
63 account: None,
64 starting_balance: Money::zero(),
65 });
66 }
67
68 let account_result = AccountSetupStep::run()?;
70
71 let categories_result = CategoriesSetupStep::run()?;
73
74 let period_result = PeriodSetupStep::run()?;
76
77 println!();
79 println!("===========================================");
80 println!(" Setup Summary");
81 println!("===========================================");
82 println!();
83 println!(
84 "Account: {} ({})",
85 account_result.account.name, account_result.account.account_type
86 );
87 println!("Starting Balance: {}", account_result.starting_balance);
88 println!(
89 "Categories: {}",
90 match categories_result.choice {
91 CategoryChoice::UseDefaults => "Default categories",
92 CategoryChoice::Empty => "Empty (add your own)",
93 CategoryChoice::Customize => "Custom",
94 }
95 );
96 println!("Budget Period: {:?}", period_result.period_type);
97 println!();
98
99 let confirm = prompt_string("Apply these settings? (yes/no) [yes]: ")?;
100 if !confirm.is_empty() && confirm.to_lowercase() != "yes" && confirm.to_lowercase() != "y" {
101 println!("Setup cancelled.");
102 return Ok(SetupResult {
103 completed: false,
104 account: None,
105 starting_balance: Money::zero(),
106 });
107 }
108
109 println!();
111 println!("Applying settings...");
112
113 if categories_result.choice == CategoryChoice::UseDefaults {
115 crate::storage::init::initialize_storage(&self.paths)?;
116 }
117
118 let account_service = AccountService::new(storage);
120 let saved_account = account_service.create(
121 &account_result.account.name,
122 account_result.account.account_type,
123 account_result.starting_balance,
124 account_result.account.on_budget,
125 )?;
126
127 if !account_result.starting_balance.is_zero() {
129 let txn_service = TransactionService::new(storage);
130 let input = CreateTransactionInput {
131 account_id: saved_account.id,
132 date: chrono::Local::now().naive_local().date(),
133 amount: account_result.starting_balance,
134 payee_name: Some("Starting Balance".to_string()),
135 category_id: None,
136 memo: Some("Initial account balance".to_string()),
137 status: Some(TransactionStatus::Cleared),
138 };
139 txn_service.create(input)?;
140 }
141
142 settings.budget_period_type = period_result.period_type;
144 settings.setup_completed = true;
145 settings.save(&self.paths)?;
146
147 println!();
148 println!("Setup complete!");
149 println!();
150 println!("Your budget is ready. Here are some next steps:");
151 println!(" - Run 'envelope tui' to open the interactive interface");
152 println!(" - Run 'envelope budget assign' to allocate funds to categories");
153 println!(" - Run 'envelope transaction add' to record transactions");
154 println!();
155
156 Ok(SetupResult {
157 completed: true,
158 account: Some(saved_account),
159 starting_balance: account_result.starting_balance,
160 })
161 }
162
163 pub fn run_minimal(
165 &self,
166 _storage: &Storage,
167 settings: &mut Settings,
168 ) -> EnvelopeResult<SetupResult> {
169 println!("Initializing EnvelopeCLI...");
170
171 crate::storage::init::initialize_storage(&self.paths)?;
173
174 settings.setup_completed = true;
176 settings.save(&self.paths)?;
177
178 println!("Initialization complete!");
179
180 Ok(SetupResult {
181 completed: true,
182 account: None,
183 starting_balance: Money::zero(),
184 })
185 }
186}
187
188fn prompt_string(prompt: &str) -> EnvelopeResult<String> {
190 print!("{}", prompt);
191 io::stdout()
192 .flush()
193 .map_err(|e| EnvelopeError::Io(e.to_string()))?;
194
195 let mut input = String::new();
196 io::stdin()
197 .read_line(&mut input)
198 .map_err(|e| EnvelopeError::Io(e.to_string()))?;
199
200 Ok(input.trim().to_string())
201}