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        // Header display toggle (cycle through account types)
418        KeyCode::Char('<') | KeyCode::Char(',') => {
419            app.budget_header_display = app.budget_header_display.prev();
420        }
421        KeyCode::Char('>') | KeyCode::Char('.') => {
422            app.budget_header_display = app.budget_header_display.next();
423        }
424
425        // Move funds
426        KeyCode::Char('m') => {
427            app.open_dialog(ActiveDialog::MoveFunds);
428        }
429
430        // Add new category
431        KeyCode::Char('a') => {
432            app.open_dialog(ActiveDialog::AddCategory);
433        }
434
435        // Add new category group
436        KeyCode::Char('A') => {
437            app.open_dialog(ActiveDialog::AddGroup);
438        }
439
440        // Open unified budget dialog (period budget + target)
441        KeyCode::Enter | KeyCode::Char('b') | KeyCode::Char('t') => {
442            if let Some(cat) = categories.get(app.selected_category_index) {
443                app.selected_category = Some(cat.id);
444                app.open_dialog(ActiveDialog::Budget);
445            }
446        }
447
448        _ => {}
449    }
450
451    Ok(())
452}
453
454/// Handle keys in the reports view
455fn handle_reports_view_key(_app: &mut App, _key: KeyEvent) -> Result<()> {
456    // Reports view keys will be added later
457    Ok(())
458}
459
460/// Handle keys in the reconcile view
461fn handle_reconcile_view_key(app: &mut App, key: KeyEvent) -> Result<()> {
462    // Delegate to the reconcile view's key handler
463    super::views::reconcile::handle_key(app, key.code);
464    Ok(())
465}
466
467/// Handle keys in editing mode
468fn handle_editing_key(app: &mut App, key: KeyEvent) -> Result<()> {
469    match key.code {
470        KeyCode::Esc => {
471            app.input_mode = InputMode::Normal;
472        }
473        _ => {
474            // Pass to dialog if active
475        }
476    }
477    Ok(())
478}
479
480/// Handle keys in command mode (command palette)
481fn handle_command_key(app: &mut App, key: KeyEvent) -> Result<()> {
482    match key.code {
483        KeyCode::Esc => {
484            app.close_dialog();
485        }
486        KeyCode::Enter => {
487            // Get filtered commands (same logic as render)
488            let filtered_commands: Vec<&crate::tui::commands::Command> = COMMANDS
489                .iter()
490                .filter(|cmd| {
491                    if app.command_input.is_empty() {
492                        true
493                    } else {
494                        let query = app.command_input.to_lowercase();
495                        cmd.name.to_lowercase().contains(&query)
496                            || cmd.description.to_lowercase().contains(&query)
497                    }
498                })
499                .collect();
500
501            // Get the selected command
502            if !filtered_commands.is_empty() {
503                let selected_idx = app
504                    .selected_command_index
505                    .min(filtered_commands.len().saturating_sub(1));
506                let command = filtered_commands[selected_idx];
507                let action = command.action;
508
509                // Close dialog first
510                app.close_dialog();
511
512                // Execute the command action
513                execute_command_action(app, action)?;
514            } else {
515                app.close_dialog();
516            }
517        }
518        KeyCode::Char(c) => {
519            app.command_input.push(c);
520            // Reset selection when input changes
521            app.selected_command_index = 0;
522        }
523        KeyCode::Backspace => {
524            app.command_input.pop();
525            // Reset selection when input changes
526            app.selected_command_index = 0;
527        }
528        KeyCode::Up => {
529            if app.selected_command_index > 0 {
530                app.selected_command_index -= 1;
531            }
532        }
533        KeyCode::Down => {
534            // Get filtered count to bound selection
535            let filtered_count = COMMANDS
536                .iter()
537                .filter(|cmd| {
538                    if app.command_input.is_empty() {
539                        true
540                    } else {
541                        let query = app.command_input.to_lowercase();
542                        cmd.name.to_lowercase().contains(&query)
543                            || cmd.description.to_lowercase().contains(&query)
544                    }
545                })
546                .count();
547            if app.selected_command_index + 1 < filtered_count {
548                app.selected_command_index += 1;
549            }
550        }
551        _ => {}
552    }
553    Ok(())
554}
555
556/// Execute a command action from the command palette
557fn execute_command_action(app: &mut App, action: CommandAction) -> Result<()> {
558    match action {
559        // Navigation
560        CommandAction::ViewAccounts => {
561            app.switch_view(ActiveView::Accounts);
562        }
563        CommandAction::ViewBudget => {
564            app.switch_view(ActiveView::Budget);
565        }
566        CommandAction::ViewReports => {
567            app.switch_view(ActiveView::Reports);
568        }
569        CommandAction::ViewRegister => {
570            app.switch_view(ActiveView::Register);
571        }
572
573        // Account operations
574        CommandAction::AddAccount => {
575            app.open_dialog(ActiveDialog::AddAccount);
576        }
577        CommandAction::EditAccount => {
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::EditAccount(account.id));
581                }
582            }
583        }
584        CommandAction::ArchiveAccount => {
585            // Archive selected account with confirmation
586            if let Ok(accounts) = app.storage.accounts.get_active() {
587                if let Some(account) = accounts.get(app.selected_account_index) {
588                    app.open_dialog(ActiveDialog::Confirm(format!(
589                        "Archive account '{}'?",
590                        account.name
591                    )));
592                } else {
593                    app.set_status("No account selected".to_string());
594                }
595            }
596        }
597
598        // Transaction operations
599        CommandAction::AddTransaction => {
600            app.open_dialog(ActiveDialog::AddTransaction);
601        }
602        CommandAction::EditTransaction => {
603            if let Some(tx_id) = app.selected_transaction {
604                app.open_dialog(ActiveDialog::EditTransaction(tx_id));
605            } else {
606                app.set_status("No transaction selected".to_string());
607            }
608        }
609        CommandAction::DeleteTransaction => {
610            if app.selected_transaction.is_some() {
611                app.open_dialog(ActiveDialog::Confirm("Delete transaction?".to_string()));
612            } else {
613                app.set_status("No transaction selected".to_string());
614            }
615        }
616        CommandAction::ClearTransaction => {
617            // Toggle cleared status for selected transaction
618            if let Some(txn_id) = app.selected_transaction {
619                if let Ok(Some(txn)) = app.storage.transactions.get(txn_id) {
620                    use crate::models::TransactionStatus;
621                    let new_status = match txn.status {
622                        TransactionStatus::Pending => TransactionStatus::Cleared,
623                        TransactionStatus::Cleared => TransactionStatus::Pending,
624                        TransactionStatus::Reconciled => TransactionStatus::Reconciled,
625                    };
626                    if new_status != txn.status {
627                        let mut txn = txn.clone();
628                        txn.set_status(new_status);
629                        let _ = app.storage.transactions.upsert(txn);
630                        let _ = app.storage.transactions.save();
631                        app.set_status(format!("Transaction marked as {}", new_status));
632                    }
633                }
634            } else {
635                app.set_status("No transaction selected".to_string());
636            }
637        }
638
639        // Budget operations
640        CommandAction::MoveFunds => {
641            app.open_dialog(ActiveDialog::MoveFunds);
642        }
643        CommandAction::AssignBudget => {
644            // Open unified budget dialog for the selected category
645            if app.selected_category.is_some() {
646                app.open_dialog(ActiveDialog::Budget);
647            } else {
648                app.set_status("No category selected. Switch to Budget view first.".to_string());
649            }
650        }
651        CommandAction::NextPeriod => {
652            app.next_period();
653        }
654        CommandAction::PrevPeriod => {
655            app.prev_period();
656        }
657
658        // Category operations
659        CommandAction::AddCategory => {
660            // Initialize category form with available groups
661            let groups: Vec<_> = app
662                .storage
663                .categories
664                .get_all_groups()
665                .unwrap_or_default()
666                .into_iter()
667                .map(|g| (g.id, g.name.clone()))
668                .collect();
669            app.category_form.init_with_groups(groups);
670            app.open_dialog(ActiveDialog::AddCategory);
671        }
672        CommandAction::AddGroup => {
673            // Reset group form
674            app.group_form = super::dialogs::group::GroupFormState::new();
675            app.open_dialog(ActiveDialog::AddGroup);
676        }
677        CommandAction::EditCategory => {
678            // Open EditCategory dialog for the selected category
679            if let Some(category_id) = app.selected_category {
680                app.open_dialog(ActiveDialog::EditCategory(category_id));
681            } else {
682                app.set_status("No category selected. Switch to Budget view first.".to_string());
683            }
684        }
685        CommandAction::DeleteCategory => {
686            // Delete selected category with confirmation
687            if let Some(category_id) = app.selected_category {
688                if let Ok(Some(category)) = app.storage.categories.get_category(category_id) {
689                    app.open_dialog(ActiveDialog::Confirm(format!(
690                        "Delete category '{}'?",
691                        category.name
692                    )));
693                }
694            } else {
695                app.set_status("No category selected".to_string());
696            }
697        }
698
699        // General
700        CommandAction::Help => {
701            app.open_dialog(ActiveDialog::Help);
702        }
703        CommandAction::Quit => {
704            app.quit();
705        }
706        CommandAction::Refresh => {
707            // Reload all data from disk
708            if let Err(e) = app.storage.accounts.load() {
709                app.set_status(format!("Failed to refresh accounts: {}", e));
710                return Ok(());
711            }
712            if let Err(e) = app.storage.transactions.load() {
713                app.set_status(format!("Failed to refresh transactions: {}", e));
714                return Ok(());
715            }
716            if let Err(e) = app.storage.categories.load() {
717                app.set_status(format!("Failed to refresh categories: {}", e));
718                return Ok(());
719            }
720            if let Err(e) = app.storage.budget.load() {
721                app.set_status(format!("Failed to refresh budget: {}", e));
722                return Ok(());
723            }
724            app.set_status("Data refreshed from disk".to_string());
725        }
726        CommandAction::ToggleArchived => {
727            app.show_archived = !app.show_archived;
728        }
729
730        // Target operations
731        CommandAction::AutoFillTargets => {
732            use crate::services::BudgetService;
733            let budget_service = BudgetService::new(app.storage);
734            match budget_service.auto_fill_all_targets(&app.current_period) {
735                Ok(allocations) => {
736                    if allocations.is_empty() {
737                        app.set_status("No targets to auto-fill".to_string());
738                    } else {
739                        let count = allocations.len();
740                        let plural = if count == 1 { "category" } else { "categories" };
741                        app.set_status(format!("{} {} updated from targets", count, plural));
742                    }
743                }
744                Err(e) => {
745                    app.set_status(format!("Auto-fill failed: {}", e));
746                }
747            }
748        }
749    }
750    Ok(())
751}
752
753/// Handle keys when a dialog is open
754fn handle_dialog_key(app: &mut App, key: KeyEvent) -> Result<()> {
755    match &app.active_dialog {
756        ActiveDialog::Help => {
757            // Close help on any key
758            app.close_dialog();
759        }
760        ActiveDialog::CommandPalette => {
761            handle_command_key(app, key)?;
762        }
763        ActiveDialog::Confirm(msg) => {
764            let msg = msg.clone();
765            match key.code {
766                KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter => {
767                    // Execute confirmed action based on the message
768                    app.close_dialog();
769                    execute_confirmed_action(app, &msg)?;
770                }
771                KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
772                    app.close_dialog();
773                }
774                _ => {}
775            }
776        }
777        ActiveDialog::AddTransaction | ActiveDialog::EditTransaction(_) => {
778            // Delegate to transaction dialog key handler
779            super::dialogs::transaction::handle_key(app, key);
780        }
781        ActiveDialog::MoveFunds => {
782            super::dialogs::move_funds::handle_key(app, key);
783        }
784        ActiveDialog::BulkCategorize => {
785            super::dialogs::bulk_categorize::handle_key(app, key);
786        }
787        ActiveDialog::ReconcileStart => {
788            match key.code {
789                KeyCode::Esc => {
790                    app.close_dialog();
791                }
792                KeyCode::Enter => {
793                    // Start reconciliation
794                    app.close_dialog();
795                    app.switch_view(ActiveView::Reconcile);
796                }
797                _ => {
798                    super::dialogs::reconcile_start::handle_key(app, key.code);
799                }
800            }
801        }
802        ActiveDialog::UnlockConfirm(_) => {
803            match key.code {
804                KeyCode::Char('y') | KeyCode::Char('Y') => {
805                    // Unlock the transaction
806                    app.close_dialog();
807                }
808                KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
809                    app.close_dialog();
810                }
811                _ => {}
812            }
813        }
814        ActiveDialog::Adjustment => {
815            match key.code {
816                KeyCode::Esc => {
817                    app.close_dialog();
818                }
819                KeyCode::Enter => {
820                    // Create adjustment
821                    app.close_dialog();
822                }
823                _ => {
824                    super::dialogs::adjustment::handle_key(app, key.code);
825                }
826            }
827        }
828        ActiveDialog::Budget => {
829            super::dialogs::budget::handle_key(app, key);
830        }
831        ActiveDialog::AddAccount | ActiveDialog::EditAccount(_) => {
832            super::dialogs::account::handle_key(app, key);
833        }
834        ActiveDialog::AddCategory | ActiveDialog::EditCategory(_) => {
835            super::dialogs::category::handle_key(app, key);
836        }
837        ActiveDialog::AddGroup => {
838            super::dialogs::group::handle_key(app, key);
839        }
840        ActiveDialog::None => {}
841    }
842    Ok(())
843}
844
845/// Execute an action after user confirmation
846fn execute_confirmed_action(app: &mut App, message: &str) -> Result<()> {
847    // Delete transaction
848    if message.contains("Delete") && message.contains("transaction") {
849        if let Some(txn_id) = app.selected_transaction {
850            if let Err(e) = app.storage.transactions.delete(txn_id) {
851                app.set_status(format!("Failed to delete: {}", e));
852            } else {
853                let _ = app.storage.transactions.save();
854                app.selected_transaction = None;
855                app.set_status("Transaction deleted".to_string());
856            }
857        }
858    }
859    // Archive account
860    else if message.contains("Archive account") {
861        if let Ok(accounts) = app.storage.accounts.get_active() {
862            if let Some(account) = accounts.get(app.selected_account_index) {
863                let mut account = account.clone();
864                account.archive();
865                if let Err(e) = app.storage.accounts.upsert(account.clone()) {
866                    app.set_status(format!("Failed to archive: {}", e));
867                } else {
868                    let _ = app.storage.accounts.save();
869                    app.set_status(format!("Account '{}' archived", account.name));
870                    // Reset selection
871                    app.selected_account_index = 0;
872                    if let Ok(active) = app.storage.accounts.get_active() {
873                        app.selected_account = active.first().map(|a| a.id);
874                    }
875                }
876            }
877        }
878    }
879    // Delete category
880    else if message.contains("Delete category") {
881        if let Some(category_id) = app.selected_category {
882            use crate::services::CategoryService;
883            let category_service = CategoryService::new(app.storage);
884            match category_service.delete_category(category_id) {
885                Ok(()) => {
886                    app.set_status("Category deleted".to_string());
887                    app.selected_category = None;
888                    app.selected_category_index = 0;
889                }
890                Err(e) => {
891                    app.set_status(format!("Failed to delete: {}", e));
892                }
893            }
894        }
895    }
896
897    Ok(())
898}