SendClubInvitationEmailAsync(string toEmail, string inviteUrl,
+ string clubName, Guid clubId, CancellationToken ct = default)
+ {
+ if (!_settings.Enabled)
+ {
+ _logger.LogInformation("[EMAIL DISABLED] Club invitation - To: {Email}, Club: {Club}", toEmail, clubName);
+ return false;
+ }
+
+ try
+ {
+ await using var context = await _contextFactory.CreateDbContextAsync(ct);
+ var club = await context.Clubs.FirstOrDefaultAsync(c => c.Id == clubId, ct);
+ if (club is null)
+ return false;
+
+ var senderEmail = GetSenderEmail(club);
+ var senderName = clubName;
+
+ var subject = $"Einladung zu {clubName}";
+ var body = $@"
+
+
+Einladung zu {clubName}
+Du wurdest eingeladen, dem Club {clubName} beizutreten.
+Klicke auf den folgenden Link, um die Einladung anzunehmen:
+Einladung annehmen
+Falls du bereits ein Konto hast, kannst du dich direkt anmelden. Andernfalls wirst du zur Registrierung weitergeleitet.
+Viele Grüße,
{clubName}
+
+";
+
+ await SendEmailAsync(senderEmail, senderName, toEmail, subject, body, ct);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to send club invitation email to {Email}", toEmail);
+ return false;
+ }
+ }
+
private string GetSenderEmail(Club club)
{
var parts = _settings.DefaultSenderEmail.Split('@');
diff --git a/src/Koogle.Web/Components/Pages/Account/JoinClub.razor b/src/Koogle.Web/Components/Pages/Account/JoinClub.razor
index 6e78955..1e3a091 100644
--- a/src/Koogle.Web/Components/Pages/Account/JoinClub.razor
+++ b/src/Koogle.Web/Components/Pages/Account/JoinClub.razor
@@ -45,10 +45,10 @@
@@ -160,7 +160,7 @@
try
{
- _searchResult = await ClubService.GetByNameAsync(_clubName.Trim());
+ _searchResult = await ClubService.GetByLoginNameAsync(_clubName.Trim());
_searched = true;
// Check if user is already member
diff --git a/src/Koogle.Web/Components/Pages/Account/Login.razor b/src/Koogle.Web/Components/Pages/Account/Login.razor
index f6aa4e5..495a2b3 100644
--- a/src/Koogle.Web/Components/Pages/Account/Login.razor
+++ b/src/Koogle.Web/Components/Pages/Account/Login.razor
@@ -96,7 +96,7 @@
@code {
- private string _returnUrl = "/";
+ private string _returnUrl = "/dashboard";
private string? _error;
private string _antiToken = "";
private bool _registered;
diff --git a/src/Koogle.Web/Components/Pages/Admin/InviteByEmailDialog.razor b/src/Koogle.Web/Components/Pages/Admin/InviteByEmailDialog.razor
new file mode 100644
index 0000000..cb60b22
--- /dev/null
+++ b/src/Koogle.Web/Components/Pages/Admin/InviteByEmailDialog.razor
@@ -0,0 +1,112 @@
+@using Koogle.Application.DTOs
+@using Koogle.Application.Interfaces
+@using Koogle.Domain.Interfaces
+@using System.ComponentModel.DataAnnotations
+
+@inject IClubService ClubService
+@inject IUserService UserService
+@inject IEmailService EmailService
+@inject NavigationManager NavigationManager
+@inject ISnackbar Snackbar
+
+
+
+
+
+
+
+ @if (_isSending)
+ {
+
+ }
+
+
+ Abbrechen
+
+ @if (_isSending)
+ {
+
+ }
+ Einladung senden
+
+
+
+
+@code {
+ [CascadingParameter] private IMudDialogInstance MudDialog { get; set; } = null!;
+
+ [Parameter] public Guid ClubId { get; set; }
+ [Parameter] public string ClubName { get; set; } = "";
+
+ private MudForm _form = null!;
+ private string _email = "";
+ private bool _isValid;
+ private bool _isSending;
+
+ private void Cancel() => MudDialog.Cancel();
+
+ private async Task Send()
+ {
+ await _form.Validate();
+ if (!_isValid) return;
+
+ _isSending = true;
+ StateHasChanged();
+
+ try
+ {
+ var currentUser = await UserService.GetCurrentUserAsync();
+ if (currentUser is null)
+ {
+ Snackbar.Add("Nicht angemeldet", Severity.Error);
+ return;
+ }
+
+ // Create invitation with 7 days expiry
+ var dto = new CreateClubInvitationDto
+ {
+ ClubId = ClubId,
+ ExpiresAt = DateTime.UtcNow.AddDays(7),
+ MaxUses = 1
+ };
+
+ var invitation = await ClubService.CreateInvitationAsync(dto, currentUser.ProfileId);
+
+ // Build invite URL
+ var baseUrl = NavigationManager.BaseUri.TrimEnd('/');
+ var inviteUrl = $"{baseUrl}/club/join/{invitation.Token}";
+
+ // Send email
+ var sent = await EmailService.SendClubInvitationEmailAsync(_email, inviteUrl, ClubName, ClubId);
+
+ if (sent)
+ {
+ Snackbar.Add($"Einladung an {_email} gesendet", Severity.Success);
+ MudDialog.Close(DialogResult.Ok(true));
+ }
+ else
+ {
+ Snackbar.Add("E-Mail konnte nicht gesendet werden", Severity.Error);
+ }
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"Fehler: {ex.Message}", Severity.Error);
+ }
+ finally
+ {
+ _isSending = false;
+ StateHasChanged();
+ }
+ }
+}
diff --git a/src/Koogle.Web/Components/Pages/Settings.razor b/src/Koogle.Web/Components/Pages/Settings.razor
index 8512cd6..76714c3 100644
--- a/src/Koogle.Web/Components/Pages/Settings.razor
+++ b/src/Koogle.Web/Components/Pages/Settings.razor
@@ -11,8 +11,10 @@
@using Koogle.Web.Store.ClubState
@using Koogle.Web.Components.Pages.Admin
@using Microsoft.AspNetCore.Authorization
+@using Microsoft.AspNetCore.Components.Web
@inject IClubService ClubService
+@inject IUserService UserService
@inject ICurrentClubContext CurrentClubContext
@inject IState ClubState
@inject IDispatcher Dispatcher
@@ -20,6 +22,8 @@
@inject IDialogService DialogService
@inject IEmailService EmailService
@inject IState AuthState
+@inject NavigationManager NavigationManager
+@inject IJSRuntime JSRuntime
Vereins-Einstellungen
@@ -42,50 +46,175 @@ else if (_club is null)
}
else
{
-
-
-
-
- Name
- @_club.Name
-
-
- Login-Name
- @_club.LoginName
-
-
- Kostenberechnung
-
- @GetCalculationLabel(_club.ExpenseCalculation)
-
-
-
-
-
-
- Bearbeiten
-
-
- @*
- Test
- *@
-
-
+
+
+
+
+
+
+ Name
+ @_club.Name
+
+
+ Login-Name
+ @_club.LoginName
+
+
+ Kostenberechnung
+
+ @GetCalculationLabel(_club.ExpenseCalculation)
+
+
+
+
+
+
+ Bearbeiten
+
+
+
+
+
+
+
+
+
+
+ Einladungslink erstellen
+
+
+ Per E-Mail einladen
+
+
+
+ @if (_isLoadingInvitations)
+ {
+
+ }
+ else if (!_invitations.Any())
+ {
+ Keine Einladungen vorhanden.
+ }
+ else
+ {
+
+
+ Einladungslink
+ Ablaufdatum
+ Nutzung
+ Status
+ Aktionen
+
+
+
+
+
+ @GetInviteUrl(context.Token)
+
+
+
+
+
+
+ @context.ExpiresAt.ToString("dd.MM.yyyy HH:mm")
+
+ @context.UsedCount / @(context.MaxUses?.ToString() ?? "∞")
+
+
+ @if (context.IsValid)
+ {
+ Aktiv
+ }
+ else
+ {
+ Abgelaufen
+ }
+
+
+
+
+
+
+
+
+ }
+
+
+
+
+
+
+
+ @if (_isLoadingPending)
+ {
+
+ }
+ else if (!_pendingMemberships.Any())
+ {
+ Keine ausstehenden Mitgliedschaftsanträge.
+ }
+ else
+ {
+
+
+ Name
+ E-Mail
+ Angefragt am
+ Aktionen
+
+
+ @context.DisplayName
+ @context.Email
+ @context.RequestedAt.ToString("dd.MM.yyyy HH:mm")
+
+
+
+
+
+
+
+
+
+
+ }
+
+
+
+
}
@code {
private ClubDto? _club;
private bool _isLoading = true;
+ private bool _isLoadingInvitations = true;
+ private bool _isLoadingPending = true;
+ private int _activeTabIndex = 0;
+ private List _invitations = new();
+ private List _pendingMemberships = new();
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
- await LoadClubAsync();
+ await LoadAllDataAsync();
}
- private async Task LoadClubAsync()
+ private async Task LoadAllDataAsync()
{
_isLoading = true;
try
@@ -94,6 +223,8 @@ else
if (clubId != Guid.Empty)
{
_club = await ClubService.GetByIdAsync(clubId);
+ await LoadInvitationsAsync();
+ await LoadPendingMembershipsAsync();
}
}
finally
@@ -102,6 +233,40 @@ else
}
}
+ private async Task LoadInvitationsAsync()
+ {
+ _isLoadingInvitations = true;
+ try
+ {
+ var clubId = CurrentClubContext.ClubId;
+ if (clubId != Guid.Empty)
+ {
+ _invitations = (await ClubService.GetInvitationsByClubAsync(clubId)).ToList();
+ }
+ }
+ finally
+ {
+ _isLoadingInvitations = false;
+ }
+ }
+
+ private async Task LoadPendingMembershipsAsync()
+ {
+ _isLoadingPending = true;
+ try
+ {
+ var clubId = CurrentClubContext.ClubId;
+ if (clubId != Guid.Empty)
+ {
+ _pendingMemberships = (await UserService.GetPendingMembershipsAsync(clubId)).ToList();
+ }
+ }
+ finally
+ {
+ _isLoadingPending = false;
+ }
+ }
+
private void ClearError()
{
Dispatcher.Dispatch(new ClearClubErrorAction());
@@ -139,13 +304,161 @@ else
{
Dispatcher.Dispatch(new UpdateClubAction(dto));
Snackbar.Add("Einstellungen werden gespeichert...", Severity.Info);
- await LoadClubAsync();
+ await LoadAllDataAsync();
}
}
- // private async Task RunTest(MouseEventArgs obj)
- // {
- // await EmailService.SendTestMailAsync("christian@kauer-buchhagen.de", AuthState.Value.CurrentClub.ClubId);
- // }
+ private string GetInviteUrl(string token)
+ {
+ var baseUrl = NavigationManager.BaseUri.TrimEnd('/');
+ return $"{baseUrl}/club/join/{token}";
+ }
+ private async Task CreateInvitationLink()
+ {
+ if (_club is null) return;
+
+ var currentUser = await UserService.GetCurrentUserAsync();
+ if (currentUser is null)
+ {
+ Snackbar.Add("Nicht angemeldet", Severity.Error);
+ return;
+ }
+
+ try
+ {
+ var dto = new CreateClubInvitationDto
+ {
+ ClubId = _club.Id,
+ ExpiresAt = DateTime.UtcNow.AddDays(7),
+ MaxUses = null
+ };
+
+ var invitation = await ClubService.CreateInvitationAsync(dto, currentUser.ProfileId);
+ Snackbar.Add("Einladungslink erstellt", Severity.Success);
+
+ // Copy to clipboard immediately
+ await CopyToClipboard(GetInviteUrl(invitation.Token));
+
+ await LoadInvitationsAsync();
+ StateHasChanged();
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"Fehler: {ex.Message}", Severity.Error);
+ }
+ }
+
+ private async Task OpenInviteByEmailDialog()
+ {
+ if (_club is null) return;
+
+ var parameters = new DialogParameters
+ {
+ { "ClubId", _club.Id },
+ { "ClubName", _club.Name }
+ };
+
+ var dialog = await DialogService.ShowAsync("Per E-Mail einladen", parameters);
+ var result = await dialog.Result;
+
+ if (result != null && !result.Canceled)
+ {
+ await LoadInvitationsAsync();
+ StateHasChanged();
+ }
+ }
+
+ private async Task DeleteInvitation(ClubInvitationDto invitation)
+ {
+ var confirmed = await DialogService.ShowMessageBox(
+ "Einladung löschen",
+ $"Möchten Sie diese Einladung wirklich löschen?",
+ yesText: "Löschen",
+ cancelText: "Abbrechen");
+
+ if (confirmed == true)
+ {
+ var success = await ClubService.DeleteInvitationAsync(invitation.Id);
+ if (success)
+ {
+ Snackbar.Add("Einladung gelöscht", Severity.Success);
+ await LoadInvitationsAsync();
+ StateHasChanged();
+ }
+ else
+ {
+ Snackbar.Add("Fehler beim Löschen", Severity.Error);
+ }
+ }
+ }
+
+ private async Task CopyToClipboard(string text)
+ {
+ try
+ {
+ await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", text);
+ Snackbar.Add("Link in Zwischenablage kopiert", Severity.Success);
+ }
+ catch
+ {
+ Snackbar.Add("Kopieren fehlgeschlagen", Severity.Warning);
+ }
+ }
+
+ 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 LoadPendingMembershipsAsync();
+ StateHasChanged();
+ }
+ 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("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 LoadPendingMembershipsAsync();
+ StateHasChanged();
+ }
+ else
+ {
+ Snackbar.Add("Fehler beim Ablehnen der Mitgliedschaft", Severity.Error);
+ }
+ }
+ }
}