diff --git a/src/Koogle.Web/Components/Pages/Persons/PersonEditDialog.razor b/src/Koogle.Web/Components/Pages/Persons/PersonEditDialog.razor
new file mode 100644
index 0000000..dfa27da
--- /dev/null
+++ b/src/Koogle.Web/Components/Pages/Persons/PersonEditDialog.razor
@@ -0,0 +1,95 @@
+@using Koogle.Application.DTOs
+@using Koogle.Domain.Enums
+
+
+
+
+ @(IsEditMode ? "Person bearbeiten" : "Neue Person")
+
+
+
+
+
+
+
+ Mitglied
+ Gast
+
+
+
+ @GetStatusDescription(_personStatus)
+
+
+
+
+ Abbrechen
+
+ @(IsEditMode ? "Speichern" : "Erstellen")
+
+
+
+
+@code {
+ [CascadingParameter]
+ private IMudDialogInstance? MudDialog { get; set; }
+
+ [Parameter]
+ public PersonDto? Person { get; set; }
+
+ private MudForm? _form;
+ private bool _isValid;
+ private string _name = "";
+ private PersonStatus _personStatus = PersonStatus.Member;
+
+ private bool IsEditMode => Person is not null;
+
+ protected override void OnInitialized()
+ {
+ if (Person is not null)
+ {
+ _name = Person.Name;
+ _personStatus = Person.PersonStatus;
+ }
+ }
+
+ private static string GetStatusDescription(PersonStatus status) => status switch
+ {
+ PersonStatus.Member => "Mitglieder sind feste Vereinsmitglieder und werden bei neuen Spieltagen automatisch als Teilnehmer vorausgewählt.",
+ PersonStatus.Guest => "Gäste sind keine festen Mitglieder und müssen bei Spieltagen manuell hinzugefügt werden.",
+ _ => ""
+ };
+
+ private void Cancel() => MudDialog?.Cancel();
+
+ private void Submit()
+ {
+ if (!_isValid) return;
+
+ if (IsEditMode)
+ {
+ var dto = new UpdatePersonDto
+ {
+ Id = Person!.Id,
+ Name = _name,
+ PersonStatus = _personStatus
+ };
+ MudDialog?.Close(DialogResult.Ok(dto));
+ }
+ else
+ {
+ var dto = new CreatePersonDto
+ {
+ Name = _name,
+ PersonStatus = _personStatus
+ };
+ MudDialog?.Close(DialogResult.Ok(dto));
+ }
+ }
+}
diff --git a/src/Koogle.Web/Components/Pages/Persons/Persons.razor b/src/Koogle.Web/Components/Pages/Persons/Persons.razor
new file mode 100644
index 0000000..db34b8a
--- /dev/null
+++ b/src/Koogle.Web/Components/Pages/Persons/Persons.razor
@@ -0,0 +1,171 @@
+@page "/persons"
+@attribute [Authorize(Policy = "ClubViewer")]
+
+@inherits Fluxor.Blazor.Web.Components.FluxorComponent
+
+@using Fluxor
+@using Koogle.Application.DTOs
+@using Koogle.Domain.Enums
+@using Koogle.Web.Store.PersonState
+@using Microsoft.AspNetCore.Authorization
+
+@inject IState PersonState
+@inject IDispatcher Dispatcher
+@inject ISnackbar Snackbar
+@inject IDialogService DialogService
+
+Personen
+
+Personen
+
+@if (PersonState.Value.Error is not null)
+{
+
+ @PersonState.Value.Error
+
+}
+
+
+
+
+
+
+ Mitglied
+ Gast
+
+
+ Neue Person
+
+
+
+ Name
+ Status
+ Erstellt
+ Aktionen
+
+
+ @context.Name
+
+
+ @GetStatusLabel(context.PersonStatus)
+
+
+ @context.CreatedAt.ToString("dd.MM.yyyy")
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Keine Personen gefunden
+
+
+
+@code {
+ private string _searchString = "";
+ private PersonStatus? _statusFilter;
+
+ private IEnumerable FilteredPersons => _statusFilter.HasValue
+ ? PersonState.Value.Persons.Where(p => p.PersonStatus == _statusFilter.Value)
+ : PersonState.Value.Persons;
+
+ protected override void OnInitialized()
+ {
+ base.OnInitialized();
+ Dispatcher.Dispatch(new LoadPersonsAction());
+ }
+
+ private bool FilterFunc(PersonDto person)
+ {
+ if (string.IsNullOrWhiteSpace(_searchString))
+ return true;
+
+ return person.Name.Contains(_searchString, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private void ClearError()
+ {
+ Dispatcher.Dispatch(new ClearPersonErrorAction());
+ }
+
+ private static string GetStatusLabel(PersonStatus status) => status switch
+ {
+ PersonStatus.Member => "Mitglied",
+ PersonStatus.Guest => "Gast",
+ _ => status.ToString()
+ };
+
+ private static Color GetStatusColor(PersonStatus status) => status switch
+ {
+ PersonStatus.Member => Color.Primary,
+ PersonStatus.Guest => Color.Secondary,
+ _ => Color.Default
+ };
+
+ private async Task OpenCreateDialog()
+ {
+ var dialog = await DialogService.ShowAsync("Neue Person");
+ var result = await dialog.Result;
+
+ if (result != null && !result.Canceled && result.Data is CreatePersonDto dto)
+ {
+ Dispatcher.Dispatch(new CreatePersonAction(dto));
+ Snackbar.Add("Person wird erstellt...", Severity.Info);
+ }
+ }
+
+ private async Task OpenEditDialog(PersonDto person)
+ {
+ var parameters = new DialogParameters
+ {
+ { "Person", person }
+ };
+
+ var dialog = await DialogService.ShowAsync("Person bearbeiten", parameters);
+ var result = await dialog.Result;
+
+ if (result != null && !result.Canceled && result.Data is UpdatePersonDto dto)
+ {
+ Dispatcher.Dispatch(new UpdatePersonAction(dto));
+ Snackbar.Add("Person wird aktualisiert...", Severity.Info);
+ }
+ }
+
+ private async Task ConfirmDelete(PersonDto person)
+ {
+ var parameters = new DialogParameters
+ {
+ { "ContentText", $"Möchten Sie \"{person.Name}\" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden." },
+ { "ButtonText", "Löschen" },
+ { "Color", Color.Error }
+ };
+
+ var dialog = await DialogService.ShowAsync("Person löschen", parameters);
+ var result = await dialog.Result;
+
+ if (result != null && !result.Canceled)
+ {
+ Dispatcher.Dispatch(new DeletePersonAction(person.Id));
+ Snackbar.Add("Person wird gelöscht...", Severity.Info);
+ }
+ }
+}