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        // Bulk delete
362        KeyCode::Char('D') if app.multi_select_mode && !app.selected_transactions.is_empty() => {
363            let count = app.selected_transactions.len();
364            app.open_dialog(ActiveDialog::Confirm(format!(
365                "Delete {} transaction{}?",
366                count,
367                if count == 1 { "" } else { "s" }
368            )));
369        }
370
371        _ => {}
372    }
373
374    Ok(())
375}
376
377/// Get categories in visual order (grouped by group, same as render)
378fn get_categories_in_visual_order(app: &App) -> Vec<crate::models::Category> {
379    let groups = app.storage.categories.get_all_groups().unwrap_or_default();
380    let all_categories = app
381        .storage
382        .categories
383        .get_all_categories()
384        .unwrap_or_default();
385
386    let mut result = Vec::new();
387    for group in &groups {
388        let group_cats: Vec<_> = all_categories
389            .iter()
390            .filter(|c| c.group_id == group.id)
391            .cloned()
392            .collect();
393        result.extend(group_cats);
394    }
395    result
396}
397
398/// Handle keys in the budget view
399fn handle_budget_view_key(app: &mut App, key: KeyEvent) -> Result<()> {
400    // Get categories in visual order (matches display)
401    let categories = get_categories_in_visual_order(app);
402    let category_count = categories.len();
403
404    match key.code {
405        // Navigation
406        KeyCode::Char('j') | KeyCode::Down => {
407            app.move_down(category_count);
408            if let Some(cat) = categories.get(app.selected_category_index) {
409                app.selected_category = Some(cat.id);
410            }
411        }
412        KeyCode::Char('k') | KeyCode::Up => {
413            app.move_up();
414            if let Some(cat) = categories.get(app.selected_category_index) {
415                app.selected_category = Some(cat.id);
416            }
417        }
418
419        // Period navigation
420        KeyCode::Char('[') | KeyCode::Char('H') => {
421            app.prev_period();
422        }
423        KeyCode::Char(']') | KeyCode::Char('L') => {
424            app.next_period();
425        }
426
427        // Header display toggle (cycle through account types)
428        KeyCode::Char('<') | KeyCode::Char(',') => {
429            app.budget_header_display = app.budget_header_display.prev();
430        }
431        KeyCode::Char('>') | KeyCode::Char('.') => {
432            app.budget_header_display = app.budget_header_display.next();
433        }
434
435        // Move funds
436        KeyCode::Char('m') => {
437            app.open_dialog(ActiveDialog::MoveFunds);
438        }
439
440        // Add new category
441        KeyCode::Char('a') => {
442            app.open_dialog(ActiveDialog::AddCategory);
443        }
444
445        // Add new category group
446        KeyCode::Char('A') => {
447            app.open_dialog(ActiveDialog::AddGroup);
448        }
449
450        // Edit category group (Shift+E)
451        KeyCode::Char('E') => {
452            if let Some(cat) = categories.get(app.selected_category_index) {
453                app.open_dialog(ActiveDialog::EditGroup(cat.group_id));
454            }
455        }
456
457        // Delete category group (Shift+D)
458        KeyCode::Char('D') => {
459            if let Some(cat) = categories.get(app.selected_category_index) {
460                if let Ok(Some(group)) = app.storage.categories.get_group(cat.group_id) {
461                    let group_categories = app
462                        .storage
463                        .categories
464                        .get_categories_in_group(group.id)
465                        .unwrap_or_default();
466                    let warning = if group_categories.is_empty() {
467                        format!("Delete group '{}'?", group.name)
468                    } else {
469                        format!(
470                            "Delete group '{}' and its {} categories?",
471                            group.name,
472                            group_categories.len()
473                        )
474                    };
475                    app.open_dialog(ActiveDialog::Confirm(warning));
476                }
477            }
478        }
479
480        // Edit category
481        KeyCode::Char('e') => {
482            if let Some(cat) = categories.get(app.selected_category_index) {
483                app.selected_category = Some(cat.id);
484                app.open_dialog(ActiveDialog::EditCategory(cat.id));
485            }
486        }
487
488        // Delete category
489        KeyCode::Char('d') => {
490            if let Some(cat) = categories.get(app.selected_category_index) {
491                app.selected_category = Some(cat.id);
492                if let Ok(Some(category)) = app.storage.categories.get_category(cat.id) {
493                    app.open_dialog(ActiveDialog::Confirm(format!(
494                        "Delete category '{}'?",
495                        category.name
496                    )));
497                }
498            }
499        }
500
501        // Open unified budget dialog (period budget + target)
502        KeyCode::Enter | KeyCode::Char('b') | KeyCode::Char('t') => {
503            if let Some(cat) = categories.get(app.selected_category_index) {
504                app.selected_category = Some(cat.id);
505                app.open_dialog(ActiveDialog::Budget);
506            }
507        }
508
509        _ => {}
510    }
511
512    Ok(())
513}
514
515/// Handle keys in the reports view
516fn handle_reports_view_key(_app: &mut App, _key: KeyEvent) -> Result<()> {
517    // Reports view keys will be added later
518    Ok(())
519}
520
521/// Handle keys in the reconcile view
522fn handle_reconcile_view_key(app: &mut App, key: KeyEvent) -> Result<()> {
523    // Delegate to the reconcile view's key handler
524    super::views::reconcile::handle_key(app, key.code);
525    Ok(())
526}
527
528/// Handle keys in editing mode
529fn handle_editing_key(app: &mut App, key: KeyEvent) -> Result<()> {
530    match key.code {
531        KeyCode::Esc => {
532            app.input_mode = InputMode::Normal;
533        }
534        _ => {
535            // Pass to dialog if active
536        }
537    }
538    Ok(())
539}
540
541/// Handle keys in command mode (command palette)
542fn handle_command_key(app: &mut App, key: KeyEvent) -> Result<()> {
543    match key.code {
544        KeyCode::Esc => {
545            app.close_dialog();
546        }
547        KeyCode::Enter => {
548            // Get filtered commands (same logic as render)
549            let filtered_commands: Vec<&crate::tui::commands::Command> = COMMANDS
550                .iter()
551                .filter(|cmd| {
552                    if app.command_input.is_empty() {
553                        true
554                    } else {
555                        let query = app.command_input.to_lowercase();
556                        cmd.name.to_lowercase().contains(&query)
557                            || cmd.description.to_lowercase().contains(&query)
558                    }
559                })
560                .collect();
561
562            // Get the selected command
563            if !filtered_commands.is_empty() {
564                let selected_idx = app
565                    .selected_command_index
566                    .min(filtered_commands.len().saturating_sub(1));
567                let command = filtered_commands[selected_idx];
568                let action = command.action;
569
570                // Close dialog first
571                app.close_dialog();
572
573                // Execute the command action
574                execute_command_action(app, action)?;
575            } else {
576                app.close_dialog();
577            }
578        }
579        KeyCode::Char(c) => {
580            app.command_input.push(c);
581            // Reset selection when input changes
582            app.selected_command_index = 0;
583        }
584        KeyCode::Backspace => {
585            app.command_input.pop();
586            // Reset selection when input changes
587            app.selected_command_index = 0;
588        }
589        KeyCode::Up => {
590            if app.selected_command_index > 0 {
591                app.selected_command_index -= 1;
592            }
593        }
594        KeyCode::Down => {
595            // Get filtered count to bound selection
596            let filtered_count = COMMANDS
597                .iter()
598                .filter(|cmd| {
599                    if app.command_input.is_empty() {
600                        true
601                    } else {
602                        let query = app.command_input.to_lowercase();
603                        cmd.name.to_lowercase().contains(&query)
604                            || cmd.description.to_lowercase().contains(&query)
605                    }
606                })
607                .count();
608            if app.selected_command_index + 1 < filtered_count {
609                app.selected_command_index += 1;
610            }
611        }
612        _ => {}
613    }
614    Ok(())
615}
616
617/// Execute a command action from the command palette
618fn execute_command_action(app: &mut App, action: CommandAction) -> Result<()> {
619    match action {
620        // Navigation
621        CommandAction::ViewAccounts => {
622            app.switch_view(ActiveView::Accounts);
623        }
624        CommandAction::ViewBudget => {
625            app.switch_view(ActiveView::Budget);
626        }
627        CommandAction::ViewReports => {
628            app.switch_view(ActiveView::Reports);
629        }
630        CommandAction::ViewRegister => {
631            app.switch_view(ActiveView::Register);
632        }
633
634        // Account operations
635        CommandAction::AddAccount => {
636            app.open_dialog(ActiveDialog::AddAccount);
637        }
638        CommandAction::EditAccount => {
639            if let Ok(accounts) = app.storage.accounts.get_active() {
640                if let Some(account) = accounts.get(app.selected_account_index) {
641                    app.open_dialog(ActiveDialog::EditAccount(account.id));
642                }
643            }
644        }
645        CommandAction::ArchiveAccount => {
646            // Archive selected account with confirmation
647            if let Ok(accounts) = app.storage.accounts.get_active() {
648                if let Some(account) = accounts.get(app.selected_account_index) {
649                    app.open_dialog(ActiveDialog::Confirm(format!(
650                        "Archive account '{}'?",
651                        account.name
652                    )));
653                } else {
654                    app.set_status("No account selected".to_string());
655                }
656            }
657        }
658
659        // Transaction operations
660        CommandAction::AddTransaction => {
661            app.open_dialog(ActiveDialog::AddTransaction);
662        }
663        CommandAction::EditTransaction => {
664            if let Some(tx_id) = app.selected_transaction {
665                app.open_dialog(ActiveDialog::EditTransaction(tx_id));
666            } else {
667                app.set_status("No transaction selected".to_string());
668            }
669        }
670        CommandAction::DeleteTransaction => {
671            if app.selected_transaction.is_some() {
672                app.open_dialog(ActiveDialog::Confirm("Delete transaction?".to_string()));
673            } else {
674                app.set_status("No transaction selected".to_string());
675            }
676        }
677        CommandAction::ClearTransaction => {
678            // Toggle cleared status for selected transaction
679            if let Some(txn_id) = app.selected_transaction {
680                if let Ok(Some(txn)) = app.storage.transactions.get(txn_id) {
681                    use crate::models::TransactionStatus;
682                    let new_status = match txn.status {
683                        TransactionStatus::Pending => TransactionStatus::Cleared,
684                        TransactionStatus::Cleared => TransactionStatus::Pending,
685                        TransactionStatus::Reconciled => TransactionStatus::Reconciled,
686                    };
687                    if new_status != txn.status {
688                        let mut txn = txn.clone();
689                        txn.set_status(new_status);
690                        let _ = app.storage.transactions.upsert(txn);
691                        let _ = app.storage.transactions.save();
692                        app.set_status(format!("Transaction marked as {}", new_status));
693                    }
694                }
695            } else {
696                app.set_status("No transaction selected".to_string());
697            }
698        }
699
700        // Budget operations
701        CommandAction::MoveFunds => {
702            app.open_dialog(ActiveDialog::MoveFunds);
703        }
704        CommandAction::AssignBudget => {
705            // Open unified budget dialog for the selected category
706            if app.selected_category.is_some() {
707                app.open_dialog(ActiveDialog::Budget);
708            } else {
709                app.set_status("No category selected. Switch to Budget view first.".to_string());
710            }
711        }
712        CommandAction::NextPeriod => {
713            app.next_period();
714        }
715        CommandAction::PrevPeriod => {
716            app.prev_period();
717        }
718
719        // Category operations
720        CommandAction::AddCategory => {
721            // Initialize category form with available groups
722            let groups: Vec<_> = app
723                .storage
724                .categories
725                .get_all_groups()
726                .unwrap_or_default()
727                .into_iter()
728                .map(|g| (g.id, g.name.clone()))
729                .collect();
730            app.category_form.init_with_groups(groups);
731            app.open_dialog(ActiveDialog::AddCategory);
732        }
733        CommandAction::AddGroup => {
734            // Reset group form
735            app.group_form = super::dialogs::group::GroupFormState::new();
736            app.open_dialog(ActiveDialog::AddGroup);
737        }
738        CommandAction::EditCategory => {
739            // Open EditCategory dialog for the selected category
740            if let Some(category_id) = app.selected_category {
741                app.open_dialog(ActiveDialog::EditCategory(category_id));
742            } else {
743                app.set_status("No category selected. Switch to Budget view first.".to_string());
744            }
745        }
746        CommandAction::DeleteCategory => {
747            // Delete selected category with confirmation
748            if let Some(category_id) = app.selected_category {
749                if let Ok(Some(category)) = app.storage.categories.get_category(category_id) {
750                    app.open_dialog(ActiveDialog::Confirm(format!(
751                        "Delete category '{}'?",
752                        category.name
753                    )));
754                }
755            } else {
756                app.set_status("No category selected".to_string());
757            }
758        }
759        CommandAction::EditGroup => {
760            // Edit the group of the currently selected category
761            if let Some(category_id) = app.selected_category {
762                if let Ok(Some(category)) = app.storage.categories.get_category(category_id) {
763                    app.open_dialog(ActiveDialog::EditGroup(category.group_id));
764                }
765            } else {
766                app.set_status("No category selected. Switch to Budget view first.".to_string());
767            }
768        }
769        CommandAction::DeleteGroup => {
770            // Delete the group of the currently selected category with confirmation
771            if let Some(category_id) = app.selected_category {
772                if let Ok(Some(category)) = app.storage.categories.get_category(category_id) {
773                    if let Ok(Some(group)) = app.storage.categories.get_group(category.group_id) {
774                        // Check if group has categories
775                        let categories = app
776                            .storage
777                            .categories
778                            .get_categories_in_group(group.id)
779                            .unwrap_or_default();
780                        let warning = if categories.is_empty() {
781                            format!("Delete group '{}'?", group.name)
782                        } else {
783                            format!(
784                                "Delete group '{}' and its {} categories?",
785                                group.name,
786                                categories.len()
787                            )
788                        };
789                        app.open_dialog(ActiveDialog::Confirm(warning));
790                    }
791                }
792            } else {
793                app.set_status("No category selected".to_string());
794            }
795        }
796
797        // General
798        CommandAction::Help => {
799            app.open_dialog(ActiveDialog::Help);
800        }
801        CommandAction::Quit => {
802            app.quit();
803        }
804        CommandAction::Refresh => {
805            // Reload all data from disk
806            if let Err(e) = app.storage.accounts.load() {
807                app.set_status(format!("Failed to refresh accounts: {}", e));
808                return Ok(());
809            }
810            if let Err(e) = app.storage.transactions.load() {
811                app.set_status(format!("Failed to refresh transactions: {}", e));
812                return Ok(());
813            }
814            if let Err(e) = app.storage.categories.load() {
815                app.set_status(format!("Failed to refresh categories: {}", e));
816                return Ok(());
817            }
818            if let Err(e) = app.storage.budget.load() {
819                app.set_status(format!("Failed to refresh budget: {}", e));
820                return Ok(());
821            }
822            app.set_status("Data refreshed from disk".to_string());
823        }
824        CommandAction::ToggleArchived => {
825            app.show_archived = !app.show_archived;
826        }
827
828        // Target operations
829        CommandAction::AutoFillTargets => {
830            use crate::services::BudgetService;
831            let budget_service = BudgetService::new(app.storage);
832            match budget_service.auto_fill_all_targets(&app.current_period) {
833                Ok(allocations) => {
834                    if allocations.is_empty() {
835                        app.set_status("No targets to auto-fill".to_string());
836                    } else {
837                        let count = allocations.len();
838                        let plural = if count == 1 { "category" } else { "categories" };
839                        app.set_status(format!("{} {} updated from targets", count, plural));
840                    }
841                }
842                Err(e) => {
843                    app.set_status(format!("Auto-fill failed: {}", e));
844                }
845            }
846        }
847    }
848    Ok(())
849}
850
851/// Handle keys when a dialog is open
852fn handle_dialog_key(app: &mut App, key: KeyEvent) -> Result<()> {
853    match &app.active_dialog {
854        ActiveDialog::Help => {
855            // Close help on any key
856            app.close_dialog();
857        }
858        ActiveDialog::CommandPalette => {
859            handle_command_key(app, key)?;
860        }
861        ActiveDialog::Confirm(msg) => {
862            let msg = msg.clone();
863            match key.code {
864                KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter => {
865                    // Execute confirmed action based on the message
866                    app.close_dialog();
867                    execute_confirmed_action(app, &msg)?;
868                }
869                KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
870                    app.close_dialog();
871                }
872                _ => {}
873            }
874        }
875        ActiveDialog::AddTransaction | ActiveDialog::EditTransaction(_) => {
876            // Delegate to transaction dialog key handler
877            super::dialogs::transaction::handle_key(app, key);
878        }
879        ActiveDialog::MoveFunds => {
880            super::dialogs::move_funds::handle_key(app, key);
881        }
882        ActiveDialog::BulkCategorize => {
883            super::dialogs::bulk_categorize::handle_key(app, key);
884        }
885        ActiveDialog::ReconcileStart => {
886            match key.code {
887                KeyCode::Esc => {
888                    app.close_dialog();
889                }
890                KeyCode::Enter => {
891                    // Start reconciliation
892                    app.close_dialog();
893                    app.switch_view(ActiveView::Reconcile);
894                }
895                _ => {
896                    super::dialogs::reconcile_start::handle_key(app, key.code);
897                }
898            }
899        }
900        ActiveDialog::UnlockConfirm(_) => {
901            match key.code {
902                KeyCode::Char('y') | KeyCode::Char('Y') => {
903                    // Unlock the transaction
904                    app.close_dialog();
905                }
906                KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
907                    app.close_dialog();
908                }
909                _ => {}
910            }
911        }
912        ActiveDialog::Adjustment => {
913            match key.code {
914                KeyCode::Esc => {
915                    app.close_dialog();
916                }
917                KeyCode::Enter => {
918                    // Create adjustment
919                    app.close_dialog();
920                }
921                _ => {
922                    super::dialogs::adjustment::handle_key(app, key.code);
923                }
924            }
925        }
926        ActiveDialog::Budget => {
927            super::dialogs::budget::handle_key(app, key);
928        }
929        ActiveDialog::AddAccount | ActiveDialog::EditAccount(_) => {
930            super::dialogs::account::handle_key(app, key);
931        }
932        ActiveDialog::AddCategory | ActiveDialog::EditCategory(_) => {
933            super::dialogs::category::handle_key(app, key);
934        }
935        ActiveDialog::AddGroup | ActiveDialog::EditGroup(_) => {
936            super::dialogs::group::handle_key(app, key);
937        }
938        ActiveDialog::None => {}
939    }
940    Ok(())
941}
942
943/// Execute an action after user confirmation
944fn execute_confirmed_action(app: &mut App, message: &str) -> Result<()> {
945    // Bulk delete transactions
946    if message.contains("Delete")
947        && message.contains("transaction")
948        && !app.selected_transactions.is_empty()
949    {
950        let transaction_ids = app.selected_transactions.clone();
951        let mut deleted_count = 0;
952        let mut error_count = 0;
953
954        for txn_id in &transaction_ids {
955            if app.storage.transactions.delete(*txn_id).is_err() {
956                error_count += 1;
957            } else {
958                deleted_count += 1;
959            }
960        }
961
962        let _ = app.storage.transactions.save();
963        app.selected_transactions.clear();
964        app.multi_select_mode = false;
965
966        if error_count > 0 {
967            app.set_status(format!(
968                "Deleted {} transaction(s), {} failed",
969                deleted_count, error_count
970            ));
971        } else {
972            app.set_status(format!("Deleted {} transaction(s)", deleted_count));
973        }
974    }
975    // Delete single transaction
976    else if message.contains("Delete") && message.contains("transaction") {
977        if let Some(txn_id) = app.selected_transaction {
978            if let Err(e) = app.storage.transactions.delete(txn_id) {
979                app.set_status(format!("Failed to delete: {}", e));
980            } else {
981                let _ = app.storage.transactions.save();
982                app.selected_transaction = None;
983                app.set_status("Transaction deleted".to_string());
984            }
985        }
986    }
987    // Archive account
988    else if message.contains("Archive account") {
989        if let Ok(accounts) = app.storage.accounts.get_active() {
990            if let Some(account) = accounts.get(app.selected_account_index) {
991                let mut account = account.clone();
992                account.archive();
993                if let Err(e) = app.storage.accounts.upsert(account.clone()) {
994                    app.set_status(format!("Failed to archive: {}", e));
995                } else {
996                    let _ = app.storage.accounts.save();
997                    app.set_status(format!("Account '{}' archived", account.name));
998                    // Reset selection
999                    app.selected_account_index = 0;
1000                    if let Ok(active) = app.storage.accounts.get_active() {
1001                        app.selected_account = active.first().map(|a| a.id);
1002                    }
1003                }
1004            }
1005        }
1006    }
1007    // Delete category
1008    else if message.contains("Delete category") {
1009        if let Some(category_id) = app.selected_category {
1010            use crate::services::CategoryService;
1011            let category_service = CategoryService::new(app.storage);
1012            match category_service.delete_category(category_id) {
1013                Ok(()) => {
1014                    app.set_status("Category deleted".to_string());
1015                    app.selected_category = None;
1016                    app.selected_category_index = 0;
1017                }
1018                Err(e) => {
1019                    app.set_status(format!("Failed to delete: {}", e));
1020                }
1021            }
1022        }
1023    }
1024    // Delete group
1025    else if message.contains("Delete group") {
1026        if let Some(category_id) = app.selected_category {
1027            use crate::services::CategoryService;
1028            if let Ok(Some(category)) = app.storage.categories.get_category(category_id) {
1029                let group_id = category.group_id;
1030                let category_service = CategoryService::new(app.storage);
1031                // force_delete_categories = true since user confirmed
1032                match category_service.delete_group(group_id, true) {
1033                    Ok(()) => {
1034                        app.set_status("Category group deleted".to_string());
1035                        app.selected_category = None;
1036                        app.selected_category_index = 0;
1037                    }
1038                    Err(e) => {
1039                        app.set_status(format!("Failed to delete: {}", e));
1040                    }
1041                }
1042            }
1043        }
1044    }
1045
1046    Ok(())
1047}