Add Expense Pages (D4)
This commit is contained in:
parent
69f0f404a6
commit
51600596c2
|
|
@ -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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles ClubRoleRequirement when no resource is passed (e.g., [Authorize(Policy = "ClubViewer")]).
|
||||
/// Uses current_club_id claim to determine the club context.
|
||||
/// </summary>
|
||||
public sealed class ClubRoleHandler : AuthorizationHandler<ClubRoleRequirement>
|
||||
{
|
||||
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<ClubRoleRequirement>
|
|||
}
|
||||
}
|
||||
|
||||
public sealed class ClubRoleRequirement : IAuthorizationRequirement
|
||||
{
|
||||
public ClubRoleRequirement(string requiredRole) => RequiredRole = requiredRole;
|
||||
public string RequiredRole { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles ClubRoleRequirement when no resource is passed (e.g., [Authorize(Policy = "ClubViewer")]).
|
||||
/// Uses current_club_id claim to determine the club context.
|
||||
/// </summary>
|
||||
public sealed class ClubRoleHandler : AuthorizationHandler<ClubRoleRequirement>
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles ClubRoleRequirement when a specific clubId resource is passed.
|
||||
/// </summary>
|
||||
|
|
@ -82,33 +86,6 @@ public sealed class ClubRoleResourceHandler : AuthorizationHandler<ClubRoleRequi
|
|||
ClubRoleRequirement requirement,
|
||||
Guid clubId)
|
||||
{
|
||||
static int Rank(string role) => 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,136 @@
|
|||
@using Koogle.Application.DTOs
|
||||
@using Koogle.Domain.Enums
|
||||
|
||||
<MudDialog>
|
||||
<TitleContent>
|
||||
<MudText Typo="Typo.h6">
|
||||
@(IsEditMode ? "Kosten-Vorlage bearbeiten" : "Neue Kosten-Vorlage")
|
||||
</MudText>
|
||||
</TitleContent>
|
||||
<DialogContent>
|
||||
<MudForm @ref="_form" @bind-IsValid="_isValid">
|
||||
<MudTextField @bind-Value="_name"
|
||||
Label="Name"
|
||||
Required="true"
|
||||
RequiredError="Name ist erforderlich"
|
||||
Immediate="true"
|
||||
Class="mb-4" />
|
||||
|
||||
<MudTextField @bind-Value="_description"
|
||||
Label="Beschreibung"
|
||||
Lines="2"
|
||||
Class="mb-4" />
|
||||
|
||||
<MudNumericField @bind-Value="_price"
|
||||
Label="Preis"
|
||||
Format="N2"
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentText="€"
|
||||
Min="0m"
|
||||
Class="mb-4" />
|
||||
|
||||
<MudSelect T="ExpenseType" @bind-Value="_expenseType"
|
||||
Label="Typ"
|
||||
AnchorOrigin="Origin.BottomCenter"
|
||||
Class="mb-4">
|
||||
<MudSelectItem Value="ExpenseType.Monetary">Geldstrafe</MudSelectItem>
|
||||
<MudSelectItem Value="ExpenseType.Material">Sachleistung</MudSelectItem>
|
||||
</MudSelect>
|
||||
|
||||
<MudDivider Class="my-4" />
|
||||
<MudText Typo="Typo.subtitle2" Class="mb-2">Optionen</MudText>
|
||||
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
||||
<MudCheckBox @bind-Value="_isOneClick" Label="One-Click" Color="Color.Primary" Class="mb-2"/>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">Als Schnellauswahl-Button anzeigen</MudText>
|
||||
</MudStack>
|
||||
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
||||
<MudCheckBox @bind-Value="_isVariable" Label="Variabler Preis" Color="Color.Primary" Class="mb-2"/>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">Preis kann bei Zuweisung angepasst werden</MudText>
|
||||
</MudStack>
|
||||
|
||||
<MudStack Row="true" AlignItems="AlignItems.Center">
|
||||
<MudCheckBox @bind-Value="_isInverse" Label="Inverse Kosten" Color="Color.Primary"/>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">Alle außer der Verursacher zahlen</MudText>
|
||||
</MudStack>
|
||||
</MudForm>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel">Abbrechen</MudButton>
|
||||
<MudButton Color="Color.Primary" Variant="Variant.Filled" Disabled="!_isValid" OnClick="Submit">
|
||||
@(IsEditMode ? "Speichern" : "Erstellen")
|
||||
</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
|
||||
@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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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> ExpenseState
|
||||
@inject IDispatcher Dispatcher
|
||||
@inject ISnackbar Snackbar
|
||||
@inject IDialogService DialogService
|
||||
|
||||
<PageTitle>Kosten-Vorlagen</PageTitle>
|
||||
|
||||
<MudText Typo="Typo.h4" Class="mb-4">Kosten-Vorlagen</MudText>
|
||||
|
||||
@if (ExpenseState.Value.Error is not null)
|
||||
{
|
||||
<MudAlert Severity="Severity.Error" Class="mb-4" ShowCloseIcon="true" CloseIconClicked="ClearError">
|
||||
@ExpenseState.Value.Error
|
||||
</MudAlert>
|
||||
}
|
||||
|
||||
<MudTable Items="FilteredExpenses" Dense="true" Hover="true" Loading="ExpenseState.Value.IsLoading"
|
||||
Filter="FilterFunc">
|
||||
<ToolBarContent>
|
||||
<MudTextField @bind-Value="_searchString"
|
||||
Placeholder="Suchen..."
|
||||
Adornment="Adornment.Start"
|
||||
AdornmentIcon="@Icons.Material.Filled.Search"
|
||||
IconSize="Size.Medium"
|
||||
Class="mt-0" />
|
||||
<MudSpacer />
|
||||
<MudSelect T="ExpenseType?" @bind-Value="_typeFilter" Label="Typ" Clearable="true" Class="mr-4" Style="width: 150px;">
|
||||
<MudSelectItem Value="@((ExpenseType?)ExpenseType.Monetary)">Geldstrafe</MudSelectItem>
|
||||
<MudSelectItem Value="@((ExpenseType?)ExpenseType.Material)">Sachleistung</MudSelectItem>
|
||||
</MudSelect>
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add"
|
||||
OnClick="OpenCreateDialog">
|
||||
Neue Vorlage
|
||||
</MudButton>
|
||||
</ToolBarContent>
|
||||
<HeaderContent>
|
||||
<MudTh>Name</MudTh>
|
||||
<MudTh>Preis</MudTh>
|
||||
<MudTh>Typ</MudTh>
|
||||
<MudTh>Optionen</MudTh>
|
||||
<MudTh>Aktionen</MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd DataLabel="Name">@context.Name</MudTd>
|
||||
<MudTd DataLabel="Preis">@context.Price.ToString("C")</MudTd>
|
||||
<MudTd DataLabel="Typ">
|
||||
<MudChip T="string" Size="Size.Small" Color="GetTypeColor(context.ExpenseType)">
|
||||
@GetTypeLabel(context.ExpenseType)
|
||||
</MudChip>
|
||||
</MudTd>
|
||||
<MudTd DataLabel="Optionen">
|
||||
@if (context.IsOneClick)
|
||||
{
|
||||
<MudTooltip Text="One-Click: Schnellauswahl">
|
||||
<MudIcon Icon="@Icons.Material.Filled.TouchApp" Size="Size.Small" Class="mr-1" />
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (context.IsInverse)
|
||||
{
|
||||
<MudTooltip Text="Invers: Alle außer einer Person">
|
||||
<MudIcon Icon="@Icons.Material.Filled.GroupRemove" Size="Size.Small" Class="mr-1" />
|
||||
</MudTooltip>
|
||||
}
|
||||
@if (context.IsVariable)
|
||||
{
|
||||
<MudTooltip Text="Variabel: Preis anpassbar">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Edit" Size="Size.Small" />
|
||||
</MudTooltip>
|
||||
}
|
||||
</MudTd>
|
||||
<MudTd DataLabel="Aktionen">
|
||||
<MudTooltip Text="Bearbeiten">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit"
|
||||
Size="Size.Small"
|
||||
OnClick="@(() => OpenEditDialog(context))"/>
|
||||
</MudTooltip>
|
||||
<MudTooltip Text="Löschen">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||
Size="Size.Small"
|
||||
Color="Color.Error"
|
||||
OnClick="@(() => ConfirmDelete(context))"/>
|
||||
</MudTooltip>
|
||||
</MudTd>
|
||||
</RowTemplate>
|
||||
<PagerContent>
|
||||
<MudTablePager />
|
||||
</PagerContent>
|
||||
<NoRecordsContent>
|
||||
<MudText>Keine Kosten-Vorlagen gefunden</MudText>
|
||||
</NoRecordsContent>
|
||||
</MudTable>
|
||||
|
||||
@code {
|
||||
private string _searchString = "";
|
||||
private ExpenseType? _typeFilter;
|
||||
|
||||
private IEnumerable<ExpenseDto> 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<ExpenseEditDialog>("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<ExpenseEditDialog>("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<Koogle.Web.Components.Shared.ConfirmDialog>("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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue