K11: add Fluxor CategoryState

- CategoryState: categories list, loading, error
- CategoryActions: CRUD + select/clear
- CategoryReducers: state transitions
- CategoryEffects: async service calls
This commit is contained in:
beo3000 2026-01-03 15:05:29 +01:00
parent 595c92df76
commit 6917fa6bed
5 changed files with 396 additions and 1 deletions

View File

@ -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 |

View File

@ -0,0 +1,73 @@
using Koogle.Application.DTOs;
namespace Koogle.Web.Store.CategoryState;
/// <summary>
/// Action to load all booking categories for the current club.
/// </summary>
public record LoadCategoriesAction(bool IncludeInactive = false);
/// <summary>
/// Action dispatched when categories are loaded successfully.
/// </summary>
public record LoadCategoriesSuccessAction(IReadOnlyList<BookingCategoryDto> Categories);
/// <summary>
/// Action dispatched when loading categories fails.
/// </summary>
public record LoadCategoriesFailureAction(string Error);
/// <summary>
/// Action to create a new booking category.
/// </summary>
public record CreateCategoryAction(CreateBookingCategoryDto Dto);
/// <summary>
/// Action dispatched when category is created successfully.
/// </summary>
public record CreateCategorySuccessAction(BookingCategoryDto Category);
/// <summary>
/// Action dispatched when creating category fails.
/// </summary>
public record CreateCategoryFailureAction(string Error);
/// <summary>
/// Action to update an existing booking category.
/// </summary>
public record UpdateCategoryAction(UpdateBookingCategoryDto Dto);
/// <summary>
/// Action dispatched when category is updated successfully.
/// </summary>
public record UpdateCategorySuccessAction(BookingCategoryDto Category);
/// <summary>
/// Action dispatched when updating category fails.
/// </summary>
public record UpdateCategoryFailureAction(string Error);
/// <summary>
/// Action to delete a booking category.
/// </summary>
public record DeleteCategoryAction(Guid Id);
/// <summary>
/// Action dispatched when category is deleted successfully.
/// </summary>
public record DeleteCategorySuccessAction(Guid Id);
/// <summary>
/// Action dispatched when deleting category fails.
/// </summary>
public record DeleteCategoryFailureAction(string Error);
/// <summary>
/// Action to select a category for editing.
/// </summary>
public record SelectCategoryAction(BookingCategoryDto? Category);
/// <summary>
/// Action to clear category error state.
/// </summary>
public record ClearCategoryErrorAction;

View File

@ -0,0 +1,112 @@
using Fluxor;
using Koogle.Application.Interfaces;
namespace Koogle.Web.Store.CategoryState;
/// <summary>
/// Side effects for booking category state management.
/// Handles async operations like API calls.
/// </summary>
public class CategoryEffects
{
private readonly IBookingCategoryService _categoryService;
private readonly ILogger<CategoryEffects> _logger;
/// <summary>
/// Initializes a new instance of the CategoryEffects class.
/// </summary>
public CategoryEffects(IBookingCategoryService categoryService, ILogger<CategoryEffects> logger)
{
_categoryService = categoryService;
_logger = logger;
}
/// <summary>
/// Handles LoadCategoriesAction - loads all categories from service.
/// </summary>
[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));
}
}
/// <summary>
/// Handles CreateCategoryAction - creates a new category.
/// </summary>
[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));
}
}
/// <summary>
/// Handles UpdateCategoryAction - updates an existing category.
/// </summary>
[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));
}
}
/// <summary>
/// Handles DeleteCategoryAction - deletes a category.
/// </summary>
[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));
}
}
}

View File

@ -0,0 +1,163 @@
using Fluxor;
namespace Koogle.Web.Store.CategoryState;
/// <summary>
/// Reducers for booking category state management.
/// </summary>
public static class CategoryReducers
{
/// <summary>
/// Handles LoadCategoriesAction - sets loading state.
/// </summary>
[ReducerMethod(typeof(LoadCategoriesAction))]
public static CategoryState OnLoadCategories(CategoryState state)
=> state with
{
IsLoading = true,
Error = null
};
/// <summary>
/// Handles LoadCategoriesSuccessAction - updates categories list.
/// </summary>
[ReducerMethod]
public static CategoryState OnLoadCategoriesSuccess(CategoryState state, LoadCategoriesSuccessAction action)
=> state with
{
Categories = action.Categories,
IsLoading = false
};
/// <summary>
/// Handles LoadCategoriesFailureAction - sets error state.
/// </summary>
[ReducerMethod]
public static CategoryState OnLoadCategoriesFailure(CategoryState state, LoadCategoriesFailureAction action)
=> state with
{
IsLoading = false,
Error = action.Error
};
/// <summary>
/// Handles CreateCategoryAction - sets loading state.
/// </summary>
[ReducerMethod(typeof(CreateCategoryAction))]
public static CategoryState OnCreateCategory(CategoryState state)
=> state with
{
IsLoading = true,
Error = null
};
/// <summary>
/// Handles CreateCategorySuccessAction - adds category to list.
/// </summary>
[ReducerMethod]
public static CategoryState OnCreateCategorySuccess(CategoryState state, CreateCategorySuccessAction action)
=> state with
{
Categories = [.. state.Categories, action.Category],
IsLoading = false
};
/// <summary>
/// Handles CreateCategoryFailureAction - sets error state.
/// </summary>
[ReducerMethod]
public static CategoryState OnCreateCategoryFailure(CategoryState state, CreateCategoryFailureAction action)
=> state with
{
IsLoading = false,
Error = action.Error
};
/// <summary>
/// Handles UpdateCategoryAction - sets loading state.
/// </summary>
[ReducerMethod(typeof(UpdateCategoryAction))]
public static CategoryState OnUpdateCategory(CategoryState state)
=> state with
{
IsLoading = true,
Error = null
};
/// <summary>
/// Handles UpdateCategorySuccessAction - replaces category in list.
/// </summary>
[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
};
/// <summary>
/// Handles UpdateCategoryFailureAction - sets error state.
/// </summary>
[ReducerMethod]
public static CategoryState OnUpdateCategoryFailure(CategoryState state, UpdateCategoryFailureAction action)
=> state with
{
IsLoading = false,
Error = action.Error
};
/// <summary>
/// Handles DeleteCategoryAction - sets loading state.
/// </summary>
[ReducerMethod(typeof(DeleteCategoryAction))]
public static CategoryState OnDeleteCategory(CategoryState state)
=> state with
{
IsLoading = true,
Error = null
};
/// <summary>
/// Handles DeleteCategorySuccessAction - removes category from list.
/// </summary>
[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
};
/// <summary>
/// Handles DeleteCategoryFailureAction - sets error state.
/// </summary>
[ReducerMethod]
public static CategoryState OnDeleteCategoryFailure(CategoryState state, DeleteCategoryFailureAction action)
=> state with
{
IsLoading = false,
Error = action.Error
};
/// <summary>
/// Handles SelectCategoryAction - sets selected category.
/// </summary>
[ReducerMethod]
public static CategoryState OnSelectCategory(CategoryState state, SelectCategoryAction action)
=> state with
{
SelectedCategory = action.Category
};
/// <summary>
/// Handles ClearCategoryErrorAction - clears error state.
/// </summary>
[ReducerMethod(typeof(ClearCategoryErrorAction))]
public static CategoryState OnClearError(CategoryState state)
=> state with
{
Error = null
};
}

View File

@ -0,0 +1,47 @@
using Fluxor;
using Koogle.Application.DTOs;
namespace Koogle.Web.Store.CategoryState;
/// <summary>
/// Fluxor state for booking category management.
/// </summary>
[FeatureState]
public record CategoryState
{
/// <summary>
/// List of all booking categories for the current club.
/// </summary>
public IReadOnlyList<BookingCategoryDto> Categories { get; init; } = [];
/// <summary>
/// Currently selected category for editing.
/// </summary>
public BookingCategoryDto? SelectedCategory { get; init; }
/// <summary>
/// Indicates whether a category operation is in progress.
/// </summary>
public bool IsLoading { get; init; }
/// <summary>
/// Error message if operation failed.
/// </summary>
public string? Error { get; init; }
/// <summary>
/// Private constructor for Fluxor initialization.
/// </summary>
private CategoryState() { }
/// <summary>
/// Creates the initial state.
/// </summary>
public static CategoryState Initial => new()
{
Categories = [],
SelectedCategory = null,
IsLoading = false,
Error = null
};
}