Add PersonState Fluxor state management (Phase D1)

- 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 <noreply@anthropic.com>
This commit is contained in:
beo3000 2025-12-24 14:59:44 +01:00
parent 660d143333
commit d8ca8ed0a9
4 changed files with 395 additions and 0 deletions

View File

@ -0,0 +1,73 @@
using Koogle.Application.DTOs;
namespace Koogle.Web.Store.PersonState;
/// <summary>
/// Action to load all persons for the current club.
/// </summary>
public record LoadPersonsAction;
/// <summary>
/// Action dispatched when persons are loaded successfully.
/// </summary>
public record LoadPersonsSuccessAction(IReadOnlyList<PersonDto> Persons);
/// <summary>
/// Action dispatched when loading persons fails.
/// </summary>
public record LoadPersonsFailureAction(string Error);
/// <summary>
/// Action to create a new person.
/// </summary>
public record CreatePersonAction(CreatePersonDto Dto);
/// <summary>
/// Action dispatched when person is created successfully.
/// </summary>
public record CreatePersonSuccessAction(PersonDto Person);
/// <summary>
/// Action dispatched when creating person fails.
/// </summary>
public record CreatePersonFailureAction(string Error);
/// <summary>
/// Action to update an existing person.
/// </summary>
public record UpdatePersonAction(UpdatePersonDto Dto);
/// <summary>
/// Action dispatched when person is updated successfully.
/// </summary>
public record UpdatePersonSuccessAction(PersonDto Person);
/// <summary>
/// Action dispatched when updating person fails.
/// </summary>
public record UpdatePersonFailureAction(string Error);
/// <summary>
/// Action to delete a person.
/// </summary>
public record DeletePersonAction(Guid Id);
/// <summary>
/// Action dispatched when person is deleted successfully.
/// </summary>
public record DeletePersonSuccessAction(Guid Id);
/// <summary>
/// Action dispatched when deleting person fails.
/// </summary>
public record DeletePersonFailureAction(string Error);
/// <summary>
/// Action to select a person for editing.
/// </summary>
public record SelectPersonAction(PersonDto? Person);
/// <summary>
/// Action to clear person error state.
/// </summary>
public record ClearPersonErrorAction;

View File

@ -0,0 +1,112 @@
using Fluxor;
using Koogle.Application.Interfaces;
namespace Koogle.Web.Store.PersonState;
/// <summary>
/// Side effects for person state management.
/// Handles async operations like API calls.
/// </summary>
public class PersonEffects
{
private readonly IPersonService _personService;
private readonly ILogger<PersonEffects> _logger;
/// <summary>
/// Initializes a new instance of the PersonEffects class.
/// </summary>
public PersonEffects(IPersonService personService, ILogger<PersonEffects> logger)
{
_personService = personService;
_logger = logger;
}
/// <summary>
/// Handles LoadPersonsAction - loads all persons from service.
/// </summary>
[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));
}
}
/// <summary>
/// Handles CreatePersonAction - creates a new person.
/// </summary>
[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));
}
}
/// <summary>
/// Handles UpdatePersonAction - updates an existing person.
/// </summary>
[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));
}
}
/// <summary>
/// Handles DeletePersonAction - deletes a person.
/// </summary>
[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));
}
}
}

View File

@ -0,0 +1,163 @@
using Fluxor;
namespace Koogle.Web.Store.PersonState;
/// <summary>
/// Reducers for person state management.
/// </summary>
public static class PersonReducers
{
/// <summary>
/// Handles LoadPersonsAction - sets loading state.
/// </summary>
[ReducerMethod(typeof(LoadPersonsAction))]
public static PersonState OnLoadPersons(PersonState state)
=> state with
{
IsLoading = true,
Error = null
};
/// <summary>
/// Handles LoadPersonsSuccessAction - updates persons list.
/// </summary>
[ReducerMethod]
public static PersonState OnLoadPersonsSuccess(PersonState state, LoadPersonsSuccessAction action)
=> state with
{
Persons = action.Persons,
IsLoading = false
};
/// <summary>
/// Handles LoadPersonsFailureAction - sets error state.
/// </summary>
[ReducerMethod]
public static PersonState OnLoadPersonsFailure(PersonState state, LoadPersonsFailureAction action)
=> state with
{
IsLoading = false,
Error = action.Error
};
/// <summary>
/// Handles CreatePersonAction - sets loading state.
/// </summary>
[ReducerMethod(typeof(CreatePersonAction))]
public static PersonState OnCreatePerson(PersonState state)
=> state with
{
IsLoading = true,
Error = null
};
/// <summary>
/// Handles CreatePersonSuccessAction - adds person to list.
/// </summary>
[ReducerMethod]
public static PersonState OnCreatePersonSuccess(PersonState state, CreatePersonSuccessAction action)
=> state with
{
Persons = [.. state.Persons, action.Person],
IsLoading = false
};
/// <summary>
/// Handles CreatePersonFailureAction - sets error state.
/// </summary>
[ReducerMethod]
public static PersonState OnCreatePersonFailure(PersonState state, CreatePersonFailureAction action)
=> state with
{
IsLoading = false,
Error = action.Error
};
/// <summary>
/// Handles UpdatePersonAction - sets loading state.
/// </summary>
[ReducerMethod(typeof(UpdatePersonAction))]
public static PersonState OnUpdatePerson(PersonState state)
=> state with
{
IsLoading = true,
Error = null
};
/// <summary>
/// Handles UpdatePersonSuccessAction - replaces person in list.
/// </summary>
[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
};
/// <summary>
/// Handles UpdatePersonFailureAction - sets error state.
/// </summary>
[ReducerMethod]
public static PersonState OnUpdatePersonFailure(PersonState state, UpdatePersonFailureAction action)
=> state with
{
IsLoading = false,
Error = action.Error
};
/// <summary>
/// Handles DeletePersonAction - sets loading state.
/// </summary>
[ReducerMethod(typeof(DeletePersonAction))]
public static PersonState OnDeletePerson(PersonState state)
=> state with
{
IsLoading = true,
Error = null
};
/// <summary>
/// Handles DeletePersonSuccessAction - removes person from list.
/// </summary>
[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
};
/// <summary>
/// Handles DeletePersonFailureAction - sets error state.
/// </summary>
[ReducerMethod]
public static PersonState OnDeletePersonFailure(PersonState state, DeletePersonFailureAction action)
=> state with
{
IsLoading = false,
Error = action.Error
};
/// <summary>
/// Handles SelectPersonAction - sets selected person.
/// </summary>
[ReducerMethod]
public static PersonState OnSelectPerson(PersonState state, SelectPersonAction action)
=> state with
{
SelectedPerson = action.Person
};
/// <summary>
/// Handles ClearPersonErrorAction - clears error state.
/// </summary>
[ReducerMethod(typeof(ClearPersonErrorAction))]
public static PersonState OnClearError(PersonState state)
=> state with
{
Error = null
};
}

View File

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