diff --git a/src/Koogle.Infrastructure/Security/ClubRoleRequirement.cs b/src/Koogle.Infrastructure/Security/ClubRoleRequirement.cs
index 87819eb..e7ca08d 100644
--- a/src/Koogle.Infrastructure/Security/ClubRoleRequirement.cs
+++ b/src/Koogle.Infrastructure/Security/ClubRoleRequirement.cs
@@ -7,34 +7,9 @@ using System.Threading.Tasks;
namespace Koogle.Infrastructure.Security;
-
-public sealed class ClubRoleRequirement : IAuthorizationRequirement
+public static class ClubRoleHelper
{
- public ClubRoleRequirement(string requiredRole) => RequiredRole = requiredRole;
- public string RequiredRole { get; }
-}
-
-///
-/// Handles ClubRoleRequirement when no resource is passed (e.g., [Authorize(Policy = "ClubViewer")]).
-/// Uses current_club_id claim to determine the club context.
-///
-public sealed class ClubRoleHandler : AuthorizationHandler
-{
- protected override Task HandleRequirementAsync(
- AuthorizationHandlerContext context,
- ClubRoleRequirement requirement)
- {
- // Get current club from claims
- var currentClubClaim = context.User.FindFirst("current_club_id");
- if (currentClubClaim == null || !Guid.TryParse(currentClubClaim.Value, out var clubId))
- {
- return Task.CompletedTask; // No club context, fail
- }
-
- return CheckClubRole(context, requirement, clubId);
- }
-
- private static Task CheckClubRole(
+ public static Task CheckClubRole(
AuthorizationHandlerContext context,
ClubRoleRequirement requirement,
Guid clubId)
@@ -72,6 +47,35 @@ public sealed class ClubRoleHandler : AuthorizationHandler
}
}
+public sealed class ClubRoleRequirement : IAuthorizationRequirement
+{
+ public ClubRoleRequirement(string requiredRole) => RequiredRole = requiredRole;
+ public string RequiredRole { get; }
+}
+
+///
+/// Handles ClubRoleRequirement when no resource is passed (e.g., [Authorize(Policy = "ClubViewer")]).
+/// Uses current_club_id claim to determine the club context.
+///
+public sealed class ClubRoleHandler : AuthorizationHandler
+{
+ protected override Task HandleRequirementAsync(
+ AuthorizationHandlerContext context,
+ ClubRoleRequirement requirement)
+ {
+ // Get current club from claims
+ var currentClubClaim = context.User.FindFirst("current_club_id");
+ if (currentClubClaim == null || !Guid.TryParse(currentClubClaim.Value, out var clubId))
+ {
+ return Task.CompletedTask; // No club context, fail
+ }
+
+ return ClubRoleHelper.CheckClubRole(context, requirement, clubId);
+ }
+
+
+}
+
///
/// Handles ClubRoleRequirement when a specific clubId resource is passed.
///
@@ -82,33 +86,6 @@ public sealed class ClubRoleResourceHandler : AuthorizationHandler role switch
- {
- "Admin" => 3,
- "Editor" => 2,
- "Viewer" => 1,
- _ => 0
- };
-
- var requiredRank = Rank(requirement.RequiredRole);
-
- var claims = context.User.FindAll("club_role");
- foreach (var c in claims)
- {
- var parts = c.Value.Split(':', 2);
- if (parts.Length != 2) continue;
-
- if (Guid.TryParse(parts[0], out var claimClubId) && claimClubId == clubId)
- {
- var userRole = parts[1];
- if (Rank(userRole) >= requiredRank)
- {
- context.Succeed(requirement);
- break;
- }
- }
- }
-
- return Task.CompletedTask;
+ return ClubRoleHelper.CheckClubRole(context, requirement, clubId);
}
}
diff --git a/src/Koogle.Web/Components/Pages/Expenses/ExpenseEditDialog.razor b/src/Koogle.Web/Components/Pages/Expenses/ExpenseEditDialog.razor
new file mode 100644
index 0000000..7667986
--- /dev/null
+++ b/src/Koogle.Web/Components/Pages/Expenses/ExpenseEditDialog.razor
@@ -0,0 +1,136 @@
+@using Koogle.Application.DTOs
+@using Koogle.Domain.Enums
+
+
+
+
+ @(IsEditMode ? "Kosten-Vorlage bearbeiten" : "Neue Kosten-Vorlage")
+
+
+
+
+
+
+
+
+
+
+
+ Geldstrafe
+ Sachleistung
+
+
+
+ Optionen
+
+
+
+ Als Schnellauswahl-Button anzeigen
+
+
+
+
+ Preis kann bei Zuweisung angepasst werden
+
+
+
+
+ Alle außer der Verursacher zahlen
+
+
+
+
+ Abbrechen
+
+ @(IsEditMode ? "Speichern" : "Erstellen")
+
+
+
+
+@code {
+ [CascadingParameter]
+ private IMudDialogInstance? MudDialog { get; set; }
+
+ [Parameter]
+ public ExpenseDto? Expense { get; set; }
+
+ private MudForm? _form;
+ private bool _isValid;
+ private string _name = "";
+ private string _description = "";
+ private decimal _price;
+ private ExpenseType _expenseType = ExpenseType.Monetary;
+ private bool _isOneClick;
+ private bool _isInverse;
+ private bool _isVariable;
+
+ private bool IsEditMode => Expense is not null;
+
+ protected override void OnInitialized()
+ {
+ if (Expense is not null)
+ {
+ _name = Expense.Name;
+ _description = Expense.Description;
+ _price = Expense.Price;
+ _expenseType = Expense.ExpenseType;
+ _isOneClick = Expense.IsOneClick;
+ _isInverse = Expense.IsInverse;
+ _isVariable = Expense.IsVariable;
+ }
+ }
+
+ private void Cancel() => MudDialog?.Cancel();
+
+ private void Submit()
+ {
+ if (!_isValid) return;
+
+ if (IsEditMode)
+ {
+ var dto = new UpdateExpenseDto
+ {
+ Id = Expense!.Id,
+ Name = _name,
+ Description = _description,
+ Price = _price,
+ ExpenseType = _expenseType,
+ IsOneClick = _isOneClick,
+ IsInverse = _isInverse,
+ IsVariable = _isVariable
+ };
+ MudDialog?.Close(DialogResult.Ok(dto));
+ }
+ else
+ {
+ var dto = new CreateExpenseDto
+ {
+ Name = _name,
+ Description = _description,
+ Price = _price,
+ ExpenseType = _expenseType,
+ IsOneClick = _isOneClick,
+ IsInverse = _isInverse,
+ IsVariable = _isVariable
+ };
+ MudDialog?.Close(DialogResult.Ok(dto));
+ }
+ }
+}
diff --git a/src/Koogle.Web/Components/Pages/Expenses/Expenses.razor b/src/Koogle.Web/Components/Pages/Expenses/Expenses.razor
new file mode 100644
index 0000000..ac37822
--- /dev/null
+++ b/src/Koogle.Web/Components/Pages/Expenses/Expenses.razor
@@ -0,0 +1,193 @@
+@page "/expenses"
+@attribute [Authorize(Policy = "ClubViewer")]
+
+@inherits Fluxor.Blazor.Web.Components.FluxorComponent
+
+@using Fluxor
+@using Koogle.Application.DTOs
+@using Koogle.Domain.Enums
+@using Koogle.Web.Store.ExpenseState
+@using Microsoft.AspNetCore.Authorization
+
+@inject IState ExpenseState
+@inject IDispatcher Dispatcher
+@inject ISnackbar Snackbar
+@inject IDialogService DialogService
+
+Kosten-Vorlagen
+
+Kosten-Vorlagen
+
+@if (ExpenseState.Value.Error is not null)
+{
+
+ @ExpenseState.Value.Error
+
+}
+
+
+
+
+
+
+ Geldstrafe
+ Sachleistung
+
+
+ Neue Vorlage
+
+
+
+ Name
+ Preis
+ Typ
+ Optionen
+ Aktionen
+
+
+ @context.Name
+ @context.Price.ToString("C")
+
+
+ @GetTypeLabel(context.ExpenseType)
+
+
+
+ @if (context.IsOneClick)
+ {
+
+
+
+ }
+ @if (context.IsInverse)
+ {
+
+
+
+ }
+ @if (context.IsVariable)
+ {
+
+
+
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Keine Kosten-Vorlagen gefunden
+
+
+
+@code {
+ private string _searchString = "";
+ private ExpenseType? _typeFilter;
+
+ private IEnumerable FilteredExpenses => _typeFilter.HasValue
+ ? ExpenseState.Value.Expenses.Where(e => e.ExpenseType == _typeFilter.Value)
+ : ExpenseState.Value.Expenses;
+
+ protected override void OnInitialized()
+ {
+ base.OnInitialized();
+ Dispatcher.Dispatch(new LoadExpensesAction());
+ }
+
+ private bool FilterFunc(ExpenseDto expense)
+ {
+ if (string.IsNullOrWhiteSpace(_searchString))
+ return true;
+
+ return expense.Name.Contains(_searchString, StringComparison.OrdinalIgnoreCase) ||
+ expense.Description.Contains(_searchString, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private void ClearError()
+ {
+ Dispatcher.Dispatch(new ClearExpenseErrorAction());
+ }
+
+ private static string GetTypeLabel(ExpenseType type) => type switch
+ {
+ ExpenseType.Monetary => "Geldstrafe",
+ ExpenseType.Material => "Sachleistung",
+ _ => type.ToString()
+ };
+
+ private static Color GetTypeColor(ExpenseType type) => type switch
+ {
+ ExpenseType.Monetary => Color.Warning,
+ ExpenseType.Material => Color.Info,
+ _ => Color.Default
+ };
+
+ private async Task OpenCreateDialog()
+ {
+ var dialog = await DialogService.ShowAsync("Neue Kosten-Vorlage");
+ var result = await dialog.Result;
+
+ if (result != null && !result.Canceled && result.Data is CreateExpenseDto dto)
+ {
+ Dispatcher.Dispatch(new CreateExpenseAction(dto));
+ Snackbar.Add("Vorlage wird erstellt...", Severity.Info);
+ }
+ }
+
+ private async Task OpenEditDialog(ExpenseDto expense)
+ {
+ var parameters = new DialogParameters
+ {
+ { "Expense", expense }
+ };
+
+ var dialog = await DialogService.ShowAsync("Kosten-Vorlage bearbeiten", parameters);
+ var result = await dialog.Result;
+
+ if (result != null && !result.Canceled && result.Data is UpdateExpenseDto dto)
+ {
+ Dispatcher.Dispatch(new UpdateExpenseAction(dto));
+ Snackbar.Add("Vorlage wird aktualisiert...", Severity.Info);
+ }
+ }
+
+ private async Task ConfirmDelete(ExpenseDto expense)
+ {
+ var parameters = new DialogParameters
+ {
+ { "ContentText", $"Möchten Sie \"{expense.Name}\" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden." },
+ { "ButtonText", "Löschen" },
+ { "Color", Color.Error }
+ };
+
+ var dialog = await DialogService.ShowAsync("Kosten-Vorlage löschen", parameters);
+ var result = await dialog.Result;
+
+ if (result != null && !result.Canceled)
+ {
+ Dispatcher.Dispatch(new DeleteExpenseAction(expense.Id));
+ Snackbar.Add("Vorlage wird gelöscht...", Severity.Info);
+ }
+ }
+}