Add ExpenseState Fluxor (D3)

- ExpenseState.cs: state record with Expenses, SelectedExpense, IsLoading, Error
- ExpenseActions.cs: Load/Create/Update/Delete actions + Select/ClearError
- ExpenseReducers.cs: pure state transformations
- ExpenseEffects.cs: async service calls with logging

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beo3000 2025-12-24 15:53:39 +01:00
parent c3839d2363
commit 479bfe4ba5
4 changed files with 395 additions and 0 deletions

View File

@ -0,0 +1,73 @@
using Koogle.Application.DTOs;
namespace Koogle.Web.Store.ExpenseState;
/// <summary>
/// Action to load all expenses for the current club.
/// </summary>
public record LoadExpensesAction;
/// <summary>
/// Action dispatched when expenses are loaded successfully.
/// </summary>
public record LoadExpensesSuccessAction(IReadOnlyList<ExpenseDto> Expenses);
/// <summary>
/// Action dispatched when loading expenses fails.
/// </summary>
public record LoadExpensesFailureAction(string Error);
/// <summary>
/// Action to create a new expense.
/// </summary>
public record CreateExpenseAction(CreateExpenseDto Dto);
/// <summary>
/// Action dispatched when expense is created successfully.
/// </summary>
public record CreateExpenseSuccessAction(ExpenseDto Expense);
/// <summary>
/// Action dispatched when creating expense fails.
/// </summary>
public record CreateExpenseFailureAction(string Error);
/// <summary>
/// Action to update an existing expense.
/// </summary>
public record UpdateExpenseAction(UpdateExpenseDto Dto);
/// <summary>
/// Action dispatched when expense is updated successfully.
/// </summary>
public record UpdateExpenseSuccessAction(ExpenseDto Expense);
/// <summary>
/// Action dispatched when updating expense fails.
/// </summary>
public record UpdateExpenseFailureAction(string Error);
/// <summary>
/// Action to delete an expense.
/// </summary>
public record DeleteExpenseAction(Guid Id);
/// <summary>
/// Action dispatched when expense is deleted successfully.
/// </summary>
public record DeleteExpenseSuccessAction(Guid Id);
/// <summary>
/// Action dispatched when deleting expense fails.
/// </summary>
public record DeleteExpenseFailureAction(string Error);
/// <summary>
/// Action to select an expense for editing.
/// </summary>
public record SelectExpenseAction(ExpenseDto? Expense);
/// <summary>
/// Action to clear expense error state.
/// </summary>
public record ClearExpenseErrorAction;

View File

@ -0,0 +1,112 @@
using Fluxor;
using Koogle.Application.Interfaces;
namespace Koogle.Web.Store.ExpenseState;
/// <summary>
/// Side effects for expense state management.
/// Handles async operations like API calls.
/// </summary>
public class ExpenseEffects
{
private readonly IExpenseService _expenseService;
private readonly ILogger<ExpenseEffects> _logger;
/// <summary>
/// Initializes a new instance of the ExpenseEffects class.
/// </summary>
public ExpenseEffects(IExpenseService expenseService, ILogger<ExpenseEffects> logger)
{
_expenseService = expenseService;
_logger = logger;
}
/// <summary>
/// Handles LoadExpensesAction - loads all expenses from service.
/// </summary>
[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));
}
}
/// <summary>
/// Handles CreateExpenseAction - creates a new expense.
/// </summary>
[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));
}
}
/// <summary>
/// Handles UpdateExpenseAction - updates an existing expense.
/// </summary>
[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));
}
}
/// <summary>
/// Handles DeleteExpenseAction - deletes an expense.
/// </summary>
[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));
}
}
}

View File

@ -0,0 +1,163 @@
using Fluxor;
namespace Koogle.Web.Store.ExpenseState;
/// <summary>
/// Reducers for expense state management.
/// </summary>
public static class ExpenseReducers
{
/// <summary>
/// Handles LoadExpensesAction - sets loading state.
/// </summary>
[ReducerMethod(typeof(LoadExpensesAction))]
public static ExpenseState OnLoadExpenses(ExpenseState state)
=> state with
{
IsLoading = true,
Error = null
};
/// <summary>
/// Handles LoadExpensesSuccessAction - updates expenses list.
/// </summary>
[ReducerMethod]
public static ExpenseState OnLoadExpensesSuccess(ExpenseState state, LoadExpensesSuccessAction action)
=> state with
{
Expenses = action.Expenses,
IsLoading = false
};
/// <summary>
/// Handles LoadExpensesFailureAction - sets error state.
/// </summary>
[ReducerMethod]
public static ExpenseState OnLoadExpensesFailure(ExpenseState state, LoadExpensesFailureAction action)
=> state with
{
IsLoading = false,
Error = action.Error
};
/// <summary>
/// Handles CreateExpenseAction - sets loading state.
/// </summary>
[ReducerMethod(typeof(CreateExpenseAction))]
public static ExpenseState OnCreateExpense(ExpenseState state)
=> state with
{
IsLoading = true,
Error = null
};
/// <summary>
/// Handles CreateExpenseSuccessAction - adds expense to list.
/// </summary>
[ReducerMethod]
public static ExpenseState OnCreateExpenseSuccess(ExpenseState state, CreateExpenseSuccessAction action)
=> state with
{
Expenses = [.. state.Expenses, action.Expense],
IsLoading = false
};
/// <summary>
/// Handles CreateExpenseFailureAction - sets error state.
/// </summary>
[ReducerMethod]
public static ExpenseState OnCreateExpenseFailure(ExpenseState state, CreateExpenseFailureAction action)
=> state with
{
IsLoading = false,
Error = action.Error
};
/// <summary>
/// Handles UpdateExpenseAction - sets loading state.
/// </summary>
[ReducerMethod(typeof(UpdateExpenseAction))]
public static ExpenseState OnUpdateExpense(ExpenseState state)
=> state with
{
IsLoading = true,
Error = null
};
/// <summary>
/// Handles UpdateExpenseSuccessAction - replaces expense in list.
/// </summary>
[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
};
/// <summary>
/// Handles UpdateExpenseFailureAction - sets error state.
/// </summary>
[ReducerMethod]
public static ExpenseState OnUpdateExpenseFailure(ExpenseState state, UpdateExpenseFailureAction action)
=> state with
{
IsLoading = false,
Error = action.Error
};
/// <summary>
/// Handles DeleteExpenseAction - sets loading state.
/// </summary>
[ReducerMethod(typeof(DeleteExpenseAction))]
public static ExpenseState OnDeleteExpense(ExpenseState state)
=> state with
{
IsLoading = true,
Error = null
};
/// <summary>
/// Handles DeleteExpenseSuccessAction - removes expense from list.
/// </summary>
[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
};
/// <summary>
/// Handles DeleteExpenseFailureAction - sets error state.
/// </summary>
[ReducerMethod]
public static ExpenseState OnDeleteExpenseFailure(ExpenseState state, DeleteExpenseFailureAction action)
=> state with
{
IsLoading = false,
Error = action.Error
};
/// <summary>
/// Handles SelectExpenseAction - sets selected expense.
/// </summary>
[ReducerMethod]
public static ExpenseState OnSelectExpense(ExpenseState state, SelectExpenseAction action)
=> state with
{
SelectedExpense = action.Expense
};
/// <summary>
/// Handles ClearExpenseErrorAction - clears error state.
/// </summary>
[ReducerMethod(typeof(ClearExpenseErrorAction))]
public static ExpenseState OnClearError(ExpenseState state)
=> state with
{
Error = null
};
}

View File

@ -0,0 +1,47 @@
using Fluxor;
using Koogle.Application.DTOs;
namespace Koogle.Web.Store.ExpenseState;
/// <summary>
/// Fluxor state for expense template management.
/// </summary>
[FeatureState]
public record ExpenseState
{
/// <summary>
/// List of all expense templates for the current club.
/// </summary>
public IReadOnlyList<ExpenseDto> Expenses { get; init; } = [];
/// <summary>
/// Currently selected expense for editing.
/// </summary>
public ExpenseDto? SelectedExpense { get; init; }
/// <summary>
/// Indicates whether an expense 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 ExpenseState() { }
/// <summary>
/// Creates the initial state.
/// </summary>
public static ExpenseState Initial => new()
{
Expenses = [],
SelectedExpense = null,
IsLoading = false,
Error = null
};
}