diff --git a/src/Koogle.Web/Components/Pages/Admin/ClubEditDialog.razor b/src/Koogle.Web/Components/Pages/Admin/ClubEditDialog.razor
new file mode 100644
index 0000000..0f04825
--- /dev/null
+++ b/src/Koogle.Web/Components/Pages/Admin/ClubEditDialog.razor
@@ -0,0 +1,97 @@
+@using Koogle.Application.DTOs
+@using Koogle.Domain.Enums
+
+
+
+
+ @(IsEditMode ? "Club bearbeiten" : "Neuer Club")
+
+
+
+
+
+
+
+ Keine
+ Durchschnitt
+ Maximum
+
+
+
+ @GetCalculationDescription(_expenseCalculation)
+
+
+
+
+ Abbrechen
+
+ @(IsEditMode ? "Speichern" : "Erstellen")
+
+
+
+
+@code {
+ [CascadingParameter]
+ private IMudDialogInstance? MudDialog { get; set; }
+
+ [Parameter]
+ public ClubDto? Club { get; set; }
+
+ private MudForm? _form;
+ private bool _isValid;
+ private string _name = "";
+ private ExpenseCalculation _expenseCalculation = ExpenseCalculation.None;
+
+ private bool IsEditMode => Club is not null;
+
+ protected override void OnInitialized()
+ {
+ if (Club is not null)
+ {
+ _name = Club.Name;
+ _expenseCalculation = Club.ExpenseCalculation;
+ }
+ }
+
+ private static string GetCalculationDescription(ExpenseCalculation calculation) => calculation switch
+ {
+ ExpenseCalculation.None => "Keine automatische Kostenberechnung für abwesende Personen",
+ ExpenseCalculation.Average => "Abwesende Personen zahlen den Durchschnitt aller Kosten des Tages",
+ ExpenseCalculation.Maximum => "Abwesende Personen zahlen das Maximum aller Kosten des Tages",
+ _ => ""
+ };
+
+ private void Cancel() => MudDialog?.Cancel();
+
+ private void Submit()
+ {
+ if (!_isValid) return;
+
+ if (IsEditMode)
+ {
+ var dto = new UpdateClubDto
+ {
+ Id = Club!.Id,
+ Name = _name,
+ ExpenseCalculation = _expenseCalculation
+ };
+ MudDialog?.Close(DialogResult.Ok(dto));
+ }
+ else
+ {
+ var dto = new CreateClubDto
+ {
+ Name = _name,
+ ExpenseCalculation = _expenseCalculation
+ };
+ MudDialog?.Close(DialogResult.Ok(dto));
+ }
+ }
+}
diff --git a/src/Koogle.Web/Components/Pages/Admin/Clubs.razor b/src/Koogle.Web/Components/Pages/Admin/Clubs.razor
new file mode 100644
index 0000000..992b21f
--- /dev/null
+++ b/src/Koogle.Web/Components/Pages/Admin/Clubs.razor
@@ -0,0 +1,170 @@
+@page "/admin/clubs"
+@attribute [Authorize(Roles = "SuperAdmin")]
+
+@inherits Fluxor.Blazor.Web.Components.FluxorComponent
+
+@using Fluxor
+@using Koogle.Application.DTOs
+@using Koogle.Domain.Enums
+@using Koogle.Web.Store.ClubState
+@using Microsoft.AspNetCore.Authorization
+
+@inject IState ClubState
+@inject IDispatcher Dispatcher
+@inject ISnackbar Snackbar
+@inject IDialogService DialogService
+
+Clubs verwalten
+
+Clubs verwalten
+
+@if (ClubState.Value.Error is not null)
+{
+
+ @ClubState.Value.Error
+
+}
+
+
+
+
+
+
+ Neuer Club
+
+
+
+ Name
+ Kostenberechnung
+ Mitglieder
+ Gäste
+ Tage
+ Erstellt
+ Aktionen
+
+
+ @context.Name
+
+
+ @GetCalculationLabel(context.ExpenseCalculation)
+
+
+ @context.MemberCount
+ @context.GuestCount
+ @context.DayCount
+ @context.CreatedAt.ToString("dd.MM.yyyy")
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Keine Clubs gefunden
+
+
+
+@code {
+ private string _searchString = "";
+
+ protected override void OnInitialized()
+ {
+ base.OnInitialized();
+ Dispatcher.Dispatch(new LoadClubsAction());
+ }
+
+ private bool FilterFunc(ClubDto club)
+ {
+ if (string.IsNullOrWhiteSpace(_searchString))
+ return true;
+
+ return club.Name.Contains(_searchString, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private void ClearError()
+ {
+ Dispatcher.Dispatch(new ClearClubErrorAction());
+ }
+
+ private static string GetCalculationLabel(ExpenseCalculation calculation) => calculation switch
+ {
+ ExpenseCalculation.None => "Keine",
+ ExpenseCalculation.Average => "Durchschnitt",
+ ExpenseCalculation.Maximum => "Maximum",
+ _ => calculation.ToString()
+ };
+
+ private static Color GetCalculationColor(ExpenseCalculation calculation) => calculation switch
+ {
+ ExpenseCalculation.None => Color.Default,
+ ExpenseCalculation.Average => Color.Info,
+ ExpenseCalculation.Maximum => Color.Warning,
+ _ => Color.Default
+ };
+
+ private async Task OpenCreateDialog()
+ {
+ var dialog = await DialogService.ShowAsync("Neuer Club");
+ var result = await dialog.Result;
+
+ if (result != null && !result.Canceled && result.Data is CreateClubDto dto)
+ {
+ Dispatcher.Dispatch(new CreateClubAction(dto));
+ Snackbar.Add("Club wird erstellt...", Severity.Info);
+ }
+ }
+
+ private async Task OpenEditDialog(ClubDto club)
+ {
+ var parameters = new DialogParameters
+ {
+ { "Club", club }
+ };
+
+ var dialog = await DialogService.ShowAsync("Club bearbeiten", parameters);
+ var result = await dialog.Result;
+
+ if (result != null && !result.Canceled && result.Data is UpdateClubDto dto)
+ {
+ Dispatcher.Dispatch(new UpdateClubAction(dto));
+ Snackbar.Add("Club wird aktualisiert...", Severity.Info);
+ }
+ }
+
+ private async Task ConfirmDelete(ClubDto club)
+ {
+ var parameters = new DialogParameters
+ {
+ { "ContentText", $"Möchten Sie den Club \"{club.Name}\" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden." },
+ { "ButtonText", "Löschen" },
+ { "Color", Color.Error }
+ };
+
+ var dialog = await DialogService.ShowAsync("Club löschen", parameters);
+ var result = await dialog.Result;
+
+ if (result != null && !result.Canceled)
+ {
+ Dispatcher.Dispatch(new DeleteClubAction(club.Id));
+ Snackbar.Add("Club wird gelöscht...", Severity.Info);
+ }
+ }
+}
diff --git a/src/Koogle.Web/Components/Shared/ConfirmDialog.razor b/src/Koogle.Web/Components/Shared/ConfirmDialog.razor
new file mode 100644
index 0000000..08b57c8
--- /dev/null
+++ b/src/Koogle.Web/Components/Shared/ConfirmDialog.razor
@@ -0,0 +1,26 @@
+
+
+ @ContentText
+
+
+ Abbrechen
+ @ButtonText
+
+
+
+@code {
+ [CascadingParameter]
+ private IMudDialogInstance? MudDialog { get; set; }
+
+ [Parameter]
+ public string ContentText { get; set; } = "Sind Sie sicher?";
+
+ [Parameter]
+ public string ButtonText { get; set; } = "OK";
+
+ [Parameter]
+ public Color Color { get; set; } = Color.Primary;
+
+ private void Cancel() => MudDialog?.Cancel();
+ private void Submit() => MudDialog?.Close(DialogResult.Ok(true));
+}