Compare commits
9 Commits
8e2342fd74
...
8d2222de8f
| Author | SHA1 | Date |
|---|---|---|
|
|
8d2222de8f | |
|
|
033a3ebd4e | |
|
|
0911236a0d | |
|
|
637a3b120c | |
|
|
97ea3b35ca | |
|
|
e8b2154f69 | |
|
|
e62bea77d8 | |
|
|
c48a518dda | |
|
|
ec50e971a9 |
|
|
@ -357,10 +357,10 @@ NavMenu.razor:
|
|||
| ✓ | G2 | Erweiterte Reg. | ClubInvitation Entity | 1 Entity + Migration |
|
||||
| ✓ | G3 | Erweiterte Reg. | IEmailService (Stub) | 2 Dateien |
|
||||
| ✓ | G4 | Erweiterte Reg. | Services erweitern | 2 Services |
|
||||
| ☐ | G5 | Erweiterte Reg. | Dashboard Pending-Widget | 1 Component |
|
||||
| ☐ | G6 | Erweiterte Reg. | Admin Users Page erweitern | 1 Razor |
|
||||
| ☐ | G7 | Erweiterte Reg. | Club-Beitritt UI | 1 Razor |
|
||||
| ☐ | G8 | Erweiterte Reg. | Einladungslink-Handling | 2 Dateien |
|
||||
| ✓ | G5 | Erweiterte Reg. | Dashboard Pending-Widget | 1 Component |
|
||||
| ✓ | G6 | Erweiterte Reg. | Admin Users Page erweitern | 1 Razor |
|
||||
| ✓ | G7 | Erweiterte Reg. | Club-Beitritt UI | 1 Razor |
|
||||
| ✓ | G8 | Erweiterte Reg. | Einladungslink-Handling | 2 Dateien |
|
||||
|
||||
**Legende:** ☐ = Offen | ☑ = In Arbeit | ✓ = Fertig
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
## Optimierung Spieltag-Details
|
||||
Die Erfassung von Strafen muss möglichst schnell und komfortabel mögiich sein. Dafür muss die Seite DayDetails opimiert werden.
|
||||
Es soll möglich sein eine Person in der Teilnehmerliste auszuwählen. Bei der Erfassung einer neuen Strafe kann diese Person direkt vorbelegt werden. Außerdem soll zusätzlich zum Button "Strafe hinzufügen" ein Menü-Button angezeigt werden, der eine Kurzwahl aller Strafen mit der Option "IsOneClick" ermöglicht.
|
||||
Die Erfassung von Strafen muss möglichst schnell und komfortabel mögiich sein. Dafür muss die Seite DayDetails optimiert werden.
|
||||
Es soll möglich sein eine Person in der Teilnehmerliste auszuwählen. Bei der Erfassung einer neuen Strafe soll diese Person direkt vorbelegt werden. Außerdem soll zusätzlich zum Button "Strafe hinzufügen" ein Menü-Button angezeigt werden, der eine Kurzwahl aller Strafen mit der Option "IsOneClick" ermöglicht.
|
||||
Der Benutzer soll visuell leicht erkennen können, ob und welcher Teilnehmer aktuell markiert wurde.
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -71,6 +71,11 @@ namespace Koogle.Application.DTOs
|
|||
/// Optional club name to assign user to during registration.
|
||||
/// </summary>
|
||||
public string? ClubName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional invitation token for club membership during registration.
|
||||
/// </summary>
|
||||
public string? InviteToken { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,231 @@
|
|||
@page "/account/join-club"
|
||||
@attribute [Authorize]
|
||||
|
||||
@using Koogle.Application.DTOs
|
||||
@using Koogle.Application.Interfaces
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
|
||||
@inject IUserService UserService
|
||||
@inject IClubService ClubService
|
||||
@inject ISnackbar Snackbar
|
||||
@inject NavigationManager NavigationManager
|
||||
|
||||
<PageTitle>Club beitreten</PageTitle>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.Small" Class="mt-4">
|
||||
<MudText Typo="Typo.h4" Class="mb-4">Club beitreten</MudText>
|
||||
|
||||
@if (_isLoading)
|
||||
{
|
||||
<MudProgressCircular Indeterminate="true" />
|
||||
}
|
||||
else if (_requestSent)
|
||||
{
|
||||
<MudAlert Severity="Severity.Success" Class="mb-4">
|
||||
<MudText Typo="Typo.body1">
|
||||
Dein Beitrittsantrag wurde erfolgreich gesendet!
|
||||
</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mt-2">
|
||||
Ein Admin des Clubs wird deinen Antrag pruefen. Du erhaeltst eine Benachrichtigung, sobald dein Antrag bearbeitet wurde.
|
||||
</MudText>
|
||||
</MudAlert>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="ResetForm">
|
||||
Weiteren Antrag stellen
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Default" Class="ml-2" Href="/account/profile">
|
||||
Zum Profil
|
||||
</MudButton>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudPaper Class="pa-4" Elevation="2">
|
||||
<MudText Typo="Typo.body1" Class="mb-4">
|
||||
Gib den Namen des Clubs ein, dem du beitreten moechtest. Dein Antrag wird vom Club-Admin geprueft.
|
||||
</MudText>
|
||||
|
||||
<MudForm @ref="_form" @bind-IsValid="_isValid">
|
||||
<MudTextField @bind-Value="_clubName"
|
||||
Label="Club-Name"
|
||||
Variant="Variant.Outlined"
|
||||
Required="true"
|
||||
RequiredError="Bitte gib einen Club-Namen ein"
|
||||
Immediate="true"
|
||||
OnKeyUp="OnClubNameKeyUp"
|
||||
Class="mb-3" />
|
||||
|
||||
@if (_searchResult is not null)
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Class="mb-3">
|
||||
<div class="d-flex justify-space-between align-center">
|
||||
<div>
|
||||
<MudText Typo="Typo.body1"><strong>@_searchResult.Name</strong></MudText>
|
||||
<MudText Typo="Typo.caption">@_searchResult.MemberCount Mitglieder</MudText>
|
||||
</div>
|
||||
<MudIcon Icon="@Icons.Material.Filled.Check" Color="Color.Success" />
|
||||
</div>
|
||||
</MudAlert>
|
||||
}
|
||||
else if (_searched && !string.IsNullOrWhiteSpace(_clubName))
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning" Class="mb-3">
|
||||
Kein Club mit diesem Namen gefunden. Bitte pruefe die Schreibweise.
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(_error))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Class="mb-3">@_error</MudAlert>
|
||||
}
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<MudButton Variant="Variant.Outlined"
|
||||
Color="Color.Primary"
|
||||
OnClick="SearchClub"
|
||||
Disabled="_isSearching || string.IsNullOrWhiteSpace(_clubName)">
|
||||
@if (_isSearching)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
|
||||
}
|
||||
Club suchen
|
||||
</MudButton>
|
||||
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
OnClick="SubmitRequest"
|
||||
Disabled="_isSubmitting || _searchResult is null">
|
||||
@if (_isSubmitting)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
|
||||
}
|
||||
Beitrittsantrag senden
|
||||
</MudButton>
|
||||
</div>
|
||||
</MudForm>
|
||||
</MudPaper>
|
||||
}
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
private MudForm? _form;
|
||||
private bool _isValid;
|
||||
private string _clubName = "";
|
||||
private ClubDto? _searchResult;
|
||||
private bool _searched;
|
||||
private bool _isLoading;
|
||||
private bool _isSearching;
|
||||
private bool _isSubmitting;
|
||||
private bool _requestSent;
|
||||
private string? _error;
|
||||
private UserDto? _currentUser;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_isLoading = true;
|
||||
try
|
||||
{
|
||||
_currentUser = await UserService.GetCurrentUserAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnClubNameKeyUp(KeyboardEventArgs e)
|
||||
{
|
||||
// Reset search result when user types
|
||||
if (_searched)
|
||||
{
|
||||
_searched = false;
|
||||
_searchResult = null;
|
||||
_error = null;
|
||||
}
|
||||
|
||||
// Search on Enter key
|
||||
if (e.Key == "Enter" && !string.IsNullOrWhiteSpace(_clubName))
|
||||
{
|
||||
await SearchClub();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SearchClub()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_clubName))
|
||||
return;
|
||||
|
||||
_isSearching = true;
|
||||
_error = null;
|
||||
_searched = false;
|
||||
_searchResult = null;
|
||||
|
||||
try
|
||||
{
|
||||
_searchResult = await ClubService.GetByNameAsync(_clubName.Trim());
|
||||
_searched = true;
|
||||
|
||||
// Check if user is already member
|
||||
if (_searchResult is not null && _currentUser is not null)
|
||||
{
|
||||
var existingMembership = _currentUser.ClubMemberships
|
||||
.FirstOrDefault(m => m.ClubId == _searchResult.Id);
|
||||
|
||||
if (existingMembership is not null)
|
||||
{
|
||||
_error = "Du bist bereits Mitglied dieses Clubs oder hast einen offenen Antrag.";
|
||||
_searchResult = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_error = $"Fehler bei der Suche: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isSearching = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SubmitRequest()
|
||||
{
|
||||
if (_searchResult is null || _currentUser is null)
|
||||
return;
|
||||
|
||||
_isSubmitting = true;
|
||||
_error = null;
|
||||
|
||||
try
|
||||
{
|
||||
var success = await UserService.RequestClubMembershipAsync(
|
||||
_currentUser.ProfileId,
|
||||
_searchResult.Id);
|
||||
|
||||
if (success)
|
||||
{
|
||||
_requestSent = true;
|
||||
Snackbar.Add("Beitrittsantrag erfolgreich gesendet", Severity.Success);
|
||||
}
|
||||
else
|
||||
{
|
||||
_error = "Der Antrag konnte nicht gesendet werden. Moeglicherweise existiert bereits ein Antrag.";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_error = $"Fehler beim Senden des Antrags: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isSubmitting = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetForm()
|
||||
{
|
||||
_clubName = "";
|
||||
_searchResult = null;
|
||||
_searched = false;
|
||||
_requestSent = false;
|
||||
_error = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,12 @@
|
|||
<input type="hidden" name="__RequestVerificationToken" value="@_antiToken" />
|
||||
|
||||
<MudPaper Class="pa-6" Elevation="4" MaxWidth="400px">
|
||||
@if (!string.IsNullOrWhiteSpace(_inviteToken))
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Class="mb-3">
|
||||
Bitte melde dich an, um die Club-Einladung anzunehmen.
|
||||
</MudAlert>
|
||||
}
|
||||
@if (_registered)
|
||||
{
|
||||
<MudAlert Severity="Severity.Success" Class="mb-3">Registrierung erfolgreich! Bitte anmelden.</MudAlert>
|
||||
|
|
@ -86,6 +92,7 @@
|
|||
private string? _error;
|
||||
private string _antiToken = "";
|
||||
private bool _registered;
|
||||
private string? _inviteToken;
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
|
|
@ -101,6 +108,12 @@
|
|||
{
|
||||
_registered = true;
|
||||
}
|
||||
if (query.TryGetValue("invite", out var invite))
|
||||
{
|
||||
_inviteToken = invite.ToString();
|
||||
// After login, redirect to join by invite
|
||||
_returnUrl = $"/club/join/{_inviteToken}";
|
||||
}
|
||||
|
||||
// Antiforgery Token generieren (klassischer MVC Token)
|
||||
var http = HttpContextAccessor.HttpContext!;
|
||||
|
|
|
|||
|
|
@ -9,8 +9,18 @@
|
|||
<form method="post" action="/auth/register">
|
||||
<!-- Hidden Fields -->
|
||||
<input type="hidden" name="__RequestVerificationToken" value="@_antiToken" />
|
||||
@if (!string.IsNullOrWhiteSpace(_inviteToken))
|
||||
{
|
||||
<input type="hidden" name="InviteToken" value="@_inviteToken" />
|
||||
}
|
||||
|
||||
<MudPaper Class="pa-6" Elevation="4" MaxWidth="400px">
|
||||
@if (!string.IsNullOrWhiteSpace(_inviteToken))
|
||||
{
|
||||
<MudAlert Severity="Severity.Info" Class="mb-3">
|
||||
Du wurdest zu einem Club eingeladen. Registriere dich, um beizutreten.
|
||||
</MudAlert>
|
||||
}
|
||||
@if (!string.IsNullOrWhiteSpace(_error))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Class="mb-3">@_error</MudAlert>
|
||||
|
|
@ -75,6 +85,7 @@
|
|||
|
||||
@code {
|
||||
private string? _error;
|
||||
private string? _inviteToken;
|
||||
private string _antiToken = "";
|
||||
|
||||
protected override void OnInitialized()
|
||||
|
|
@ -87,6 +98,11 @@
|
|||
_error = MapErrorCodes(err.ToString());
|
||||
}
|
||||
|
||||
if (query.TryGetValue("invite", out var invite))
|
||||
{
|
||||
_inviteToken = invite.ToString();
|
||||
}
|
||||
|
||||
// Antiforgery Token generieren
|
||||
var http = HttpContextAccessor.HttpContext!;
|
||||
var tokens = Antiforgery.GetAndStoreTokens(http);
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,201 @@
|
|||
@page "/club/join/{Token}"
|
||||
|
||||
@using Koogle.Application.DTOs
|
||||
@using Koogle.Application.Interfaces
|
||||
@using Microsoft.AspNetCore.Components.Authorization
|
||||
|
||||
@inject IClubService ClubService
|
||||
@inject IUserService UserService
|
||||
@inject AuthenticationStateProvider AuthStateProvider
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<PageTitle>Club beitreten</PageTitle>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.Small" Class="mt-4">
|
||||
<MudText Typo="Typo.h4" Class="mb-4">Einladung zum Club</MudText>
|
||||
|
||||
@if (_isLoading)
|
||||
{
|
||||
<MudProgressCircular Indeterminate="true" />
|
||||
}
|
||||
else if (_invalidToken)
|
||||
{
|
||||
<MudPaper Class="pa-6" Elevation="2">
|
||||
<div class="d-flex flex-column align-center text-center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.LinkOff" Size="Size.Large" Color="Color.Error" Class="mb-4" />
|
||||
<MudText Typo="Typo.h5" Class="mb-2">Ungueltige Einladung</MudText>
|
||||
<MudText Typo="Typo.body1" Color="Color.Secondary" Class="mb-4">
|
||||
@_errorMessage
|
||||
</MudText>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" Href="/account/join-club">
|
||||
Club manuell suchen
|
||||
</MudButton>
|
||||
</div>
|
||||
</MudPaper>
|
||||
}
|
||||
else if (_requestSent)
|
||||
{
|
||||
<MudPaper Class="pa-6" Elevation="2">
|
||||
<div class="d-flex flex-column align-center text-center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Size="Size.Large" Color="Color.Success" Class="mb-4" />
|
||||
<MudText Typo="Typo.h5" Class="mb-2">Beitrittsantrag gesendet!</MudText>
|
||||
<MudText Typo="Typo.body1" Color="Color.Secondary" Class="mb-4">
|
||||
Dein Antrag fuer <strong>@_invitation?.ClubName</strong> wurde gesendet.
|
||||
Ein Admin wird ihn pruefen.
|
||||
</MudText>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" Href="/dashboard">
|
||||
Zum Dashboard
|
||||
</MudButton>
|
||||
</div>
|
||||
</MudPaper>
|
||||
}
|
||||
else if (_invitation is not null)
|
||||
{
|
||||
<MudPaper Class="pa-6" Elevation="2">
|
||||
<div class="d-flex flex-column align-center text-center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.GroupAdd" Size="Size.Large" Color="Color.Primary" Class="mb-4" />
|
||||
<MudText Typo="Typo.h5" Class="mb-2">@_invitation.ClubName</MudText>
|
||||
<MudText Typo="Typo.body1" Color="Color.Secondary" Class="mb-4">
|
||||
Du wurdest eingeladen, diesem Club beizutreten.
|
||||
</MudText>
|
||||
|
||||
@if (!string.IsNullOrEmpty(_error))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Class="mb-4">@_error</MudAlert>
|
||||
}
|
||||
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
OnClick="SubmitRequest"
|
||||
Disabled="_isSubmitting"
|
||||
StartIcon="@Icons.Material.Filled.PersonAdd">
|
||||
@if (_isSubmitting)
|
||||
{
|
||||
<MudProgressCircular Size="Size.Small" Indeterminate="true" Class="mr-2" />
|
||||
}
|
||||
Beitreten
|
||||
</MudButton>
|
||||
</div>
|
||||
</MudPaper>
|
||||
}
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
[Parameter]
|
||||
public string Token { get; set; } = "";
|
||||
|
||||
private ClubInvitationDto? _invitation;
|
||||
private UserDto? _currentUser;
|
||||
private bool _isLoading = true;
|
||||
private bool _invalidToken;
|
||||
private bool _requestSent;
|
||||
private bool _isSubmitting;
|
||||
private string? _error;
|
||||
private string? _errorMessage;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await ValidateAndProcess();
|
||||
}
|
||||
|
||||
private async Task ValidateAndProcess()
|
||||
{
|
||||
_isLoading = true;
|
||||
|
||||
try
|
||||
{
|
||||
// Check if user is authenticated
|
||||
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
|
||||
var isAuthenticated = authState.User.Identity?.IsAuthenticated ?? false;
|
||||
|
||||
if (!isAuthenticated)
|
||||
{
|
||||
// Redirect to register with token
|
||||
NavigationManager.NavigateTo($"/account/register?invite={Uri.EscapeDataString(Token)}");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate token
|
||||
_invitation = await ClubService.GetInvitationByTokenAsync(Token);
|
||||
|
||||
if (_invitation is null)
|
||||
{
|
||||
_invalidToken = true;
|
||||
_errorMessage = "Diese Einladung existiert nicht oder wurde geloescht.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_invitation.IsValid)
|
||||
{
|
||||
_invalidToken = true;
|
||||
_errorMessage = "Diese Einladung ist abgelaufen oder wurde bereits zu oft verwendet.";
|
||||
return;
|
||||
}
|
||||
|
||||
// Load current user
|
||||
_currentUser = await UserService.GetCurrentUserAsync();
|
||||
|
||||
if (_currentUser is null)
|
||||
{
|
||||
_invalidToken = true;
|
||||
_errorMessage = "Benutzerprofil konnte nicht geladen werden.";
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if already member
|
||||
var existingMembership = _currentUser.ClubMemberships
|
||||
.FirstOrDefault(m => m.ClubId == _invitation.ClubId);
|
||||
|
||||
if (existingMembership is not null)
|
||||
{
|
||||
_invalidToken = true;
|
||||
_errorMessage = "Du bist bereits Mitglied dieses Clubs oder hast einen offenen Antrag.";
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_invalidToken = true;
|
||||
_errorMessage = $"Fehler beim Laden der Einladung: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SubmitRequest()
|
||||
{
|
||||
if (_invitation is null || _currentUser is null)
|
||||
return;
|
||||
|
||||
_isSubmitting = true;
|
||||
_error = null;
|
||||
|
||||
try
|
||||
{
|
||||
var success = await UserService.RequestClubMembershipByInviteAsync(
|
||||
_currentUser.ProfileId,
|
||||
Token);
|
||||
|
||||
if (success)
|
||||
{
|
||||
_requestSent = true;
|
||||
Snackbar.Add("Beitrittsantrag erfolgreich gesendet", Severity.Success);
|
||||
}
|
||||
else
|
||||
{
|
||||
_error = "Der Antrag konnte nicht gesendet werden.";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_error = $"Fehler: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isSubmitting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,16 @@
|
|||
@page "/dashboard"
|
||||
@attribute [Authorize(Policy = "ClubViewer")]
|
||||
@attribute [Authorize]
|
||||
|
||||
@using Koogle.Application.DTOs
|
||||
@using Koogle.Application.Interfaces
|
||||
@using Koogle.Domain.Enums
|
||||
@using Koogle.Infrastructure.Security
|
||||
@using Koogle.Web.Components.Shared
|
||||
@using Microsoft.AspNetCore.Authorization
|
||||
|
||||
@inject IDashboardService DashboardService
|
||||
@inject IUserService UserService
|
||||
@inject ICurrentClubContext CurrentClubContext
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
|
|
@ -18,6 +22,21 @@
|
|||
{
|
||||
<MudProgressLinear Indeterminate="true" Color="Color.Primary" />
|
||||
}
|
||||
else if (_hasNoClub)
|
||||
{
|
||||
<MudPaper Class="pa-6" Elevation="2" MaxWidth="600px">
|
||||
<div class="d-flex flex-column align-center text-center">
|
||||
<MudIcon Icon="@Icons.Material.Filled.GroupOff" Size="Size.Large" Color="Color.Secondary" Class="mb-4" />
|
||||
<MudText Typo="Typo.h5" Class="mb-2">Keinem Club zugeordnet</MudText>
|
||||
<MudText Typo="Typo.body1" Color="Color.Secondary" Class="mb-4">
|
||||
Du bist noch keinem Club zugeordnet. Um Koogle zu nutzen, tritt einem bestehenden Club bei.
|
||||
</MudText>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" Href="/account/join-club" StartIcon="@Icons.Material.Filled.GroupAdd">
|
||||
Einem Club beitreten
|
||||
</MudButton>
|
||||
</div>
|
||||
</MudPaper>
|
||||
}
|
||||
else if (_error is not null)
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Class="mb-4">@_error</MudAlert>
|
||||
|
|
@ -102,6 +121,11 @@ else if (_dashboard is not null)
|
|||
</MudCard>
|
||||
</MudItem>
|
||||
|
||||
<!-- Pending Memberships Widget (Admin only) -->
|
||||
<MudItem xs="12">
|
||||
<PendingMembershipsWidget />
|
||||
</MudItem>
|
||||
|
||||
<!-- Top Penalty Recipients -->
|
||||
<MudItem xs="12" md="6">
|
||||
<MudCard Elevation="2">
|
||||
|
|
@ -143,6 +167,7 @@ else if (_dashboard is not null)
|
|||
@code {
|
||||
private DashboardDto? _dashboard;
|
||||
private bool _isLoading = true;
|
||||
private bool _hasNoClub;
|
||||
private string? _error;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
|
|
@ -156,6 +181,20 @@ else if (_dashboard is not null)
|
|||
{
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
|
||||
// Check if user has club context
|
||||
var clubId = CurrentClubContext.ClubId;
|
||||
if (clubId == Guid.Empty)
|
||||
{
|
||||
// Double-check via user service
|
||||
var user = await UserService.GetCurrentUserAsync();
|
||||
if (user == null || user.ClubMemberships.Count == 0)
|
||||
{
|
||||
_hasNoClub = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
_dashboard = await DashboardService.GetDashboardAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
|
|
|||
|
|
@ -124,17 +124,28 @@
|
|||
[Parameter]
|
||||
public Guid DayId { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public Guid? PreselectedPersonId { get; set; }
|
||||
|
||||
private MudForm? _form;
|
||||
private bool _isValid;
|
||||
private ExpenseDto? _selectedExpense;
|
||||
private DayParticipantDto? _selectedParticipant;
|
||||
private decimal _customPrice;
|
||||
private bool _initialized;
|
||||
|
||||
private bool IsInverseMode => _selectedExpense?.IsInverse == true;
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
base.OnParametersSet();
|
||||
|
||||
if (!_initialized && PreselectedPersonId.HasValue)
|
||||
{
|
||||
_selectedParticipant = Participants.FirstOrDefault(p => p.PersonId == PreselectedPersonId.Value);
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
if (_selectedExpense is not null)
|
||||
{
|
||||
_customPrice = _selectedExpense.Price;
|
||||
|
|
|
|||
|
|
@ -99,16 +99,27 @@ else
|
|||
}
|
||||
else
|
||||
{
|
||||
<MudList T="DayParticipantDto" Dense="true">
|
||||
<MudList T="DayParticipantDto" Dense="true" Class="participant-list">
|
||||
@foreach (var participant in Day.Participants.OrderBy(p => p.PersonName))
|
||||
{
|
||||
<MudListItem T="DayParticipantDto">
|
||||
var isSelected = _selectedParticipantId == participant.PersonId;
|
||||
<MudListItem T="DayParticipantDto"
|
||||
Class="@(isSelected ? "selected-participant" : "selectable-participant")"
|
||||
OnClick="@(() => ToggleParticipantSelection(participant.PersonId))">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Style="width: 100%">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
||||
<MudAvatar Size="Size.Small" Color="@(participant.PersonStatus == PersonStatus.Member ? Color.Primary : Color.Secondary)">
|
||||
@participant.PersonName[0]
|
||||
</MudAvatar>
|
||||
<MudText>@participant.PersonName</MudText>
|
||||
<MudBadge Visible="@isSelected"
|
||||
Color="Color.Success"
|
||||
Icon="@Icons.Material.Filled.Check"
|
||||
Overlap="true"
|
||||
Bordered="true">
|
||||
<MudAvatar Size="Size.Small"
|
||||
Color="@(isSelected ? Color.Success : (participant.PersonStatus == PersonStatus.Member ? Color.Primary : Color.Secondary))"
|
||||
Style="@(isSelected ? "border: 2px solid var(--mud-palette-success)" : "")">
|
||||
@participant.PersonName[0]
|
||||
</MudAvatar>
|
||||
</MudBadge>
|
||||
<MudText Style="@(isSelected ? "font-weight: 600" : "")">@participant.PersonName</MudText>
|
||||
@if (participant.PersonStatus == PersonStatus.Guest)
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Color="Color.Secondary">Gast</MudChip>
|
||||
|
|
@ -119,12 +130,21 @@ else
|
|||
<MudIconButton Icon="@Icons.Material.Filled.PersonRemove"
|
||||
Size="Size.Small"
|
||||
Color="Color.Error"
|
||||
OnClick="@(() => RemoveParticipant(participant))" />
|
||||
OnClick="@(() => RemoveParticipant(participant))"
|
||||
OnClickStopPropagation="true" />
|
||||
}
|
||||
</MudStack>
|
||||
</MudListItem>
|
||||
}
|
||||
</MudList>
|
||||
@if (_selectedParticipantId.HasValue)
|
||||
{
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Class="mt-2">
|
||||
<MudChip T="string" Color="Color.Success" Size="Size.Small" OnClose="ClearParticipantSelection">
|
||||
@SelectedParticipant?.PersonName ausgewählt
|
||||
</MudChip>
|
||||
</MudStack>
|
||||
}
|
||||
}
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
|
|
@ -204,14 +224,36 @@ else
|
|||
</MudText>
|
||||
@if (Day.Status != DayStatus.Closed)
|
||||
{
|
||||
<MudButton Variant="Variant.Text"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="OpenAddExpenseDialog"
|
||||
Size="Size.Small"
|
||||
Disabled="@(Day.Participants.Count == 0)">
|
||||
Strafe hinzufügen
|
||||
</MudButton>
|
||||
<MudStack Row="true" Spacing="1">
|
||||
@if (OneClickExpenses.Count > 0 && _selectedParticipantId.HasValue)
|
||||
{
|
||||
<MudMenu Icon="@Icons.Material.Filled.FlashOn"
|
||||
Color="Color.Warning"
|
||||
Variant="Variant.Filled"
|
||||
Size="Size.Small"
|
||||
Title="Schnellzuweisung"
|
||||
AnchorOrigin="Origin.BottomRight"
|
||||
TransformOrigin="Origin.TopRight">
|
||||
@foreach (var expense in OneClickExpenses.OrderBy(e => e.Name))
|
||||
{
|
||||
<MudMenuItem OnClick="@(() => QuickAssignExpense(expense))">
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center" Justify="Justify.SpaceBetween" Style="min-width: 200px">
|
||||
<MudText>@expense.Name</MudText>
|
||||
<MudText Color="Color.Secondary">@expense.Price.ToString("C")</MudText>
|
||||
</MudStack>
|
||||
</MudMenuItem>
|
||||
}
|
||||
</MudMenu>
|
||||
}
|
||||
<MudButton Variant="Variant.Text"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="OpenAddExpenseDialog"
|
||||
Size="Size.Small"
|
||||
Disabled="@(Day.Participants.Count == 0)">
|
||||
Strafe hinzufügen
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
}
|
||||
</MudStack>
|
||||
|
||||
|
|
@ -331,6 +373,10 @@ else
|
|||
private DayDto? Day => DayState.Value.SelectedDay;
|
||||
private IReadOnlyList<PersonExpenseDto> Expenses => DayState.Value.SelectedDayExpenses;
|
||||
|
||||
private Guid? _selectedParticipantId;
|
||||
private DayParticipantDto? SelectedParticipant => Day?.Participants.FirstOrDefault(p => p.PersonId == _selectedParticipantId);
|
||||
private IReadOnlyList<ExpenseDto> OneClickExpenses => DayState.Value.AvailableExpenses.Where(e => e.IsOneClick && !e.IsVariable).ToList();
|
||||
|
||||
private decimal TotalExpenseAmount => Expenses.Sum(e => e.Price);
|
||||
private decimal OpenExpenseAmount => Expenses.Where(e => e.PersonExpenseStatus == PersonExpenseStatus.Open).Sum(e => e.Price);
|
||||
private decimal PaidExpenseAmount => Expenses.Where(e => e.PersonExpenseStatus == PersonExpenseStatus.Done).Sum(e => e.Price);
|
||||
|
|
@ -359,6 +405,16 @@ else
|
|||
Dispatcher.Dispatch(new ClearDayErrorAction());
|
||||
}
|
||||
|
||||
private void ToggleParticipantSelection(Guid personId)
|
||||
{
|
||||
_selectedParticipantId = _selectedParticipantId == personId ? null : personId;
|
||||
}
|
||||
|
||||
private void ClearParticipantSelection()
|
||||
{
|
||||
_selectedParticipantId = null;
|
||||
}
|
||||
|
||||
private void NavigateBack()
|
||||
{
|
||||
NavigationManager.NavigateTo("/days");
|
||||
|
|
@ -546,7 +602,8 @@ else
|
|||
{
|
||||
{ "AvailableExpenses", availableExpenses },
|
||||
{ "Participants", Day.Participants },
|
||||
{ "DayId", Day.Id }
|
||||
{ "DayId", Day.Id },
|
||||
{ "PreselectedPersonId", _selectedParticipantId }
|
||||
};
|
||||
|
||||
var dialog = await DialogService.ShowAsync<AddPersonExpenseDialog>("Strafe hinzufügen", parameters);
|
||||
|
|
@ -567,6 +624,37 @@ else
|
|||
}
|
||||
}
|
||||
|
||||
private void QuickAssignExpense(ExpenseDto expense)
|
||||
{
|
||||
if (Day is null || !_selectedParticipantId.HasValue) return;
|
||||
|
||||
if (expense.IsInverse)
|
||||
{
|
||||
var dto = new CreateInversePersonExpenseDto
|
||||
{
|
||||
ExcludedPersonId = _selectedParticipantId.Value,
|
||||
ExpenseId = expense.Id,
|
||||
DayId = Day.Id,
|
||||
GameId = null
|
||||
};
|
||||
Dispatcher.Dispatch(new CreateInversePersonExpenseAction(dto));
|
||||
Snackbar.Add($"{expense.Name} für alle außer {SelectedParticipant?.PersonName}...", Severity.Info);
|
||||
}
|
||||
else
|
||||
{
|
||||
var dto = new CreatePersonExpenseDto
|
||||
{
|
||||
PersonId = _selectedParticipantId.Value,
|
||||
ExpenseId = expense.Id,
|
||||
DayId = Day.Id,
|
||||
GameId = null,
|
||||
Price = null
|
||||
};
|
||||
Dispatcher.Dispatch(new CreatePersonExpenseAction(dto));
|
||||
Snackbar.Add($"{expense.Name} für {SelectedParticipant?.PersonName}...", Severity.Info);
|
||||
}
|
||||
}
|
||||
|
||||
private void MarkAsPaid(PersonExpenseDto expense)
|
||||
{
|
||||
var dto = new UpdatePersonExpenseStatusDto
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
.participant-list ::deep .mud-list-item {
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.selectable-participant {
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
|
||||
.selectable-participant:hover {
|
||||
background-color: var(--mud-palette-action-default-hover);
|
||||
}
|
||||
|
||||
.selected-participant {
|
||||
background-color: color-mix(in srgb, var(--mud-palette-success) 15%, transparent);
|
||||
border-left: 3px solid var(--mud-palette-success);
|
||||
}
|
||||
|
||||
.selected-participant:hover {
|
||||
background-color: color-mix(in srgb, var(--mud-palette-success) 25%, transparent);
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
@using Fluxor
|
||||
@using Koogle.Application.DTOs
|
||||
@using Koogle.Application.Interfaces
|
||||
@using Koogle.Web.Store.AuthState
|
||||
|
||||
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
|
||||
|
||||
@inject IUserService UserService
|
||||
@inject IState<AuthState> AuthState
|
||||
@inject NavigationManager NavigationManager
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
@if (_isLoading)
|
||||
{
|
||||
<MudProgressLinear Indeterminate="true" Color="Color.Primary" />
|
||||
}
|
||||
else if (_pendingCount > 0)
|
||||
{
|
||||
<MudCard Elevation="2" Style="cursor: pointer" @onclick="NavigateToUsers">
|
||||
<MudCardContent Class="d-flex flex-column align-center">
|
||||
<MudBadge Content="@_pendingCount" Color="Color.Error" Overlap="true">
|
||||
<MudIcon Icon="@Icons.Material.Filled.PersonAdd" Size="Size.Large" Color="Color.Warning" />
|
||||
</MudBadge>
|
||||
<MudText Typo="Typo.h6" Class="mt-2">Offene Beitrittsanträge</MudText>
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary">
|
||||
@(_pendingCount == 1 ? "1 Antrag wartet auf Freigabe" : $"{_pendingCount} Anträge warten auf Freigabe")
|
||||
</MudText>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
}
|
||||
|
||||
@code {
|
||||
private int _pendingCount;
|
||||
private bool _isLoading = true;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
await LoadPendingCountAsync();
|
||||
}
|
||||
|
||||
private async Task LoadPendingCountAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_isLoading = true;
|
||||
var state = AuthState.Value;
|
||||
var clubId = state.CurrentClub?.ClubId ?? Guid.Empty;
|
||||
|
||||
if (clubId == Guid.Empty)
|
||||
{
|
||||
_pendingCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// Only load for club admins or super admins
|
||||
if (!state.IsClubAdmin && !state.IsSuperAdmin)
|
||||
{
|
||||
_pendingCount = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var pending = await UserService.GetPendingMembershipsAsync(clubId);
|
||||
_pendingCount = pending.Count;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add($"Fehler beim Laden der Anträge: {ex.Message}", Severity.Error);
|
||||
_pendingCount = 0;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reloads pending membership count.
|
||||
/// </summary>
|
||||
public async Task RefreshAsync()
|
||||
{
|
||||
await LoadPendingCountAsync();
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void NavigateToUsers()
|
||||
{
|
||||
NavigationManager.NavigateTo("/admin/users");
|
||||
}
|
||||
}
|
||||
|
|
@ -62,12 +62,20 @@ namespace Koogle.Web.Controllers
|
|||
var result = await _userService.RegisterUserAsync(input);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
// If invite token was provided, redirect to join page
|
||||
if (!string.IsNullOrWhiteSpace(input.InviteToken))
|
||||
{
|
||||
return LocalRedirect($"/account/login?registered=true&invite={Uri.EscapeDataString(input.InviteToken)}");
|
||||
}
|
||||
return LocalRedirect("/account/login?registered=true");
|
||||
}
|
||||
|
||||
// Build error string from IdentityResult
|
||||
var errors = string.Join(",", result.Errors.Select(e => e.Code));
|
||||
return LocalRedirect($"/account/register?error={errors}");
|
||||
var inviteParam = !string.IsNullOrWhiteSpace(input.InviteToken)
|
||||
? $"&invite={Uri.EscapeDataString(input.InviteToken)}"
|
||||
: "";
|
||||
return LocalRedirect($"/account/register?error={errors}{inviteParam}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
Loading…
Reference in New Issue