1use crate::error::EnvelopeResult;
6use crate::models::BudgetPeriod;
7use crate::reports::{
8 AccountRegisterReport, BudgetOverviewReport, NetWorthReport, RegisterFilter, SpendingReport,
9};
10use crate::services::AccountService;
11use crate::storage::Storage;
12use chrono::NaiveDate;
13use clap::Subcommand;
14use std::fs::File;
15use std::io::BufWriter;
16use std::path::PathBuf;
17
18#[derive(Subcommand, Debug)]
20pub enum ReportCommands {
21 #[command(alias = "budget-overview")]
23 Budget {
24 #[arg(short, long)]
26 period: Option<String>,
27
28 #[arg(short, long)]
30 output: Option<PathBuf>,
31 },
32
33 Spending {
35 #[arg(short, long)]
37 start: Option<String>,
38
39 #[arg(short, long)]
41 end: Option<String>,
42
43 #[arg(short, long)]
45 period: Option<String>,
46
47 #[arg(short, long)]
49 output: Option<PathBuf>,
50
51 #[arg(long)]
53 top: Option<usize>,
54 },
55
56 #[command(alias = "transactions")]
58 Register {
59 account: String,
61
62 #[arg(short, long)]
64 start: Option<String>,
65
66 #[arg(short, long)]
68 end: Option<String>,
69
70 #[arg(long)]
72 payee: Option<String>,
73
74 #[arg(long)]
76 uncategorized: bool,
77
78 #[arg(short, long)]
80 output: Option<PathBuf>,
81 },
82
83 #[command(alias = "networth")]
85 NetWorth {
86 #[arg(short, long)]
88 all: bool,
89
90 #[arg(short, long)]
92 output: Option<PathBuf>,
93 },
94}
95
96pub fn handle_report_command(storage: &Storage, cmd: ReportCommands) -> EnvelopeResult<()> {
98 match cmd {
99 ReportCommands::Budget { period, output } => handle_budget_report(storage, period, output),
100 ReportCommands::Spending {
101 start,
102 end,
103 period,
104 output,
105 top,
106 } => handle_spending_report(storage, start, end, period, output, top),
107 ReportCommands::Register {
108 account,
109 start,
110 end,
111 payee,
112 uncategorized,
113 output,
114 } => handle_register_report(storage, account, start, end, payee, uncategorized, output),
115 ReportCommands::NetWorth { all, output } => handle_net_worth_report(storage, all, output),
116 }
117}
118
119fn handle_budget_report(
121 storage: &Storage,
122 period: Option<String>,
123 output: Option<PathBuf>,
124) -> EnvelopeResult<()> {
125 let budget_period = if let Some(period_str) = period {
127 BudgetPeriod::parse(&period_str).map_err(|e| {
128 crate::error::EnvelopeError::Validation(format!(
129 "Invalid period format: {}. Use YYYY-MM (e.g., 2025-01)",
130 e
131 ))
132 })?
133 } else {
134 BudgetPeriod::current_month()
135 };
136
137 let report = BudgetOverviewReport::generate(storage, &budget_period)?;
139
140 if let Some(path) = output {
142 let file = File::create(&path).map_err(|e| {
143 crate::error::EnvelopeError::Export(format!(
144 "Failed to create file {}: {}",
145 path.display(),
146 e
147 ))
148 })?;
149 let mut writer = BufWriter::new(file);
150 report.export_csv(&mut writer)?;
151 println!("Budget report exported to: {}", path.display());
152 } else {
153 println!("{}", report.format_terminal());
154 }
155
156 Ok(())
157}
158
159fn handle_spending_report(
161 storage: &Storage,
162 start: Option<String>,
163 end: Option<String>,
164 period: Option<String>,
165 output: Option<PathBuf>,
166 top: Option<usize>,
167) -> EnvelopeResult<()> {
168 let (start_date, end_date) = if let Some(period_str) = period {
170 let budget_period = BudgetPeriod::parse(&period_str).map_err(|e| {
171 crate::error::EnvelopeError::Validation(format!(
172 "Invalid period format: {}. Use YYYY-MM (e.g., 2025-01)",
173 e
174 ))
175 })?;
176 (budget_period.start_date(), budget_period.end_date())
177 } else {
178 let start_date = if let Some(s) = start {
179 NaiveDate::parse_from_str(&s, "%Y-%m-%d").map_err(|_| {
180 crate::error::EnvelopeError::Validation(format!(
181 "Invalid start date format: {}. Use YYYY-MM-DD",
182 s
183 ))
184 })?
185 } else {
186 let today = chrono::Local::now().date_naive();
188 NaiveDate::from_ymd_opt(today.year(), today.month(), 1).unwrap_or(today)
189 };
190
191 let end_date = if let Some(e) = end {
192 NaiveDate::parse_from_str(&e, "%Y-%m-%d").map_err(|_| {
193 crate::error::EnvelopeError::Validation(format!(
194 "Invalid end date format: {}. Use YYYY-MM-DD",
195 e
196 ))
197 })?
198 } else {
199 chrono::Local::now().date_naive()
201 };
202
203 (start_date, end_date)
204 };
205
206 let report = SpendingReport::generate(storage, start_date, end_date)?;
208
209 if let Some(path) = output {
211 let file = File::create(&path).map_err(|e| {
212 crate::error::EnvelopeError::Export(format!(
213 "Failed to create file {}: {}",
214 path.display(),
215 e
216 ))
217 })?;
218 let mut writer = BufWriter::new(file);
219 report.export_csv(&mut writer)?;
220 println!("Spending report exported to: {}", path.display());
221 } else if let Some(n) = top {
222 println!(
224 "Top {} Spending Categories: {} to {}\n",
225 n, start_date, end_date
226 );
227 println!("{:<35} {:>12} {:>8}", "Category", "Amount", "%");
228 println!("{}", "-".repeat(60));
229
230 for cat in report.top_categories(n) {
231 println!(
232 "{:<35} {:>12} {:>7.1}%",
233 cat.category_name,
234 cat.total_spending.abs(),
235 cat.percentage
236 );
237 }
238 println!("\nTotal Spending: {}", report.total_spending.abs());
239 } else {
240 println!("{}", report.format_terminal());
241 }
242
243 Ok(())
244}
245
246fn handle_register_report(
248 storage: &Storage,
249 account: String,
250 start: Option<String>,
251 end: Option<String>,
252 payee: Option<String>,
253 uncategorized: bool,
254 output: Option<PathBuf>,
255) -> EnvelopeResult<()> {
256 let account_service = AccountService::new(storage);
257
258 let account = account_service
260 .find(&account)?
261 .ok_or_else(|| crate::error::EnvelopeError::account_not_found(&account))?;
262
263 let filter = RegisterFilter {
265 start_date: start
266 .map(|s| {
267 NaiveDate::parse_from_str(&s, "%Y-%m-%d").map_err(|_| {
268 crate::error::EnvelopeError::Validation(format!(
269 "Invalid start date format: {}. Use YYYY-MM-DD",
270 s
271 ))
272 })
273 })
274 .transpose()?,
275 end_date: end
276 .map(|s| {
277 NaiveDate::parse_from_str(&s, "%Y-%m-%d").map_err(|_| {
278 crate::error::EnvelopeError::Validation(format!(
279 "Invalid end date format: {}. Use YYYY-MM-DD",
280 s
281 ))
282 })
283 })
284 .transpose()?,
285 payee_contains: payee,
286 uncategorized_only: uncategorized,
287 ..Default::default()
288 };
289
290 let report = AccountRegisterReport::generate(storage, account.id, filter)?;
292
293 if let Some(path) = output {
295 let file = File::create(&path).map_err(|e| {
296 crate::error::EnvelopeError::Export(format!(
297 "Failed to create file {}: {}",
298 path.display(),
299 e
300 ))
301 })?;
302 let mut writer = BufWriter::new(file);
303 report.export_csv(&mut writer)?;
304 println!("Register report exported to: {}", path.display());
305 } else {
306 println!("{}", report.format_terminal());
307 }
308
309 Ok(())
310}
311
312fn handle_net_worth_report(
314 storage: &Storage,
315 include_archived: bool,
316 output: Option<PathBuf>,
317) -> EnvelopeResult<()> {
318 let report = NetWorthReport::generate(storage, include_archived)?;
320
321 if let Some(path) = output {
323 let file = File::create(&path).map_err(|e| {
324 crate::error::EnvelopeError::Export(format!(
325 "Failed to create file {}: {}",
326 path.display(),
327 e
328 ))
329 })?;
330 let mut writer = BufWriter::new(file);
331 report.export_csv(&mut writer)?;
332 println!("Net worth report exported to: {}", path.display());
333 } else {
334 println!("{}", report.format_terminal());
335 }
336
337 Ok(())
338}
339
340use chrono::Datelike;