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> <MudText Typo="Typo.h4" Class="mb-4">Benutzer verwalten</MudText>
@if (_isLoading) <MudTabs @bind-ActivePanelIndex="_activeTabIndex" Elevation="0" Rounded="true" ApplyEffectsToContainer="true" Class="mb-4">
{ <MudTabPanel Text="Alle Benutzer" BadgeData="@(_users.Count)" BadgeColor="Color.Primary">
<MudProgressCircular Indeterminate="true" /> @if (_isLoading)
} {
else <MudProgressCircular Indeterminate="true" />
{ }
<MudTable Items="_users" Dense="true" Hover="true" Filter="FilterFunc" Loading="_isLoading"> else
<ToolBarContent> {
<MudTextField @bind-Value="_searchString" <MudTable Items="_users" Dense="true" Hover="true" Filter="FilterFunc" Loading="_isLoading">
Placeholder="Suchen..." <ToolBarContent>
Adornment="Adornment.Start" <MudTextField @bind-Value="_searchString"
AdornmentIcon="@Icons.Material.Filled.Search" Placeholder="Suchen..."
IconSize="Size.Medium" Adornment="Adornment.Start"
Class="mt-0" /> AdornmentIcon="@Icons.Material.Filled.Search"
</ToolBarContent> IconSize="Size.Medium"
<HeaderContent> Class="mt-0" />
<MudTh>Name</MudTh> </ToolBarContent>
<MudTh>E-Mail</MudTh> <HeaderContent>
<MudTh>Clubs</MudTh> <MudTh>Name</MudTh>
<MudTh>Aktionen</MudTh> <MudTh>E-Mail</MudTh>
</HeaderContent> <MudTh>Clubs</MudTh>
<RowTemplate> <MudTh>Aktionen</MudTh>
<MudTd DataLabel="Name">@context.DisplayName</MudTd> </HeaderContent>
<MudTd DataLabel="E-Mail">@context.Identity.Email</MudTd> <RowTemplate>
<MudTd DataLabel="Clubs"> <MudTd DataLabel="Name">@context.DisplayName</MudTd>
@if (context.ClubMemberships.Count == 0) <MudTd DataLabel="E-Mail">@context.Identity.Email</MudTd>
{ <MudTd DataLabel="Clubs">
<MudText Typo="Typo.caption" Color="Color.Warning">Kein Club</MudText> @if (context.ClubMemberships.Count == 0)
} {
else <MudText Typo="Typo.caption" Color="Color.Warning">Kein Club</MudText>
{ }
@foreach (var membership in context.ClubMemberships) else
{ {
<MudChip T="string" Size="Size.Small" Color="@(membership.IsDefault ? Color.Primary : Color.Default)" Class="mr-1 mb-1"> @foreach (var membership in context.ClubMemberships)
@membership.ClubName
@if (membership.Roles.Any())
{ {
<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">
</MudTd> <MudTooltip Text="Club zuweisen">
<MudTd DataLabel="Aktionen"> <MudIconButton Icon="@Icons.Material.Filled.GroupAdd"
<MudTooltip Text="Club zuweisen"> Size="Size.Small"
<MudIconButton Icon="@Icons.Material.Filled.GroupAdd" OnClick="@(() => OpenAssignClubDialog(context))"/>
Size="Size.Small" </MudTooltip>
OnClick="@(() => OpenAssignClubDialog(context))"/> <MudTooltip Text="Rollen verwalten">
</MudTooltip> <MudIconButton Icon="@Icons.Material.Filled.Security"
<MudTooltip Text="Rollen verwalten"> Size="Size.Small"
<MudIconButton Icon="@Icons.Material.Filled.Security" OnClick="@(() => OpenManageRolesDialog(context))"/>
Size="Size.Small" </MudTooltip>
OnClick="@(() => OpenManageRolesDialog(context))"/> </MudTd>
</MudTooltip> </RowTemplate>
</MudTd> <PagerContent>
</RowTemplate> <MudTablePager />
<PagerContent> </PagerContent>
<MudTablePager /> </MudTable>
</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 { @code {
private List<UserDto> _users = new(); private List<UserDto> _users = new();
private List<ClubDto> _clubs = new(); private List<ClubDto> _clubs = new();
private List<PendingMembershipDto> _pendingMemberships = new();
private bool _isLoading = true; private bool _isLoading = true;
private bool _isLoadingPending = true;
private string _searchString = ""; private string _searchString = "";
private int _activeTabIndex = 0;
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
@ -91,6 +141,7 @@ else
private async Task LoadData() private async Task LoadData()
{ {
_isLoading = true; _isLoading = true;
_isLoadingPending = true;
try try
{ {
var usersTask = UserService.GetAllUsersAsync(); var usersTask = UserService.GetAllUsersAsync();
@ -99,10 +150,33 @@ else
_users = (await usersTask).ToList(); _users = (await usersTask).ToList();
_clubs = await clubsTask; _clubs = await clubsTask;
_isLoading = false;
await LoadPendingMemberships();
} }
finally finally
{ {
_isLoading = false; _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(); 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);
}
}
}
} }