From d8ca8ed0a9415e4a765ea8d718f7c809be2bb953 Mon Sep 17 00:00:00 2001 From: beo3000 Date: Wed, 24 Dec 2025 14:59:44 +0100 Subject: [PATCH] Add PersonState Fluxor state management (Phase D1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PersonState.cs: State record with Persons, SelectedPerson, IsLoading, Error - PersonActions.cs: Load/Create/Update/Delete actions with success/failure variants - PersonReducers.cs: Pure reducers for all person actions - PersonEffects.cs: Async effects calling IPersonService 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../Store/PersonState/PersonActions.cs | 73 ++++++++ .../Store/PersonState/PersonEffects.cs | 112 ++++++++++++ .../Store/PersonState/PersonReducers.cs | 163 ++++++++++++++++++ .../Store/PersonState/PersonState.cs | 47 +++++ 4 files changed, 395 insertions(+) create mode 100644 src/Koogle.Web/Store/PersonState/PersonActions.cs create mode 100644 src/Koogle.Web/Store/PersonState/PersonEffects.cs create mode 100644 src/Koogle.Web/Store/PersonState/PersonReducers.cs create mode 100644 src/Koogle.Web/Store/PersonState/PersonState.cs diff --git a/src/Koogle.Web/Store/PersonState/PersonActions.cs b/src/Koogle.Web/Store/PersonState/PersonActions.cs new file mode 100644 index 0000000..a54edb1 --- /dev/null +++ b/src/Koogle.Web/Store/PersonState/PersonActions.cs @@ -0,0 +1,73 @@ +using Koogle.Application.DTOs; + +namespace Koogle.Web.Store.PersonState; + +/// +/// Action to load all persons for the current club. +/// +public record LoadPersonsAction; + +/// +/// Action dispatched when persons are loaded successfully. +/// +public record LoadPersonsSuccessAction(IReadOnlyList Persons); + +/// +/// Action dispatched when loading persons fails. +/// +public record LoadPersonsFailureAction(string Error); + +/// +/// Action to create a new person. +/// +public record CreatePersonAction(CreatePersonDto Dto); + +/// +/// Action dispatched when person is created successfully. +/// +public record CreatePersonSuccessAction(PersonDto Person); + +/// +/// Action dispatched when creating person fails. +/// +public record CreatePersonFailureAction(string Error); + +/// +/// Action to update an existing person. +/// +public record UpdatePersonAction(UpdatePersonDto Dto); + +/// +/// Action dispatched when person is updated successfully. +/// +public record UpdatePersonSuccessAction(PersonDto Person); + +/// +/// Action dispatched when updating person fails. +/// +public record UpdatePersonFailureAction(string Error); + +/// +/// Action to delete a person. +/// +public record DeletePersonAction(Guid Id); + +/// +/// Action dispatched when person is deleted successfully. +/// +public record DeletePersonSuccessAction(Guid Id); + +/// +/// Action dispatched when deleting person fails. +/// +public record DeletePersonFailureAction(string Error); + +/// +/// Action to select a person for editing. +/// +public record SelectPersonAction(PersonDto? Person); + +/// +/// Action to clear person error state. +/// +public record ClearPersonErrorAction; diff --git a/src/Koogle.Web/Store/PersonState/PersonEffects.cs b/src/Koogle.Web/Store/PersonState/PersonEffects.cs new file mode 100644 index 0000000..d23e815 --- /dev/null +++ b/src/Koogle.Web/Store/PersonState/PersonEffects.cs @@ -0,0 +1,112 @@ +using Fluxor; +using Koogle.Application.Interfaces; + +namespace Koogle.Web.Store.PersonState; + +/// +/// Side effects for person state management. +/// Handles async operations like API calls. +/// +public class PersonEffects +{ + private readonly IPersonService _personService; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the PersonEffects class. + /// + public PersonEffects(IPersonService personService, ILogger logger) + { + _personService = personService; + _logger = logger; + } + + /// + /// Handles LoadPersonsAction - loads all persons from service. + /// + [EffectMethod] + public async Task HandleLoadPersons(LoadPersonsAction action, IDispatcher dispatcher) + { + try + { + _logger.LogDebug("Loading persons"); + var persons = await _personService.GetAllAsync(); + dispatcher.Dispatch(new LoadPersonsSuccessAction(persons)); + _logger.LogInformation("Loaded {Count} persons", persons.Count); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to load persons"); + dispatcher.Dispatch(new LoadPersonsFailureAction(ex.Message)); + } + } + + /// + /// Handles CreatePersonAction - creates a new person. + /// + [EffectMethod] + public async Task HandleCreatePerson(CreatePersonAction action, IDispatcher dispatcher) + { + try + { + _logger.LogDebug("Creating person {Name}", action.Dto.Name); + var person = await _personService.CreateAsync(action.Dto); + dispatcher.Dispatch(new CreatePersonSuccessAction(person)); + _logger.LogInformation("Created person {Name} with ID {Id}", person.Name, person.Id); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to create person {Name}", action.Dto.Name); + dispatcher.Dispatch(new CreatePersonFailureAction(ex.Message)); + } + } + + /// + /// Handles UpdatePersonAction - updates an existing person. + /// + [EffectMethod] + public async Task HandleUpdatePerson(UpdatePersonAction action, IDispatcher dispatcher) + { + try + { + _logger.LogDebug("Updating person {Id}", action.Dto.Id); + var person = await _personService.UpdateAsync(action.Dto); + dispatcher.Dispatch(new UpdatePersonSuccessAction(person)); + _logger.LogInformation("Updated person {Name}", person.Name); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to update person {Id}", action.Dto.Id); + dispatcher.Dispatch(new UpdatePersonFailureAction(ex.Message)); + } + } + + /// + /// Handles DeletePersonAction - deletes a person. + /// + [EffectMethod] + public async Task HandleDeletePerson(DeletePersonAction action, IDispatcher dispatcher) + { + try + { + _logger.LogDebug("Deleting person {Id}", action.Id); + var success = await _personService.DeleteAsync(action.Id); + + if (success) + { + dispatcher.Dispatch(new DeletePersonSuccessAction(action.Id)); + _logger.LogInformation("Deleted person {Id}", action.Id); + } + else + { + dispatcher.Dispatch(new DeletePersonFailureAction("Person konnte nicht gelöscht werden.")); + _logger.LogWarning("Failed to delete person {Id} - not found or already deleted", action.Id); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to delete person {Id}", action.Id); + dispatcher.Dispatch(new DeletePersonFailureAction(ex.Message)); + } + } +} diff --git a/src/Koogle.Web/Store/PersonState/PersonReducers.cs b/src/Koogle.Web/Store/PersonState/PersonReducers.cs new file mode 100644 index 0000000..1ee3c1d --- /dev/null +++ b/src/Koogle.Web/Store/PersonState/PersonReducers.cs @@ -0,0 +1,163 @@ +using Fluxor; + +namespace Koogle.Web.Store.PersonState; + +/// +/// Reducers for person state management. +/// +public static class PersonReducers +{ + /// + /// Handles LoadPersonsAction - sets loading state. + /// + [ReducerMethod(typeof(LoadPersonsAction))] + public static PersonState OnLoadPersons(PersonState state) + => state with + { + IsLoading = true, + Error = null + }; + + /// + /// Handles LoadPersonsSuccessAction - updates persons list. + /// + [ReducerMethod] + public static PersonState OnLoadPersonsSuccess(PersonState state, LoadPersonsSuccessAction action) + => state with + { + Persons = action.Persons, + IsLoading = false + }; + + /// + /// Handles LoadPersonsFailureAction - sets error state. + /// + [ReducerMethod] + public static PersonState OnLoadPersonsFailure(PersonState state, LoadPersonsFailureAction action) + => state with + { + IsLoading = false, + Error = action.Error + }; + + /// + /// Handles CreatePersonAction - sets loading state. + /// + [ReducerMethod(typeof(CreatePersonAction))] + public static PersonState OnCreatePerson(PersonState state) + => state with + { + IsLoading = true, + Error = null + }; + + /// + /// Handles CreatePersonSuccessAction - adds person to list. + /// + [ReducerMethod] + public static PersonState OnCreatePersonSuccess(PersonState state, CreatePersonSuccessAction action) + => state with + { + Persons = [.. state.Persons, action.Person], + IsLoading = false + }; + + /// + /// Handles CreatePersonFailureAction - sets error state. + /// + [ReducerMethod] + public static PersonState OnCreatePersonFailure(PersonState state, CreatePersonFailureAction action) + => state with + { + IsLoading = false, + Error = action.Error + }; + + /// + /// Handles UpdatePersonAction - sets loading state. + /// + [ReducerMethod(typeof(UpdatePersonAction))] + public static PersonState OnUpdatePerson(PersonState state) + => state with + { + IsLoading = true, + Error = null + }; + + /// + /// Handles UpdatePersonSuccessAction - replaces person in list. + /// + [ReducerMethod] + public static PersonState OnUpdatePersonSuccess(PersonState state, UpdatePersonSuccessAction action) + => state with + { + Persons = state.Persons.Select(p => p.Id == action.Person.Id ? action.Person : p).ToList(), + SelectedPerson = state.SelectedPerson?.Id == action.Person.Id ? action.Person : state.SelectedPerson, + IsLoading = false + }; + + /// + /// Handles UpdatePersonFailureAction - sets error state. + /// + [ReducerMethod] + public static PersonState OnUpdatePersonFailure(PersonState state, UpdatePersonFailureAction action) + => state with + { + IsLoading = false, + Error = action.Error + }; + + /// + /// Handles DeletePersonAction - sets loading state. + /// + [ReducerMethod(typeof(DeletePersonAction))] + public static PersonState OnDeletePerson(PersonState state) + => state with + { + IsLoading = true, + Error = null + }; + + /// + /// Handles DeletePersonSuccessAction - removes person from list. + /// + [ReducerMethod] + public static PersonState OnDeletePersonSuccess(PersonState state, DeletePersonSuccessAction action) + => state with + { + Persons = state.Persons.Where(p => p.Id != action.Id).ToList(), + SelectedPerson = state.SelectedPerson?.Id == action.Id ? null : state.SelectedPerson, + IsLoading = false + }; + + /// + /// Handles DeletePersonFailureAction - sets error state. + /// + [ReducerMethod] + public static PersonState OnDeletePersonFailure(PersonState state, DeletePersonFailureAction action) + => state with + { + IsLoading = false, + Error = action.Error + }; + + /// + /// Handles SelectPersonAction - sets selected person. + /// + [ReducerMethod] + public static PersonState OnSelectPerson(PersonState state, SelectPersonAction action) + => state with + { + SelectedPerson = action.Person + }; + + /// + /// Handles ClearPersonErrorAction - clears error state. + /// + [ReducerMethod(typeof(ClearPersonErrorAction))] + public static PersonState OnClearError(PersonState state) + => state with + { + Error = null + }; +} diff --git a/src/Koogle.Web/Store/PersonState/PersonState.cs b/src/Koogle.Web/Store/PersonState/PersonState.cs new file mode 100644 index 0000000..9fbf1ad --- /dev/null +++ b/src/Koogle.Web/Store/PersonState/PersonState.cs @@ -0,0 +1,47 @@ +using Fluxor; +using Koogle.Application.DTOs; + +namespace Koogle.Web.Store.PersonState; + +/// +/// Fluxor state for person management. +/// +[FeatureState] +public record PersonState +{ + /// + /// List of all persons for the current club. + /// + public IReadOnlyList Persons { get; init; } = []; + + /// + /// Currently selected person for editing. + /// + public PersonDto? SelectedPerson { get; init; } + + /// + /// Indicates whether a person 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 PersonState() { } + + /// + /// Creates the initial state. + /// + public static PersonState Initial => new() + { + Persons = [], + SelectedPerson = null, + IsLoading = false, + Error = null + }; +}