@page "/admin/menu-structure" @using Microsoft.AspNetCore.Authorization @using TrafagSalesExporter.Models @using TrafagSalesExporter.Security @using TrafagSalesExporter.Services @attribute [Authorize(Policy = SecurityPolicies.AdminOnly)] @inject INavigationMenuService NavigationMenuService @inject IUiTextService UiText @inject ISnackbar Snackbar @T("Menuestruktur", "Menu structure") @T("Menuestruktur", "Menu structure") @T("Speichern", "Save") @T("Standard wiederherstellen", "Restore default") @T("Bestehende Menuepunkte koennen in andere Gruppen gehaengt, sortiert, aus- oder eingeblendet und umbenannt werden. Die Zielseite bleibt unveraendert.", "Existing menu entries can be moved into other groups, sorted, hidden/shown and renamed. The target page stays unchanged.") @if (_loading) { } else { @T("Drag & Drop", "Drag & drop") @foreach (var item in OrderedItems) { } @T("Details", "Details") @T("Reihenfolge", "Order") @T("Titel", "Title") @T("Typ", "Type") @T("Untermenue von", "Parent") @T("Sichtbar", "Visible") @T("Aktion", "Action") @if (!string.IsNullOrWhiteSpace(context.Href)) { @context.Href } @context.ItemType @T("Hauptmenue", "Root menu") @foreach (var group in ParentOptions(context)) { @GroupLabel(group) } @context.Key } @code { private const string RootParentValue = "__root__"; private List _items = []; private NavigationMenuItem? _draggedItem; private bool _loading = true; private IEnumerable OrderedItems => _items .OrderBy(x => x.ParentKey ?? string.Empty) .ThenBy(x => x.SortOrder) .ThenBy(x => x.TitleDe); protected override async Task OnInitializedAsync() { await LoadAsync(); } private async Task LoadAsync() { _loading = true; _items = await NavigationMenuService.GetItemsAsync(); _loading = false; } private async Task SaveAsync() { NormalizeSortOrders(); await NavigationMenuService.SaveItemsAsync(_items); Snackbar.Add(T("Menuestruktur gespeichert.", "Menu structure saved."), Severity.Success); } private async Task ResetAsync() { await NavigationMenuService.ResetToDefaultsAsync(); await LoadAsync(); Snackbar.Add(T("Standard-Menuestruktur wiederhergestellt.", "Default menu structure restored."), Severity.Info); } private IEnumerable ParentOptions(NavigationMenuItem item) => _items .Where(x => x.ItemType == NavigationMenuItemTypes.Group) .Where(x => x.Key != item.Key) .Where(x => !WouldCreateCycle(item.Key, x.Key)) .OrderBy(x => x.TitleDe); private void ChangeParent(NavigationMenuItem item, string parentKey) { item.ParentKey = parentKey == RootParentValue ? null : parentKey; item.SortOrder = NextSortOrder(item.ParentKey); } private void Move(NavigationMenuItem item, int direction) { var siblings = _items .Where(x => string.Equals(x.ParentKey, item.ParentKey, StringComparison.OrdinalIgnoreCase)) .OrderBy(x => x.SortOrder) .ThenBy(x => x.TitleDe) .ToList(); var index = siblings.FindIndex(x => x.Key == item.Key); var targetIndex = index + direction; if (index < 0 || targetIndex < 0 || targetIndex >= siblings.Count) return; (siblings[index].SortOrder, siblings[targetIndex].SortOrder) = (siblings[targetIndex].SortOrder, siblings[index].SortOrder); } private void StartDrag(NavigationMenuItem item) { _draggedItem = item; } private void DropOnRoot() { if (_draggedItem is null) return; _draggedItem.ParentKey = null; _draggedItem.SortOrder = NextSortOrder(null); _draggedItem = null; } private void DropOn(NavigationMenuItem target) { if (_draggedItem is null || _draggedItem.Key == target.Key) return; if (target.ItemType == NavigationMenuItemTypes.Group && !WouldCreateCycle(_draggedItem.Key, target.Key)) { _draggedItem.ParentKey = target.Key; _draggedItem.SortOrder = NextSortOrder(target.Key); } else { _draggedItem.ParentKey = target.ParentKey; _draggedItem.SortOrder = target.SortOrder - 1; NormalizeSortOrders(); } _draggedItem = null; } private bool WouldCreateCycle(string itemKey, string candidateParentKey) { var current = _items.FirstOrDefault(x => x.Key == candidateParentKey); while (current is not null) { if (current.Key == itemKey) return true; current = string.IsNullOrWhiteSpace(current.ParentKey) ? null : _items.FirstOrDefault(x => x.Key == current.ParentKey); } return false; } private int NextSortOrder(string? parentKey) => _items .Where(x => string.Equals(x.ParentKey, parentKey, StringComparison.OrdinalIgnoreCase)) .Select(x => x.SortOrder) .DefaultIfEmpty(0) .Max() + 10; private void NormalizeSortOrders() { foreach (var group in _items.GroupBy(x => x.ParentKey ?? string.Empty)) { var sortOrder = 10; foreach (var item in group.OrderBy(x => x.SortOrder).ThenBy(x => x.TitleDe)) { item.SortOrder = sortOrder; sortOrder += 10; } } } private static string NormalizeParent(string? parentKey) => string.IsNullOrWhiteSpace(parentKey) ? RootParentValue : parentKey; private string GroupLabel(NavigationMenuItem item) => UiText.Text(item.TitleDe, item.TitleEn); private string T(string german, string english) => UiText.Text(german, english); private string Indent(NavigationMenuItem item) { var depth = 0; var current = item; while (!string.IsNullOrWhiteSpace(current.ParentKey)) { depth++; var next = _items.FirstOrDefault(x => x.Key == current.ParentKey); if (next is null || next.Key == current.Key) break; current = next; } return new string(' ', depth * 4); } }