Skip to content

Commit 68f602c

Browse files
committed
✨ feat: add support for editing and deleting category groups
1 parent 421971d commit 68f602c

5 files changed

Lines changed: 179 additions & 15 deletions

File tree

src/tui/app.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
55
use crate::config::paths::EnvelopePaths;
66
use crate::config::settings::Settings;
7-
use crate::models::{AccountId, BudgetPeriod, CategoryId, TransactionId};
7+
use crate::models::{AccountId, BudgetPeriod, CategoryGroupId, CategoryId, TransactionId};
88
use crate::storage::Storage;
99

1010
use super::dialogs::account::AccountFormState;
@@ -125,6 +125,7 @@ pub enum ActiveDialog {
125125
AddCategory,
126126
EditCategory(CategoryId),
127127
AddGroup,
128+
EditGroup(CategoryGroupId),
128129
MoveFunds,
129130
CommandPalette,
130131
Help,
@@ -496,6 +497,14 @@ impl<'a> App<'a> {
496497
self.group_form = GroupFormState::new();
497498
self.input_mode = InputMode::Editing;
498499
}
500+
ActiveDialog::EditGroup(group_id) => {
501+
// Load group data into form for editing
502+
if let Ok(Some(group)) = self.storage.categories.get_group(*group_id) {
503+
self.group_form = GroupFormState::new();
504+
self.group_form.init_for_edit(&group);
505+
}
506+
self.input_mode = InputMode::Editing;
507+
}
499508
ActiveDialog::Budget => {
500509
// Initialize unified budget dialog for selected category
501510
if let Some(category_id) = self.selected_category {

src/tui/commands.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ pub enum CommandAction {
4646
AddGroup,
4747
EditCategory,
4848
DeleteCategory,
49+
EditGroup,
50+
DeleteGroup,
4951

5052
// General
5153
Help,
@@ -184,6 +186,18 @@ pub static COMMANDS: &[Command] = &[
184186
shortcut: None,
185187
action: CommandAction::DeleteCategory,
186188
},
189+
Command {
190+
name: "edit-group",
191+
description: "Edit selected category group",
192+
shortcut: Some("E"),
193+
action: CommandAction::EditGroup,
194+
},
195+
Command {
196+
name: "delete-group",
197+
description: "Delete selected category group",
198+
shortcut: Some("D"),
199+
action: CommandAction::DeleteGroup,
200+
},
187201
// General commands
188202
Command {
189203
name: "help",

src/tui/dialogs/group.rs

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use ratatui::{
1111
Frame,
1212
};
1313

14-
use crate::models::CategoryGroup;
14+
use crate::models::{CategoryGroup, CategoryGroupId};
1515
use crate::services::CategoryService;
1616
use crate::tui::app::App;
1717
use crate::tui::layout::centered_rect;
@@ -25,6 +25,9 @@ pub struct GroupFormState {
2525

2626
/// Error message to display
2727
pub error_message: Option<String>,
28+
29+
/// Group ID being edited (None for new group)
30+
pub editing_id: Option<CategoryGroupId>,
2831
}
2932

3033
impl Default for GroupFormState {
@@ -41,9 +44,20 @@ impl GroupFormState {
4144
.label("Name")
4245
.placeholder("Group name (e.g., Bills, Savings)"),
4346
error_message: None,
47+
editing_id: None,
4448
}
4549
}
4650

51+
/// Initialize the form for editing an existing group
52+
pub fn init_for_edit(&mut self, group: &CategoryGroup) {
53+
self.editing_id = Some(group.id);
54+
self.name_input = TextInput::new()
55+
.label("Name")
56+
.placeholder("Group name (e.g., Bills, Savings)")
57+
.content(&group.name);
58+
self.error_message = None;
59+
}
60+
4761
/// Validate the form and return any error
4862
pub fn validate(&self) -> Result<(), String> {
4963
let name = self.name_input.value().trim();
@@ -81,8 +95,15 @@ pub fn render(frame: &mut Frame, app: &mut App) {
8195
// Clear the background
8296
frame.render_widget(Clear, area);
8397

98+
// Choose title based on whether we're editing or adding
99+
let title = if app.group_form.editing_id.is_some() {
100+
" Edit Category Group "
101+
} else {
102+
" Add Category Group "
103+
};
104+
84105
let block = Block::default()
85-
.title(" Add Category Group ")
106+
.title(title)
86107
.title_style(
87108
Style::default()
88109
.fg(Color::Cyan)
@@ -270,19 +291,30 @@ fn save_group(app: &mut App) -> Result<(), String> {
270291
app.group_form.validate()?;
271292

272293
let name = app.group_form.name_input.value().trim().to_string();
273-
274-
// Use CategoryService to create the group
275294
let category_service = CategoryService::new(app.storage);
276-
category_service
277-
.create_group(&name)
278-
.map_err(|e| e.to_string())?;
279295

280-
// Save to disk
281-
app.storage.categories.save().map_err(|e| e.to_string())?;
296+
if let Some(group_id) = app.group_form.editing_id {
297+
// Update existing group
298+
category_service
299+
.update_group(group_id, Some(&name))
300+
.map_err(|e| e.to_string())?;
301+
302+
// Close dialog
303+
app.close_dialog();
304+
app.set_status(format!("Category group '{}' updated", name));
305+
} else {
306+
// Create new group
307+
category_service
308+
.create_group(&name)
309+
.map_err(|e| e.to_string())?;
310+
311+
// Save to disk
312+
app.storage.categories.save().map_err(|e| e.to_string())?;
282313

283-
// Close dialog
284-
app.close_dialog();
285-
app.set_status(format!("Category group '{}' created", name));
314+
// Close dialog
315+
app.close_dialog();
316+
app.set_status(format!("Category group '{}' created", name));
317+
}
286318

287319
Ok(())
288320
}

src/tui/handler.rs

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,57 @@ fn handle_budget_view_key(app: &mut App, key: KeyEvent) -> Result<()> {
447447
app.open_dialog(ActiveDialog::AddGroup);
448448
}
449449

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+
450501
// Open unified budget dialog (period budget + target)
451502
KeyCode::Enter | KeyCode::Char('b') | KeyCode::Char('t') => {
452503
if let Some(cat) = categories.get(app.selected_category_index) {
@@ -705,6 +756,43 @@ fn execute_command_action(app: &mut App, action: CommandAction) -> Result<()> {
705756
app.set_status("No category selected".to_string());
706757
}
707758
}
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+
}
708796

709797
// General
710798
CommandAction::Help => {
@@ -844,7 +932,7 @@ fn handle_dialog_key(app: &mut App, key: KeyEvent) -> Result<()> {
844932
ActiveDialog::AddCategory | ActiveDialog::EditCategory(_) => {
845933
super::dialogs::category::handle_key(app, key);
846934
}
847-
ActiveDialog::AddGroup => {
935+
ActiveDialog::AddGroup | ActiveDialog::EditGroup(_) => {
848936
super::dialogs::group::handle_key(app, key);
849937
}
850938
ActiveDialog::None => {}
@@ -933,6 +1021,27 @@ fn execute_confirmed_action(app: &mut App, message: &str) -> Result<()> {
9331021
}
9341022
}
9351023
}
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+
}
9361045

9371046
Ok(())
9381047
}

src/tui/views/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ fn render_dialog(frame: &mut Frame, app: &mut App) {
9191
ActiveDialog::AddCategory | ActiveDialog::EditCategory(_) => {
9292
dialogs::category::render(frame, app);
9393
}
94-
ActiveDialog::AddGroup => {
94+
ActiveDialog::AddGroup | ActiveDialog::EditGroup(_) => {
9595
dialogs::group::render(frame, app);
9696
}
9797
ActiveDialog::None => {}

0 commit comments

Comments
 (0)