envelope_cli/display/
category.rs1use crate::models::{Category, CategoryGroup};
6use crate::services::category::CategoryGroupWithCategories;
7
8pub fn format_category_tree(groups_with_categories: &[CategoryGroupWithCategories]) -> String {
10 if groups_with_categories.is_empty() {
11 return "No categories found.\n\nRun 'envelope init' to create default categories."
12 .to_string();
13 }
14
15 let mut output = String::new();
16
17 for (i, gwc) in groups_with_categories.iter().enumerate() {
18 output.push_str(&format!("{}\n", gwc.group.name));
20
21 if gwc.categories.is_empty() {
23 output.push_str(" (no categories)\n");
24 } else {
25 for (j, category) in gwc.categories.iter().enumerate() {
26 let is_last = j == gwc.categories.len() - 1;
27 let prefix = if is_last { "└── " } else { "├── " };
28
29 let goal_str = if let Some(goal) = category.goal_amount {
30 format!(" (goal: {})", crate::models::Money::from_cents(goal))
31 } else {
32 String::new()
33 };
34
35 output.push_str(&format!(" {}{}{}\n", prefix, category.name, goal_str));
36 }
37 }
38
39 if i < groups_with_categories.len() - 1 {
41 output.push('\n');
42 }
43 }
44
45 output
46}
47
48pub fn format_group_list(groups: &[CategoryGroup]) -> String {
50 if groups.is_empty() {
51 return "No category groups found.".to_string();
52 }
53
54 let mut output = String::new();
55 output.push_str("Category Groups:\n");
56
57 for group in groups {
58 let hidden = if group.hidden { " (hidden)" } else { "" };
59 output.push_str(&format!(
60 " {} - order: {}{}\n",
61 group.name, group.sort_order, hidden
62 ));
63 }
64
65 output
66}
67
68pub fn format_category_list(categories: &[Category]) -> String {
70 if categories.is_empty() {
71 return "No categories found.".to_string();
72 }
73
74 let name_width = categories
75 .iter()
76 .map(|c| c.name.len())
77 .max()
78 .unwrap_or(4)
79 .max(4);
80
81 let mut output = String::new();
82 output.push_str(&format!(
83 "{:<width$} {:>10} {}\n",
84 "Category",
85 "Goal",
86 "ID",
87 width = name_width
88 ));
89 output.push_str(&format!(
90 "{:-<width$} {:->10} {:-<12}\n",
91 "",
92 "",
93 "",
94 width = name_width
95 ));
96
97 for category in categories {
98 let goal_str = category
99 .goal_amount
100 .map(|g| crate::models::Money::from_cents(g).to_string())
101 .unwrap_or_else(|| "-".to_string());
102
103 output.push_str(&format!(
104 "{:<width$} {:>10} {}\n",
105 category.name,
106 goal_str,
107 category.id,
108 width = name_width
109 ));
110 }
111
112 output
113}
114
115pub fn format_category_details(category: &Category, group: Option<&CategoryGroup>) -> String {
117 let mut output = String::new();
118
119 output.push_str(&format!("Category: {}\n", category.name));
120 output.push_str(&format!(" ID: {}\n", category.id));
121
122 if let Some(g) = group {
123 output.push_str(&format!(" Group: {}\n", g.name));
124 }
125
126 output.push_str(&format!(
127 " Hidden: {}\n",
128 if category.hidden { "Yes" } else { "No" }
129 ));
130 output.push_str(&format!(" Sort Order: {}\n", category.sort_order));
131
132 if let Some(goal) = category.goal_amount {
133 output.push_str(&format!(
134 " Goal: {}\n",
135 crate::models::Money::from_cents(goal)
136 ));
137 }
138
139 if !category.notes.is_empty() {
140 output.push_str(&format!(" Notes: {}\n", category.notes));
141 }
142
143 output.push('\n');
144 output.push_str(&format!(
145 " Created: {}\n",
146 category.created_at.format("%Y-%m-%d %H:%M UTC")
147 ));
148 output.push_str(&format!(
149 " Modified: {}\n",
150 category.updated_at.format("%Y-%m-%d %H:%M UTC")
151 ));
152
153 output
154}
155
156pub fn format_group_details(group: &CategoryGroup, categories: &[Category]) -> String {
158 let mut output = String::new();
159
160 output.push_str(&format!("Category Group: {}\n", group.name));
161 output.push_str(&format!(" ID: {}\n", group.id));
162 output.push_str(&format!(
163 " Hidden: {}\n",
164 if group.hidden { "Yes" } else { "No" }
165 ));
166 output.push_str(&format!(" Sort Order: {}\n", group.sort_order));
167 output.push_str(&format!(" Categories: {}\n", categories.len()));
168
169 if !categories.is_empty() {
170 output.push_str("\n Categories in this group:\n");
171 for category in categories {
172 output.push_str(&format!(" - {}\n", category.name));
173 }
174 }
175
176 output.push('\n');
177 output.push_str(&format!(
178 " Created: {}\n",
179 group.created_at.format("%Y-%m-%d %H:%M UTC")
180 ));
181 output.push_str(&format!(
182 " Modified: {}\n",
183 group.updated_at.format("%Y-%m-%d %H:%M UTC")
184 ));
185
186 output
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn test_format_empty_tree() {
195 let output = format_category_tree(&[]);
196 assert!(output.contains("No categories found"));
197 }
198
199 #[test]
200 fn test_format_category_tree() {
201 let group = CategoryGroup::new("Bills");
202 let cat1 = Category::new("Rent", group.id);
203 let cat2 = Category::new("Electric", group.id);
204
205 let gwc = CategoryGroupWithCategories {
206 group,
207 categories: vec![cat1, cat2],
208 };
209
210 let output = format_category_tree(&[gwc]);
211 assert!(output.contains("Bills"));
212 assert!(output.contains("Rent"));
213 assert!(output.contains("Electric"));
214 assert!(output.contains("├──"));
215 assert!(output.contains("└──"));
216 }
217
218 #[test]
219 fn test_format_category_with_goal() {
220 let group = CategoryGroup::new("Savings");
221 let mut cat = Category::new("Emergency Fund", group.id);
222 cat.set_goal(100000); let gwc = CategoryGroupWithCategories {
225 group,
226 categories: vec![cat],
227 };
228
229 let output = format_category_tree(&[gwc]);
230 assert!(output.contains("Emergency Fund"));
231 assert!(output.contains("goal:"));
232 assert!(output.contains("$1000.00"));
233 }
234}