diff --git a/docs/IMPLEMENTATION_PLAN.md b/docs/IMPLEMENTATION_PLAN.md index 50c0f3c..7af3e30 100644 --- a/docs/IMPLEMENTATION_PLAN.md +++ b/docs/IMPLEMENTATION_PLAN.md @@ -1709,7 +1709,7 @@ public enum CashBookEntryType { Income = 0, Expense = 1 } | ✓ | K8 | Application | Service Implementations | 4 | | ✓ | K9 | Application | Category Seeder | 1 | | ✓ | K10 | Application | Day Close Integration | 1 | -| ☐ | K11 | Web | Fluxor CategoryState | 4 | +| ✓ | K11 | Web | Fluxor CategoryState | 4 | | ☐ | K12 | Web | Fluxor CashBookState | 4 | | ☐ | K13 | Web | CashBook UI | 3 | | ☐ | K14 | Web | Categories UI | 2 | diff --git a/src/Koogle.Web/Store/CategoryState/CategoryActions.cs b/src/Koogle.Web/Store/CategoryState/CategoryActions.cs new file mode 100644 index 0000000..c53fd5b --- /dev/null +++ b/src/Koogle.Web/Store/CategoryState/CategoryActions.cs @@ -0,0 +1,73 @@ +using Koogle.Application.DTOs; + +namespace Koogle.Web.Store.CategoryState; + +/// +/// Action to load all booking categories for the current club. +/// +public record LoadCategoriesAction(bool IncludeInactive = false); + +/// +/// Action dispatched when categories are loaded successfully. +/// +public record LoadCategoriesSuccessAction(IReadOnlyList Categories); + +/// +/// Action dispatched when loading categories fails. +/// +public record LoadCategoriesFailureAction(string Error); + +/// +/// Action to create a new booking category. +/// +public record CreateCategoryAction(CreateBookingCategoryDto Dto); + +/// +/// Action dispatched when category is created successfully. +/// +public record CreateCategorySuccessAction(BookingCategoryDto Category); + +/// +/// Action dispatched when creating category fails. +/// +public record CreateCategoryFailureAction(string Error); + +/// +/// Action to update an existing booking category. +/// +public record UpdateCategoryAction(UpdateBookingCategoryDto Dto); + +/// +/// Action dispatched when category is updated successfully. +/// +public record UpdateCategorySuccessAction(BookingCategoryDto Category); + +/// +/// Action dispatched when updating category fails. +/// +public record UpdateCategoryFailureAction(string Error); + +/// +/// Action to delete a booking category. +/// +public record DeleteCategoryAction(Guid Id); + +/// +/// Action dispatched when category is deleted successfully. +/// +public record DeleteCategorySuccessAction(Guid Id); + +/// +/// Action dispatched when deleting category fails. +/// +public record DeleteCategoryFailureAction(string Error); + +/// +/// Action to select a category for editing. +/// +public record SelectCategoryAction(BookingCategoryDto? Category); + +/// +/// Action to clear category error state. +/// +public record ClearCategoryErrorAction; diff --git a/src/Koogle.Web/Store/CategoryState/CategoryEffects.cs b/src/Koogle.Web/Store/CategoryState/CategoryEffects.cs new file mode 100644 index 0000000..a8175af --- /dev/null +++ b/src/Koogle.Web/Store/CategoryState/CategoryEffects.cs @@ -0,0 +1,112 @@ +using Fluxor; +using Koogle.Application.Interfaces; + +namespace Koogle.Web.Store.CategoryState; + +/// +/// Side effects for booking category state management. +/// Handles async operations like API calls. +/// +public class CategoryEffects +{ + private readonly IBookingCategoryService _categoryService; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the CategoryEffects class. + /// + public CategoryEffects(IBookingCategoryService categoryService, ILogger logger) + { + _categoryService = categoryService; + _logger = logger; + } + + /// + /// Handles LoadCategoriesAction - loads all categories from service. + /// + [EffectMethod] + public async Task HandleLoadCategories(LoadCategoriesAction action, IDispatcher dispatcher) + { + try + { + _logger.LogDebug("Loading booking categories"); + var categories = await _categoryService.GetAllAsync(action.IncludeInactive); + dispatcher.Dispatch(new LoadCategoriesSuccessAction(categories)); + _logger.LogInformation("Loaded {Count} booking categories", categories.Count); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to load booking categories"); + dispatcher.Dispatch(new LoadCategoriesFailureAction(ex.Message)); + } + } + + /// + /// Handles CreateCategoryAction - creates a new category. + /// + [EffectMethod] + public async Task HandleCreateCategory(CreateCategoryAction action, IDispatcher dispatcher) + { + try + { + _logger.LogDebug("Creating booking category {Name}", action.Dto.Name); + var category = await _categoryService.CreateAsync(action.Dto); + dispatcher.Dispatch(new CreateCategorySuccessAction(category)); + _logger.LogInformation("Created booking category {Name} with ID {Id}", category.Name, category.Id); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create booking category {Name}", action.Dto.Name); + dispatcher.Dispatch(new CreateCategoryFailureAction(ex.Message)); + } + } + + /// + /// Handles UpdateCategoryAction - updates an existing category. + /// + [EffectMethod] + public async Task HandleUpdateCategory(UpdateCategoryAction action, IDispatcher dispatcher) + { + try + { + _logger.LogDebug("Updating booking category {Id}", action.Dto.Id); + var category = await _categoryService.UpdateAsync(action.Dto); + dispatcher.Dispatch(new UpdateCategorySuccessAction(category)); + _logger.LogInformation("Updated booking category {Name}", category.Name); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to update booking category {Id}", action.Dto.Id); + dispatcher.Dispatch(new UpdateCategoryFailureAction(ex.Message)); + } + } + + /// + /// Handles DeleteCategoryAction - deletes a category. + /// + [EffectMethod] + public async Task HandleDeleteCategory(DeleteCategoryAction action, IDispatcher dispatcher) + { + try + { + _logger.LogDebug("Deleting booking category {Id}", action.Id); + var success = await _categoryService.DeleteAsync(action.Id); + + if (success) + { + dispatcher.Dispatch(new DeleteCategorySuccessAction(action.Id)); + _logger.LogInformation("Deleted booking category {Id}", action.Id); + } + else + { + dispatcher.Dispatch(new DeleteCategoryFailureAction("Kategorie konnte nicht gelöscht werden.")); + _logger.LogWarning("Failed to delete booking category {Id} - not found or system category", action.Id); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to delete booking category {Id}", action.Id); + dispatcher.Dispatch(new DeleteCategoryFailureAction(ex.Message)); + } + } +} diff --git a/src/Koogle.Web/Store/CategoryState/CategoryReducers.cs b/src/Koogle.Web/Store/CategoryState/CategoryReducers.cs new file mode 100644 index 0000000..1107f8f --- /dev/null +++ b/src/Koogle.Web/Store/CategoryState/CategoryReducers.cs @@ -0,0 +1,163 @@ +using Fluxor; + +namespace Koogle.Web.Store.CategoryState; + +/// +/// Reducers for booking category state management. +/// +public static class CategoryReducers +{ + /// + /// Handles LoadCategoriesAction - sets loading state. + /// + [ReducerMethod(typeof(LoadCategoriesAction))] + public static CategoryState OnLoadCategories(CategoryState state) + => state with + { + IsLoading = true, + Error = null + }; + + /// + /// Handles LoadCategoriesSuccessAction - updates categories list. + /// + [ReducerMethod] + public static CategoryState OnLoadCategoriesSuccess(CategoryState state, LoadCategoriesSuccessAction action) + => state with + { + Categories = action.Categories, + IsLoading = false + }; + + /// + /// Handles LoadCategoriesFailureAction - sets error state. + /// + [ReducerMethod] + public static CategoryState OnLoadCategoriesFailure(CategoryState state, LoadCategoriesFailureAction action) + => state with + { + IsLoading = false, + Error = action.Error + }; + + /// + /// Handles CreateCategoryAction - sets loading state. + /// + [ReducerMethod(typeof(CreateCategoryAction))] + public static CategoryState OnCreateCategory(CategoryState state) + => state with + { + IsLoading = true, + Error = null + }; + + /// + /// Handles CreateCategorySuccessAction - adds category to list. + /// + [ReducerMethod] + public static CategoryState OnCreateCategorySuccess(CategoryState state, CreateCategorySuccessAction action) + => state with + { + Categories = [.. state.Categories, action.Category], + IsLoading = false + }; + + /// + /// Handles CreateCategoryFailureAction - sets error state. + /// + [ReducerMethod] + public static CategoryState OnCreateCategoryFailure(CategoryState state, CreateCategoryFailureAction action) + => state with + { + IsLoading = false, + Error = action.Error + }; + + /// + /// Handles UpdateCategoryAction - sets loading state. + /// + [ReducerMethod(typeof(UpdateCategoryAction))] + public static CategoryState OnUpdateCategory(CategoryState state) + => state with + { + IsLoading = true, + Error = null + }; + + /// + /// Handles UpdateCategorySuccessAction - replaces category in list. + /// + [ReducerMethod] + public static CategoryState OnUpdateCategorySuccess(CategoryState state, UpdateCategorySuccessAction action) + => state with + { + Categories = state.Categories.Select(c => c.Id == action.Category.Id ? action.Category : c).ToList(), + SelectedCategory = state.SelectedCategory?.Id == action.Category.Id ? action.Category : state.SelectedCategory, + IsLoading = false + }; + + /// + /// Handles UpdateCategoryFailureAction - sets error state. + /// + [ReducerMethod] + public static CategoryState OnUpdateCategoryFailure(CategoryState state, UpdateCategoryFailureAction action) + => state with + { + IsLoading = false, + Error = action.Error + }; + + /// + /// Handles DeleteCategoryAction - sets loading state. + /// + [ReducerMethod(typeof(DeleteCategoryAction))] + public static CategoryState OnDeleteCategory(CategoryState state) + => state with + { + IsLoading = true, + Error = null + }; + + /// + /// Handles DeleteCategorySuccessAction - removes category from list. + /// + [ReducerMethod] + public static CategoryState OnDeleteCategorySuccess(CategoryState state, DeleteCategorySuccessAction action) + => state with + { + Categories = state.Categories.Where(c => c.Id != action.Id).ToList(), + SelectedCategory = state.SelectedCategory?.Id == action.Id ? null : state.SelectedCategory, + IsLoading = false + }; + + /// + /// Handles DeleteCategoryFailureAction - sets error state. + /// + [ReducerMethod] + public static CategoryState OnDeleteCategoryFailure(CategoryState state, DeleteCategoryFailureAction action) + => state with + { + IsLoading = false, + Error = action.Error + }; + + /// + /// Handles SelectCategoryAction - sets selected category. + /// + [ReducerMethod] + public static CategoryState OnSelectCategory(CategoryState state, SelectCategoryAction action) + => state with + { + SelectedCategory = action.Category + }; + + /// + /// Handles ClearCategoryErrorAction - clears error state. + /// + [ReducerMethod(typeof(ClearCategoryErrorAction))] + public static CategoryState OnClearError(CategoryState state) + => state with + { + Error = null + }; +} diff --git a/src/Koogle.Web/Store/CategoryState/CategoryState.cs b/src/Koogle.Web/Store/CategoryState/CategoryState.cs new file mode 100644 index 0000000..30b4fb2 --- /dev/null +++ b/src/Koogle.Web/Store/CategoryState/CategoryState.cs @@ -0,0 +1,47 @@ +using Fluxor; +using Koogle.Application.DTOs; + +namespace Koogle.Web.Store.CategoryState; + +/// +/// Fluxor state for booking category management. +/// +[FeatureState] +public record CategoryState +{ + /// + /// List of all booking categories for the current club. + /// + public IReadOnlyList Categories { get; init; } = []; + + /// + /// Currently selected category for editing. + /// + public BookingCategoryDto? SelectedCategory { get; init; } + + /// + /// Indicates whether a category operation is in progress. + /// + public bool IsLoading { get; init; } + + /// + /// Error message if operation failed. + /// + public string? Error { get; init; } + + /// + /// Private constructor for Fluxor initialization. + /// + private CategoryState() { } + + /// + /// Creates the initial state. + /// + public static CategoryState Initial => new() + { + Categories = [], + SelectedCategory = null, + IsLoading = false, + Error = null + }; +}