envelope_cli/display/
account.rs1use crate::models::Account;
6use crate::services::account::AccountSummary;
7use tabled::{
8 settings::{object::Columns, Alignment, Modify, Style},
9 Table, Tabled,
10};
11
12#[derive(Tabled)]
14struct AccountRow {
15 #[tabled(rename = "Name")]
16 name: String,
17 #[tabled(rename = "Type")]
18 account_type: String,
19 #[tabled(rename = "Balance")]
20 balance: String,
21 #[tabled(rename = "Cleared")]
22 cleared: String,
23 #[tabled(rename = "Status")]
24 status: String,
25}
26
27pub fn format_account_list(summaries: &[AccountSummary], pretty: bool) -> String {
32 if summaries.is_empty() {
33 return "No accounts found.".to_string();
34 }
35
36 if pretty {
37 format_account_list_pretty(summaries)
38 } else {
39 format_account_list_plain(summaries)
40 }
41}
42
43fn format_account_list_pretty(summaries: &[AccountSummary]) -> String {
45 let mut rows: Vec<AccountRow> = summaries
46 .iter()
47 .map(|summary| {
48 let status = get_account_status(summary);
49 AccountRow {
50 name: summary.account.name.clone(),
51 account_type: summary.account.account_type.to_string(),
52 balance: summary.balance.to_string(),
53 cleared: summary.cleared_balance.to_string(),
54 status,
55 }
56 })
57 .collect();
58
59 let total_balance: crate::models::Money = summaries.iter().map(|s| s.balance).sum();
61 let total_cleared: crate::models::Money = summaries.iter().map(|s| s.cleared_balance).sum();
62
63 rows.push(AccountRow {
64 name: "TOTAL".to_string(),
65 account_type: String::new(),
66 balance: total_balance.to_string(),
67 cleared: total_cleared.to_string(),
68 status: String::new(),
69 });
70
71 Table::new(&rows)
72 .with(Style::rounded())
73 .with(Modify::new(Columns::new(2..=3)).with(Alignment::right()))
74 .to_string()
75}
76
77fn format_account_list_plain(summaries: &[AccountSummary]) -> String {
80 let mut output = String::new();
81
82 output.push_str("Accounts\n");
84 output.push_str(&"=".repeat(80));
85 output.push('\n');
86
87 output.push_str(&format!(
90 "{:<26} {:>14} {:>12} {:>12} {:>12}\n",
91 "Name", "Type", "Balance", "Cleared", "Status"
92 ));
93 output.push_str(&"-".repeat(80));
94 output.push('\n');
95
96 for summary in summaries {
100 let status = get_account_status(summary);
101 output.push_str(&format!(
102 "{:<26} {:>14} {:>12} {:>12} {:>12}\n",
103 truncate_str(&summary.account.name, 26),
104 summary.account.account_type.to_string(),
105 summary.balance,
106 summary.cleared_balance,
107 status,
108 ));
109 }
110
111 let total_balance: crate::models::Money = summaries.iter().map(|s| s.balance).sum();
113 let total_cleared: crate::models::Money = summaries.iter().map(|s| s.cleared_balance).sum();
114
115 output.push('\n');
116 output.push_str(&"=".repeat(80));
117 output.push('\n');
118 output.push_str(&format!(
119 "{:<26} {:>14} {:>12} {:>12}\n",
120 "TOTALS:", "", total_balance, total_cleared
121 ));
122
123 output
124}
125
126fn truncate_str(s: &str, max_len: usize) -> String {
128 if s.len() <= max_len {
129 s.to_string()
130 } else if max_len > 3 {
131 format!("{}...", &s[..max_len - 3])
132 } else {
133 s[..max_len].to_string()
134 }
135}
136
137fn get_account_status(summary: &AccountSummary) -> String {
139 if summary.account.archived {
140 "Archived".to_string()
141 } else if !summary.account.on_budget {
142 "Off-Budget".to_string()
143 } else if summary.uncleared_count > 0 {
144 format!("{} pending", summary.uncleared_count)
145 } else {
146 "Active".to_string()
147 }
148}
149
150pub fn format_account_details(summary: &AccountSummary) -> String {
152 let account = &summary.account;
153
154 let mut output = String::new();
155
156 output.push_str(&format!("Account: {}\n", account.name));
157 output.push_str(&format!(" Type: {}\n", account.account_type));
158 output.push_str(&format!(" ID: {}\n", account.id));
159 output.push_str(&format!(
160 " On Budget: {}\n",
161 if account.on_budget { "Yes" } else { "No" }
162 ));
163 output.push_str(&format!(
164 " Archived: {}\n",
165 if account.archived { "Yes" } else { "No" }
166 ));
167 output.push('\n');
168 output.push_str(&format!(
169 " Starting Balance: {}\n",
170 account.starting_balance
171 ));
172 output.push_str(&format!(" Current Balance: {}\n", summary.balance));
173 output.push_str(&format!(
174 " Cleared Balance: {}\n",
175 summary.cleared_balance
176 ));
177 output.push_str(&format!(
178 " Uncleared Count: {}\n",
179 summary.uncleared_count
180 ));
181
182 if let Some(date) = account.last_reconciled_date {
183 output.push('\n');
184 output.push_str(&format!(" Last Reconciled: {}\n", date));
185 if let Some(balance) = account.last_reconciled_balance {
186 output.push_str(&format!(" Reconciled Balance: {}\n", balance));
187 }
188 }
189
190 if !account.notes.is_empty() {
191 output.push('\n');
192 output.push_str(&format!(" Notes: {}\n", account.notes));
193 }
194
195 output.push('\n');
196 output.push_str(&format!(
197 " Created: {}\n",
198 account.created_at.format("%Y-%m-%d %H:%M UTC")
199 ));
200 output.push_str(&format!(
201 " Modified: {}\n",
202 account.updated_at.format("%Y-%m-%d %H:%M UTC")
203 ));
204
205 output
206}
207
208pub fn format_account_list_simple(accounts: &[Account]) -> String {
210 if accounts.is_empty() {
211 return "No accounts found.".to_string();
212 }
213
214 let mut output = String::new();
215 for account in accounts {
216 let status = if account.archived { " (archived)" } else { "" };
217 output.push_str(&format!(
218 " {} - {}{}\n",
219 account.name, account.account_type, status
220 ));
221 }
222 output
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228 use crate::models::{AccountType, Money};
229
230 fn create_test_summary(name: &str, balance: i64, cleared: i64) -> AccountSummary {
231 let account =
232 Account::with_starting_balance(name, AccountType::Checking, Money::from_cents(0));
233 AccountSummary {
234 account,
235 balance: Money::from_cents(balance),
236 cleared_balance: Money::from_cents(cleared),
237 uncleared_count: if balance != cleared { 1 } else { 0 },
238 }
239 }
240
241 #[test]
242 fn test_format_account_list_plain() {
243 let summaries = vec![
244 create_test_summary("Checking", 100000, 95000),
245 create_test_summary("Savings", 500000, 500000),
246 ];
247
248 let output = format_account_list(&summaries, false);
249 assert!(output.contains("Accounts"));
250 assert!(output.contains("====")); assert!(output.contains("----")); assert!(output.contains("Checking"));
253 assert!(output.contains("Savings"));
254 assert!(output.contains("TOTALS:")); }
256
257 #[test]
258 fn test_format_account_list_pretty() {
259 let summaries = vec![
260 create_test_summary("Checking", 100000, 95000),
261 create_test_summary("Savings", 500000, 500000),
262 ];
263
264 let output = format_account_list(&summaries, true);
265 assert!(output.contains("Checking"));
266 assert!(output.contains("Savings"));
267 assert!(output.contains("TOTAL"));
268 assert!(output.contains("│")); }
270
271 #[test]
272 fn test_format_empty_list() {
273 let output = format_account_list(&[], false);
274 assert!(output.contains("No accounts found"));
275 }
276
277 #[test]
278 fn test_format_account_details() {
279 let summary = create_test_summary("My Account", 100000, 90000);
280 let output = format_account_details(&summary);
281
282 assert!(output.contains("My Account"));
283 assert!(output.contains("Checking"));
284 assert!(output.contains("Current Balance"));
285 assert!(output.contains("Cleared Balance"));
286 }
287}