Compare commits
No commits in common. "8d2222de8f16d2907884d5321a936550ba8123f6" and "8e2342fd7496376514053245714bc4912fb88745" have entirely different histories.
8d2222de8f
...
8e2342fd74
|
|
@ -357,10 +357,10 @@ NavMenu.razor:
|
||||||
| ✓ | G2 | Erweiterte Reg. | ClubInvitation Entity | 1 Entity + Migration |
|
| ✓ | G2 | Erweiterte Reg. | ClubInvitation Entity | 1 Entity + Migration |
|
||||||
| ✓ | G3 | Erweiterte Reg. | IEmailService (Stub) | 2 Dateien |
|
| ✓ | G3 | Erweiterte Reg. | IEmailService (Stub) | 2 Dateien |
|
||||||
| ✓ | G4 | Erweiterte Reg. | Services erweitern | 2 Services |
|
| ✓ | G4 | Erweiterte Reg. | Services erweitern | 2 Services |
|
||||||
| ✓ | G5 | Erweiterte Reg. | Dashboard Pending-Widget | 1 Component |
|
| ☐ | G5 | Erweiterte Reg. | Dashboard Pending-Widget | 1 Component |
|
||||||
| ✓ | G6 | Erweiterte Reg. | Admin Users Page erweitern | 1 Razor |
|
| ☐ | G6 | Erweiterte Reg. | Admin Users Page erweitern | 1 Razor |
|
||||||
| ✓ | G7 | Erweiterte Reg. | Club-Beitritt UI | 1 Razor |
|
| ☐ | G7 | Erweiterte Reg. | Club-Beitritt UI | 1 Razor |
|
||||||
| ✓ | G8 | Erweiterte Reg. | Einladungslink-Handling | 2 Dateien |
|
| ☐ | G8 | Erweiterte Reg. | Einladungslink-Handling | 2 Dateien |
|
||||||
|
|
||||||
**Legende:** ☐ = Offen | ☑ = In Arbeit | ✓ = Fertig
|
**Legende:** ☐ = Offen | ☑ = In Arbeit | ✓ = Fertig
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
|
|
||||||
## Optimierung Spieltag-Details
|
## Optimierung Spieltag-Details
|
||||||
Die Erfassung von Strafen muss möglichst schnell und komfortabel mögiich sein. Dafür muss die Seite DayDetails optimiert werden.
|
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 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.
|
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.
|
||||||
Der Benutzer soll visuell leicht erkennen können, ob und welcher Teilnehmer aktuell markiert wurde.
|
Der Benutzer soll visuell leicht erkennen können, ob und welcher Teilnehmer aktuell markiert wurde.
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,11 +71,6 @@ namespace Koogle.Application.DTOs
|
||||||
/// Optional club name to assign user to during registration.
|
/// Optional club name to assign user to during registration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? ClubName { get; set; }
|
public string? ClubName { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Optional invitation token for club membership during registration.
|
|
||||||
/// </summary>
|
|
||||||
public string? InviteToken { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -1,231 +0,0 @@
|
||||||
@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,12 +14,6 @@
|
||||||
<input type="hidden" name="__RequestVerificationToken" value="@_antiToken" />
|
<input type="hidden" name="__RequestVerificationToken" value="@_antiToken" />
|
||||||
|
|
||||||
<MudPaper Class="pa-6" Elevation="4" MaxWidth="400px">
|
<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)
|
@if (_registered)
|
||||||
{
|
{
|
||||||
<MudAlert Severity="Severity.Success" Class="mb-3">Registrierung erfolgreich! Bitte anmelden.</MudAlert>
|
<MudAlert Severity="Severity.Success" Class="mb-3">Registrierung erfolgreich! Bitte anmelden.</MudAlert>
|
||||||
|
|
@ -92,7 +86,6 @@
|
||||||
private string? _error;
|
private string? _error;
|
||||||
private string _antiToken = "";
|
private string _antiToken = "";
|
||||||
private bool _registered;
|
private bool _registered;
|
||||||
private string? _inviteToken;
|
|
||||||
|
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
|
|
@ -108,12 +101,6 @@
|
||||||
{
|
{
|
||||||
_registered = true;
|
_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)
|
// Antiforgery Token generieren (klassischer MVC Token)
|
||||||
var http = HttpContextAccessor.HttpContext!;
|
var http = HttpContextAccessor.HttpContext!;
|
||||||
|
|
|
||||||
|
|
@ -9,18 +9,8 @@
|
||||||
<form method="post" action="/auth/register">
|
<form method="post" action="/auth/register">
|
||||||
<!-- Hidden Fields -->
|
<!-- Hidden Fields -->
|
||||||
<input type="hidden" name="__RequestVerificationToken" value="@_antiToken" />
|
<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">
|
<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))
|
@if (!string.IsNullOrWhiteSpace(_error))
|
||||||
{
|
{
|
||||||
<MudAlert Severity="Severity.Error" Class="mb-3">@_error</MudAlert>
|
<MudAlert Severity="Severity.Error" Class="mb-3">@_error</MudAlert>
|
||||||
|
|
@ -85,7 +75,6 @@
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private string? _error;
|
private string? _error;
|
||||||
private string? _inviteToken;
|
|
||||||
private string _antiToken = "";
|
private string _antiToken = "";
|
||||||
|
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
|
|
@ -98,11 +87,6 @@
|
||||||
_error = MapErrorCodes(err.ToString());
|
_error = MapErrorCodes(err.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.TryGetValue("invite", out var invite))
|
|
||||||
{
|
|
||||||
_inviteToken = invite.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Antiforgery Token generieren
|
// Antiforgery Token generieren
|
||||||
var http = HttpContextAccessor.HttpContext!;
|
var http = HttpContextAccessor.HttpContext!;
|
||||||
var tokens = Antiforgery.GetAndStoreTokens(http);
|
var tokens = Antiforgery.GetAndStoreTokens(http);
|
||||||
|
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
@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,123 +15,73 @@
|
||||||
|
|
||||||
<MudText Typo="Typo.h4" Class="mb-4">Benutzer verwalten</MudText>
|
<MudText Typo="Typo.h4" Class="mb-4">Benutzer verwalten</MudText>
|
||||||
|
|
||||||
<MudTabs @bind-ActivePanelIndex="_activeTabIndex" Elevation="0" Rounded="true" ApplyEffectsToContainer="true" Class="mb-4">
|
@if (_isLoading)
|
||||||
<MudTabPanel Text="Alle Benutzer" BadgeData="@(_users.Count)" BadgeColor="Color.Primary">
|
{
|
||||||
@if (_isLoading)
|
<MudProgressCircular Indeterminate="true" />
|
||||||
{
|
}
|
||||||
<MudProgressCircular Indeterminate="true" />
|
else
|
||||||
}
|
{
|
||||||
else
|
<MudTable Items="_users" Dense="true" Hover="true" Filter="FilterFunc" Loading="_isLoading">
|
||||||
{
|
<ToolBarContent>
|
||||||
<MudTable Items="_users" Dense="true" Hover="true" Filter="FilterFunc" Loading="_isLoading">
|
<MudTextField @bind-Value="_searchString"
|
||||||
<ToolBarContent>
|
Placeholder="Suchen..."
|
||||||
<MudTextField @bind-Value="_searchString"
|
Adornment="Adornment.Start"
|
||||||
Placeholder="Suchen..."
|
AdornmentIcon="@Icons.Material.Filled.Search"
|
||||||
Adornment="Adornment.Start"
|
IconSize="Size.Medium"
|
||||||
AdornmentIcon="@Icons.Material.Filled.Search"
|
Class="mt-0" />
|
||||||
IconSize="Size.Medium"
|
</ToolBarContent>
|
||||||
Class="mt-0" />
|
<HeaderContent>
|
||||||
</ToolBarContent>
|
<MudTh>Name</MudTh>
|
||||||
<HeaderContent>
|
<MudTh>E-Mail</MudTh>
|
||||||
<MudTh>Name</MudTh>
|
<MudTh>Clubs</MudTh>
|
||||||
<MudTh>E-Mail</MudTh>
|
<MudTh>Aktionen</MudTh>
|
||||||
<MudTh>Clubs</MudTh>
|
</HeaderContent>
|
||||||
<MudTh>Aktionen</MudTh>
|
<RowTemplate>
|
||||||
</HeaderContent>
|
<MudTd DataLabel="Name">@context.DisplayName</MudTd>
|
||||||
<RowTemplate>
|
<MudTd DataLabel="E-Mail">@context.Identity.Email</MudTd>
|
||||||
<MudTd DataLabel="Name">@context.DisplayName</MudTd>
|
<MudTd DataLabel="Clubs">
|
||||||
<MudTd DataLabel="E-Mail">@context.Identity.Email</MudTd>
|
@if (context.ClubMemberships.Count == 0)
|
||||||
<MudTd DataLabel="Clubs">
|
{
|
||||||
@if (context.ClubMemberships.Count == 0)
|
<MudText Typo="Typo.caption" Color="Color.Warning">Kein Club</MudText>
|
||||||
{
|
}
|
||||||
<MudText Typo="Typo.caption" Color="Color.Warning">Kein Club</MudText>
|
else
|
||||||
}
|
{
|
||||||
else
|
@foreach (var membership in context.ClubMemberships)
|
||||||
{
|
{
|
||||||
@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())
|
||||||
{
|
{
|
||||||
<MudChip T="string" Size="Size.Small" Color="@(membership.IsDefault ? Color.Primary : Color.Default)" Class="mr-1 mb-1">
|
<span class="ml-1">(@string.Join(", ", membership.Roles))</span>
|
||||||
@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">
|
</MudTd>
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.GroupAdd"
|
<MudTd DataLabel="Aktionen">
|
||||||
Size="Size.Small"
|
<MudTooltip Text="Club zuweisen">
|
||||||
OnClick="@(() => OpenAssignClubDialog(context))"/>
|
<MudIconButton Icon="@Icons.Material.Filled.GroupAdd"
|
||||||
</MudTooltip>
|
Size="Size.Small"
|
||||||
<MudTooltip Text="Rollen verwalten">
|
OnClick="@(() => OpenAssignClubDialog(context))"/>
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Security"
|
</MudTooltip>
|
||||||
Size="Size.Small"
|
<MudTooltip Text="Rollen verwalten">
|
||||||
OnClick="@(() => OpenManageRolesDialog(context))"/>
|
<MudIconButton Icon="@Icons.Material.Filled.Security"
|
||||||
</MudTooltip>
|
Size="Size.Small"
|
||||||
</MudTd>
|
OnClick="@(() => OpenManageRolesDialog(context))"/>
|
||||||
</RowTemplate>
|
</MudTooltip>
|
||||||
<PagerContent>
|
</MudTd>
|
||||||
<MudTablePager />
|
</RowTemplate>
|
||||||
</PagerContent>
|
<PagerContent>
|
||||||
</MudTable>
|
<MudTablePager />
|
||||||
}
|
</PagerContent>
|
||||||
</MudTabPanel>
|
</MudTable>
|
||||||
|
}
|
||||||
<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()
|
||||||
{
|
{
|
||||||
|
|
@ -141,7 +91,6 @@
|
||||||
private async Task LoadData()
|
private async Task LoadData()
|
||||||
{
|
{
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
_isLoadingPending = true;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var usersTask = UserService.GetAllUsersAsync();
|
var usersTask = UserService.GetAllUsersAsync();
|
||||||
|
|
@ -150,33 +99,10 @@
|
||||||
|
|
||||||
_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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,58 +174,4 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,201 +0,0 @@
|
||||||
@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,16 +1,12 @@
|
||||||
@page "/dashboard"
|
@page "/dashboard"
|
||||||
@attribute [Authorize]
|
@attribute [Authorize(Policy = "ClubViewer")]
|
||||||
|
|
||||||
@using Koogle.Application.DTOs
|
@using Koogle.Application.DTOs
|
||||||
@using Koogle.Application.Interfaces
|
@using Koogle.Application.Interfaces
|
||||||
@using Koogle.Domain.Enums
|
@using Koogle.Domain.Enums
|
||||||
@using Koogle.Infrastructure.Security
|
|
||||||
@using Koogle.Web.Components.Shared
|
|
||||||
@using Microsoft.AspNetCore.Authorization
|
@using Microsoft.AspNetCore.Authorization
|
||||||
|
|
||||||
@inject IDashboardService DashboardService
|
@inject IDashboardService DashboardService
|
||||||
@inject IUserService UserService
|
|
||||||
@inject ICurrentClubContext CurrentClubContext
|
|
||||||
@inject NavigationManager NavigationManager
|
@inject NavigationManager NavigationManager
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
|
|
||||||
|
|
@ -22,21 +18,6 @@
|
||||||
{
|
{
|
||||||
<MudProgressLinear Indeterminate="true" Color="Color.Primary" />
|
<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)
|
else if (_error is not null)
|
||||||
{
|
{
|
||||||
<MudAlert Severity="Severity.Error" Class="mb-4">@_error</MudAlert>
|
<MudAlert Severity="Severity.Error" Class="mb-4">@_error</MudAlert>
|
||||||
|
|
@ -121,11 +102,6 @@ else if (_dashboard is not null)
|
||||||
</MudCard>
|
</MudCard>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
|
|
||||||
<!-- Pending Memberships Widget (Admin only) -->
|
|
||||||
<MudItem xs="12">
|
|
||||||
<PendingMembershipsWidget />
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
<!-- Top Penalty Recipients -->
|
<!-- Top Penalty Recipients -->
|
||||||
<MudItem xs="12" md="6">
|
<MudItem xs="12" md="6">
|
||||||
<MudCard Elevation="2">
|
<MudCard Elevation="2">
|
||||||
|
|
@ -167,7 +143,6 @@ else if (_dashboard is not null)
|
||||||
@code {
|
@code {
|
||||||
private DashboardDto? _dashboard;
|
private DashboardDto? _dashboard;
|
||||||
private bool _isLoading = true;
|
private bool _isLoading = true;
|
||||||
private bool _hasNoClub;
|
|
||||||
private string? _error;
|
private string? _error;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
|
|
@ -181,20 +156,6 @@ else if (_dashboard is not null)
|
||||||
{
|
{
|
||||||
_isLoading = true;
|
_isLoading = true;
|
||||||
_error = null;
|
_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();
|
_dashboard = await DashboardService.GetDashboardAsync();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
|
||||||
|
|
@ -124,28 +124,17 @@
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public Guid DayId { get; set; }
|
public Guid DayId { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
|
||||||
public Guid? PreselectedPersonId { get; set; }
|
|
||||||
|
|
||||||
private MudForm? _form;
|
private MudForm? _form;
|
||||||
private bool _isValid;
|
private bool _isValid;
|
||||||
private ExpenseDto? _selectedExpense;
|
private ExpenseDto? _selectedExpense;
|
||||||
private DayParticipantDto? _selectedParticipant;
|
private DayParticipantDto? _selectedParticipant;
|
||||||
private decimal _customPrice;
|
private decimal _customPrice;
|
||||||
private bool _initialized;
|
|
||||||
|
|
||||||
private bool IsInverseMode => _selectedExpense?.IsInverse == true;
|
private bool IsInverseMode => _selectedExpense?.IsInverse == true;
|
||||||
|
|
||||||
protected override void OnParametersSet()
|
protected override void OnParametersSet()
|
||||||
{
|
{
|
||||||
base.OnParametersSet();
|
base.OnParametersSet();
|
||||||
|
|
||||||
if (!_initialized && PreselectedPersonId.HasValue)
|
|
||||||
{
|
|
||||||
_selectedParticipant = Participants.FirstOrDefault(p => p.PersonId == PreselectedPersonId.Value);
|
|
||||||
_initialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_selectedExpense is not null)
|
if (_selectedExpense is not null)
|
||||||
{
|
{
|
||||||
_customPrice = _selectedExpense.Price;
|
_customPrice = _selectedExpense.Price;
|
||||||
|
|
|
||||||
|
|
@ -99,27 +99,16 @@ else
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<MudList T="DayParticipantDto" Dense="true" Class="participant-list">
|
<MudList T="DayParticipantDto" Dense="true">
|
||||||
@foreach (var participant in Day.Participants.OrderBy(p => p.PersonName))
|
@foreach (var participant in Day.Participants.OrderBy(p => p.PersonName))
|
||||||
{
|
{
|
||||||
var isSelected = _selectedParticipantId == participant.PersonId;
|
<MudListItem T="DayParticipantDto">
|
||||||
<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" Justify="Justify.SpaceBetween" Style="width: 100%">
|
||||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
<MudStack Row="true" AlignItems="AlignItems.Center">
|
||||||
<MudBadge Visible="@isSelected"
|
<MudAvatar Size="Size.Small" Color="@(participant.PersonStatus == PersonStatus.Member ? Color.Primary : Color.Secondary)">
|
||||||
Color="Color.Success"
|
@participant.PersonName[0]
|
||||||
Icon="@Icons.Material.Filled.Check"
|
</MudAvatar>
|
||||||
Overlap="true"
|
<MudText>@participant.PersonName</MudText>
|
||||||
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)
|
@if (participant.PersonStatus == PersonStatus.Guest)
|
||||||
{
|
{
|
||||||
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Color="Color.Secondary">Gast</MudChip>
|
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Color="Color.Secondary">Gast</MudChip>
|
||||||
|
|
@ -130,21 +119,12 @@ else
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.PersonRemove"
|
<MudIconButton Icon="@Icons.Material.Filled.PersonRemove"
|
||||||
Size="Size.Small"
|
Size="Size.Small"
|
||||||
Color="Color.Error"
|
Color="Color.Error"
|
||||||
OnClick="@(() => RemoveParticipant(participant))"
|
OnClick="@(() => RemoveParticipant(participant))" />
|
||||||
OnClickStopPropagation="true" />
|
|
||||||
}
|
}
|
||||||
</MudStack>
|
</MudStack>
|
||||||
</MudListItem>
|
</MudListItem>
|
||||||
}
|
}
|
||||||
</MudList>
|
</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>
|
</MudPaper>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
|
|
@ -224,36 +204,14 @@ else
|
||||||
</MudText>
|
</MudText>
|
||||||
@if (Day.Status != DayStatus.Closed)
|
@if (Day.Status != DayStatus.Closed)
|
||||||
{
|
{
|
||||||
<MudStack Row="true" Spacing="1">
|
<MudButton Variant="Variant.Text"
|
||||||
@if (OneClickExpenses.Count > 0 && _selectedParticipantId.HasValue)
|
Color="Color.Primary"
|
||||||
{
|
StartIcon="@Icons.Material.Filled.Add"
|
||||||
<MudMenu Icon="@Icons.Material.Filled.FlashOn"
|
OnClick="OpenAddExpenseDialog"
|
||||||
Color="Color.Warning"
|
Size="Size.Small"
|
||||||
Variant="Variant.Filled"
|
Disabled="@(Day.Participants.Count == 0)">
|
||||||
Size="Size.Small"
|
Strafe hinzufügen
|
||||||
Title="Schnellzuweisung"
|
</MudButton>
|
||||||
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>
|
</MudStack>
|
||||||
|
|
||||||
|
|
@ -373,10 +331,6 @@ else
|
||||||
private DayDto? Day => DayState.Value.SelectedDay;
|
private DayDto? Day => DayState.Value.SelectedDay;
|
||||||
private IReadOnlyList<PersonExpenseDto> Expenses => DayState.Value.SelectedDayExpenses;
|
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 TotalExpenseAmount => Expenses.Sum(e => e.Price);
|
||||||
private decimal OpenExpenseAmount => Expenses.Where(e => e.PersonExpenseStatus == PersonExpenseStatus.Open).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);
|
private decimal PaidExpenseAmount => Expenses.Where(e => e.PersonExpenseStatus == PersonExpenseStatus.Done).Sum(e => e.Price);
|
||||||
|
|
@ -405,16 +359,6 @@ else
|
||||||
Dispatcher.Dispatch(new ClearDayErrorAction());
|
Dispatcher.Dispatch(new ClearDayErrorAction());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ToggleParticipantSelection(Guid personId)
|
|
||||||
{
|
|
||||||
_selectedParticipantId = _selectedParticipantId == personId ? null : personId;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ClearParticipantSelection()
|
|
||||||
{
|
|
||||||
_selectedParticipantId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void NavigateBack()
|
private void NavigateBack()
|
||||||
{
|
{
|
||||||
NavigationManager.NavigateTo("/days");
|
NavigationManager.NavigateTo("/days");
|
||||||
|
|
@ -602,8 +546,7 @@ else
|
||||||
{
|
{
|
||||||
{ "AvailableExpenses", availableExpenses },
|
{ "AvailableExpenses", availableExpenses },
|
||||||
{ "Participants", Day.Participants },
|
{ "Participants", Day.Participants },
|
||||||
{ "DayId", Day.Id },
|
{ "DayId", Day.Id }
|
||||||
{ "PreselectedPersonId", _selectedParticipantId }
|
|
||||||
};
|
};
|
||||||
|
|
||||||
var dialog = await DialogService.ShowAsync<AddPersonExpenseDialog>("Strafe hinzufügen", parameters);
|
var dialog = await DialogService.ShowAsync<AddPersonExpenseDialog>("Strafe hinzufügen", parameters);
|
||||||
|
|
@ -624,37 +567,6 @@ 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)
|
private void MarkAsPaid(PersonExpenseDto expense)
|
||||||
{
|
{
|
||||||
var dto = new UpdatePersonExpenseStatusDto
|
var dto = new UpdatePersonExpenseStatusDto
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
.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);
|
|
||||||
}
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
@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,20 +62,12 @@ namespace Koogle.Web.Controllers
|
||||||
var result = await _userService.RegisterUserAsync(input);
|
var result = await _userService.RegisterUserAsync(input);
|
||||||
if (result.Succeeded)
|
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");
|
return LocalRedirect("/account/login?registered=true");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build error string from IdentityResult
|
// Build error string from IdentityResult
|
||||||
var errors = string.Join(",", result.Errors.Select(e => e.Code));
|
var errors = string.Join(",", result.Errors.Select(e => e.Code));
|
||||||
var inviteParam = !string.IsNullOrWhiteSpace(input.InviteToken)
|
return LocalRedirect($"/account/register?error={errors}");
|
||||||
? $"&invite={Uri.EscapeDataString(input.InviteToken)}"
|
|
||||||
: "";
|
|
||||||
return LocalRedirect($"/account/register?error={errors}{inviteParam}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue