envelope_cli/display/
account.rs

1//! Account display formatting
2//!
3//! Formats accounts for terminal output in table and detail views.
4
5use crate::models::Account;
6use crate::services::account::AccountSummary;
7
8/// Format a list of accounts with balances as a table
9pub fn format_account_list(summaries: &[AccountSummary]) -> String {
10    if summaries.is_empty() {
11        return "No accounts found.".to_string();
12    }
13
14    // Calculate column widths
15    let name_width = summaries
16        .iter()
17        .map(|s| s.account.name.len())
18        .max()
19        .unwrap_or(4)
20        .max(4);
21
22    let type_width = summaries
23        .iter()
24        .map(|s| s.account.account_type.to_string().len())
25        .max()
26        .unwrap_or(4)
27        .max(4);
28
29    // Build header
30    let mut output = String::new();
31    output.push_str(&format!(
32        "{:<name_width$}  {:<type_width$}  {:>12}  {:>12}  {}\n",
33        "Name",
34        "Type",
35        "Balance",
36        "Cleared",
37        "Status",
38        name_width = name_width,
39        type_width = type_width,
40    ));
41
42    // Separator line
43    output.push_str(&format!(
44        "{:-<name_width$}  {:-<type_width$}  {:->12}  {:->12}  {:-<10}\n",
45        "",
46        "",
47        "",
48        "",
49        "",
50        name_width = name_width,
51        type_width = type_width,
52    ));
53
54    // Account rows
55    for summary in summaries {
56        let status = if summary.account.archived {
57            "Archived"
58        } else if !summary.account.on_budget {
59            "Off-Budget"
60        } else if summary.uncleared_count > 0 {
61            &format!("{} pending", summary.uncleared_count)
62        } else {
63            ""
64        };
65
66        output.push_str(&format!(
67            "{:<name_width$}  {:<type_width$}  {:>12}  {:>12}  {}\n",
68            summary.account.name,
69            summary.account.account_type,
70            summary.balance.to_string(),
71            summary.cleared_balance.to_string(),
72            status,
73            name_width = name_width,
74            type_width = type_width,
75        ));
76    }
77
78    // Total row
79    let total_balance: crate::models::Money = summaries.iter().map(|s| s.balance).sum();
80    let total_cleared: crate::models::Money = summaries.iter().map(|s| s.cleared_balance).sum();
81
82    output.push_str(&format!(
83        "{:-<name_width$}  {:-<type_width$}  {:->12}  {:->12}  {:-<10}\n",
84        "",
85        "",
86        "",
87        "",
88        "",
89        name_width = name_width,
90        type_width = type_width,
91    ));
92
93    output.push_str(&format!(
94        "{:<name_width$}  {:<type_width$}  {:>12}  {:>12}\n",
95        "TOTAL",
96        "",
97        total_balance.to_string(),
98        total_cleared.to_string(),
99        name_width = name_width,
100        type_width = type_width,
101    ));
102
103    output
104}
105
106/// Format a single account's details
107pub fn format_account_details(summary: &AccountSummary) -> String {
108    let account = &summary.account;
109
110    let mut output = String::new();
111
112    output.push_str(&format!("Account: {}\n", account.name));
113    output.push_str(&format!("  Type:           {}\n", account.account_type));
114    output.push_str(&format!("  ID:             {}\n", account.id));
115    output.push_str(&format!(
116        "  On Budget:      {}\n",
117        if account.on_budget { "Yes" } else { "No" }
118    ));
119    output.push_str(&format!(
120        "  Archived:       {}\n",
121        if account.archived { "Yes" } else { "No" }
122    ));
123    output.push('\n');
124    output.push_str(&format!(
125        "  Starting Balance: {}\n",
126        account.starting_balance
127    ));
128    output.push_str(&format!("  Current Balance:  {}\n", summary.balance));
129    output.push_str(&format!(
130        "  Cleared Balance:  {}\n",
131        summary.cleared_balance
132    ));
133    output.push_str(&format!(
134        "  Uncleared Count:  {}\n",
135        summary.uncleared_count
136    ));
137
138    if let Some(date) = account.last_reconciled_date {
139        output.push('\n');
140        output.push_str(&format!("  Last Reconciled:  {}\n", date));
141        if let Some(balance) = account.last_reconciled_balance {
142            output.push_str(&format!("  Reconciled Balance: {}\n", balance));
143        }
144    }
145
146    if !account.notes.is_empty() {
147        output.push('\n');
148        output.push_str(&format!("  Notes: {}\n", account.notes));
149    }
150
151    output.push('\n');
152    output.push_str(&format!(
153        "  Created:  {}\n",
154        account.created_at.format("%Y-%m-%d %H:%M UTC")
155    ));
156    output.push_str(&format!(
157        "  Modified: {}\n",
158        account.updated_at.format("%Y-%m-%d %H:%M UTC")
159    ));
160
161    output
162}
163
164/// Format a simple account list (name and type only)
165pub fn format_account_list_simple(accounts: &[Account]) -> String {
166    if accounts.is_empty() {
167        return "No accounts found.".to_string();
168    }
169
170    let mut output = String::new();
171    for account in accounts {
172        let status = if account.archived { " (archived)" } else { "" };
173        output.push_str(&format!(
174            "  {} - {}{}\n",
175            account.name, account.account_type, status
176        ));
177    }
178    output
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use crate::models::{AccountType, Money};
185
186    fn create_test_summary(name: &str, balance: i64, cleared: i64) -> AccountSummary {
187        let account =
188            Account::with_starting_balance(name, AccountType::Checking, Money::from_cents(0));
189        AccountSummary {
190            account,
191            balance: Money::from_cents(balance),
192            cleared_balance: Money::from_cents(cleared),
193            uncleared_count: if balance != cleared { 1 } else { 0 },
194        }
195    }
196
197    #[test]
198    fn test_format_account_list() {
199        let summaries = vec![
200            create_test_summary("Checking", 100000, 95000),
201            create_test_summary("Savings", 500000, 500000),
202        ];
203
204        let output = format_account_list(&summaries);
205        assert!(output.contains("Checking"));
206        assert!(output.contains("Savings"));
207        assert!(output.contains("TOTAL"));
208        assert!(
209            output.contains("$1,000.00")
210                || output.contains("$6000.00")
211                || output.contains("$6,000.00")
212        );
213    }
214
215    #[test]
216    fn test_format_empty_list() {
217        let output = format_account_list(&[]);
218        assert!(output.contains("No accounts found"));
219    }
220
221    #[test]
222    fn test_format_account_details() {
223        let summary = create_test_summary("My Account", 100000, 90000);
224        let output = format_account_details(&summary);
225
226        assert!(output.contains("My Account"));
227        assert!(output.contains("Checking"));
228        assert!(output.contains("Current Balance"));
229        assert!(output.contains("Cleared Balance"));
230    }
231}