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.pending_g = false;
249 app.move_down(txn_count);
250 if let Some(txn) = txns.get(app.selected_transaction_index) {
252 app.selected_transaction = Some(txn.id);
253 }
254 }
255 KeyCode::Char('k') | KeyCode::Up => {
256 app.pending_g = false;
257 app.move_up();
258 if let Some(txn) = txns.get(app.selected_transaction_index) {
260 app.selected_transaction = Some(txn.id);
261 }
262 }
263
264 KeyCode::Char('G') => {
266 app.pending_g = false;
268 if txn_count > 0 {
269 app.selected_transaction_index = txn_count - 1;
270 if let Some(txn) = txns.get(app.selected_transaction_index) {
271 app.selected_transaction = Some(txn.id);
272 }
273 }
274 }
275 KeyCode::Char('g') => {
276 if app.pending_g {
278 app.pending_g = false;
280 app.selected_transaction_index = 0;
281 if let Some(txn) = txns.first() {
282 app.selected_transaction = Some(txn.id);
283 }
284 } else {
285 app.pending_g = true;
287 }
288 }
289
290 KeyCode::Char('a') | KeyCode::Char('n') => {
292 app.pending_g = false;
293 app.open_dialog(ActiveDialog::AddTransaction);
294 }
295
296 KeyCode::Char('e') => {
298 app.pending_g = false;
299 if app.selected_transaction.is_none() {
301 let txns = get_sorted_transactions(app);
302 if let Some(txn) = txns.get(app.selected_transaction_index) {
303 app.selected_transaction = Some(txn.id);
304 }
305 }
306 if let Some(txn_id) = app.selected_transaction {
307 app.open_dialog(ActiveDialog::EditTransaction(txn_id));
308 }
309 }
310 KeyCode::Enter => {
311 app.pending_g = false;
312 if app.selected_transaction.is_none() {
313 let txns = get_sorted_transactions(app);
314 if let Some(txn) = txns.get(app.selected_transaction_index) {
315 app.selected_transaction = Some(txn.id);
316 }
317 }
318 if let Some(txn_id) = app.selected_transaction {
319 app.open_dialog(ActiveDialog::EditTransaction(txn_id));
320 }
321 }
322
323 KeyCode::Char('c') => {
325 app.pending_g = false;
326 if let Some(txn_id) = app.selected_transaction {
327 if let Ok(Some(txn)) = app.storage.transactions.get(txn_id) {
329 use crate::models::TransactionStatus;
330 let new_status = match txn.status {
331 TransactionStatus::Pending => TransactionStatus::Cleared,
332 TransactionStatus::Cleared => TransactionStatus::Pending,
333 TransactionStatus::Reconciled => TransactionStatus::Reconciled,
334 };
335 if new_status != txn.status {
336 let mut txn = txn.clone();
337 txn.set_status(new_status);
338 let _ = app.storage.transactions.upsert(txn);
339 let _ = app.storage.transactions.save();
340 app.set_status(format!("Transaction marked as {}", new_status));
341 }
342 }
343 }
344 }
345
346 KeyCode::Char('d') if key.modifiers.contains(KeyModifiers::CONTROL) => {
348 app.pending_g = false;
349 if app.selected_transaction.is_some() {
350 app.open_dialog(ActiveDialog::Confirm(
351 "Delete this transaction?".to_string(),
352 ));
353 }
354 }
355
356 KeyCode::Char('v') => {
358 app.pending_g = false;
359 app.toggle_multi_select();
360 if app.multi_select_mode {
361 app.set_status("Multi-select mode ON");
362 } else {
363 app.set_status("Multi-select mode OFF");
364 }
365 }
366
367 KeyCode::Char(' ') if app.multi_select_mode => {
369 app.pending_g = false;
370 app.toggle_transaction_selection();
371 }
372
373 KeyCode::Char('C') if app.multi_select_mode && !app.selected_transactions.is_empty() => {
375 app.pending_g = false;
376 app.open_dialog(ActiveDialog::BulkCategorize);
377 }
378
379 KeyCode::Char('D') if app.multi_select_mode && !app.selected_transactions.is_empty() => {
381 app.pending_g = false;
382 let count = app.selected_transactions.len();
383 app.open_dialog(ActiveDialog::Confirm(format!(
384 "Delete {} transaction{}?",
385 count,
386 if count == 1 { "" } else { "s" }
387 )));
388 }
389
390 _ => {
391 app.pending_g = false;
392 }
393 }
394
395 Ok(())
396}
397
398fn get_categories_in_visual_order(app: &App) -> Vec<crate::models::Category> {
400 let groups = app.storage.categories.get_all_groups().unwrap_or_default();
401 let all_categories = app
402 .storage
403 .categories
404 .get_all_categories()
405 .unwrap_or_default();
406
407 let mut result = Vec::new();
408 for group in &groups {
409 let group_cats: Vec<_> = all_categories
410 .iter()
411 .filter(|c| c.group_id == group.id)
412 .cloned()
413 .collect();
414 result.extend(group_cats);
415 }
416 result
417}
418
419fn handle_budget_view_key(app: &mut App, key: KeyEvent) -> Result<()> {
421 let categories = get_categories_in_visual_order(app);
423 let category_count = categories.len();
424
425 match key.code {
426 KeyCode::Char('j') | KeyCode::Down => {
428 app.pending_g = false;
429 app.move_down(category_count);
430 if let Some(cat) = categories.get(app.selected_category_index) {
431 app.selected_category = Some(cat.id);
432 }
433 }
434 KeyCode::Char('k') | KeyCode::Up => {
435 app.pending_g = false;
436 app.move_up();
437 if let Some(cat) = categories.get(app.selected_category_index) {
438 app.selected_category = Some(cat.id);
439 }
440 }
441
442 KeyCode::Char('G') => {
444 app.pending_g = false;
446 if category_count > 0 {
447 app.selected_category_index = category_count - 1;
448 if let Some(cat) = categories.get(app.selected_category_index) {
449 app.selected_category = Some(cat.id);
450 }
451 }
452 }
453 KeyCode::Char('g') => {
454 if app.pending_g {
456 app.pending_g = false;
458 app.selected_category_index = 0;
459 if let Some(cat) = categories.first() {
460 app.selected_category = Some(cat.id);
461 }
462 } else {
463 app.pending_g = true;
465 }
466 }
467
468 KeyCode::Char('[') | KeyCode::Char('H') => {
470 app.pending_g = false;
471 app.prev_period();
472 }
473 KeyCode::Char(']') | KeyCode::Char('L') => {
474 app.pending_g = false;
475 app.next_period();
476 }
477
478 KeyCode::Char('<') | KeyCode::Char(',') => {
480 app.pending_g = false;
481 app.budget_header_display = app.budget_header_display.prev();
482 }
483 KeyCode::Char('>') | KeyCode::Char('.') => {
484 app.pending_g = false;
485 app.budget_header_display = app.budget_header_display.next();
486 }
487
488 KeyCode::Char('m') => {
490 app.pending_g = false;
491 app.open_dialog(ActiveDialog::MoveFunds);
492 }
493
494 KeyCode::Char('a') => {
496 app.pending_g = false;
497 app.open_dialog(ActiveDialog::AddCategory);
498 }
499
500 KeyCode::Char('A') => {
502 app.pending_g = false;
503 app.open_dialog(ActiveDialog::AddGroup);
504 }
505
506 KeyCode::Char('E') => {
508 app.pending_g = false;
509 if let Some(cat) = categories.get(app.selected_category_index) {
510 app.open_dialog(ActiveDialog::EditGroup(cat.group_id));
511 }
512 }
513
514 KeyCode::Char('D') => {
516 app.pending_g = false;
517 if let Some(cat) = categories.get(app.selected_category_index) {
518 if let Ok(Some(group)) = app.storage.categories.get_group(cat.group_id) {
519 let group_categories = app
520 .storage
521 .categories
522 .get_categories_in_group(group.id)
523 .unwrap_or_default();
524 let warning = if group_categories.is_empty() {
525 format!("Delete group '{}'?", group.name)
526 } else {
527 format!(
528 "Delete group '{}' and its {} categories?",
529 group.name,
530 group_categories.len()
531 )
532 };
533 app.open_dialog(ActiveDialog::Confirm(warning));
534 }
535 }
536 }
537
538 KeyCode::Char('e') => {
540 app.pending_g = false;
541 if let Some(cat) = categories.get(app.selected_category_index) {
542 app.selected_category = Some(cat.id);
543 app.open_dialog(ActiveDialog::EditCategory(cat.id));
544 }
545 }
546
547 KeyCode::Char('d') => {
549 app.pending_g = false;
550 if let Some(cat) = categories.get(app.selected_category_index) {
551 app.selected_category = Some(cat.id);
552 if let Ok(Some(category)) = app.storage.categories.get_category(cat.id) {
553 app.open_dialog(ActiveDialog::Confirm(format!(
554 "Delete category '{}'?",
555 category.name
556 )));
557 }
558 }
559 }
560
561 KeyCode::Enter | KeyCode::Char('b') | KeyCode::Char('t') => {
563 app.pending_g = false;
564 if let Some(cat) = categories.get(app.selected_category_index) {
565 app.selected_category = Some(cat.id);
566 app.open_dialog(ActiveDialog::Budget);
567 }
568 }
569
570 _ => {
571 app.pending_g = false;
572 }
573 }
574
575 Ok(())
576}
577
578fn handle_reports_view_key(_app: &mut App, _key: KeyEvent) -> Result<()> {
580 Ok(())
582}
583
584fn handle_reconcile_view_key(app: &mut App, key: KeyEvent) -> Result<()> {
586 super::views::reconcile::handle_key(app, key.code);
588 Ok(())
589}
590
591fn handle_editing_key(app: &mut App, key: KeyEvent) -> Result<()> {
593 match key.code {
594 KeyCode::Esc => {
595 app.input_mode = InputMode::Normal;
596 }
597 _ => {
598 }
600 }
601 Ok(())
602}
603
604fn handle_command_key(app: &mut App, key: KeyEvent) -> Result<()> {
606 match key.code {
607 KeyCode::Esc => {
608 app.close_dialog();
609 }
610 KeyCode::Enter => {
611 let filtered_commands: Vec<&crate::tui::commands::Command> = COMMANDS
613 .iter()
614 .filter(|cmd| {
615 if app.command_input.is_empty() {
616 true
617 } else {
618 let query = app.command_input.to_lowercase();
619 cmd.name.to_lowercase().contains(&query)
620 || cmd.description.to_lowercase().contains(&query)
621 }
622 })
623 .collect();
624
625 if !filtered_commands.is_empty() {
627 let selected_idx = app
628 .selected_command_index
629 .min(filtered_commands.len().saturating_sub(1));
630 let command = filtered_commands[selected_idx];
631 let action = command.action;
632
633 app.close_dialog();
635
636 execute_command_action(app, action)?;
638 } else {
639 app.close_dialog();
640 }
641 }
642 KeyCode::Char(c) => {
643 app.command_input.push(c);
644 app.selected_command_index = 0;
646 }
647 KeyCode::Backspace => {
648 app.command_input.pop();
649 app.selected_command_index = 0;
651 }
652 KeyCode::Up => {
653 if app.selected_command_index > 0 {
654 app.selected_command_index -= 1;
655 }
656 }
657 KeyCode::Down => {
658 let filtered_count = COMMANDS
660 .iter()
661 .filter(|cmd| {
662 if app.command_input.is_empty() {
663 true
664 } else {
665 let query = app.command_input.to_lowercase();
666 cmd.name.to_lowercase().contains(&query)
667 || cmd.description.to_lowercase().contains(&query)
668 }
669 })
670 .count();
671 if app.selected_command_index + 1 < filtered_count {
672 app.selected_command_index += 1;
673 }
674 }
675 _ => {}
676 }
677 Ok(())
678}
679
680fn execute_command_action(app: &mut App, action: CommandAction) -> Result<()> {
682 match action {
683 CommandAction::ViewAccounts => {
685 app.switch_view(ActiveView::Accounts);
686 }
687 CommandAction::ViewBudget => {
688 app.switch_view(ActiveView::Budget);
689 }
690 CommandAction::ViewReports => {
691 app.switch_view(ActiveView::Reports);
692 }
693 CommandAction::ViewRegister => {
694 app.switch_view(ActiveView::Register);
695 }
696
697 CommandAction::AddAccount => {
699 app.open_dialog(ActiveDialog::AddAccount);
700 }
701 CommandAction::EditAccount => {
702 if let Ok(accounts) = app.storage.accounts.get_active() {
703 if let Some(account) = accounts.get(app.selected_account_index) {
704 app.open_dialog(ActiveDialog::EditAccount(account.id));
705 }
706 }
707 }
708 CommandAction::ArchiveAccount => {
709 if let Ok(accounts) = app.storage.accounts.get_active() {
711 if let Some(account) = accounts.get(app.selected_account_index) {
712 app.open_dialog(ActiveDialog::Confirm(format!(
713 "Archive account '{}'?",
714 account.name
715 )));
716 } else {
717 app.set_status("No account selected".to_string());
718 }
719 }
720 }
721
722 CommandAction::AddTransaction => {
724 app.open_dialog(ActiveDialog::AddTransaction);
725 }
726 CommandAction::EditTransaction => {
727 if let Some(tx_id) = app.selected_transaction {
728 app.open_dialog(ActiveDialog::EditTransaction(tx_id));
729 } else {
730 app.set_status("No transaction selected".to_string());
731 }
732 }
733 CommandAction::DeleteTransaction => {
734 if app.selected_transaction.is_some() {
735 app.open_dialog(ActiveDialog::Confirm("Delete transaction?".to_string()));
736 } else {
737 app.set_status("No transaction selected".to_string());
738 }
739 }
740 CommandAction::ClearTransaction => {
741 if let Some(txn_id) = app.selected_transaction {
743 if let Ok(Some(txn)) = app.storage.transactions.get(txn_id) {
744 use crate::models::TransactionStatus;
745 let new_status = match txn.status {
746 TransactionStatus::Pending => TransactionStatus::Cleared,
747 TransactionStatus::Cleared => TransactionStatus::Pending,
748 TransactionStatus::Reconciled => TransactionStatus::Reconciled,
749 };
750 if new_status != txn.status {
751 let mut txn = txn.clone();
752 txn.set_status(new_status);
753 let _ = app.storage.transactions.upsert(txn);
754 let _ = app.storage.transactions.save();
755 app.set_status(format!("Transaction marked as {}", new_status));
756 }
757 }
758 } else {
759 app.set_status("No transaction selected".to_string());
760 }
761 }
762
763 CommandAction::MoveFunds => {
765 app.open_dialog(ActiveDialog::MoveFunds);
766 }
767 CommandAction::AssignBudget => {
768 if app.selected_category.is_some() {
770 app.open_dialog(ActiveDialog::Budget);
771 } else {
772 app.set_status("No category selected. Switch to Budget view first.".to_string());
773 }
774 }
775 CommandAction::NextPeriod => {
776 app.next_period();
777 }
778 CommandAction::PrevPeriod => {
779 app.prev_period();
780 }
781
782 CommandAction::AddCategory => {
784 let groups: Vec<_> = app
786 .storage
787 .categories
788 .get_all_groups()
789 .unwrap_or_default()
790 .into_iter()
791 .map(|g| (g.id, g.name.clone()))
792 .collect();
793 app.category_form.init_with_groups(groups);
794 app.open_dialog(ActiveDialog::AddCategory);
795 }
796 CommandAction::AddGroup => {
797 app.group_form = super::dialogs::group::GroupFormState::new();
799 app.open_dialog(ActiveDialog::AddGroup);
800 }
801 CommandAction::EditCategory => {
802 if let Some(category_id) = app.selected_category {
804 app.open_dialog(ActiveDialog::EditCategory(category_id));
805 } else {
806 app.set_status("No category selected. Switch to Budget view first.".to_string());
807 }
808 }
809 CommandAction::DeleteCategory => {
810 if let Some(category_id) = app.selected_category {
812 if let Ok(Some(category)) = app.storage.categories.get_category(category_id) {
813 app.open_dialog(ActiveDialog::Confirm(format!(
814 "Delete category '{}'?",
815 category.name
816 )));
817 }
818 } else {
819 app.set_status("No category selected".to_string());
820 }
821 }
822 CommandAction::EditGroup => {
823 if let Some(category_id) = app.selected_category {
825 if let Ok(Some(category)) = app.storage.categories.get_category(category_id) {
826 app.open_dialog(ActiveDialog::EditGroup(category.group_id));
827 }
828 } else {
829 app.set_status("No category selected. Switch to Budget view first.".to_string());
830 }
831 }
832 CommandAction::DeleteGroup => {
833 if let Some(category_id) = app.selected_category {
835 if let Ok(Some(category)) = app.storage.categories.get_category(category_id) {
836 if let Ok(Some(group)) = app.storage.categories.get_group(category.group_id) {
837 let categories = app
839 .storage
840 .categories
841 .get_categories_in_group(group.id)
842 .unwrap_or_default();
843 let warning = if categories.is_empty() {
844 format!("Delete group '{}'?", group.name)
845 } else {
846 format!(
847 "Delete group '{}' and its {} categories?",
848 group.name,
849 categories.len()
850 )
851 };
852 app.open_dialog(ActiveDialog::Confirm(warning));
853 }
854 }
855 } else {
856 app.set_status("No category selected".to_string());
857 }
858 }
859
860 CommandAction::Help => {
862 app.open_dialog(ActiveDialog::Help);
863 }
864 CommandAction::Quit => {
865 app.quit();
866 }
867 CommandAction::Refresh => {
868 if let Err(e) = app.storage.accounts.load() {
870 app.set_status(format!("Failed to refresh accounts: {}", e));
871 return Ok(());
872 }
873 if let Err(e) = app.storage.transactions.load() {
874 app.set_status(format!("Failed to refresh transactions: {}", e));
875 return Ok(());
876 }
877 if let Err(e) = app.storage.categories.load() {
878 app.set_status(format!("Failed to refresh categories: {}", e));
879 return Ok(());
880 }
881 if let Err(e) = app.storage.budget.load() {
882 app.set_status(format!("Failed to refresh budget: {}", e));
883 return Ok(());
884 }
885 app.set_status("Data refreshed from disk".to_string());
886 }
887 CommandAction::ToggleArchived => {
888 app.show_archived = !app.show_archived;
889 }
890
891 CommandAction::AutoFillTargets => {
893 use crate::services::BudgetService;
894 let budget_service = BudgetService::new(app.storage);
895 match budget_service.auto_fill_all_targets(&app.current_period) {
896 Ok(allocations) => {
897 if allocations.is_empty() {
898 app.set_status("No targets to auto-fill".to_string());
899 } else {
900 let count = allocations.len();
901 let plural = if count == 1 { "category" } else { "categories" };
902 app.set_status(format!("{} {} updated from targets", count, plural));
903 }
904 }
905 Err(e) => {
906 app.set_status(format!("Auto-fill failed: {}", e));
907 }
908 }
909 }
910 }
911 Ok(())
912}
913
914fn handle_dialog_key(app: &mut App, key: KeyEvent) -> Result<()> {
916 match &app.active_dialog {
917 ActiveDialog::Help => {
918 app.close_dialog();
920 }
921 ActiveDialog::CommandPalette => {
922 handle_command_key(app, key)?;
923 }
924 ActiveDialog::Confirm(msg) => {
925 let msg = msg.clone();
926 match key.code {
927 KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter => {
928 app.close_dialog();
930 execute_confirmed_action(app, &msg)?;
931 }
932 KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
933 app.close_dialog();
934 }
935 _ => {}
936 }
937 }
938 ActiveDialog::AddTransaction | ActiveDialog::EditTransaction(_) => {
939 super::dialogs::transaction::handle_key(app, key);
941 }
942 ActiveDialog::MoveFunds => {
943 super::dialogs::move_funds::handle_key(app, key);
944 }
945 ActiveDialog::BulkCategorize => {
946 super::dialogs::bulk_categorize::handle_key(app, key);
947 }
948 ActiveDialog::ReconcileStart => {
949 match key.code {
950 KeyCode::Esc => {
951 app.close_dialog();
952 }
953 KeyCode::Enter => {
954 app.close_dialog();
956 app.switch_view(ActiveView::Reconcile);
957 }
958 _ => {
959 super::dialogs::reconcile_start::handle_key(app, key.code);
960 }
961 }
962 }
963 ActiveDialog::UnlockConfirm(_) => {
964 match key.code {
965 KeyCode::Char('y') | KeyCode::Char('Y') => {
966 app.close_dialog();
968 }
969 KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
970 app.close_dialog();
971 }
972 _ => {}
973 }
974 }
975 ActiveDialog::Adjustment => {
976 match key.code {
977 KeyCode::Esc => {
978 app.close_dialog();
979 }
980 KeyCode::Enter => {
981 app.close_dialog();
983 }
984 _ => {
985 super::dialogs::adjustment::handle_key(app, key.code);
986 }
987 }
988 }
989 ActiveDialog::Budget => {
990 super::dialogs::budget::handle_key(app, key);
991 }
992 ActiveDialog::AddAccount | ActiveDialog::EditAccount(_) => {
993 super::dialogs::account::handle_key(app, key);
994 }
995 ActiveDialog::AddCategory | ActiveDialog::EditCategory(_) => {
996 super::dialogs::category::handle_key(app, key);
997 }
998 ActiveDialog::AddGroup | ActiveDialog::EditGroup(_) => {
999 super::dialogs::group::handle_key(app, key);
1000 }
1001 ActiveDialog::None => {}
1002 }
1003 Ok(())
1004}
1005
1006fn execute_confirmed_action(app: &mut App, message: &str) -> Result<()> {
1008 if message.contains("Delete")
1010 && message.contains("transaction")
1011 && !app.selected_transactions.is_empty()
1012 {
1013 let transaction_ids = app.selected_transactions.clone();
1014 let mut deleted_count = 0;
1015 let mut error_count = 0;
1016
1017 for txn_id in &transaction_ids {
1018 if app.storage.transactions.delete(*txn_id).is_err() {
1019 error_count += 1;
1020 } else {
1021 deleted_count += 1;
1022 }
1023 }
1024
1025 let _ = app.storage.transactions.save();
1026 app.selected_transactions.clear();
1027 app.multi_select_mode = false;
1028
1029 if error_count > 0 {
1030 app.set_status(format!(
1031 "Deleted {} transaction(s), {} failed",
1032 deleted_count, error_count
1033 ));
1034 } else {
1035 app.set_status(format!("Deleted {} transaction(s)", deleted_count));
1036 }
1037 }
1038 else if message.contains("Delete") && message.contains("transaction") {
1040 if let Some(txn_id) = app.selected_transaction {
1041 if let Err(e) = app.storage.transactions.delete(txn_id) {
1042 app.set_status(format!("Failed to delete: {}", e));
1043 } else {
1044 let _ = app.storage.transactions.save();
1045 app.selected_transaction = None;
1046 app.set_status("Transaction deleted".to_string());
1047 }
1048 }
1049 }
1050 else if message.contains("Archive account") {
1052 if let Ok(accounts) = app.storage.accounts.get_active() {
1053 if let Some(account) = accounts.get(app.selected_account_index) {
1054 let mut account = account.clone();
1055 account.archive();
1056 if let Err(e) = app.storage.accounts.upsert(account.clone()) {
1057 app.set_status(format!("Failed to archive: {}", e));
1058 } else {
1059 let _ = app.storage.accounts.save();
1060 app.set_status(format!("Account '{}' archived", account.name));
1061 app.selected_account_index = 0;
1063 if let Ok(active) = app.storage.accounts.get_active() {
1064 app.selected_account = active.first().map(|a| a.id);
1065 }
1066 }
1067 }
1068 }
1069 }
1070 else if message.contains("Delete category") {
1072 if let Some(category_id) = app.selected_category {
1073 use crate::services::CategoryService;
1074 let category_service = CategoryService::new(app.storage);
1075 match category_service.delete_category(category_id) {
1076 Ok(()) => {
1077 app.set_status("Category deleted".to_string());
1078 app.selected_category = None;
1079 app.selected_category_index = 0;
1080 }
1081 Err(e) => {
1082 app.set_status(format!("Failed to delete: {}", e));
1083 }
1084 }
1085 }
1086 }
1087 else if message.contains("Delete group") {
1089 if let Some(category_id) = app.selected_category {
1090 use crate::services::CategoryService;
1091 if let Ok(Some(category)) = app.storage.categories.get_category(category_id) {
1092 let group_id = category.group_id;
1093 let category_service = CategoryService::new(app.storage);
1094 match category_service.delete_group(group_id, true) {
1096 Ok(()) => {
1097 app.set_status("Category group deleted".to_string());
1098 app.selected_category = None;
1099 app.selected_category_index = 0;
1100 }
1101 Err(e) => {
1102 app.set_status(format!("Failed to delete: {}", e));
1103 }
1104 }
1105 }
1106 }
1107 }
1108
1109 Ok(())
1110}