envelope_cli/tui/
handler.rs

1//! Event handler for the TUI
2//!
3//! Routes keyboard and mouse events to the appropriate handlers
4//! based on the current application state.
5
6use anyhow::Result;
7use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
8
9use super::app::{ActiveDialog, ActiveView, App, FocusedPanel, InputMode};
10use super::commands::{CommandAction, COMMANDS};
11use super::event::Event;
12
13/// Handle an incoming event
14pub fn handle_event(app: &mut App, event: Event) -> Result<()> {
15    match event {
16        Event::Key(key) => handle_key_event(app, key),
17        Event::Mouse(_mouse) => {
18            // Mouse handling can be added later
19            Ok(())
20        }
21        Event::Tick => Ok(()),
22        Event::Resize(_, _) => Ok(()),
23    }
24}
25
26/// Handle a key event
27fn handle_key_event(app: &mut App, key: KeyEvent) -> Result<()> {
28    // Check if we're in a dialog first
29    if app.has_dialog() {
30        return handle_dialog_key(app, key);
31    }
32
33    // Check input mode
34    match app.input_mode {
35        InputMode::Normal => handle_normal_key(app, key),
36        InputMode::Editing => handle_editing_key(app, key),
37        InputMode::Command => handle_command_key(app, key),
38    }
39}
40
41/// Handle keys in normal mode
42fn handle_normal_key(app: &mut App, key: KeyEvent) -> Result<()> {
43    // Global keys (work everywhere)
44    match key.code {
45        // Quit
46        KeyCode::Char('q') | KeyCode::Char('Q') => {
47            app.quit();
48            return Ok(());
49        }
50
51        // Help
52        KeyCode::Char('?') => {
53            app.open_dialog(ActiveDialog::Help);
54            return Ok(());
55        }
56
57        // Command palette
58        KeyCode::Char(':') | KeyCode::Char('/') => {
59            app.open_dialog(ActiveDialog::CommandPalette);
60            return Ok(());
61        }
62
63        // Panel navigation
64        KeyCode::Tab => {
65            app.toggle_panel_focus();
66            return Ok(());
67        }
68        KeyCode::Char('h') | KeyCode::Left if key.modifiers.is_empty() => {
69            if app.focused_panel == FocusedPanel::Main {
70                app.focused_panel = FocusedPanel::Sidebar;
71                return Ok(());
72            }
73        }
74        KeyCode::Char('l') | KeyCode::Right if key.modifiers.is_empty() => {
75            if app.focused_panel == FocusedPanel::Sidebar {
76                app.focused_panel = FocusedPanel::Main;
77                app.ensure_selection_initialized();
78                return Ok(());
79            }
80        }
81
82        _ => {}
83    }
84
85    // View-specific keys
86    match app.focused_panel {
87        FocusedPanel::Sidebar => handle_sidebar_key(app, key),
88        FocusedPanel::Main => handle_main_panel_key(app, key),
89    }
90}
91
92/// Handle keys when sidebar is focused
93fn handle_sidebar_key(app: &mut App, key: KeyEvent) -> Result<()> {
94    // Get account count for bounds checking
95    let account_count = app
96        .storage
97        .accounts
98        .get_active()
99        .map(|a| a.len())
100        .unwrap_or(0);
101
102    match key.code {
103        // Navigation
104        KeyCode::Char('j') | KeyCode::Down => {
105            app.move_down(account_count);
106            // Update selected account
107            if let Ok(accounts) = app.storage.accounts.get_active() {
108                if let Some(account) = accounts.get(app.selected_account_index) {
109                    app.selected_account = Some(account.id);
110                }
111            }
112        }
113        KeyCode::Char('k') | KeyCode::Up => {
114            app.move_up();
115            // Update selected account
116            if let Ok(accounts) = app.storage.accounts.get_active() {
117                if let Some(account) = accounts.get(app.selected_account_index) {
118                    app.selected_account = Some(account.id);
119                }
120            }
121        }
122
123        // Select account and view register
124        KeyCode::Enter => {
125            if let Ok(accounts) = app.storage.accounts.get_active() {
126                if let Some(account) = accounts.get(app.selected_account_index) {
127                    app.selected_account = Some(account.id);
128                    app.switch_view(ActiveView::Register);
129                    app.focused_panel = FocusedPanel::Main;
130                }
131            }
132        }
133
134        // View switching from sidebar
135        KeyCode::Char('1') => app.switch_view(ActiveView::Accounts),
136        KeyCode::Char('2') => app.switch_view(ActiveView::Budget),
137        KeyCode::Char('3') => app.switch_view(ActiveView::Reports),
138
139        // Toggle archived accounts
140        KeyCode::Char('A') => {
141            app.show_archived = !app.show_archived;
142        }
143
144        // Add new account
145        KeyCode::Char('a') | KeyCode::Char('n') => {
146            app.open_dialog(ActiveDialog::AddAccount);
147        }
148
149        // Edit selected account
150        KeyCode::Char('e') => {
151            if let Ok(accounts) = app.storage.accounts.get_active() {
152                if let Some(account) = accounts.get(app.selected_account_index) {
153                    app.open_dialog(ActiveDialog::EditAccount(account.id));
154                }
155            }
156        }
157
158        _ => {}
159    }
160
161    Ok(())
162}
163
164/// Handle keys when main panel is focused
165fn handle_main_panel_key(app: &mut App, key: KeyEvent) -> Result<()> {
166    match app.active_view {
167        ActiveView::Accounts => handle_accounts_view_key(app, key),
168        ActiveView::Register => handle_register_view_key(app, key),
169        ActiveView::Budget => handle_budget_view_key(app, key),
170        ActiveView::Reports => handle_reports_view_key(app, key),
171        ActiveView::Reconcile => handle_reconcile_view_key(app, key),
172    }
173}
174
175/// Handle keys in the accounts view
176fn handle_accounts_view_key(app: &mut App, key: KeyEvent) -> Result<()> {
177    let account_count = app
178        .storage
179        .accounts
180        .get_active()
181        .map(|a| a.len())
182        .unwrap_or(0);
183
184    match key.code {
185        KeyCode::Char('j') | KeyCode::Down => {
186            app.move_down(account_count);
187            if let Ok(accounts) = app.storage.accounts.get_active() {
188                if let Some(account) = accounts.get(app.selected_account_index) {
189                    app.selected_account = Some(account.id);
190                }
191            }
192        }
193        KeyCode::Char('k') | KeyCode::Up => {
194            app.move_up();
195            if let Ok(accounts) = app.storage.accounts.get_active() {
196                if let Some(account) = accounts.get(app.selected_account_index) {
197                    app.selected_account = Some(account.id);
198                }
199            }
200        }
201        KeyCode::Enter => {
202            // Switch to register view for selected account
203            app.switch_view(ActiveView::Register);
204        }
205        // Add new account
206        KeyCode::Char('a') | KeyCode::Char('n') => {
207            app.open_dialog(ActiveDialog::AddAccount);
208        }
209        // Edit selected account
210        KeyCode::Char('e') => {
211            if let Ok(accounts) = app.storage.accounts.get_active() {
212                if let Some(account) = accounts.get(app.selected_account_index) {
213                    app.open_dialog(ActiveDialog::EditAccount(account.id));
214                }
215            }
216        }
217        _ => {}
218    }
219
220    Ok(())
221}
222
223/// Get sorted transactions for an account (matches display order)
224fn get_sorted_transactions(app: &App) -> Vec<crate::models::Transaction> {
225    if let Some(account_id) = app.selected_account {
226        let mut txns = app
227            .storage
228            .transactions
229            .get_by_account(account_id)
230            .unwrap_or_default();
231        // Sort by date descending (matches render order)
232        txns.sort_by(|a, b| b.date.cmp(&a.date));
233        txns
234    } else {
235        Vec::new()
236    }
237}
238
239/// Handle keys in the register view
240fn handle_register_view_key(app: &mut App, key: KeyEvent) -> Result<()> {
241    // Get sorted transactions (matches display order)
242    let txns = get_sorted_transactions(app);
243    let txn_count = txns.len();
244
245    match key.code {
246        // Navigation
247        KeyCode::Char('j') | KeyCode::Down => {
248            app.move_down(txn_count);
249            // Update selected transaction from sorted list
250            if let Some(txn) = txns.get(app.selected_transaction_index) {
251                app.selected_transaction = Some(txn.id);
252            }
253        }
254        KeyCode::Char('k') | KeyCode::Up => {
255            app.move_up();
256            // Update selected transaction from sorted list
257            if let Some(txn) = txns.get(app.selected_transaction_index) {
258                app.selected_transaction = Some(txn.id);
259            }
260        }
261
262        // Page navigation
263        KeyCode::Char('G') => {
264            // Go to bottom
265            if txn_count > 0 {
266                app.selected_transaction_index = txn_count - 1;
267                if let Some(txn) = txns.get(app.selected_transaction_index) {
268                    app.selected_transaction = Some(txn.id);
269                }
270            }
271        }
272        KeyCode::Char('g') => {
273            // Go to top (gg in vim, but we'll use single g)
274            app.selected_transaction_index = 0;
275            if let Some(txn) = txns.first() {
276                app.selected_transaction = Some(txn.id);
277            }
278        }
279
280        // Add transaction
281        KeyCode::Char('a') | KeyCode::Char('n') => {
282            app.open_dialog(ActiveDialog::AddTransaction);
283        }
284
285        // Edit transaction
286        KeyCode::Char('e') => {
287            // DEBUG: Force initialize selection and try edit
288            if app.selected_transaction.is_none() {
289                let txns = get_sorted_transactions(app);
290                if let Some(txn) = txns.get(app.selected_transaction_index) {
291                    app.selected_transaction = Some(txn.id);
292                }
293            }
294            if let Some(txn_id) = app.selected_transaction {
295                app.open_dialog(ActiveDialog::EditTransaction(txn_id));
296            }
297        }
298        KeyCode::Enter => {
299            if app.selected_transaction.is_none() {
300                let txns = get_sorted_transactions(app);
301                if let Some(txn) = txns.get(app.selected_transaction_index) {
302                    app.selected_transaction = Some(txn.id);
303                }
304            }
305            if let Some(txn_id) = app.selected_transaction {
306                app.open_dialog(ActiveDialog::EditTransaction(txn_id));
307            }
308        }
309
310        // Clear transaction (toggle)
311        KeyCode::Char('c') => {
312            if let Some(txn_id) = app.selected_transaction {
313                // Toggle cleared status
314                if let Ok(Some(txn)) = app.storage.transactions.get(txn_id) {
315                    use crate::models::TransactionStatus;
316                    let new_status = match txn.status {
317                        TransactionStatus::Pending => TransactionStatus::Cleared,
318                        TransactionStatus::Cleared => TransactionStatus::Pending,
319                        TransactionStatus::Reconciled => TransactionStatus::Reconciled,
320                    };
321                    if new_status != txn.status {
322                        let mut txn = txn.clone();
323                        txn.set_status(new_status);
324                        let _ = app.storage.transactions.upsert(txn);
325                        let _ = app.storage.transactions.save();
326                        app.set_status(format!("Transaction marked as {}", new_status));
327                    }
328                }
329            }
330        }
331
332        // Delete transaction
333        KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
334            if app.selected_transaction.is_some() {
335                app.open_dialog(ActiveDialog::Confirm(
336                    "Delete this transaction?".to_string(),
337                ));
338            }
339        }
340
341        // Multi-select mode
342        KeyCode::Char('v') => {
343            app.toggle_multi_select();
344            if app.multi_select_mode {
345                app.set_status("Multi-select mode ON");
346            } else {
347                app.set_status("Multi-select mode OFF");
348            }
349        }
350
351        // Toggle selection in multi-select mode
352        KeyCode::Char(' ') if app.multi_select_mode => {
353            app.toggle_transaction_selection();
354        }
355
356        // Bulk categorize
357        KeyCode::Char('C') if app.multi_select_mode && !app.selected_transactions.is_empty() => {
358            app.open_dialog(ActiveDialog::BulkCategorize);
359        }
360
361        _ => {}
362    }
363
364    Ok(())
365}
366
367/// Get categories in visual order (grouped by group, same as render)
368fn get_categories_in_visual_order(app: &App) -> Vec<crate::models::Category> {
369    let groups = app.storage.categories.get_all_groups().unwrap_or_default();
370    let all_categories = app
371        .storage
372        .categories
373        .get_all_categories()
374        .unwrap_or_default();
375
376    let mut result = Vec::new();
377    for group in &groups {
378        let group_cats: Vec<_> = all_categories
379            .iter()
380            .filter(|c| c.group_id == group.id)
381            .cloned()
382            .collect();
383        result.extend(group_cats);
384    }
385    result
386}
387
388/// Handle keys in the budget view
389fn handle_budget_view_key(app: &mut App, key: KeyEvent) -> Result<()> {
390    // Get categories in visual order (matches display)
391    let categories = get_categories_in_visual_order(app);
392    let category_count = categories.len();
393
394    match key.code {
395        // Navigation
396        KeyCode::Char('j') | KeyCode::Down => {
397            app.move_down(category_count);
398            if let Some(cat) = categories.get(app.selected_category_index) {
399                app.selected_category = Some(cat.id);
400            }
401        }
402        KeyCode::Char('k') | KeyCode::Up => {
403            app.move_up();
404            if let Some(cat) = categories.get(app.selected_category_index) {
405                app.selected_category = Some(cat.id);
406            }
407        }
408
409        // Period navigation
410        KeyCode::Char('[') | KeyCode::Char('H') => {
411            app.prev_period();
412        }
413        KeyCode::Char(']') | KeyCode::Char('L') => {
414            app.next_period();
415        }
416
417        // Move funds
418        KeyCode::Char('m') => {
419            app.open_dialog(ActiveDialog::MoveFunds);
420        }
421
422        // Add new category
423        KeyCode::Char('a') => {
424            app.open_dialog(ActiveDialog::AddCategory);
425        }
426
427        // Add new category group
428        KeyCode::Char('A') => {
429            app.open_dialog(ActiveDialog::AddGroup);
430        }
431
432        // Open unified budget dialog (period budget + target)
433        KeyCode::Enter | KeyCode::Char('b') | KeyCode::Char('t') => {
434            if let Some(cat) = categories.get(app.selected_category_index) {
435                app.selected_category = Some(cat.id);
436                app.open_dialog(ActiveDialog::Budget);
437            }
438        }
439
440        _ => {}
441    }
442
443    Ok(())
444}
445
446/// Handle keys in the reports view
447fn handle_reports_view_key(_app: &mut App, _key: KeyEvent) -> Result<()> {
448    // Reports view keys will be added later
449    Ok(())
450}
451
452/// Handle keys in the reconcile view
453fn handle_reconcile_view_key(app: &mut App, key: KeyEvent) -> Result<()> {
454    // Delegate to the reconcile view's key handler
455    super::views::reconcile::handle_key(app, key.code);
456    Ok(())
457}
458
459/// Handle keys in editing mode
460fn handle_editing_key(app: &mut App, key: KeyEvent) -> Result<()> {
461    match key.code {
462        KeyCode::Esc => {
463            app.input_mode = InputMode::Normal;
464        }
465        _ => {
466            // Pass to dialog if active
467        }
468    }
469    Ok(())
470}
471
472/// Handle keys in command mode (command palette)
473fn handle_command_key(app: &mut App, key: KeyEvent) -> Result<()> {
474    match key.code {
475        KeyCode::Esc => {
476            app.close_dialog();
477        }
478        KeyCode::Enter => {
479            // Get filtered commands (same logic as render)
480            let filtered_commands: Vec<&crate::tui::commands::Command> = COMMANDS
481                .iter()
482                .filter(|cmd| {
483                    if app.command_input.is_empty() {
484                        true
485                    } else {
486                        let query = app.command_input.to_lowercase();
487                        cmd.name.to_lowercase().contains(&query)
488                            || cmd.description.to_lowercase().contains(&query)
489                    }
490                })
491                .collect();
492
493            // Get the selected command
494            if !filtered_commands.is_empty() {
495                let selected_idx = app
496                    .selected_command_index
497                    .min(filtered_commands.len().saturating_sub(1));
498                let command = filtered_commands[selected_idx];
499                let action = command.action;
500
501                // Close dialog first
502                app.close_dialog();
503
504                // Execute the command action
505                execute_command_action(app, action)?;
506            } else {
507                app.close_dialog();
508            }
509        }
510        KeyCode::Char(c) => {
511            app.command_input.push(c);
512            // Reset selection when input changes
513            app.selected_command_index = 0;
514        }
515        KeyCode::Backspace => {
516            app.command_input.pop();
517            // Reset selection when input changes
518            app.selected_command_index = 0;
519        }
520        KeyCode::Up => {
521            if app.selected_command_index > 0 {
522                app.selected_command_index -= 1;
523            }
524        }
525        KeyCode::Down => {
526            // Get filtered count to bound selection
527            let filtered_count = COMMANDS
528                .iter()
529                .filter(|cmd| {
530                    if app.command_input.is_empty() {
531                        true
532                    } else {
533                        let query = app.command_input.to_lowercase();
534                        cmd.name.to_lowercase().contains(&query)
535                            || cmd.description.to_lowercase().contains(&query)
536                    }
537                })
538                .count();
539            if app.selected_command_index + 1 < filtered_count {
540                app.selected_command_index += 1;
541            }
542        }
543        _ => {}
544    }
545    Ok(())
546}
547
548/// Execute a command action from the command palette
549fn execute_command_action(app: &mut App, action: CommandAction) -> Result<()> {
550    match action {
551        // Navigation
552        CommandAction::ViewAccounts => {
553            app.switch_view(ActiveView::Accounts);
554        }
555        CommandAction::ViewBudget => {
556            app.switch_view(ActiveView::Budget);
557        }
558        CommandAction::ViewReports => {
559            app.switch_view(ActiveView::Reports);
560        }
561        CommandAction::ViewRegister => {
562            app.switch_view(ActiveView::Register);
563        }
564
565        // Account operations
566        CommandAction::AddAccount => {
567            app.open_dialog(ActiveDialog::AddAccount);
568        }
569        CommandAction::EditAccount => {
570            if let Ok(accounts) = app.storage.accounts.get_active() {
571                if let Some(account) = accounts.get(app.selected_account_index) {
572                    app.open_dialog(ActiveDialog::EditAccount(account.id));
573                }
574            }
575        }
576        CommandAction::ArchiveAccount => {
577            // Archive selected account with confirmation
578            if let Ok(accounts) = app.storage.accounts.get_active() {
579                if let Some(account) = accounts.get(app.selected_account_index) {
580                    app.open_dialog(ActiveDialog::Confirm(format!(
581                        "Archive account '{}'?",
582                        account.name
583                    )));
584                } else {
585                    app.set_status("No account selected".to_string());
586                }
587            }
588        }
589
590        // Transaction operations
591        CommandAction::AddTransaction => {
592            app.open_dialog(ActiveDialog::AddTransaction);
593        }
594        CommandAction::EditTransaction => {
595            if let Some(tx_id) = app.selected_transaction {
596                app.open_dialog(ActiveDialog::EditTransaction(tx_id));
597            } else {
598                app.set_status("No transaction selected".to_string());
599            }
600        }
601        CommandAction::DeleteTransaction => {
602            if app.selected_transaction.is_some() {
603                app.open_dialog(ActiveDialog::Confirm("Delete transaction?".to_string()));
604            } else {
605                app.set_status("No transaction selected".to_string());
606            }
607        }
608        CommandAction::ClearTransaction => {
609            // Toggle cleared status for selected transaction
610            if let Some(txn_id) = app.selected_transaction {
611                if let Ok(Some(txn)) = app.storage.transactions.get(txn_id) {
612                    use crate::models::TransactionStatus;
613                    let new_status = match txn.status {
614                        TransactionStatus::Pending => TransactionStatus::Cleared,
615                        TransactionStatus::Cleared => TransactionStatus::Pending,
616                        TransactionStatus::Reconciled => TransactionStatus::Reconciled,
617                    };
618                    if new_status != txn.status {
619                        let mut txn = txn.clone();
620                        txn.set_status(new_status);
621                        let _ = app.storage.transactions.upsert(txn);
622                        let _ = app.storage.transactions.save();
623                        app.set_status(format!("Transaction marked as {}", new_status));
624                    }
625                }
626            } else {
627                app.set_status("No transaction selected".to_string());
628            }
629        }
630
631        // Budget operations
632        CommandAction::MoveFunds => {
633            app.open_dialog(ActiveDialog::MoveFunds);
634        }
635        CommandAction::AssignBudget => {
636            // Open unified budget dialog for the selected category
637            if app.selected_category.is_some() {
638                app.open_dialog(ActiveDialog::Budget);
639            } else {
640                app.set_status("No category selected. Switch to Budget view first.".to_string());
641            }
642        }
643        CommandAction::NextPeriod => {
644            app.next_period();
645        }
646        CommandAction::PrevPeriod => {
647            app.prev_period();
648        }
649
650        // Category operations
651        CommandAction::AddCategory => {
652            // Initialize category form with available groups
653            let groups: Vec<_> = app
654                .storage
655                .categories
656                .get_all_groups()
657                .unwrap_or_default()
658                .into_iter()
659                .map(|g| (g.id, g.name.clone()))
660                .collect();
661            app.category_form.init_with_groups(groups);
662            app.open_dialog(ActiveDialog::AddCategory);
663        }
664        CommandAction::AddGroup => {
665            // Reset group form
666            app.group_form = super::dialogs::group::GroupFormState::new();
667            app.open_dialog(ActiveDialog::AddGroup);
668        }
669        CommandAction::EditCategory => {
670            // Open EditCategory dialog for the selected category
671            if let Some(category_id) = app.selected_category {
672                app.open_dialog(ActiveDialog::EditCategory(category_id));
673            } else {
674                app.set_status("No category selected. Switch to Budget view first.".to_string());
675            }
676        }
677        CommandAction::DeleteCategory => {
678            // Delete selected category with confirmation
679            if let Some(category_id) = app.selected_category {
680                if let Ok(Some(category)) = app.storage.categories.get_category(category_id) {
681                    app.open_dialog(ActiveDialog::Confirm(format!(
682                        "Delete category '{}'?",
683                        category.name
684                    )));
685                }
686            } else {
687                app.set_status("No category selected".to_string());
688            }
689        }
690
691        // General
692        CommandAction::Help => {
693            app.open_dialog(ActiveDialog::Help);
694        }
695        CommandAction::Quit => {
696            app.quit();
697        }
698        CommandAction::Refresh => {
699            // Reload all data from disk
700            if let Err(e) = app.storage.accounts.load() {
701                app.set_status(format!("Failed to refresh accounts: {}", e));
702                return Ok(());
703            }
704            if let Err(e) = app.storage.transactions.load() {
705                app.set_status(format!("Failed to refresh transactions: {}", e));
706                return Ok(());
707            }
708            if let Err(e) = app.storage.categories.load() {
709                app.set_status(format!("Failed to refresh categories: {}", e));
710                return Ok(());
711            }
712            if let Err(e) = app.storage.budget.load() {
713                app.set_status(format!("Failed to refresh budget: {}", e));
714                return Ok(());
715            }
716            app.set_status("Data refreshed from disk".to_string());
717        }
718        CommandAction::ToggleArchived => {
719            app.show_archived = !app.show_archived;
720        }
721
722        // Target operations
723        CommandAction::AutoFillTargets => {
724            use crate::services::BudgetService;
725            let budget_service = BudgetService::new(app.storage);
726            match budget_service.auto_fill_all_targets(&app.current_period) {
727                Ok(allocations) => {
728                    if allocations.is_empty() {
729                        app.set_status("No targets to auto-fill".to_string());
730                    } else {
731                        let count = allocations.len();
732                        let plural = if count == 1 { "category" } else { "categories" };
733                        app.set_status(format!("{} {} updated from targets", count, plural));
734                    }
735                }
736                Err(e) => {
737                    app.set_status(format!("Auto-fill failed: {}", e));
738                }
739            }
740        }
741    }
742    Ok(())
743}
744
745/// Handle keys when a dialog is open
746fn handle_dialog_key(app: &mut App, key: KeyEvent) -> Result<()> {
747    match &app.active_dialog {
748        ActiveDialog::Help => {
749            // Close help on any key
750            app.close_dialog();
751        }
752        ActiveDialog::CommandPalette => {
753            handle_command_key(app, key)?;
754        }
755        ActiveDialog::Confirm(msg) => {
756            let msg = msg.clone();
757            match key.code {
758                KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter => {
759                    // Execute confirmed action based on the message
760                    app.close_dialog();
761                    execute_confirmed_action(app, &msg)?;
762                }
763                KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
764                    app.close_dialog();
765                }
766                _ => {}
767            }
768        }
769        ActiveDialog::AddTransaction | ActiveDialog::EditTransaction(_) => {
770            // Delegate to transaction dialog key handler
771            super::dialogs::transaction::handle_key(app, key);
772        }
773        ActiveDialog::MoveFunds => {
774            super::dialogs::move_funds::handle_key(app, key);
775        }
776        ActiveDialog::BulkCategorize => {
777            super::dialogs::bulk_categorize::handle_key(app, key);
778        }
779        ActiveDialog::ReconcileStart => {
780            match key.code {
781                KeyCode::Esc => {
782                    app.close_dialog();
783                }
784                KeyCode::Enter => {
785                    // Start reconciliation
786                    app.close_dialog();
787                    app.switch_view(ActiveView::Reconcile);
788                }
789                _ => {
790                    super::dialogs::reconcile_start::handle_key(app, key.code);
791                }
792            }
793        }
794        ActiveDialog::UnlockConfirm(_) => {
795            match key.code {
796                KeyCode::Char('y') | KeyCode::Char('Y') => {
797                    // Unlock the transaction
798                    app.close_dialog();
799                }
800                KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
801                    app.close_dialog();
802                }
803                _ => {}
804            }
805        }
806        ActiveDialog::Adjustment => {
807            match key.code {
808                KeyCode::Esc => {
809                    app.close_dialog();
810                }
811                KeyCode::Enter => {
812                    // Create adjustment
813                    app.close_dialog();
814                }
815                _ => {
816                    super::dialogs::adjustment::handle_key(app, key.code);
817                }
818            }
819        }
820        ActiveDialog::Budget => {
821            super::dialogs::budget::handle_key(app, key);
822        }
823        ActiveDialog::AddAccount | ActiveDialog::EditAccount(_) => {
824            super::dialogs::account::handle_key(app, key);
825        }
826        ActiveDialog::AddCategory | ActiveDialog::EditCategory(_) => {
827            super::dialogs::category::handle_key(app, key);
828        }
829        ActiveDialog::AddGroup => {
830            super::dialogs::group::handle_key(app, key);
831        }
832        ActiveDialog::None => {}
833    }
834    Ok(())
835}
836
837/// Execute an action after user confirmation
838fn execute_confirmed_action(app: &mut App, message: &str) -> Result<()> {
839    // Delete transaction
840    if message.contains("Delete") && message.contains("transaction") {
841        if let Some(txn_id) = app.selected_transaction {
842            if let Err(e) = app.storage.transactions.delete(txn_id) {
843                app.set_status(format!("Failed to delete: {}", e));
844            } else {
845                let _ = app.storage.transactions.save();
846                app.selected_transaction = None;
847                app.set_status("Transaction deleted".to_string());
848            }
849        }
850    }
851    // Archive account
852    else if message.contains("Archive account") {
853        if let Ok(accounts) = app.storage.accounts.get_active() {
854            if let Some(account) = accounts.get(app.selected_account_index) {
855                let mut account = account.clone();
856                account.archive();
857                if let Err(e) = app.storage.accounts.upsert(account.clone()) {
858                    app.set_status(format!("Failed to archive: {}", e));
859                } else {
860                    let _ = app.storage.accounts.save();
861                    app.set_status(format!("Account '{}' archived", account.name));
862                    // Reset selection
863                    app.selected_account_index = 0;
864                    if let Ok(active) = app.storage.accounts.get_active() {
865                        app.selected_account = active.first().map(|a| a.id);
866                    }
867                }
868            }
869        }
870    }
871    // Delete category
872    else if message.contains("Delete category") {
873        if let Some(category_id) = app.selected_category {
874            use crate::services::CategoryService;
875            let category_service = CategoryService::new(app.storage);
876            match category_service.delete_category(category_id) {
877                Ok(()) => {
878                    app.set_status("Category deleted".to_string());
879                    app.selected_category = None;
880                    app.selected_category_index = 0;
881                }
882                Err(e) => {
883                    app.set_status(format!("Failed to delete: {}", e));
884                }
885            }
886        }
887    }
888
889    Ok(())
890}