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:
parent
c3839d2363
commit
479bfe4ba5
|
|
@ -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;
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue