use crate::error::EnvelopeResult;
use crate::models::BudgetPeriod;
use crate::reports::{
AccountRegisterReport, BudgetOverviewReport, NetWorthReport, RegisterFilter, SpendingReport,
};
use crate::services::AccountService;
use crate::storage::Storage;
use chrono::NaiveDate;
use clap::Subcommand;
use std::fs::File;
use std::io::BufWriter;
use std::path::PathBuf;
#[derive(Subcommand, Debug)]
pub enum ReportCommands {
#[command(alias = "budget-overview")]
Budget {
#[arg(short, long)]
period: Option<String>,
#[arg(short, long)]
output: Option<PathBuf>,
},
Spending {
#[arg(short, long)]
start: Option<String>,
#[arg(short, long)]
end: Option<String>,
#[arg(short, long)]
period: Option<String>,
#[arg(short, long)]
output: Option<PathBuf>,
#[arg(long)]
top: Option<usize>,
},
#[command(alias = "transactions")]
Register {
account: String,
#[arg(short, long)]
start: Option<String>,
#[arg(short, long)]
end: Option<String>,
#[arg(long)]
payee: Option<String>,
#[arg(long)]
uncategorized: bool,
#[arg(short, long)]
output: Option<PathBuf>,
},
#[command(alias = "networth")]
NetWorth {
#[arg(short, long)]
all: bool,
#[arg(short, long)]
output: Option<PathBuf>,
},
}
pub fn handle_report_command(storage: &Storage, cmd: ReportCommands) -> EnvelopeResult<()> {
match cmd {
ReportCommands::Budget { period, output } => handle_budget_report(storage, period, output),
ReportCommands::Spending {
start,
end,
period,
output,
top,
} => handle_spending_report(storage, start, end, period, output, top),
ReportCommands::Register {
account,
start,
end,
payee,
uncategorized,
output,
} => handle_register_report(storage, account, start, end, payee, uncategorized, output),
ReportCommands::NetWorth { all, output } => handle_net_worth_report(storage, all, output),
}
}
fn handle_budget_report(
storage: &Storage,
period: Option<String>,
output: Option<PathBuf>,
) -> EnvelopeResult<()> {
let budget_period = if let Some(period_str) = period {
BudgetPeriod::parse(&period_str).map_err(|e| {
crate::error::EnvelopeError::Validation(format!(
"Invalid period format: {}. Use YYYY-MM (e.g., 2025-01)",
e
))
})?
} else {
BudgetPeriod::current_month()
};
let report = BudgetOverviewReport::generate(storage, &budget_period)?;
if let Some(path) = output {
let file = File::create(&path).map_err(|e| {
crate::error::EnvelopeError::Export(format!(
"Failed to create file {}: {}",
path.display(),
e
))
})?;
let mut writer = BufWriter::new(file);
report.export_csv(&mut writer)?;
println!("Budget report exported to: {}", path.display());
} else {
println!("{}", report.format_terminal());
}
Ok(())
}
fn handle_spending_report(
storage: &Storage,
start: Option<String>,
end: Option<String>,
period: Option<String>,
output: Option<PathBuf>,
top: Option<usize>,
) -> EnvelopeResult<()> {
let (start_date, end_date) = if let Some(period_str) = period {
let budget_period = BudgetPeriod::parse(&period_str).map_err(|e| {
crate::error::EnvelopeError::Validation(format!(
"Invalid period format: {}. Use YYYY-MM (e.g., 2025-01)",
e
))
})?;
(budget_period.start_date(), budget_period.end_date())
} else {
let start_date = if let Some(s) = start {
NaiveDate::parse_from_str(&s, "%Y-%m-%d").map_err(|_| {
crate::error::EnvelopeError::Validation(format!(
"Invalid start date format: {}. Use YYYY-MM-DD",
s
))
})?
} else {
let today = chrono::Local::now().date_naive();
NaiveDate::from_ymd_opt(today.year(), today.month(), 1).unwrap_or(today)
};
let end_date = if let Some(e) = end {
NaiveDate::parse_from_str(&e, "%Y-%m-%d").map_err(|_| {
crate::error::EnvelopeError::Validation(format!(
"Invalid end date format: {}. Use YYYY-MM-DD",
e
))
})?
} else {
chrono::Local::now().date_naive()
};
(start_date, end_date)
};
let report = SpendingReport::generate(storage, start_date, end_date)?;
if let Some(path) = output {
let file = File::create(&path).map_err(|e| {
crate::error::EnvelopeError::Export(format!(
"Failed to create file {}: {}",
path.display(),
e
))
})?;
let mut writer = BufWriter::new(file);
report.export_csv(&mut writer)?;
println!("Spending report exported to: {}", path.display());
} else if let Some(n) = top {
println!(
"Top {} Spending Categories: {} to {}\n",
n, start_date, end_date
);
println!("{:<35} {:>12} {:>8}", "Category", "Amount", "%");
println!("{}", "-".repeat(60));
for cat in report.top_categories(n) {
println!(
"{:<35} {:>12} {:>7.1}%",
cat.category_name,
cat.total_spending.abs(),
cat.percentage
);
}
println!("\nTotal Spending: {}", report.total_spending.abs());
} else {
println!("{}", report.format_terminal());
}
Ok(())
}
fn handle_register_report(
storage: &Storage,
account: String,
start: Option<String>,
end: Option<String>,
payee: Option<String>,
uncategorized: bool,
output: Option<PathBuf>,
) -> EnvelopeResult<()> {
let account_service = AccountService::new(storage);
let account = account_service
.find(&account)?
.ok_or_else(|| crate::error::EnvelopeError::account_not_found(&account))?;
let filter = RegisterFilter {
start_date: start
.map(|s| {
NaiveDate::parse_from_str(&s, "%Y-%m-%d").map_err(|_| {
crate::error::EnvelopeError::Validation(format!(
"Invalid start date format: {}. Use YYYY-MM-DD",
s
))
})
})
.transpose()?,
end_date: end
.map(|s| {
NaiveDate::parse_from_str(&s, "%Y-%m-%d").map_err(|_| {
crate::error::EnvelopeError::Validation(format!(
"Invalid end date format: {}. Use YYYY-MM-DD",
s
))
})
})
.transpose()?,
payee_contains: payee,
uncategorized_only: uncategorized,
..Default::default()
};
let report = AccountRegisterReport::generate(storage, account.id, filter)?;
if let Some(path) = output {
let file = File::create(&path).map_err(|e| {
crate::error::EnvelopeError::Export(format!(
"Failed to create file {}: {}",
path.display(),
e
))
})?;
let mut writer = BufWriter::new(file);
report.export_csv(&mut writer)?;
println!("Register report exported to: {}", path.display());
} else {
println!("{}", report.format_terminal());
}
Ok(())
}
fn handle_net_worth_report(
storage: &Storage,
include_archived: bool,
output: Option<PathBuf>,
) -> EnvelopeResult<()> {
let report = NetWorthReport::generate(storage, include_archived)?;
if let Some(path) = output {
let file = File::create(&path).map_err(|e| {
crate::error::EnvelopeError::Export(format!(
"Failed to create file {}: {}",
path.display(),
e
))
})?;
let mut writer = BufWriter::new(file);
report.export_csv(&mut writer)?;
println!("Net worth report exported to: {}", path.display());
} else {
println!("{}", report.format_terminal());
}
Ok(())
}
use chrono::Datelike;