feat(G6): add pending memberships tab to Admin Users

- Add tabs: "Alle Benutzer" + "Ausstehende Anträge"
- Show pending membership requests with approve/reject buttons
- Add RejectMembershipDialog with optional rejection reason

🤖 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-25 20:59:27 +01:00
parent c48a518dda
commit e62bea77d8
2 changed files with 216 additions and 59 deletions

View File

@ -0,0 +1,29 @@
@using Koogle.Application.DTOs
<MudDialog>
<DialogContent>
<MudText Class="mb-3">
Mitgliedschaftsantrag von <strong>@Pending.DisplayName</strong> für Club <strong>@Pending.ClubName</strong> ablehnen?
</MudText>
<MudTextField @bind-Value="_reason"
Label="Begründung (optional)"
Variant="Variant.Outlined"
Lines="3"
Placeholder="z.B. Kein Platz im Club verfügbar..." />
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">Abbrechen</MudButton>
<MudButton Color="Color.Error" Variant="Variant.Filled" OnClick="Submit">Ablehnen</MudButton>
</DialogActions>
</MudDialog>
@code {
[CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!;
[Parameter] public PendingMembershipDto Pending { get; set; } = null!;
private string _reason = "";
private void Cancel() => MudDialog.Cancel();
private void Submit() => MudDialog.Close(DialogResult.Ok(_reason));
}

View File

@ -15,73 +15,123 @@
<MudText Typo="Typo.h4" Class="mb-4">Benutzer verwalten</MudText>
@if (_isLoading)
{
<MudProgressCircular Indeterminate="true" />
}
else
{
<MudTable Items="_users" Dense="true" Hover="true" Filter="FilterFunc" Loading="_isLoading">
<ToolBarContent>
<MudTextField @bind-Value="_searchString"
Placeholder="Suchen..."
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search"
IconSize="Size.Medium"
Class="mt-0" />
</ToolBarContent>
<HeaderContent>
<MudTh>Name</MudTh>
<MudTh>E-Mail</MudTh>
<MudTh>Clubs</MudTh>
<MudTh>Aktionen</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Name">@context.DisplayName</MudTd>
<MudTd DataLabel="E-Mail">@context.Identity.Email</MudTd>
<MudTd DataLabel="Clubs">
@if (context.ClubMemberships.Count == 0)
{
<MudText Typo="Typo.caption" Color="Color.Warning">Kein Club</MudText>
}
else
{
@foreach (var membership in context.ClubMemberships)
{
<MudChip T="string" Size="Size.Small" Color="@(membership.IsDefault ? Color.Primary : Color.Default)" Class="mr-1 mb-1">
@membership.ClubName
@if (membership.Roles.Any())
<MudTabs @bind-ActivePanelIndex="_activeTabIndex" Elevation="0" Rounded="true" ApplyEffectsToContainer="true" Class="mb-4">
<MudTabPanel Text="Alle Benutzer" BadgeData="@(_users.Count)" BadgeColor="Color.Primary">
@if (_isLoading)
{
<MudProgressCircular Indeterminate="true" />
}
else
{
<MudTable Items="_users" Dense="true" Hover="true" Filter="FilterFunc" Loading="_isLoading">
<ToolBarContent>
<MudTextField @bind-Value="_searchString"
Placeholder="Suchen..."
Adornment="Adornment.Start"
AdornmentIcon="@Icons.Material.Filled.Search"
IconSize="Size.Medium"
Class="mt-0" />
</ToolBarContent>
<HeaderContent>
<MudTh>Name</MudTh>
<MudTh>E-Mail</MudTh>
<MudTh>Clubs</MudTh>
<MudTh>Aktionen</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Name">@context.DisplayName</MudTd>
<MudTd DataLabel="E-Mail">@context.Identity.Email</MudTd>
<MudTd DataLabel="Clubs">
@if (context.ClubMemberships.Count == 0)
{
<MudText Typo="Typo.caption" Color="Color.Warning">Kein Club</MudText>
}
else
{
@foreach (var membership in context.ClubMemberships)
{
<span class="ml-1">(@string.Join(", ", membership.Roles))</span>
<MudChip T="string" Size="Size.Small" Color="@(membership.IsDefault ? Color.Primary : Color.Default)" Class="mr-1 mb-1">
@membership.ClubName
@if (membership.Roles.Any())
{
<span class="ml-1">(@string.Join(", ", membership.Roles))</span>
}
</MudChip>
}
</MudChip>
}
}
</MudTd>
<MudTd DataLabel="Aktionen">
<MudTooltip Text="Club zuweisen">
<MudIconButton Icon="@Icons.Material.Filled.GroupAdd"
Size="Size.Small"
OnClick="@(() => OpenAssignClubDialog(context))"/>
</MudTooltip>
<MudTooltip Text="Rollen verwalten">
<MudIconButton Icon="@Icons.Material.Filled.Security"
Size="Size.Small"
OnClick="@(() => OpenManageRolesDialog(context))"/>
</MudTooltip>
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager />
</PagerContent>
</MudTable>
}
}
</MudTd>
<MudTd DataLabel="Aktionen">
<MudTooltip Text="Club zuweisen">
<MudIconButton Icon="@Icons.Material.Filled.GroupAdd"
Size="Size.Small"
OnClick="@(() => OpenAssignClubDialog(context))"/>
</MudTooltip>
<MudTooltip Text="Rollen verwalten">
<MudIconButton Icon="@Icons.Material.Filled.Security"
Size="Size.Small"
OnClick="@(() => OpenManageRolesDialog(context))"/>
</MudTooltip>
</MudTd>
</RowTemplate>
<PagerContent>
<MudTablePager />
</PagerContent>
</MudTable>
}
</MudTabPanel>
<MudTabPanel Text="Ausstehende Anträge" BadgeData="@(_pendingMemberships.Count)" BadgeColor="@(_pendingMemberships.Any() ? Color.Warning : Color.Default)">
@if (_isLoadingPending)
{
<MudProgressCircular Indeterminate="true" />
}
else if (!_pendingMemberships.Any())
{
<MudAlert Severity="Severity.Info" Class="mt-4">Keine ausstehenden Mitgliedschaftsanträge.</MudAlert>
}
else
{
<MudTable Items="_pendingMemberships" Dense="true" Hover="true" Class="mt-2">
<HeaderContent>
<MudTh>Name</MudTh>
<MudTh>E-Mail</MudTh>
<MudTh>Club</MudTh>
<MudTh>Angefragt am</MudTh>
<MudTh>Aktionen</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Name">@context.DisplayName</MudTd>
<MudTd DataLabel="E-Mail">@context.Email</MudTd>
<MudTd DataLabel="Club">@context.ClubName</MudTd>
<MudTd DataLabel="Angefragt am">@context.RequestedAt.ToString("dd.MM.yyyy HH:mm")</MudTd>
<MudTd DataLabel="Aktionen">
<MudTooltip Text="Genehmigen">
<MudIconButton Icon="@Icons.Material.Filled.Check"
Color="Color.Success"
Size="Size.Small"
OnClick="@(() => ApproveMembership(context))"/>
</MudTooltip>
<MudTooltip Text="Ablehnen">
<MudIconButton Icon="@Icons.Material.Filled.Close"
Color="Color.Error"
Size="Size.Small"
OnClick="@(() => OpenRejectDialog(context))"/>
</MudTooltip>
</MudTd>
</RowTemplate>
</MudTable>
}
</MudTabPanel>
</MudTabs>
@code {
private List<UserDto> _users = new();
private List<ClubDto> _clubs = new();
private List<PendingMembershipDto> _pendingMemberships = new();
private bool _isLoading = true;
private bool _isLoadingPending = true;
private string _searchString = "";
private int _activeTabIndex = 0;
protected override async Task OnInitializedAsync()
{
@ -91,6 +141,7 @@ else
private async Task LoadData()
{
_isLoading = true;
_isLoadingPending = true;
try
{
var usersTask = UserService.GetAllUsersAsync();
@ -99,10 +150,33 @@ else
_users = (await usersTask).ToList();
_clubs = await clubsTask;
_isLoading = false;
await LoadPendingMemberships();
}
finally
{
_isLoading = false;
_isLoadingPending = false;
}
}
private async Task LoadPendingMemberships()
{
_isLoadingPending = true;
try
{
var allPending = new List<PendingMembershipDto>();
foreach (var club in _clubs)
{
var clubPending = await UserService.GetPendingMembershipsAsync(club.Id);
allPending.AddRange(clubPending);
}
_pendingMemberships = allPending.OrderByDescending(p => p.RequestedAt).ToList();
}
finally
{
_isLoadingPending = false;
}
}
@ -174,4 +248,58 @@ else
await LoadData();
}
}
private async Task ApproveMembership(PendingMembershipDto pending)
{
var currentUser = await UserService.GetCurrentUserAsync();
if (currentUser == null)
{
Snackbar.Add("Nicht angemeldet", Severity.Error);
return;
}
var success = await UserService.ApproveMembershipAsync(pending.UserProfileId, pending.ClubId, currentUser.ProfileId);
if (success)
{
Snackbar.Add($"Mitgliedschaft von {pending.DisplayName} genehmigt", Severity.Success);
await LoadData();
}
else
{
Snackbar.Add("Fehler beim Genehmigen der Mitgliedschaft", Severity.Error);
}
}
private async Task OpenRejectDialog(PendingMembershipDto pending)
{
var parameters = new DialogParameters
{
{ "Pending", pending }
};
var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small };
var dialog = await DialogService.ShowAsync<RejectMembershipDialog>("Mitgliedschaft ablehnen", parameters, options);
var result = await dialog.Result;
if (result != null && !result.Canceled && result.Data is string reason)
{
var currentUser = await UserService.GetCurrentUserAsync();
if (currentUser == null)
{
Snackbar.Add("Nicht angemeldet", Severity.Error);
return;
}
var success = await UserService.RejectMembershipAsync(pending.UserProfileId, pending.ClubId, currentUser.ProfileId, reason);
if (success)
{
Snackbar.Add($"Mitgliedschaft von {pending.DisplayName} abgelehnt", Severity.Success);
await LoadData();
}
else
{
Snackbar.Add("Fehler beim Ablehnen der Mitgliedschaft", Severity.Error);
}
}
}
}