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
+ };
+}