envelope_cli/cli/
payee.rs1use clap::Subcommand;
6
7use crate::error::{EnvelopeError, EnvelopeResult};
8use crate::services::{CategoryService, PayeeService};
9use crate::storage::Storage;
10
11#[derive(Subcommand)]
13pub enum PayeeCommands {
14 List {
16 #[arg(short, long)]
18 search: Option<String>,
19 },
20 Show {
22 payee: String,
24 },
25 SetCategory {
27 payee: String,
29 category: String,
31 },
32 ClearCategory {
34 payee: String,
36 },
37 Delete {
39 payee: String,
41 #[arg(short, long)]
43 force: bool,
44 },
45 Rename {
47 payee: String,
49 name: String,
51 },
52}
53
54pub fn handle_payee_command(storage: &Storage, cmd: PayeeCommands) -> EnvelopeResult<()> {
56 let service = PayeeService::new(storage);
57 let category_service = CategoryService::new(storage);
58
59 match cmd {
60 PayeeCommands::List { search } => {
61 let payees = if let Some(query) = search {
62 service.search(&query, 50)?
63 } else {
64 service.list()?
65 };
66
67 if payees.is_empty() {
68 println!("No payees found.");
69 return Ok(());
70 }
71
72 println!("{:30} {:20} {:10}", "Name", "Default Category", "Usage");
73 println!("{}", "-".repeat(62));
74
75 for payee in &payees {
76 let cat_name = if let Some(cat_id) = payee.default_category_id {
77 category_service
78 .get_category(cat_id)?
79 .map(|c| c.name)
80 .unwrap_or_else(|| "(deleted)".to_string())
81 } else {
82 "(auto)".to_string()
83 };
84
85 let usage: u32 = payee.category_frequency.values().sum();
86 let manual_indicator = if payee.manual { "*" } else { "" };
87
88 println!(
89 "{:30} {:20} {:>10}{}",
90 truncate(&payee.name, 30),
91 truncate(&cat_name, 20),
92 usage,
93 manual_indicator
94 );
95 }
96
97 println!("\nTotal: {} payees", payees.len());
98 println!("* = manually configured default category");
99 }
100
101 PayeeCommands::Show { payee } => {
102 let p = service
103 .find(&payee)?
104 .ok_or_else(|| EnvelopeError::payee_not_found(&payee))?;
105
106 println!("Payee: {}", p.name);
107 println!("ID: {}", p.id);
108
109 if let Some(cat_id) = p.default_category_id {
110 if let Some(cat) = category_service.get_category(cat_id)? {
111 let source = if p.manual { "manual" } else { "learned" };
112 println!("Default Category: {} ({})", cat.name, source);
113 }
114 } else if let Some(suggested) = p.suggested_category() {
115 if let Some(cat) = category_service.get_category(suggested)? {
116 println!("Suggested Category: {} (learned)", cat.name);
117 }
118 } else {
119 println!("Default Category: (none)");
120 }
121
122 if !p.category_frequency.is_empty() {
123 println!("\nCategory Usage:");
124 let mut freq: Vec<_> = p.category_frequency.iter().collect();
125 freq.sort_by(|a, b| b.1.cmp(a.1));
126
127 for (cat_id, count) in freq.iter().take(5) {
128 if let Some(cat) = category_service.get_category(**cat_id)? {
129 println!(" {:20} {:>5} times", cat.name, count);
130 }
131 }
132 }
133
134 println!("\nCreated: {}", p.created_at.format("%Y-%m-%d %H:%M"));
135 println!("Updated: {}", p.updated_at.format("%Y-%m-%d %H:%M"));
136 }
137
138 PayeeCommands::SetCategory { payee, category } => {
139 let p = service
140 .find(&payee)?
141 .ok_or_else(|| EnvelopeError::payee_not_found(&payee))?;
142
143 let cat = category_service
144 .find_category(&category)?
145 .ok_or_else(|| EnvelopeError::category_not_found(&category))?;
146
147 let updated = service.set_default_category(p.id, cat.id)?;
148 println!(
149 "Set default category for '{}' to '{}'",
150 updated.name, cat.name
151 );
152 }
153
154 PayeeCommands::ClearCategory { payee } => {
155 let p = service
156 .find(&payee)?
157 .ok_or_else(|| EnvelopeError::payee_not_found(&payee))?;
158
159 let updated = service.clear_default_category(p.id)?;
160 println!(
161 "Cleared default category for '{}' (will use learned suggestions)",
162 updated.name
163 );
164 }
165
166 PayeeCommands::Delete { payee, force } => {
167 let p = service
168 .find(&payee)?
169 .ok_or_else(|| EnvelopeError::payee_not_found(&payee))?;
170
171 if !force {
172 println!("About to delete payee: {}", p.name);
173 println!("Use --force to confirm deletion");
174 return Ok(());
175 }
176
177 let deleted = service.delete(p.id)?;
178 println!("Deleted payee: {}", deleted.name);
179 }
180
181 PayeeCommands::Rename { payee, name } => {
182 let p = service
183 .find(&payee)?
184 .ok_or_else(|| EnvelopeError::payee_not_found(&payee))?;
185
186 let old_name = p.name.clone();
187 let renamed = service.rename(p.id, &name)?;
188 println!("Renamed payee: '{}' -> '{}'", old_name, renamed.name);
189 }
190 }
191
192 Ok(())
193}
194
195fn truncate(s: &str, max_len: usize) -> String {
197 if s.len() <= max_len {
198 s.to_string()
199 } else {
200 format!("{}...", &s[..max_len - 3])
201 }
202}