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 KeyCode::Char('i') => {
572 app.pending_g = false;
573 app.open_dialog(ActiveDialog::Income);
574 }
575
576 _ => {
577 app.pending_g = false;
578 }
579 }
580
581 Ok(())
582}
583
584fn handle_reports_view_key(_app: &mut App, _key: KeyEvent) -> Result<()> {
586 Ok(())
588}
589
590fn handle_reconcile_view_key(app: &mut App, key: KeyEvent) -> Result<()> {
592 super::views::reconcile::handle_key(app, key.code);
594 Ok(())
595}
596
597fn handle_editing_key(app: &mut App, key: KeyEvent) -> Result<()> {
599 match key.code {
600 KeyCode::Esc => {
601 app.input_mode = InputMode::Normal;
602 }
603 _ => {
604 }
606 }
607 Ok(())
608}
609
610fn handle_command_key(app: &mut App, key: KeyEvent) -> Result<()> {
612 match key.code {
613 KeyCode::Esc => {
614 app.close_dialog();
615 }
616 KeyCode::Enter => {
617 let filtered_commands: Vec<&crate::tui::commands::Command> = COMMANDS
619 .iter()
620 .filter(|cmd| {
621 if app.command_input.is_empty() {
622 true
623 } else {
624 let query = app.command_input.to_lowercase();
625 cmd.name.to_lowercase().contains(&query)
626 || cmd.description.to_lowercase().contains(&query)
627 }
628 })
629 .collect();
630
631 if !filtered_commands.is_empty() {
633 let selected_idx = app
634 .selected_command_index
635 .min(filtered_commands.len().saturating_sub(1));
636 let command = filtered_commands[selected_idx];
637 let action = command.action;
638
639 app.close_dialog();
641
642 execute_command_action(app, action)?;
644 } else {
645 app.close_dialog();
646 }
647 }
648 KeyCode::Char(c) => {
649 app.command_input.push(c);
650 app.selected_command_index = 0;
652 }
653 KeyCode::Backspace => {
654 app.command_input.pop();
655 app.selected_command_index = 0;
657 }
658 KeyCode::Up => {
659 if app.selected_command_index > 0 {
660 app.selected_command_index -= 1;
661 }
662 }
663 KeyCode::Down => {
664 let filtered_count = COMMANDS
666 .iter()
667 .filter(|cmd| {
668 if app.command_input.is_empty() {
669 true
670 } else {
671 let query = app.command_input.to_lowercase();
672 cmd.name.to_lowercase().contains(&query)
673 || cmd.description.to_lowercase().contains(&query)
674 }
675 })
676 .count();
677 if app.selected_command_index + 1 < filtered_count {
678 app.selected_command_index += 1;
679 }
680 }
681 _ => {}
682 }
683 Ok(())
684}
685
686fn execute_command_action(app: &mut App, action: CommandAction) -> Result<()> {
688 match action {
689 CommandAction::ViewAccounts => {
691 app.switch_view(ActiveView::Accounts);
692 }
693 CommandAction::ViewBudget => {
694 app.switch_view(ActiveView::Budget);
695 }
696 CommandAction::ViewReports => {
697 app.switch_view(ActiveView::Reports);
698 }
699 CommandAction::ViewRegister => {
700 app.switch_view(ActiveView::Register);
701 }
702
703 CommandAction::AddAccount => {
705 app.open_dialog(ActiveDialog::AddAccount);
706 }
707 CommandAction::EditAccount => {
708 if let Ok(accounts) = app.storage.accounts.get_active() {
709 if let Some(account) = accounts.get(app.selected_account_index) {
710 app.open_dialog(ActiveDialog::EditAccount(account.id));
711 }
712 }
713 }
714 CommandAction::ArchiveAccount => {
715 if let Ok(accounts) = app.storage.accounts.get_active() {
717 if let Some(account) = accounts.get(app.selected_account_index) {
718 app.open_dialog(ActiveDialog::Confirm(format!(
719 "Archive account '{}'?",
720 account.name
721 )));
722 } else {
723 app.set_status("No account selected".to_string());
724 }
725 }
726 }
727
728 CommandAction::AddTransaction => {
730 app.open_dialog(ActiveDialog::AddTransaction);
731 }
732 CommandAction::EditTransaction => {
733 if let Some(tx_id) = app.selected_transaction {
734 app.open_dialog(ActiveDialog::EditTransaction(tx_id));
735 } else {
736 app.set_status("No transaction selected".to_string());
737 }
738 }
739 CommandAction::DeleteTransaction => {
740 if app.selected_transaction.is_some() {
741 app.open_dialog(ActiveDialog::Confirm("Delete transaction?".to_string()));
742 } else {
743 app.set_status("No transaction selected".to_string());
744 }
745 }
746 CommandAction::ClearTransaction => {
747 if let Some(txn_id) = app.selected_transaction {
749 if let Ok(Some(txn)) = app.storage.transactions.get(txn_id) {
750 use crate::models::TransactionStatus;
751 let new_status = match txn.status {
752 TransactionStatus::Pending => TransactionStatus::Cleared,
753 TransactionStatus::Cleared => TransactionStatus::Pending,
754 TransactionStatus::Reconciled => TransactionStatus::Reconciled,
755 };
756 if new_status != txn.status {
757 let mut txn = txn.clone();
758 txn.set_status(new_status);
759 let _ = app.storage.transactions.upsert(txn);
760 let _ = app.storage.transactions.save();
761 app.set_status(format!("Transaction marked as {}", new_status));
762 }
763 }
764 } else {
765 app.set_status("No transaction selected".to_string());
766 }
767 }
768
769 CommandAction::MoveFunds => {
771 app.open_dialog(ActiveDialog::MoveFunds);
772 }
773 CommandAction::AssignBudget => {
774 if app.selected_category.is_some() {
776 app.open_dialog(ActiveDialog::Budget);
777 } else {
778 app.set_status("No category selected. Switch to Budget view first.".to_string());
779 }
780 }
781 CommandAction::NextPeriod => {
782 app.next_period();
783 }
784 CommandAction::PrevPeriod => {
785 app.prev_period();
786 }
787
788 CommandAction::SetIncome => {
790 app.open_dialog(ActiveDialog::Income);
791 }
792
793 CommandAction::AddCategory => {
795 let groups: Vec<_> = app
797 .storage
798 .categories
799 .get_all_groups()
800 .unwrap_or_default()
801 .into_iter()
802 .map(|g| (g.id, g.name.clone()))
803 .collect();
804 app.category_form.init_with_groups(groups);
805 app.open_dialog(ActiveDialog::AddCategory);
806 }
807 CommandAction::AddGroup => {
808 app.group_form = super::dialogs::group::GroupFormState::new();
810 app.open_dialog(ActiveDialog::AddGroup);
811 }
812 CommandAction::EditCategory => {
813 if let Some(category_id) = app.selected_category {
815 app.open_dialog(ActiveDialog::EditCategory(category_id));
816 } else {
817 app.set_status("No category selected. Switch to Budget view first.".to_string());
818 }
819 }
820 CommandAction::DeleteCategory => {
821 if let Some(category_id) = app.selected_category {
823 if let Ok(Some(category)) = app.storage.categories.get_category(category_id) {
824 app.open_dialog(ActiveDialog::Confirm(format!(
825 "Delete category '{}'?",
826 category.name
827 )));
828 }
829 } else {
830 app.set_status("No category selected".to_string());
831 }
832 }
833 CommandAction::EditGroup => {
834 if let Some(category_id) = app.selected_category {
836 if let Ok(Some(category)) = app.storage.categories.get_category(category_id) {
837 app.open_dialog(ActiveDialog::EditGroup(category.group_id));
838 }
839 } else {
840 app.set_status("No category selected. Switch to Budget view first.".to_string());
841 }
842 }
843 CommandAction::DeleteGroup => {
844 if let Some(category_id) = app.selected_category {
846 if let Ok(Some(category)) = app.storage.categories.get_category(category_id) {
847 if let Ok(Some(group)) = app.storage.categories.get_group(category.group_id) {
848 let categories = app
850 .storage
851 .categories
852 .get_categories_in_group(group.id)
853 .unwrap_or_default();
854 let warning = if categories.is_empty() {
855 format!("Delete group '{}'?", group.name)
856 } else {
857 format!(
858 "Delete group '{}' and its {} categories?",
859 group.name,
860 categories.len()
861 )
862 };
863 app.open_dialog(ActiveDialog::Confirm(warning));
864 }
865 }
866 } else {
867 app.set_status("No category selected".to_string());
868 }
869 }
870
871 CommandAction::Help => {
873 app.open_dialog(ActiveDialog::Help);
874 }
875 CommandAction::Quit => {
876 app.quit();
877 }
878 CommandAction::Refresh => {
879 if let Err(e) = app.storage.accounts.load() {
881 app.set_status(format!("Failed to refresh accounts: {}", e));
882 return Ok(());
883 }
884 if let Err(e) = app.storage.transactions.load() {
885 app.set_status(format!("Failed to refresh transactions: {}", e));
886 return Ok(());
887 }
888 if let Err(e) = app.storage.categories.load() {
889 app.set_status(format!("Failed to refresh categories: {}", e));
890 return Ok(());
891 }
892 if let Err(e) = app.storage.budget.load() {
893 app.set_status(format!("Failed to refresh budget: {}", e));
894 return Ok(());
895 }
896 app.set_status("Data refreshed from disk".to_string());
897 }
898 CommandAction::ToggleArchived => {
899 app.show_archived = !app.show_archived;
900 }
901
902 CommandAction::AutoFillTargets => {
904 use crate::services::BudgetService;
905 let budget_service = BudgetService::new(app.storage);
906 match budget_service.auto_fill_all_targets(&app.current_period) {
907 Ok(allocations) => {
908 if allocations.is_empty() {
909 app.set_status("No targets to auto-fill".to_string());
910 } else {
911 let count = allocations.len();
912 let plural = if count == 1 { "category" } else { "categories" };
913 app.set_status(format!("{} {} updated from targets", count, plural));
914 }
915 }
916 Err(e) => {
917 app.set_status(format!("Auto-fill failed: {}", e));
918 }
919 }
920 }
921 }
922 Ok(())
923}
924
925fn handle_dialog_key(app: &mut App, key: KeyEvent) -> Result<()> {
927 match &app.active_dialog {
928 ActiveDialog::Help => {
929 app.close_dialog();
931 }
932 ActiveDialog::CommandPalette => {
933 handle_command_key(app, key)?;
934 }
935 ActiveDialog::Confirm(msg) => {
936 let msg = msg.clone();
937 match key.code {
938 KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter => {
939 app.close_dialog();
941 execute_confirmed_action(app, &msg)?;
942 }
943 KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
944 app.close_dialog();
945 }
946 _ => {}
947 }
948 }
949 ActiveDialog::AddTransaction | ActiveDialog::EditTransaction(_) => {
950 super::dialogs::transaction::handle_key(app, key);
952 }
953 ActiveDialog::MoveFunds => {
954 super::dialogs::move_funds::handle_key(app, key);
955 }
956 ActiveDialog::BulkCategorize => {
957 super::dialogs::bulk_categorize::handle_key(app, key);
958 }
959 ActiveDialog::ReconcileStart => {
960 match key.code {
961 KeyCode::Esc => {
962 app.close_dialog();
963 }
964 KeyCode::Enter => {
965 app.close_dialog();
967 app.switch_view(ActiveView::Reconcile);
968 }
969 _ => {
970 super::dialogs::reconcile_start::handle_key(app, key.code);
971 }
972 }
973 }
974 ActiveDialog::UnlockConfirm(_) => {
975 match key.code {
976 KeyCode::Char('y') | KeyCode::Char('Y') => {
977 app.close_dialog();
979 }
980 KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
981 app.close_dialog();
982 }
983 _ => {}
984 }
985 }
986 ActiveDialog::Adjustment => {
987 match key.code {
988 KeyCode::Esc => {
989 app.close_dialog();
990 }
991 KeyCode::Enter => {
992 app.close_dialog();
994 }
995 _ => {
996 super::dialogs::adjustment::handle_key(app, key.code);
997 }
998 }
999 }
1000 ActiveDialog::Budget => {
1001 super::dialogs::budget::handle_key(app, key);
1002 }
1003 ActiveDialog::Income => {
1004 super::dialogs::income::handle_key(app, key);
1005 }
1006 ActiveDialog::AddAccount | ActiveDialog::EditAccount(_) => {
1007 super::dialogs::account::handle_key(app, key);
1008 }
1009 ActiveDialog::AddCategory | ActiveDialog::EditCategory(_) => {
1010 super::dialogs::category::handle_key(app, key);
1011 }
1012 ActiveDialog::AddGroup | ActiveDialog::EditGroup(_) => {
1013 super::dialogs::group::handle_key(app, key);
1014 }
1015 ActiveDialog::None => {}
1016 }
1017 Ok(())
1018}
1019
1020fn execute_confirmed_action(app: &mut App, message: &str) -> Result<()> {
1022 if message.contains("Delete")
1024 && message.contains("transaction")
1025 && !app.selected_transactions.is_empty()
1026 {
1027 let transaction_ids = app.selected_transactions.clone();
1028 let mut deleted_count = 0;
1029 let mut error_count = 0;
1030
1031 for txn_id in &transaction_ids {
1032 if app.storage.transactions.delete(*txn_id).is_err() {
1033 error_count += 1;
1034 } else {
1035 deleted_count += 1;
1036 }
1037 }
1038
1039 let _ = app.storage.transactions.save();
1040 app.selected_transactions.clear();
1041 app.multi_select_mode = false;
1042
1043 if error_count > 0 {
1044 app.set_status(format!(
1045 "Deleted {} transaction(s), {} failed",
1046 deleted_count, error_count
1047 ));
1048 } else {
1049 app.set_status(format!("Deleted {} transaction(s)", deleted_count));
1050 }
1051 }
1052 else if message.contains("Delete") && message.contains("transaction") {
1054 if let Some(txn_id) = app.selected_transaction {
1055 if let Err(e) = app.storage.transactions.delete(txn_id) {
1056 app.set_status(format!("Failed to delete: {}", e));
1057 } else {
1058 let _ = app.storage.transactions.save();
1059 app.selected_transaction = None;
1060 app.set_status("Transaction deleted".to_string());
1061 }
1062 }
1063 }
1064 else if message.contains("Archive account") {
1066 if let Ok(accounts) = app.storage.accounts.get_active() {
1067 if let Some(account) = accounts.get(app.selected_account_index) {
1068 let mut account = account.clone();
1069 account.archive();
1070 if let Err(e) = app.storage.accounts.upsert(account.clone()) {
1071 app.set_status(format!("Failed to archive: {}", e));
1072 } else {
1073 let _ = app.storage.accounts.save();
1074 app.set_status(format!("Account '{}' archived", account.name));
1075 app.selected_account_index = 0;
1077 if let Ok(active) = app.storage.accounts.get_active() {
1078 app.selected_account = active.first().map(|a| a.id);
1079 }
1080 }
1081 }
1082 }
1083 }
1084 else if message.contains("Delete category") {
1086 if let Some(category_id) = app.selected_category {
1087 use crate::services::CategoryService;
1088 let category_service = CategoryService::new(app.storage);
1089 match category_service.delete_category(category_id) {
1090 Ok(()) => {
1091 app.set_status("Category deleted".to_string());
1092 app.selected_category = None;
1093 app.selected_category_index = 0;
1094 }
1095 Err(e) => {
1096 app.set_status(format!("Failed to delete: {}", e));
1097 }
1098 }
1099 }
1100 }
1101 else if message.contains("Delete group") {
1103 if let Some(category_id) = app.selected_category {
1104 use crate::services::CategoryService;
1105 if let Ok(Some(category)) = app.storage.categories.get_category(category_id) {
1106 let group_id = category.group_id;
1107 let category_service = CategoryService::new(app.storage);
1108 match category_service.delete_group(group_id, true) {
1110 Ok(()) => {
1111 app.set_status("Category group deleted".to_string());
1112 app.selected_category = None;
1113 app.selected_category_index = 0;
1114 }
1115 Err(e) => {
1116 app.set_status(format!("Failed to delete: {}", e));
1117 }
1118 }
1119 }
1120 }
1121 }
1122
1123 Ok(())
1124}