1use 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
13pub 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 Ok(())
20 }
21 Event::Tick => Ok(()),
22 Event::Resize(_, _) => Ok(()),
23 }
24}
25
26fn handle_key_event(app: &mut App, key: KeyEvent) -> Result<()> {
28 if app.has_dialog() {
30 return handle_dialog_key(app, key);
31 }
32
33 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
41fn handle_normal_key(app: &mut App, key: KeyEvent) -> Result<()> {
43 match key.code {
45 KeyCode::Char('q') | KeyCode::Char('Q') => {
47 app.quit();
48 return Ok(());
49 }
50
51 KeyCode::Char('?') => {
53 app.open_dialog(ActiveDialog::Help);
54 return Ok(());
55 }
56
57 KeyCode::Char(':') | KeyCode::Char('/') => {
59 app.open_dialog(ActiveDialog::CommandPalette);
60 return Ok(());
61 }
62
63 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 match app.focused_panel {
87 FocusedPanel::Sidebar => handle_sidebar_key(app, key),
88 FocusedPanel::Main => handle_main_panel_key(app, key),
89 }
90}
91
92fn handle_sidebar_key(app: &mut App, key: KeyEvent) -> Result<()> {
94 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 KeyCode::Char('j') | KeyCode::Down => {
105 app.move_down(account_count);
106 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 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 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 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 KeyCode::Char('A') => {
141 app.show_archived = !app.show_archived;
142 }
143
144 KeyCode::Char('a') | KeyCode::Char('n') => {
146 app.open_dialog(ActiveDialog::AddAccount);
147 }
148
149 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
164fn 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
175fn 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 app.switch_view(ActiveView::Register);
204 }
205 KeyCode::Char('a') | KeyCode::Char('n') => {
207 app.open_dialog(ActiveDialog::AddAccount);
208 }
209 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
223fn 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 txns.sort_by(|a, b| b.date.cmp(&a.date));
233 txns
234 } else {
235 Vec::new()
236 }
237}
238
239fn handle_register_view_key(app: &mut App, key: KeyEvent) -> Result<()> {
241 let txns = get_sorted_transactions(app);
243 let txn_count = txns.len();
244
245 match key.code {
246 KeyCode::Char('j') | KeyCode::Down => {
248 app.move_down(txn_count);
249 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 if let Some(txn) = txns.get(app.selected_transaction_index) {
258 app.selected_transaction = Some(txn.id);
259 }
260 }
261
262 KeyCode::Char('G') => {
264 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 app.selected_transaction_index = 0;
275 if let Some(txn) = txns.first() {
276 app.selected_transaction = Some(txn.id);
277 }
278 }
279
280 KeyCode::Char('a') | KeyCode::Char('n') => {
282 app.open_dialog(ActiveDialog::AddTransaction);
283 }
284
285 KeyCode::Char('e') => {
287 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 KeyCode::Char('c') => {
312 if let Some(txn_id) = app.selected_transaction {
313 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 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 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 KeyCode::Char(' ') if app.multi_select_mode => {
353 app.toggle_transaction_selection();
354 }
355
356 KeyCode::Char('C') if app.multi_select_mode && !app.selected_transactions.is_empty() => {
358 app.open_dialog(ActiveDialog::BulkCategorize);
359 }
360
361 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
377fn 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
398fn handle_budget_view_key(app: &mut App, key: KeyEvent) -> Result<()> {
400 let categories = get_categories_in_visual_order(app);
402 let category_count = categories.len();
403
404 match key.code {
405 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 KeyCode::Char('[') | KeyCode::Char('H') => {
421 app.prev_period();
422 }
423 KeyCode::Char(']') | KeyCode::Char('L') => {
424 app.next_period();
425 }
426
427 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 KeyCode::Char('m') => {
437 app.open_dialog(ActiveDialog::MoveFunds);
438 }
439
440 KeyCode::Char('a') => {
442 app.open_dialog(ActiveDialog::AddCategory);
443 }
444
445 KeyCode::Char('A') => {
447 app.open_dialog(ActiveDialog::AddGroup);
448 }
449
450 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 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 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 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 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
515fn handle_reports_view_key(_app: &mut App, _key: KeyEvent) -> Result<()> {
517 Ok(())
519}
520
521fn handle_reconcile_view_key(app: &mut App, key: KeyEvent) -> Result<()> {
523 super::views::reconcile::handle_key(app, key.code);
525 Ok(())
526}
527
528fn 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 }
537 }
538 Ok(())
539}
540
541fn 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 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 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 app.close_dialog();
572
573 execute_command_action(app, action)?;
575 } else {
576 app.close_dialog();
577 }
578 }
579 KeyCode::Char(c) => {
580 app.command_input.push(c);
581 app.selected_command_index = 0;
583 }
584 KeyCode::Backspace => {
585 app.command_input.pop();
586 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 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
617fn execute_command_action(app: &mut App, action: CommandAction) -> Result<()> {
619 match action {
620 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 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 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 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 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 CommandAction::MoveFunds => {
702 app.open_dialog(ActiveDialog::MoveFunds);
703 }
704 CommandAction::AssignBudget => {
705 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 CommandAction::AddCategory => {
721 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 app.group_form = super::dialogs::group::GroupFormState::new();
736 app.open_dialog(ActiveDialog::AddGroup);
737 }
738 CommandAction::EditCategory => {
739 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 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 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 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 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 CommandAction::Help => {
799 app.open_dialog(ActiveDialog::Help);
800 }
801 CommandAction::Quit => {
802 app.quit();
803 }
804 CommandAction::Refresh => {
805 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 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
851fn handle_dialog_key(app: &mut App, key: KeyEvent) -> Result<()> {
853 match &app.active_dialog {
854 ActiveDialog::Help => {
855 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 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 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 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 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 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
943fn execute_confirmed_action(app: &mut App, message: &str) -> Result<()> {
945 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 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 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 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 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 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 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}