1use ratatui::{
6 buffer::Buffer,
7 layout::{Alignment, Constraint, Direction, Layout, Rect},
8 style::{Color, Modifier, Style},
9 text::{Line, Span},
10 widgets::{Block, Borders, Clear, Paragraph, Widget, Wrap},
11};
12
13use crate::error::EnvelopeError;
14
15#[derive(Debug, Clone)]
17pub struct ErrorInfo {
18 pub title: String,
20 pub details: String,
22 pub suggestions: Vec<String>,
24 pub technical: Option<String>,
26}
27
28impl ErrorInfo {
29 pub fn from_error(error: &EnvelopeError) -> Self {
31 let (title, details, suggestions, technical) = match error {
32 EnvelopeError::Config(msg) => (
33 "Configuration Error".to_string(),
34 msg.clone(),
35 vec![
36 "Check your settings file for syntax errors".to_string(),
37 "Try running 'envelope init' to reset configuration".to_string(),
38 ],
39 None,
40 ),
41 EnvelopeError::Io(msg) => (
42 "I/O Error".to_string(),
43 msg.clone(),
44 vec![
45 "Check that you have write permissions to the data directory".to_string(),
46 "Ensure there is enough disk space".to_string(),
47 "Check if the file is locked by another process".to_string(),
48 ],
49 None,
50 ),
51 EnvelopeError::Json(msg) => (
52 "Data File Error".to_string(),
53 format!("Failed to read or write data: {}", msg),
54 vec![
55 "The data file may be corrupted".to_string(),
56 "Try restoring from a backup with 'envelope backup restore'".to_string(),
57 ],
58 Some(msg.clone()),
59 ),
60 EnvelopeError::Validation(msg) => (
61 "Validation Error".to_string(),
62 msg.clone(),
63 vec!["Review the input values and try again".to_string()],
64 None,
65 ),
66 EnvelopeError::NotFound {
67 entity_type,
68 identifier,
69 } => (
70 format!("{} Not Found", entity_type),
71 format!(
72 "Could not find {} with identifier '{}'",
73 entity_type.to_lowercase(),
74 identifier
75 ),
76 vec![
77 format!("Check that the {} exists", entity_type.to_lowercase()),
78 format!(
79 "Use 'envelope {} list' to see available {}s",
80 entity_type.to_lowercase(),
81 entity_type.to_lowercase()
82 ),
83 ],
84 None,
85 ),
86 EnvelopeError::Duplicate {
87 entity_type,
88 identifier,
89 } => (
90 format!("Duplicate {}", entity_type),
91 format!("{} '{}' already exists", entity_type, identifier),
92 vec![
93 "Use a different name".to_string(),
94 format!("Edit the existing {} instead", entity_type.to_lowercase()),
95 ],
96 None,
97 ),
98 EnvelopeError::Budget(msg) => (
99 "Budget Error".to_string(),
100 msg.clone(),
101 vec![
102 "Review your budget allocations".to_string(),
103 "Check the 'Available to Budget' amount".to_string(),
104 ],
105 None,
106 ),
107 EnvelopeError::Reconciliation(msg) => (
108 "Reconciliation Error".to_string(),
109 msg.clone(),
110 vec![
111 "Review the reconciliation difference".to_string(),
112 "Check for missing or duplicate transactions".to_string(),
113 ],
114 None,
115 ),
116 EnvelopeError::Import(msg) => (
117 "Import Error".to_string(),
118 msg.clone(),
119 vec![
120 "Check the CSV file format".to_string(),
121 "Ensure the column mapping is correct".to_string(),
122 "Try importing with a different preset".to_string(),
123 ],
124 None,
125 ),
126 EnvelopeError::Export(msg) => (
127 "Export Error".to_string(),
128 msg.clone(),
129 vec![
130 "Check that you have write permissions to the output path".to_string(),
131 "Ensure there is enough disk space".to_string(),
132 ],
133 None,
134 ),
135 EnvelopeError::Encryption(msg) => (
136 "Encryption Error".to_string(),
137 msg.clone(),
138 vec![
139 "Check that you entered the correct passphrase".to_string(),
140 "If you forgot your passphrase, data cannot be recovered".to_string(),
141 ],
142 None,
143 ),
144 EnvelopeError::Locked(msg) => (
145 "Transaction Locked".to_string(),
146 msg.clone(),
147 vec![
148 "Reconciled transactions cannot be edited".to_string(),
149 "Unlock the transaction first with 'envelope transaction unlock'".to_string(),
150 ],
151 None,
152 ),
153 EnvelopeError::InsufficientFunds {
154 category,
155 needed,
156 available,
157 } => (
158 "Insufficient Funds".to_string(),
159 format!(
160 "Category '{}' has insufficient funds: need ${:.2}, have ${:.2}",
161 category,
162 *needed as f64 / 100.0,
163 *available as f64 / 100.0
164 ),
165 vec![
166 "Move funds from another category".to_string(),
167 "Assign more funds to this category".to_string(),
168 ],
169 None,
170 ),
171 EnvelopeError::Storage(msg) => (
172 "Storage Error".to_string(),
173 msg.clone(),
174 vec![
175 "Check that the data directory is accessible".to_string(),
176 "Try running with elevated permissions".to_string(),
177 ],
178 Some(msg.clone()),
179 ),
180 EnvelopeError::Tui(msg) => (
181 "Interface Error".to_string(),
182 msg.clone(),
183 vec![
184 "Try resizing your terminal window".to_string(),
185 "Use the CLI commands instead".to_string(),
186 ],
187 None,
188 ),
189 };
190
191 Self {
192 title,
193 details,
194 suggestions,
195 technical,
196 }
197 }
198
199 pub fn simple(title: impl Into<String>, details: impl Into<String>) -> Self {
201 Self {
202 title: title.into(),
203 details: details.into(),
204 suggestions: vec![],
205 technical: None,
206 }
207 }
208
209 pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
211 self.suggestions.push(suggestion.into());
212 self
213 }
214}
215
216pub struct ErrorDialog<'a> {
218 error: &'a ErrorInfo,
219 show_technical: bool,
220}
221
222impl<'a> ErrorDialog<'a> {
223 pub fn new(error: &'a ErrorInfo) -> Self {
225 Self {
226 error,
227 show_technical: false,
228 }
229 }
230
231 pub fn with_technical(mut self, show: bool) -> Self {
233 self.show_technical = show;
234 self
235 }
236}
237
238impl<'a> Widget for ErrorDialog<'a> {
239 fn render(self, area: Rect, buf: &mut Buffer) {
240 Clear.render(area, buf);
242
243 let block = Block::default()
244 .borders(Borders::ALL)
245 .border_style(Style::default().fg(Color::Red))
246 .title(format!(" Error: {} ", self.error.title))
247 .title_style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD));
248
249 let inner = block.inner(area);
250 block.render(area, buf);
251
252 let chunks = Layout::default()
254 .direction(Direction::Vertical)
255 .margin(1)
256 .constraints([
257 Constraint::Length(3), Constraint::Min(1), Constraint::Length(1), ])
261 .split(inner);
262
263 let details = Paragraph::new(self.error.details.as_str())
265 .style(Style::default().fg(Color::White))
266 .wrap(Wrap { trim: true });
267 details.render(chunks[0], buf);
268
269 if !self.error.suggestions.is_empty() {
271 let mut lines: Vec<Line> = vec![Line::from(Span::styled(
272 "Suggestions:",
273 Style::default()
274 .fg(Color::Yellow)
275 .add_modifier(Modifier::BOLD),
276 ))];
277
278 for suggestion in &self.error.suggestions {
279 lines.push(Line::from(vec![
280 Span::raw(" - "),
281 Span::raw(suggestion.as_str()),
282 ]));
283 }
284
285 let suggestions = Paragraph::new(lines)
286 .style(Style::default().fg(Color::Yellow))
287 .wrap(Wrap { trim: true });
288 suggestions.render(chunks[1], buf);
289 }
290
291 let close_hint = Paragraph::new("Press Esc or Enter to close")
293 .style(Style::default().fg(Color::Yellow))
294 .alignment(Alignment::Center);
295 close_hint.render(chunks[2], buf);
296 }
297}
298
299pub fn error_dialog_area(parent: Rect) -> Rect {
301 let width = (parent.width * 70 / 100).clamp(40, 80);
302 let height = (parent.height * 50 / 100).clamp(10, 20);
303
304 let x = parent.x + (parent.width - width) / 2;
305 let y = parent.y + (parent.height - height) / 2;
306
307 Rect::new(x, y, width, height)
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 #[test]
315 fn test_error_info_from_validation_error() {
316 let error = EnvelopeError::Validation("Name cannot be empty".to_string());
317 let info = ErrorInfo::from_error(&error);
318
319 assert_eq!(info.title, "Validation Error");
320 assert!(info.details.contains("Name cannot be empty"));
321 }
322
323 #[test]
324 fn test_error_info_from_not_found() {
325 let error = EnvelopeError::NotFound {
326 entity_type: "Account",
327 identifier: "Checking".to_string(),
328 };
329 let info = ErrorInfo::from_error(&error);
330
331 assert_eq!(info.title, "Account Not Found");
332 assert!(info.details.contains("Checking"));
333 }
334
335 #[test]
336 fn test_simple_error_info() {
337 let info =
338 ErrorInfo::simple("Test Error", "Something went wrong").with_suggestion("Try again");
339
340 assert_eq!(info.title, "Test Error");
341 assert_eq!(info.details, "Something went wrong");
342 assert_eq!(info.suggestions.len(), 1);
343 }
344}