K14: add Categories UI page + dialog

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beo3000 2026-01-03 21:49:11 +01:00
parent d0beb200b3
commit 05d1622bd4
2 changed files with 386 additions and 0 deletions

View File

@ -0,0 +1,199 @@
@page "/cashbook/categories"
@attribute [Authorize(Policy = "ClubTreasurer")]
@inherits Fluxor.Blazor.Web.Components.FluxorComponent
@using Fluxor
@using Koogle.Application.DTOs
@using Koogle.Domain.Enums
@using Koogle.Web.Store.CategoryState
@using Microsoft.AspNetCore.Authorization
@inject IState<CategoryState> CategoryState
@inject IDispatcher Dispatcher
@inject ISnackbar Snackbar
@inject IDialogService DialogService
<PageTitle>Buchungskategorien</PageTitle>
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mb-4">
<MudText Typo="Typo.h4">Buchungskategorien</MudText>
<MudButton Variant="Variant.Outlined" Color="Color.Secondary" StartIcon="@Icons.Material.Filled.ArrowBack"
Href="/cashbook">
Zurück zum Kassenbuch
</MudButton>
</MudStack>
@if (CategoryState.Value.Error is not null)
{
<MudAlert Severity="Severity.Error" Class="mb-4" ShowCloseIcon="true" CloseIconClicked="ClearError">
@CategoryState.Value.Error
</MudAlert>
}
<MudTable Items="CategoryState.Value.Categories" Dense="true" Hover="true" Loading="CategoryState.Value.IsLoading">
<ToolBarContent>
<MudSwitch @bind-Value="_showInactive" Label="Inaktive anzeigen" Color="Color.Primary" Class="mr-4" />
<MudSpacer />
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add"
OnClick="OpenCreateDialog">
Neue Kategorie
</MudButton>
</ToolBarContent>
<HeaderContent>
<MudTh>Farbe</MudTh>
<MudTh>Name</MudTh>
<MudTh>Beschreibung</MudTh>
<MudTh>Typ</MudTh>
<MudTh>Status</MudTh>
<MudTh>Aktionen</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Farbe">
@if (!string.IsNullOrWhiteSpace(context.Color))
{
<MudIcon Icon="@Icons.Material.Filled.Circle" Style="@($"color: {context.Color}")" Size="Size.Small" />
}
</MudTd>
<MudTd DataLabel="Name" Style="@(context.IsActive ? "" : "opacity: 0.5;")">
@if (!string.IsNullOrWhiteSpace(context.Icon))
{
<MudIcon Icon="@context.Icon" Size="Size.Small" Class="mr-1" />
}
@context.Name
@if (context.IsSystemCategory)
{
<MudTooltip Text="System-Kategorie (nicht löschbar)">
<MudIcon Icon="@Icons.Material.Filled.Lock" Size="Size.Small" Color="Color.Secondary" Class="ml-1" />
</MudTooltip>
}
</MudTd>
<MudTd DataLabel="Beschreibung" Style="@(context.IsActive ? "" : "opacity: 0.5;")">
@context.Description
</MudTd>
<MudTd DataLabel="Typ">
<MudChip T="string" Size="Size.Small" Color="@GetTypeColor(context.CategoryType)">
@GetTypeLabel(context.CategoryType)
</MudChip>
</MudTd>
<MudTd DataLabel="Status">
@if (context.IsActive)
{
<MudChip T="string" Size="Size.Small" Color="Color.Success">Aktiv</MudChip>
}
else
{
<MudChip T="string" Size="Size.Small" Color="Color.Default">Inaktiv</MudChip>
}
</MudTd>
<MudTd DataLabel="Aktionen">
<MudTooltip Text="Bearbeiten">
<MudIconButton Icon="@Icons.Material.Filled.Edit"
Size="Size.Small"
OnClick="@(() => OpenEditDialog(context))"/>
</MudTooltip>
@if (!context.IsSystemCategory)
{
<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 Kategorien gefunden</MudText>
</NoRecordsContent>
</MudTable>
@code {
private bool _showInactive;
protected override void OnInitialized()
{
base.OnInitialized();
LoadCategories();
}
private void LoadCategories()
{
Dispatcher.Dispatch(new LoadCategoriesAction(_showInactive));
}
protected override void OnParametersSet()
{
base.OnParametersSet();
}
private void ClearError()
{
Dispatcher.Dispatch(new ClearCategoryErrorAction());
}
private static string GetTypeLabel(BookingCategoryType type) => type switch
{
BookingCategoryType.Income => "Einnahme",
BookingCategoryType.Expense => "Ausgabe",
_ => type.ToString()
};
private static Color GetTypeColor(BookingCategoryType type) => type switch
{
BookingCategoryType.Income => Color.Success,
BookingCategoryType.Expense => Color.Error,
_ => Color.Default
};
private async Task OpenCreateDialog()
{
var dialog = await DialogService.ShowAsync<CategoryDialog>("Neue Kategorie");
var result = await dialog.Result;
if (result != null && !result.Canceled && result.Data is CreateBookingCategoryDto dto)
{
Dispatcher.Dispatch(new CreateCategoryAction(dto));
Snackbar.Add("Kategorie wird erstellt...", Severity.Info);
}
}
private async Task OpenEditDialog(BookingCategoryDto category)
{
var parameters = new DialogParameters
{
{ "Category", category }
};
var dialog = await DialogService.ShowAsync<CategoryDialog>("Kategorie bearbeiten", parameters);
var result = await dialog.Result;
if (result != null && !result.Canceled && result.Data is UpdateBookingCategoryDto dto)
{
Dispatcher.Dispatch(new UpdateCategoryAction(dto));
Snackbar.Add("Kategorie wird aktualisiert...", Severity.Info);
}
}
private async Task ConfirmDelete(BookingCategoryDto category)
{
var parameters = new DialogParameters
{
{ "ContentText", $"Möchtest du \"{category.Name}\" wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden." },
{ "ButtonText", "Löschen" },
{ "Color", Color.Error }
};
var dialog = await DialogService.ShowAsync<Shared.ConfirmDialog>("Kategorie löschen", parameters);
var result = await dialog.Result;
if (result != null && !result.Canceled)
{
Dispatcher.Dispatch(new DeleteCategoryAction(category.Id));
Snackbar.Add("Kategorie wird gelöscht...", Severity.Info);
}
}
}

View File

@ -0,0 +1,187 @@
@using Koogle.Application.DTOs
@using Koogle.Domain.Enums
@using MudBlazor.Utilities
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h6">
@(IsEditMode ? "Kategorie bearbeiten" : "Neue Kategorie")
</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" />
@if (!IsEditMode)
{
<MudSelect T="BookingCategoryType" @bind-Value="_categoryType"
Label="Typ"
Required="true"
AnchorOrigin="Origin.BottomCenter"
Class="mb-4">
<MudSelectItem Value="BookingCategoryType.Income">
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.ArrowUpward" Color="Color.Success" Size="Size.Small" />
<MudText>Einnahme</MudText>
</MudStack>
</MudSelectItem>
<MudSelectItem Value="BookingCategoryType.Expense">
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudIcon Icon="@Icons.Material.Filled.ArrowDownward" Color="Color.Error" Size="Size.Small" />
<MudText>Ausgabe</MudText>
</MudStack>
</MudSelectItem>
</MudSelect>
}
else
{
<MudText Typo="Typo.body2" Color="Color.Secondary" Class="mb-4">
Typ: @GetTypeLabel(Category!.CategoryType)
</MudText>
}
<MudDivider Class="my-4" />
<MudText Typo="Typo.subtitle2" Class="mb-2">Darstellung</MudText>
<MudStack Row="true" Spacing="4" Class="mb-4">
<MudColorPicker @bind-Value="_color"
Label="Farbe"
DisableAlpha="true"
ColorPickerMode="ColorPickerMode.HEX"
Style="width: 150px;" />
@if (!string.IsNullOrWhiteSpace(_color?.Value))
{
<MudIcon Icon="@Icons.Material.Filled.Circle" Style="@($"color: {_color.Value}; align-self: center;")" />
}
</MudStack>
<MudSelect T="string" @bind-Value="_icon"
Label="Icon"
Clearable="true"
AnchorOrigin="Origin.BottomCenter"
Class="mb-4">
@foreach (var iconOption in IconOptions)
{
<MudSelectItem Value="@iconOption.Value">
<MudStack Row="true" AlignItems="AlignItems.Center">
<MudIcon Icon="@iconOption.Value" Size="Size.Small" />
<MudText>@iconOption.Label</MudText>
</MudStack>
</MudSelectItem>
}
</MudSelect>
@if (IsEditMode && !Category!.IsSystemCategory)
{
<MudDivider Class="my-4" />
<MudCheckBox @bind-Value="_isActive" Label="Aktiv" Color="Color.Primary" />
}
</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 BookingCategoryDto? Category { get; set; }
private MudForm? _form;
private bool _isValid;
private string _name = "";
private string? _description;
private BookingCategoryType _categoryType = BookingCategoryType.Income;
private MudColor? _color;
private string? _icon;
private bool _isActive = true;
private bool IsEditMode => Category is not null;
private static readonly List<(string Label, string Value)> IconOptions =
[
("Geld", Icons.Material.Filled.AttachMoney),
("Strafe", Icons.Material.Filled.Gavel),
("Mitglied", Icons.Material.Filled.CardMembership),
("Korrektur", Icons.Material.Filled.Build),
("Saldo", Icons.Material.Filled.Balance),
("Kasse", Icons.Material.Filled.PointOfSale),
("Geschenk", Icons.Material.Filled.CardGiftcard),
("Einkauf", Icons.Material.Filled.ShoppingCart),
("Essen", Icons.Material.Filled.Restaurant),
("Getränk", Icons.Material.Filled.LocalBar),
("Miete", Icons.Material.Filled.Home),
("Sport", Icons.Material.Filled.SportsScore),
("Veranstaltung", Icons.Material.Filled.Event),
("Sonstiges", Icons.Material.Filled.MoreHoriz)
];
protected override void OnInitialized()
{
if (Category is not null)
{
_name = Category.Name;
_description = Category.Description;
_categoryType = Category.CategoryType;
_color = string.IsNullOrWhiteSpace(Category.Color) ? null : new MudColor(Category.Color);
_icon = Category.Icon;
_isActive = Category.IsActive;
}
}
private static string GetTypeLabel(BookingCategoryType type) => type switch
{
BookingCategoryType.Income => "Einnahme",
BookingCategoryType.Expense => "Ausgabe",
_ => type.ToString()
};
private void Cancel() => MudDialog?.Cancel();
private void Submit()
{
if (!_isValid) return;
if (IsEditMode)
{
var dto = new UpdateBookingCategoryDto
{
Id = Category!.Id,
Name = _name,
Description = _description,
Color = _color?.Value,
Icon = _icon,
IsActive = Category.IsSystemCategory ? true : _isActive
};
MudDialog?.Close(DialogResult.Ok(dto));
}
else
{
var dto = new CreateBookingCategoryDto
{
Name = _name,
Description = _description,
CategoryType = _categoryType,
Color = _color?.Value,
Icon = _icon
};
MudDialog?.Close(DialogResult.Ok(dto));
}
}
}