diff --git a/src/Koogle.Web/Store/ExpenseState/ExpenseActions.cs b/src/Koogle.Web/Store/ExpenseState/ExpenseActions.cs new file mode 100644 index 0000000..0b52109 --- /dev/null +++ b/src/Koogle.Web/Store/ExpenseState/ExpenseActions.cs @@ -0,0 +1,73 @@ +using Koogle.Application.DTOs; + +namespace Koogle.Web.Store.ExpenseState; + +/// +/// Action to load all expenses for the current club. +/// +public record LoadExpensesAction; + +/// +/// Action dispatched when expenses are loaded successfully. +/// +public record LoadExpensesSuccessAction(IReadOnlyList Expenses); + +/// +/// Action dispatched when loading expenses fails. +/// +public record LoadExpensesFailureAction(string Error); + +/// +/// Action to create a new expense. +/// +public record CreateExpenseAction(CreateExpenseDto Dto); + +/// +/// Action dispatched when expense is created successfully. +/// +public record CreateExpenseSuccessAction(ExpenseDto Expense); + +/// +/// Action dispatched when creating expense fails. +/// +public record CreateExpenseFailureAction(string Error); + +/// +/// Action to update an existing expense. +/// +public record UpdateExpenseAction(UpdateExpenseDto Dto); + +/// +/// Action dispatched when expense is updated successfully. +/// +public record UpdateExpenseSuccessAction(ExpenseDto Expense); + +/// +/// Action dispatched when updating expense fails. +/// +public record UpdateExpenseFailureAction(string Error); + +/// +/// Action to delete an expense. +/// +public record DeleteExpenseAction(Guid Id); + +/// +/// Action dispatched when expense is deleted successfully. +/// +public record DeleteExpenseSuccessAction(Guid Id); + +/// +/// Action dispatched when deleting expense fails. +/// +public record DeleteExpenseFailureAction(string Error); + +/// +/// Action to select an expense for editing. +/// +public record SelectExpenseAction(ExpenseDto? Expense); + +/// +/// Action to clear expense error state. +/// +public record ClearExpenseErrorAction; diff --git a/src/Koogle.Web/Store/ExpenseState/ExpenseEffects.cs b/src/Koogle.Web/Store/ExpenseState/ExpenseEffects.cs new file mode 100644 index 0000000..d9ae1f3 --- /dev/null +++ b/src/Koogle.Web/Store/ExpenseState/ExpenseEffects.cs @@ -0,0 +1,112 @@ +using Fluxor; +using Koogle.Application.Interfaces; + +namespace Koogle.Web.Store.ExpenseState; + +/// +/// Side effects for expense state management. +/// Handles async operations like API calls. +/// +public class ExpenseEffects +{ + private readonly IExpenseService _expenseService; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the ExpenseEffects class. + /// + public ExpenseEffects(IExpenseService expenseService, ILogger logger) + { + _expenseService = expenseService; + _logger = logger; + } + + /// + /// Handles LoadExpensesAction - loads all expenses from service. + /// + [EffectMethod] + public async Task HandleLoadExpenses(LoadExpensesAction action, IDispatcher dispatcher) + { + try + { + _logger.LogDebug("Loading expenses"); + var expenses = await _expenseService.GetAllAsync(); + dispatcher.Dispatch(new LoadExpensesSuccessAction(expenses)); + _logger.LogInformation("Loaded {Count} expenses", expenses.Count); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to load expenses"); + dispatcher.Dispatch(new LoadExpensesFailureAction(ex.Message)); + } + } + + /// + /// Handles CreateExpenseAction - creates a new expense. + /// + [EffectMethod] + public async Task HandleCreateExpense(CreateExpenseAction action, IDispatcher dispatcher) + { + try + { + _logger.LogDebug("Creating expense {Name}", action.Dto.Name); + var expense = await _expenseService.CreateAsync(action.Dto); + dispatcher.Dispatch(new CreateExpenseSuccessAction(expense)); + _logger.LogInformation("Created expense {Name} with ID {Id}", expense.Name, expense.Id); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create expense {Name}", action.Dto.Name); + dispatcher.Dispatch(new CreateExpenseFailureAction(ex.Message)); + } + } + + /// + /// Handles UpdateExpenseAction - updates an existing expense. + /// + [EffectMethod] + public async Task HandleUpdateExpense(UpdateExpenseAction action, IDispatcher dispatcher) + { + try + { + _logger.LogDebug("Updating expense {Id}", action.Dto.Id); + var expense = await _expenseService.UpdateAsync(action.Dto); + dispatcher.Dispatch(new UpdateExpenseSuccessAction(expense)); + _logger.LogInformation("Updated expense {Name}", expense.Name); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to update expense {Id}", action.Dto.Id); + dispatcher.Dispatch(new UpdateExpenseFailureAction(ex.Message)); + } + } + + /// + /// Handles DeleteExpenseAction - deletes an expense. + /// + [EffectMethod] + public async Task HandleDeleteExpense(DeleteExpenseAction action, IDispatcher dispatcher) + { + try + { + _logger.LogDebug("Deleting expense {Id}", action.Id); + var success = await _expenseService.DeleteAsync(action.Id); + + if (success) + { + dispatcher.Dispatch(new DeleteExpenseSuccessAction(action.Id)); + _logger.LogInformation("Deleted expense {Id}", action.Id); + } + else + { + dispatcher.Dispatch(new DeleteExpenseFailureAction("Expense konnte nicht gelöscht werden.")); + _logger.LogWarning("Failed to delete expense {Id} - not found or already deleted", action.Id); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to delete expense {Id}", action.Id); + dispatcher.Dispatch(new DeleteExpenseFailureAction(ex.Message)); + } + } +} diff --git a/src/Koogle.Web/Store/ExpenseState/ExpenseReducers.cs b/src/Koogle.Web/Store/ExpenseState/ExpenseReducers.cs new file mode 100644 index 0000000..d3a6fe1 --- /dev/null +++ b/src/Koogle.Web/Store/ExpenseState/ExpenseReducers.cs @@ -0,0 +1,163 @@ +using Fluxor; + +namespace Koogle.Web.Store.ExpenseState; + +/// +/// Reducers for expense state management. +/// +public static class ExpenseReducers +{ + /// + /// Handles LoadExpensesAction - sets loading state. + /// + [ReducerMethod(typeof(LoadExpensesAction))] + public static ExpenseState OnLoadExpenses(ExpenseState state) + => state with + { + IsLoading = true, + Error = null + }; + + /// + /// Handles LoadExpensesSuccessAction - updates expenses list. + /// + [ReducerMethod] + public static ExpenseState OnLoadExpensesSuccess(ExpenseState state, LoadExpensesSuccessAction action) + => state with + { + Expenses = action.Expenses, + IsLoading = false + }; + + /// + /// Handles LoadExpensesFailureAction - sets error state. + /// + [ReducerMethod] + public static ExpenseState OnLoadExpensesFailure(ExpenseState state, LoadExpensesFailureAction action) + => state with + { + IsLoading = false, + Error = action.Error + }; + + /// + /// Handles CreateExpenseAction - sets loading state. + /// + [ReducerMethod(typeof(CreateExpenseAction))] + public static ExpenseState OnCreateExpense(ExpenseState state) + => state with + { + IsLoading = true, + Error = null + }; + + /// + /// Handles CreateExpenseSuccessAction - adds expense to list. + /// + [ReducerMethod] + public static ExpenseState OnCreateExpenseSuccess(ExpenseState state, CreateExpenseSuccessAction action) + => state with + { + Expenses = [.. state.Expenses, action.Expense], + IsLoading = false + }; + + /// + /// Handles CreateExpenseFailureAction - sets error state. + /// + [ReducerMethod] + public static ExpenseState OnCreateExpenseFailure(ExpenseState state, CreateExpenseFailureAction action) + => state with + { + IsLoading = false, + Error = action.Error + }; + + /// + /// Handles UpdateExpenseAction - sets loading state. + /// + [ReducerMethod(typeof(UpdateExpenseAction))] + public static ExpenseState OnUpdateExpense(ExpenseState state) + => state with + { + IsLoading = true, + Error = null + }; + + /// + /// Handles UpdateExpenseSuccessAction - replaces expense in list. + /// + [ReducerMethod] + public static ExpenseState OnUpdateExpenseSuccess(ExpenseState state, UpdateExpenseSuccessAction action) + => state with + { + Expenses = state.Expenses.Select(e => e.Id == action.Expense.Id ? action.Expense : e).ToList(), + SelectedExpense = state.SelectedExpense?.Id == action.Expense.Id ? action.Expense : state.SelectedExpense, + IsLoading = false + }; + + /// + /// Handles UpdateExpenseFailureAction - sets error state. + /// + [ReducerMethod] + public static ExpenseState OnUpdateExpenseFailure(ExpenseState state, UpdateExpenseFailureAction action) + => state with + { + IsLoading = false, + Error = action.Error + }; + + /// + /// Handles DeleteExpenseAction - sets loading state. + /// + [ReducerMethod(typeof(DeleteExpenseAction))] + public static ExpenseState OnDeleteExpense(ExpenseState state) + => state with + { + IsLoading = true, + Error = null + }; + + /// + /// Handles DeleteExpenseSuccessAction - removes expense from list. + /// + [ReducerMethod] + public static ExpenseState OnDeleteExpenseSuccess(ExpenseState state, DeleteExpenseSuccessAction action) + => state with + { + Expenses = state.Expenses.Where(e => e.Id != action.Id).ToList(), + SelectedExpense = state.SelectedExpense?.Id == action.Id ? null : state.SelectedExpense, + IsLoading = false + }; + + /// + /// Handles DeleteExpenseFailureAction - sets error state. + /// + [ReducerMethod] + public static ExpenseState OnDeleteExpenseFailure(ExpenseState state, DeleteExpenseFailureAction action) + => state with + { + IsLoading = false, + Error = action.Error + }; + + /// + /// Handles SelectExpenseAction - sets selected expense. + /// + [ReducerMethod] + public static ExpenseState OnSelectExpense(ExpenseState state, SelectExpenseAction action) + => state with + { + SelectedExpense = action.Expense + }; + + /// + /// Handles ClearExpenseErrorAction - clears error state. + /// + [ReducerMethod(typeof(ClearExpenseErrorAction))] + public static ExpenseState OnClearError(ExpenseState state) + => state with + { + Error = null + }; +} diff --git a/src/Koogle.Web/Store/ExpenseState/ExpenseState.cs b/src/Koogle.Web/Store/ExpenseState/ExpenseState.cs new file mode 100644 index 0000000..067b950 --- /dev/null +++ b/src/Koogle.Web/Store/ExpenseState/ExpenseState.cs @@ -0,0 +1,47 @@ +using Fluxor; +using Koogle.Application.DTOs; + +namespace Koogle.Web.Store.ExpenseState; + +/// +/// Fluxor state for expense template management. +/// +[FeatureState] +public record ExpenseState +{ + /// + /// List of all expense templates for the current club. + /// + public IReadOnlyList Expenses { get; init; } = []; + + /// + /// Currently selected expense for editing. + /// + public ExpenseDto? SelectedExpense { get; init; } + + /// + /// Indicates whether an expense 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 ExpenseState() { } + + /// + /// Creates the initial state. + /// + public static ExpenseState Initial => new() + { + Expenses = [], + SelectedExpense = null, + IsLoading = false, + Error = null + }; +}