envelope_cli/cli/
category.rs

1//! Category CLI commands
2//!
3//! Implements CLI commands for category and category group management.
4
5use clap::Subcommand;
6
7use crate::display::category::{
8    format_category_details, format_category_tree, format_group_details, format_group_list,
9};
10use crate::error::{EnvelopeError, EnvelopeResult};
11use crate::services::CategoryService;
12use crate::storage::Storage;
13
14/// Category subcommands
15#[derive(Subcommand)]
16pub enum CategoryCommands {
17    /// List all categories (organized by group)
18    List,
19
20    /// Create a new category
21    Create {
22        /// Category name
23        name: String,
24        /// Category group name or ID
25        #[arg(short, long)]
26        group: String,
27        /// Goal amount (e.g., "500" or "500.00")
28        #[arg(long)]
29        goal: Option<String>,
30    },
31
32    /// Show category details
33    Show {
34        /// Category name or ID
35        category: String,
36    },
37
38    /// Edit a category
39    Edit {
40        /// Category name or ID
41        category: String,
42        /// New name
43        #[arg(short, long)]
44        name: Option<String>,
45        /// New goal amount
46        #[arg(long)]
47        goal: Option<String>,
48        /// Clear the goal
49        #[arg(long)]
50        clear_goal: bool,
51    },
52
53    /// Move a category to a different group
54    Move {
55        /// Category name or ID
56        category: String,
57        /// Target group name or ID
58        #[arg(short, long)]
59        to: String,
60    },
61
62    /// Delete a category
63    Delete {
64        /// Category name or ID
65        category: String,
66    },
67
68    /// Create a new category group
69    #[command(name = "create-group")]
70    CreateGroup {
71        /// Group name
72        name: String,
73    },
74
75    /// List all category groups
76    #[command(name = "list-groups")]
77    ListGroups,
78
79    /// Show group details
80    #[command(name = "show-group")]
81    ShowGroup {
82        /// Group name or ID
83        group: String,
84    },
85
86    /// Edit a category group
87    #[command(name = "edit-group")]
88    EditGroup {
89        /// Group name or ID
90        group: String,
91        /// New name
92        #[arg(short, long)]
93        name: Option<String>,
94    },
95
96    /// Delete a category group
97    #[command(name = "delete-group")]
98    DeleteGroup {
99        /// Group name or ID
100        group: String,
101        /// Force delete (also deletes all categories in the group)
102        #[arg(long)]
103        force: bool,
104    },
105}
106
107/// Handle a category command
108pub fn handle_category_command(storage: &Storage, cmd: CategoryCommands) -> EnvelopeResult<()> {
109    let service = CategoryService::new(storage);
110
111    match cmd {
112        CategoryCommands::List => {
113            let groups = service.list_groups_with_categories()?;
114            print!("{}", format_category_tree(&groups));
115        }
116
117        CategoryCommands::Create { name, group, goal } => {
118            let group = service
119                .find_group(&group)?
120                .ok_or_else(|| EnvelopeError::NotFound {
121                    entity_type: "Category Group",
122                    identifier: group.clone(),
123                })?;
124
125            let category = service.create_category(&name, group.id)?;
126
127            // Set goal if provided
128            if let Some(goal_str) = goal {
129                let goal_money = crate::models::Money::parse(&goal_str).map_err(|e| {
130                    EnvelopeError::Validation(format!("Invalid goal amount: {}", e))
131                })?;
132                service.update_category(category.id, None, Some(goal_money.cents()), false)?;
133            }
134
135            println!("Created category: {}", category.name);
136            println!("  Group: {}", group.name);
137            println!("  ID: {}", category.id);
138        }
139
140        CategoryCommands::Show { category } => {
141            let cat = service
142                .find_category(&category)?
143                .ok_or_else(|| EnvelopeError::category_not_found(&category))?;
144
145            let group = service.get_group(cat.group_id)?;
146            print!("{}", format_category_details(&cat, group.as_ref()));
147        }
148
149        CategoryCommands::Edit {
150            category,
151            name,
152            goal,
153            clear_goal,
154        } => {
155            let cat = service
156                .find_category(&category)?
157                .ok_or_else(|| EnvelopeError::category_not_found(&category))?;
158
159            if name.is_none() && goal.is_none() && !clear_goal {
160                println!("No changes specified. Use --name, --goal, or --clear-goal.");
161                return Ok(());
162            }
163
164            let goal_cents = if let Some(goal_str) = goal {
165                let goal_money = crate::models::Money::parse(&goal_str).map_err(|e| {
166                    EnvelopeError::Validation(format!("Invalid goal amount: {}", e))
167                })?;
168                Some(goal_money.cents())
169            } else {
170                None
171            };
172
173            let updated =
174                service.update_category(cat.id, name.as_deref(), goal_cents, clear_goal)?;
175            println!("Updated category: {}", updated.name);
176        }
177
178        CategoryCommands::Move { category, to } => {
179            let cat = service
180                .find_category(&category)?
181                .ok_or_else(|| EnvelopeError::category_not_found(&category))?;
182
183            let target_group = service
184                .find_group(&to)?
185                .ok_or_else(|| EnvelopeError::NotFound {
186                    entity_type: "Category Group",
187                    identifier: to.clone(),
188                })?;
189
190            let moved = service.move_category(cat.id, target_group.id)?;
191            println!("Moved '{}' to group '{}'", moved.name, target_group.name);
192        }
193
194        CategoryCommands::Delete { category } => {
195            let cat = service
196                .find_category(&category)?
197                .ok_or_else(|| EnvelopeError::category_not_found(&category))?;
198
199            service.delete_category(cat.id)?;
200            println!("Deleted category: {}", cat.name);
201        }
202
203        CategoryCommands::CreateGroup { name } => {
204            let group = service.create_group(&name)?;
205            println!("Created category group: {}", group.name);
206            println!("  ID: {}", group.id);
207        }
208
209        CategoryCommands::ListGroups => {
210            let groups = service.list_groups()?;
211            print!("{}", format_group_list(&groups));
212        }
213
214        CategoryCommands::ShowGroup { group } => {
215            let g = service
216                .find_group(&group)?
217                .ok_or_else(|| EnvelopeError::NotFound {
218                    entity_type: "Category Group",
219                    identifier: group.clone(),
220                })?;
221
222            let categories = service.list_categories_in_group(g.id)?;
223            print!("{}", format_group_details(&g, &categories));
224        }
225
226        CategoryCommands::EditGroup { group, name } => {
227            let g = service
228                .find_group(&group)?
229                .ok_or_else(|| EnvelopeError::NotFound {
230                    entity_type: "Category Group",
231                    identifier: group.clone(),
232                })?;
233
234            if name.is_none() {
235                println!("No changes specified. Use --name to change the group name.");
236                return Ok(());
237            }
238
239            let updated = service.update_group(g.id, name.as_deref())?;
240            println!("Updated category group: {}", updated.name);
241        }
242
243        CategoryCommands::DeleteGroup { group, force } => {
244            let g = service
245                .find_group(&group)?
246                .ok_or_else(|| EnvelopeError::NotFound {
247                    entity_type: "Category Group",
248                    identifier: group.clone(),
249                })?;
250
251            service.delete_group(g.id, force)?;
252            println!("Deleted category group: {}", g.name);
253        }
254    }
255
256    Ok(())
257}