envelope_cli/tui/views/
register.rs1use ratatui::{
6 layout::Rect,
7 style::{Color, Modifier, Style},
8 widgets::{Block, Borders, Cell, Paragraph, Row, Table, TableState},
9 Frame,
10};
11
12use crate::models::TransactionStatus;
13use crate::tui::app::{App, FocusedPanel};
14use crate::tui::layout::MainPanelLayout;
15
16pub fn render(frame: &mut Frame, app: &mut App, area: Rect) {
18 let layout = MainPanelLayout::new(area);
19
20 render_header(frame, app, layout.header);
22
23 render_transaction_table(frame, app, layout.content);
25}
26
27fn render_header(frame: &mut Frame, app: &mut App, area: Rect) {
29 let account_name = if let Some(account_id) = app.selected_account {
30 app.storage
31 .accounts
32 .get(account_id)
33 .ok()
34 .flatten()
35 .map(|a| a.name.clone())
36 .unwrap_or_else(|| "Unknown".to_string())
37 } else {
38 "No Account Selected".to_string()
39 };
40
41 let title = format!(" {} - Transactions ", account_name);
42 let block = Block::default()
43 .title(title)
44 .title_style(
45 Style::default()
46 .fg(Color::Cyan)
47 .add_modifier(Modifier::BOLD),
48 )
49 .borders(Borders::ALL)
50 .border_style(Style::default().fg(Color::White));
51
52 let hints = if app.multi_select_mode {
53 "Multi-select: SPACE to select, C to categorize, D to delete, v to exit"
54 } else {
55 "a:Add e:Edit c:Clear v:Multi-select"
56 };
57
58 let paragraph = Paragraph::new(hints)
59 .block(block)
60 .style(Style::default().fg(Color::Yellow));
61
62 frame.render_widget(paragraph, area);
63}
64
65fn render_transaction_table(frame: &mut Frame, app: &mut App, area: Rect) {
67 let is_focused = app.focused_panel == FocusedPanel::Main;
68 let border_color = if is_focused { Color::Cyan } else { Color::Gray };
69
70 let block = Block::default()
71 .borders(Borders::ALL)
72 .border_style(Style::default().fg(border_color));
73
74 let transactions = if let Some(account_id) = app.selected_account {
76 let mut txns = app
77 .storage
78 .transactions
79 .get_by_account(account_id)
80 .unwrap_or_default();
81 txns.sort_by(|a, b| b.date.cmp(&a.date));
83 txns
84 } else {
85 Vec::new()
86 };
87
88 if transactions.is_empty() {
89 let text = Paragraph::new("No transactions. Press 'a' to add one.")
90 .block(block)
91 .style(Style::default().fg(Color::Yellow));
92 frame.render_widget(text, area);
93 return;
94 }
95
96 let widths = [
98 ratatui::layout::Constraint::Length(2), ratatui::layout::Constraint::Length(12), ratatui::layout::Constraint::Length(20), ratatui::layout::Constraint::Length(15), ratatui::layout::Constraint::Length(12), ratatui::layout::Constraint::Min(10), ];
105
106 let header = Row::new(vec![
108 Cell::from(""),
109 Cell::from("Date").style(Style::default().add_modifier(Modifier::BOLD)),
110 Cell::from("Payee").style(Style::default().add_modifier(Modifier::BOLD)),
111 Cell::from("Category").style(Style::default().add_modifier(Modifier::BOLD)),
112 Cell::from("Amount").style(Style::default().add_modifier(Modifier::BOLD)),
113 Cell::from("Memo").style(Style::default().add_modifier(Modifier::BOLD)),
114 ])
115 .style(Style::default().fg(Color::Yellow))
116 .height(1);
117
118 let categories = app
120 .storage
121 .categories
122 .get_all_categories()
123 .unwrap_or_default();
124
125 let rows: Vec<Row> = transactions
127 .iter()
128 .map(|txn| {
129 let status_indicator = match txn.status {
131 TransactionStatus::Pending => "○",
132 TransactionStatus::Cleared => "✓",
133 TransactionStatus::Reconciled => "🔒",
134 };
135 let status_color = match txn.status {
136 TransactionStatus::Pending => Color::Yellow,
137 TransactionStatus::Cleared => Color::Green,
138 TransactionStatus::Reconciled => Color::Blue,
139 };
140
141 let is_selected = app.selected_transactions.contains(&txn.id);
143 let select_indicator = if app.multi_select_mode {
144 if is_selected {
145 "■ "
146 } else {
147 "□ "
148 }
149 } else {
150 ""
151 };
152
153 let category_name = if txn.is_split() {
155 "Split".to_string()
156 } else if txn.is_transfer() {
157 "Transfer".to_string()
158 } else if let Some(cat_id) = txn.category_id {
159 categories
160 .iter()
161 .find(|c| c.id == cat_id)
162 .map(|c| c.name.clone())
163 .unwrap_or_else(|| "Unknown".to_string())
164 } else {
165 "-".to_string()
166 };
167
168 let amount_style = if txn.amount.is_negative() {
170 Style::default().fg(Color::Red)
171 } else {
172 Style::default().fg(Color::Green)
173 };
174
175 Row::new(vec![
176 Cell::from(format!("{}{}", select_indicator, status_indicator))
177 .style(Style::default().fg(status_color)),
178 Cell::from(txn.date.format("%Y-%m-%d").to_string()),
179 Cell::from(truncate_string(&txn.payee_name, 20)),
180 Cell::from(truncate_string(&category_name, 15)),
181 Cell::from(format!("{}", txn.amount)).style(amount_style),
182 Cell::from(truncate_string(&txn.memo, 30)),
183 ])
184 })
185 .collect();
186
187 let table = Table::new(rows, widths)
188 .header(header)
189 .block(block)
190 .highlight_style(
191 Style::default()
192 .bg(Color::DarkGray)
193 .add_modifier(Modifier::BOLD),
194 )
195 .highlight_symbol("▶ ");
196
197 let mut state = TableState::default();
198 state.select(Some(app.selected_transaction_index));
199
200 frame.render_stateful_widget(table, area, &mut state);
201}
202
203fn truncate_string(s: &str, max_len: usize) -> String {
205 if s.len() <= max_len {
206 s.to_string()
207 } else {
208 format!("{}…", &s[..max_len - 1])
209 }
210}