Add configurable menu structure and purchasing area
This commit is contained in:
@@ -1,118 +1,87 @@
|
|||||||
|
@using Microsoft.AspNetCore.Authorization
|
||||||
|
@using Microsoft.AspNetCore.Components.Authorization
|
||||||
|
@using TrafagSalesExporter.Models
|
||||||
@using TrafagSalesExporter.Security
|
@using TrafagSalesExporter.Security
|
||||||
|
@using TrafagSalesExporter.Services
|
||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
@inject TrafagSalesExporter.Services.IUiTextService UiText
|
@inject IUiTextService UiText
|
||||||
@inject TrafagSalesExporter.Services.IFinanceCockpitAccessService FinanceAccess
|
@inject IFinanceCockpitAccessService FinanceAccess
|
||||||
|
@inject INavigationMenuService NavigationMenuService
|
||||||
@inject IConfiguration Configuration
|
@inject IConfiguration Configuration
|
||||||
@inject NavigationManager Navigation
|
@inject NavigationManager Navigation
|
||||||
|
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||||
|
@inject IAuthorizationService AuthorizationService
|
||||||
|
|
||||||
<MudNavMenu>
|
<MudNavMenu>
|
||||||
<MudNavGroup Title="@T("Finance Cockpit", "Finance Cockpit")" Icon="@Icons.Material.Filled.Analytics" Expanded="true">
|
@foreach (var item in RootItems)
|
||||||
<MudNavLink Href="export-dashboard" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Dashboard">
|
{
|
||||||
@T("Export Dashboard", "Export dashboard")
|
<NavMenuNode Item="item"
|
||||||
</MudNavLink>
|
Items="_visibleItems"
|
||||||
<MudNavGroup Title="@T("Management Analyse", "Management analysis")" Icon="@Icons.Material.Filled.QueryStats">
|
HiddenKeys="_hiddenKeys"
|
||||||
<MudNavLink Href="management-cockpit" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Speed">
|
OnAction="HandleMenuActionAsync" />
|
||||||
@T("Schnelluebersicht", "Quick overview")
|
}
|
||||||
</MudNavLink>
|
|
||||||
<MudNavGroup Title="@T("Experten", "Experts")" Icon="@Icons.Material.Filled.Tune">
|
|
||||||
<MudNavLink Href="management-cockpit?section=summary" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Dashboard">
|
|
||||||
@T("Finance Summary", "Finance summary")
|
|
||||||
</MudNavLink>
|
|
||||||
<MudNavLink Href="management-cockpit?section=countries" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Public">
|
|
||||||
@T("Laender Diagnose", "Country diagnostics")
|
|
||||||
</MudNavLink>
|
|
||||||
<MudNavLink Href="management-cockpit?section=status" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.FactCheck">
|
|
||||||
@T("Datenstatus", "Data status")
|
|
||||||
</MudNavLink>
|
|
||||||
<MudNavLink Href="management-cockpit?section=deviations" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.WarningAmber">
|
|
||||||
@T("Abweichungen", "Deviations")
|
|
||||||
</MudNavLink>
|
|
||||||
<MudNavLink Href="management-cockpit?section=credits" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.AssignmentReturn">
|
|
||||||
@T("Gutschriften", "Credit notes")
|
|
||||||
</MudNavLink>
|
|
||||||
<MudNavLink Href="management-cockpit?section=quality" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Rule">
|
|
||||||
@T("Datenqualitaet", "Data quality")
|
|
||||||
</MudNavLink>
|
|
||||||
<MudNavLink Href="management-cockpit?section=division&division=finance" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.PieChart">
|
|
||||||
@T("Sparten-Finanzanalyse", "Division finance")
|
|
||||||
</MudNavLink>
|
|
||||||
<MudNavLink Href="management-cockpit?section=division&division=central" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.AccountTree">
|
|
||||||
@T("Zentrale Spartenzuordnung", "Central division mapping")
|
|
||||||
</MudNavLink>
|
|
||||||
<MudNavLink Href="management-cockpit?section=3d" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.ViewInAr">
|
|
||||||
@T("3D Datenanalyse", "3D data analysis")
|
|
||||||
</MudNavLink>
|
|
||||||
<MudNavLink Href="management-cockpit?section=raw" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.QueryStats">
|
|
||||||
@T("Rohdaten Diagnose", "Raw-data diagnostics")
|
|
||||||
</MudNavLink>
|
|
||||||
</MudNavGroup>
|
|
||||||
</MudNavGroup>
|
|
||||||
@if (ShowFinanceComparison)
|
|
||||||
{
|
|
||||||
<MudNavLink Href="finance-cockpit/vergleich" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.CompareArrows">
|
|
||||||
@T("Soll/Ist Vergleich", "Actual/reference comparison")
|
|
||||||
</MudNavLink>
|
|
||||||
}
|
|
||||||
<MudNavLink Href="finance-cockpit/schulung" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.School">
|
|
||||||
@T("Finance Schulung", "Finance training")
|
|
||||||
</MudNavLink>
|
|
||||||
<MudNavLink Href="manual-imports" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.UploadFile">
|
|
||||||
@T("Manuelle Importe", "Manual imports")
|
|
||||||
</MudNavLink>
|
|
||||||
<MudNavGroup Title="@T("Admin", "Admin")" Icon="@Icons.Material.Filled.AdminPanelSettings">
|
|
||||||
<AuthorizeView Policy="@SecurityPolicies.AdminOnly">
|
|
||||||
<Authorized>
|
|
||||||
<MudNavLink Href="standorte" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.LocationOn">
|
|
||||||
@T("Standorte", "Sites")
|
|
||||||
</MudNavLink>
|
|
||||||
<MudNavLink Href="transformations" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Transform">
|
|
||||||
@T("Transformationen", "Transformations")
|
|
||||||
</MudNavLink>
|
|
||||||
<MudNavLink Href="finance-rules" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Rule">
|
|
||||||
@T("Finance Regeln", "Finance rules")
|
|
||||||
</MudNavLink>
|
|
||||||
<MudNavLink Href="settings" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Settings">
|
|
||||||
@T("Settings", "Settings")
|
|
||||||
</MudNavLink>
|
|
||||||
</Authorized>
|
|
||||||
</AuthorizeView>
|
|
||||||
<MudNavLink Href="logs" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.List">
|
|
||||||
@T("Logs", "Logs")
|
|
||||||
</MudNavLink>
|
|
||||||
</MudNavGroup>
|
|
||||||
@if (FinanceAccess.IsEnabled && FinanceAccess.IsUnlocked)
|
|
||||||
{
|
|
||||||
<MudButton Variant="Variant.Text" Color="Color.Secondary" Size="Size.Small"
|
|
||||||
StartIcon="@Icons.Material.Filled.Lock" OnClick="LockFinanceCockpit" Class="ml-3">
|
|
||||||
@T("Finance sperren", "Lock finance")
|
|
||||||
</MudButton>
|
|
||||||
}
|
|
||||||
</MudNavGroup>
|
|
||||||
<MudNavGroup Title="@T("HR KPI (Login)", "HR KPI (login)")" Icon="@Icons.Material.Filled.Groups">
|
|
||||||
<MudNavLink Href="hr-kpi" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Dashboard">
|
|
||||||
@T("HR Dashboard", "HR dashboard")
|
|
||||||
</MudNavLink>
|
|
||||||
<MudNavLink Href="hr-kpi/schulung" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.School">
|
|
||||||
@T("HR KPI Schulung", "HR KPI training")
|
|
||||||
</MudNavLink>
|
|
||||||
</MudNavGroup>
|
|
||||||
<MudNavLink Href="admin/sessions" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.PeopleAlt">
|
|
||||||
@T("Admin Bereich", "Admin area")
|
|
||||||
</MudNavLink>
|
|
||||||
</MudNavMenu>
|
</MudNavMenu>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
private List<NavigationMenuItem> _visibleItems = [];
|
||||||
|
private readonly HashSet<string> _hiddenKeys = [];
|
||||||
|
|
||||||
private bool ShowFinanceComparison => Configuration.GetValue("Navigation:ShowFinanceComparison", true);
|
private bool ShowFinanceComparison => Configuration.GetValue("Navigation:ShowFinanceComparison", true);
|
||||||
|
|
||||||
protected override void OnInitialized()
|
private IEnumerable<NavigationMenuItem> RootItems => _visibleItems
|
||||||
|
.Where(x => string.IsNullOrWhiteSpace(x.ParentKey))
|
||||||
|
.Where(x => !_hiddenKeys.Contains(x.Key))
|
||||||
|
.OrderBy(x => x.SortOrder)
|
||||||
|
.ThenBy(x => x.TitleDe);
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
UiText.Changed += HandleLanguageChanged;
|
UiText.Changed += HandleLanguageChanged;
|
||||||
|
await LoadMenuAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LockFinanceCockpit()
|
private async Task LoadMenuAsync()
|
||||||
{
|
{
|
||||||
FinanceAccess.Lock();
|
var items = await NavigationMenuService.GetItemsAsync();
|
||||||
Navigation.NavigateTo(string.Empty);
|
var authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
|
||||||
|
var user = authenticationState.User;
|
||||||
|
var filtered = new List<NavigationMenuItem>();
|
||||||
|
|
||||||
|
foreach (var item in items.Where(x => x.IsVisible))
|
||||||
|
{
|
||||||
|
if (!await IsAuthorizedAsync(user, item))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
filtered.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
_hiddenKeys.Clear();
|
||||||
|
if (!ShowFinanceComparison)
|
||||||
|
_hiddenKeys.Add("finance-comparison");
|
||||||
|
if (!FinanceAccess.IsEnabled || !FinanceAccess.IsUnlocked)
|
||||||
|
_hiddenKeys.Add("finance-lock");
|
||||||
|
|
||||||
|
_visibleItems = filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> IsAuthorizedAsync(System.Security.Claims.ClaimsPrincipal user, NavigationMenuItem item)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(item.RequiredPolicy))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
var result = await AuthorizationService.AuthorizeAsync(user, item.RequiredPolicy);
|
||||||
|
return result.Succeeded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Task HandleMenuActionAsync(string key)
|
||||||
|
{
|
||||||
|
if (key == "finance-lock")
|
||||||
|
{
|
||||||
|
FinanceAccess.Lock();
|
||||||
|
Navigation.NavigateTo(string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleLanguageChanged()
|
private void HandleLanguageChanged()
|
||||||
@@ -120,8 +89,6 @@
|
|||||||
InvokeAsync(StateHasChanged);
|
InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string T(string german, string english) => UiText.Text(german, english);
|
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
UiText.Changed -= HandleLanguageChanged;
|
UiText.Changed -= HandleLanguageChanged;
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
@using TrafagSalesExporter.Models
|
||||||
|
@using TrafagSalesExporter.Services
|
||||||
|
@using Microsoft.AspNetCore.Components.Routing
|
||||||
|
|
||||||
|
@if (Item.ItemType == NavigationMenuItemTypes.Group)
|
||||||
|
{
|
||||||
|
<MudNavGroup Title="@Title" Icon="@Icon" Expanded="@Item.IsExpanded">
|
||||||
|
@foreach (var child in Children)
|
||||||
|
{
|
||||||
|
<NavMenuNode Item="child"
|
||||||
|
Items="Items"
|
||||||
|
HiddenKeys="HiddenKeys"
|
||||||
|
OnAction="OnAction" />
|
||||||
|
}
|
||||||
|
</MudNavGroup>
|
||||||
|
}
|
||||||
|
else if (Item.ItemType == NavigationMenuItemTypes.Action)
|
||||||
|
{
|
||||||
|
<MudButton Variant="Variant.Text" Color="Color.Secondary" Size="Size.Small"
|
||||||
|
StartIcon="@Icon" OnClick="() => OnAction.InvokeAsync(Item.Key)" Class="ml-3">
|
||||||
|
@Title
|
||||||
|
</MudButton>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudNavLink Href="@Item.Href" Match="@Match" Icon="@Icon">
|
||||||
|
@Title
|
||||||
|
</MudNavLink>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter, EditorRequired] public NavigationMenuItem Item { get; set; } = default!;
|
||||||
|
[Parameter, EditorRequired] public IReadOnlyList<NavigationMenuItem> Items { get; set; } = [];
|
||||||
|
[Parameter] public HashSet<string> HiddenKeys { get; set; } = [];
|
||||||
|
[Parameter] public EventCallback<string> OnAction { get; set; }
|
||||||
|
|
||||||
|
private string Title => UiText.Text(Item.TitleDe, Item.TitleEn);
|
||||||
|
private string Icon => NavigationIconResolver.Resolve(Item.Icon);
|
||||||
|
private NavLinkMatch Match => string.Equals(Item.Match, "All", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? NavLinkMatch.All
|
||||||
|
: NavLinkMatch.Prefix;
|
||||||
|
|
||||||
|
[Inject] private IUiTextService UiText { get; set; } = default!;
|
||||||
|
|
||||||
|
private IEnumerable<NavigationMenuItem> Children => Items
|
||||||
|
.Where(x => x.IsVisible)
|
||||||
|
.Where(x => !HiddenKeys.Contains(x.Key))
|
||||||
|
.Where(x => string.Equals(x.ParentKey, Item.Key, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.OrderBy(x => x.SortOrder)
|
||||||
|
.ThenBy(x => x.TitleDe);
|
||||||
|
}
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
@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
|
||||||
|
|
||||||
|
<PageTitle>@T("Menuestruktur", "Menu structure")</PageTitle>
|
||||||
|
|
||||||
|
<MudText Typo="Typo.h4" Class="mb-4">@T("Menuestruktur", "Menu structure")</MudText>
|
||||||
|
|
||||||
|
<MudStack Row="true" Spacing="2" Class="mb-3">
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Save" OnClick="SaveAsync" Disabled="_loading">
|
||||||
|
@T("Speichern", "Save")
|
||||||
|
</MudButton>
|
||||||
|
<MudButton Variant="Variant.Outlined" Color="Color.Secondary" StartIcon="@Icons.Material.Filled.Restore" OnClick="ResetAsync" Disabled="_loading">
|
||||||
|
@T("Standard wiederherstellen", "Restore default")
|
||||||
|
</MudButton>
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
<MudAlert Severity="Severity.Info" Variant="Variant.Outlined" Class="mb-4">
|
||||||
|
@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.")
|
||||||
|
</MudAlert>
|
||||||
|
|
||||||
|
@if (_loading)
|
||||||
|
{
|
||||||
|
<MudProgressCircular Indeterminate="true" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.h6" Class="mb-2">@T("Drag & Drop", "Drag & drop")</MudText>
|
||||||
|
<div class="menu-drop-root"
|
||||||
|
@ondragover:preventDefault
|
||||||
|
@ondrop="DropOnRoot">
|
||||||
|
@T("Hier ablegen fuer Hauptmenue", "Drop here for root menu")
|
||||||
|
</div>
|
||||||
|
<MudPaper Class="pa-2 mb-4" Outlined="true">
|
||||||
|
@foreach (var item in OrderedItems)
|
||||||
|
{
|
||||||
|
<div class="menu-drag-row"
|
||||||
|
draggable="true"
|
||||||
|
@ondragstart="() => StartDrag(item)"
|
||||||
|
@ondragover:preventDefault
|
||||||
|
@ondrop="() => DropOn(item)">
|
||||||
|
<MudIcon Icon="@NavigationIconResolver.Resolve(item.Icon)" Size="Size.Small" />
|
||||||
|
<span class="menu-drag-title">@Indent(item)@item.TitleDe</span>
|
||||||
|
<span class="menu-drag-meta">@item.ItemType</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
|
<MudText Typo="Typo.h6" Class="mb-2">@T("Details", "Details")</MudText>
|
||||||
|
<MudTable Items="@OrderedItems" Dense="true" Hover="true">
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>@T("Reihenfolge", "Order")</MudTh>
|
||||||
|
<MudTh>@T("Titel", "Title")</MudTh>
|
||||||
|
<MudTh>@T("Typ", "Type")</MudTh>
|
||||||
|
<MudTh>@T("Untermenue von", "Parent")</MudTh>
|
||||||
|
<MudTh>@T("Sichtbar", "Visible")</MudTh>
|
||||||
|
<MudTh>@T("Aktion", "Action")</MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd>
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.KeyboardArrowUp" Size="Size.Small" OnClick="@(() => Move(context, -1))" />
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.KeyboardArrowDown" Size="Size.Small" OnClick="@(() => Move(context, 1))" />
|
||||||
|
</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
<MudTextField @bind-Value="context.TitleDe" Label="DE" Dense="true" Margin="Margin.Dense" />
|
||||||
|
<MudTextField @bind-Value="context.TitleEn" Label="EN" Dense="true" Margin="Margin.Dense" />
|
||||||
|
@if (!string.IsNullOrWhiteSpace(context.Href))
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.caption">@context.Href</MudText>
|
||||||
|
}
|
||||||
|
</MudTd>
|
||||||
|
<MudTd>@context.ItemType</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
<MudSelect T="string" Value="@NormalizeParent(context.ParentKey)" ValueChanged="value => ChangeParent(context, value)" Dense="true">
|
||||||
|
<MudSelectItem Value="@RootParentValue">@T("Hauptmenue", "Root menu")</MudSelectItem>
|
||||||
|
@foreach (var group in ParentOptions(context))
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@group.Key">@GroupLabel(group)</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
<MudSwitch @bind-Value="context.IsVisible" Color="Color.Primary" />
|
||||||
|
</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
<MudText Typo="Typo.caption">@context.Key</MudText>
|
||||||
|
</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
</MudTable>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private const string RootParentValue = "__root__";
|
||||||
|
private List<NavigationMenuItem> _items = [];
|
||||||
|
private NavigationMenuItem? _draggedItem;
|
||||||
|
private bool _loading = true;
|
||||||
|
|
||||||
|
private IEnumerable<NavigationMenuItem> 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<NavigationMenuItem> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.menu-drop-root {
|
||||||
|
border: 1px dashed var(--mud-palette-lines-default);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: var(--mud-palette-text-secondary);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-drag-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 28px minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-height: 34px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-bottom: 1px solid var(--mud-palette-lines-default);
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-drag-row:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-drag-title {
|
||||||
|
white-space: pre;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-drag-meta {
|
||||||
|
color: var(--mud-palette-text-secondary);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
@page "/einkauf"
|
||||||
|
@inject TrafagSalesExporter.Services.IUiTextService UiText
|
||||||
|
|
||||||
|
<PageTitle>@T("Einkauf", "Purchasing")</PageTitle>
|
||||||
|
|
||||||
|
<MudText Typo="Typo.h4" Class="mb-4">@T("Einkauf", "Purchasing")</MudText>
|
||||||
|
|
||||||
|
<MudPaper Class="pa-4" Outlined="true">
|
||||||
|
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="2">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.ShoppingCart" Color="Color.Primary" Size="Size.Large" />
|
||||||
|
<div>
|
||||||
|
<MudText Typo="Typo.h6">@T("Einkauf Dashboard", "Purchasing dashboard")</MudText>
|
||||||
|
<MudText Typo="Typo.body2">
|
||||||
|
@T("Dieser Bereich ist als Einstiegspunkt fuer Einkaufsauswertungen vorbereitet.",
|
||||||
|
"This area is prepared as the entry point for purchasing analytics.")
|
||||||
|
</MudText>
|
||||||
|
</div>
|
||||||
|
</MudStack>
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private string T(string german, string english) => UiText.Text(german, english);
|
||||||
|
}
|
||||||
@@ -24,4 +24,5 @@ public class AppDbContext : DbContext
|
|||||||
public DbSet<SapFieldMapping> SapFieldMappings => Set<SapFieldMapping>();
|
public DbSet<SapFieldMapping> SapFieldMappings => Set<SapFieldMapping>();
|
||||||
public DbSet<ManualExcelColumnMapping> ManualExcelColumnMappings => Set<ManualExcelColumnMapping>();
|
public DbSet<ManualExcelColumnMapping> ManualExcelColumnMappings => Set<ManualExcelColumnMapping>();
|
||||||
public DbSet<CentralSalesRecord> CentralSalesRecords => Set<CentralSalesRecord>();
|
public DbSet<CentralSalesRecord> CentralSalesRecords => Set<CentralSalesRecord>();
|
||||||
|
public DbSet<NavigationMenuItem> NavigationMenuItems => Set<NavigationMenuItem>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
namespace TrafagSalesExporter.Models;
|
||||||
|
|
||||||
|
public class NavigationMenuItem
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Key { get; set; } = string.Empty;
|
||||||
|
public string? ParentKey { get; set; }
|
||||||
|
public string TitleDe { get; set; } = string.Empty;
|
||||||
|
public string TitleEn { get; set; } = string.Empty;
|
||||||
|
public string Icon { get; set; } = string.Empty;
|
||||||
|
public string Href { get; set; } = string.Empty;
|
||||||
|
public string ItemType { get; set; } = NavigationMenuItemTypes.Link;
|
||||||
|
public string Match { get; set; } = "Prefix";
|
||||||
|
public string RequiredPolicy { get; set; } = string.Empty;
|
||||||
|
public bool IsVisible { get; set; } = true;
|
||||||
|
public bool IsExpanded { get; set; }
|
||||||
|
public bool IsSystem { get; set; } = true;
|
||||||
|
public int SortOrder { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class NavigationMenuItemTypes
|
||||||
|
{
|
||||||
|
public const string Group = "Group";
|
||||||
|
public const string Link = "Link";
|
||||||
|
public const string Action = "Action";
|
||||||
|
}
|
||||||
@@ -91,6 +91,7 @@ builder.Services.AddSingleton<IDatabaseInitializationService, DatabaseInitializa
|
|||||||
builder.Services.AddSingleton<IUiTextService, UiTextService>();
|
builder.Services.AddSingleton<IUiTextService, UiTextService>();
|
||||||
builder.Services.AddSingleton<IAccessSessionTracker, AccessSessionTracker>();
|
builder.Services.AddSingleton<IAccessSessionTracker, AccessSessionTracker>();
|
||||||
builder.Services.AddSingleton<ILandingPageSettingsService, LandingPageSettingsService>();
|
builder.Services.AddSingleton<ILandingPageSettingsService, LandingPageSettingsService>();
|
||||||
|
builder.Services.AddSingleton<INavigationMenuService, NavigationMenuService>();
|
||||||
|
|
||||||
// Datenquellen-Adapter (Strategy per ConnectionKind).
|
// Datenquellen-Adapter (Strategy per ConnectionKind).
|
||||||
builder.Services.AddSingleton<IDataSourceAdapter, HanaDataSourceAdapter>();
|
builder.Services.AddSingleton<IDataSourceAdapter, HanaDataSourceAdapter>();
|
||||||
|
|||||||
@@ -215,4 +215,22 @@ CREATE TABLE FinanceRules (
|
|||||||
SortOrder INTEGER NOT NULL DEFAULT 0,
|
SortOrder INTEGER NOT NULL DEFAULT 0,
|
||||||
IsActive INTEGER NOT NULL DEFAULT 1
|
IsActive INTEGER NOT NULL DEFAULT 1
|
||||||
);";
|
);";
|
||||||
|
|
||||||
|
internal static string GetNavigationMenuItemsCreateSql() => @"
|
||||||
|
CREATE TABLE NavigationMenuItems (
|
||||||
|
Id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
Key TEXT NOT NULL,
|
||||||
|
ParentKey TEXT NULL,
|
||||||
|
TitleDe TEXT NOT NULL DEFAULT '',
|
||||||
|
TitleEn TEXT NOT NULL DEFAULT '',
|
||||||
|
Icon TEXT NOT NULL DEFAULT '',
|
||||||
|
Href TEXT NOT NULL DEFAULT '',
|
||||||
|
ItemType TEXT NOT NULL DEFAULT 'Link',
|
||||||
|
Match TEXT NOT NULL DEFAULT 'Prefix',
|
||||||
|
RequiredPolicy TEXT NOT NULL DEFAULT '',
|
||||||
|
IsVisible INTEGER NOT NULL DEFAULT 1,
|
||||||
|
IsExpanded INTEGER NOT NULL DEFAULT 0,
|
||||||
|
IsSystem INTEGER NOT NULL DEFAULT 1,
|
||||||
|
SortOrder INTEGER NOT NULL DEFAULT 0
|
||||||
|
);";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ public class DatabaseSchemaMaintenanceService : IDatabaseSchemaMaintenanceServic
|
|||||||
EnsureSapFieldMappingTable(db);
|
EnsureSapFieldMappingTable(db);
|
||||||
EnsureManualExcelColumnMappingTable(db);
|
EnsureManualExcelColumnMappingTable(db);
|
||||||
EnsureCentralSalesRecordTable(db);
|
EnsureCentralSalesRecordTable(db);
|
||||||
|
EnsureNavigationMenuItemTable(db);
|
||||||
AddColumnIfMissing(db, "CentralSalesRecords", "DocumentEntry", "INTEGER NOT NULL DEFAULT 0");
|
AddColumnIfMissing(db, "CentralSalesRecords", "DocumentEntry", "INTEGER NOT NULL DEFAULT 0");
|
||||||
AddColumnIfMissing(db, "CentralSalesRecords", "DocumentCurrency", "TEXT NOT NULL DEFAULT ''");
|
AddColumnIfMissing(db, "CentralSalesRecords", "DocumentCurrency", "TEXT NOT NULL DEFAULT ''");
|
||||||
AddColumnIfMissing(db, "CentralSalesRecords", "DocumentTotalForeignCurrency", "TEXT NOT NULL DEFAULT '0'");
|
AddColumnIfMissing(db, "CentralSalesRecords", "DocumentTotalForeignCurrency", "TEXT NOT NULL DEFAULT '0'");
|
||||||
@@ -272,6 +273,17 @@ CREATE TABLE IF NOT EXISTS FieldTransformationRules (
|
|||||||
cmd.ExecuteNonQuery();
|
cmd.ExecuteNonQuery();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void EnsureNavigationMenuItemTable(AppDbContext db)
|
||||||
|
{
|
||||||
|
var conn = db.Database.GetDbConnection();
|
||||||
|
if (conn.State != System.Data.ConnectionState.Open)
|
||||||
|
conn.Open();
|
||||||
|
|
||||||
|
using var cmd = conn.CreateCommand();
|
||||||
|
cmd.CommandText = DatabaseSchemaSql.GetNavigationMenuItemsCreateSql().Replace("CREATE TABLE", "CREATE TABLE IF NOT EXISTS");
|
||||||
|
cmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
|
||||||
private static void EnsureSapSourceTable(AppDbContext db)
|
private static void EnsureSapSourceTable(AppDbContext db)
|
||||||
{
|
{
|
||||||
var conn = db.Database.GetDbConnection();
|
var conn = db.Database.GetDbConnection();
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using TrafagSalesExporter.Data;
|
using TrafagSalesExporter.Data;
|
||||||
using TrafagSalesExporter.Models;
|
using TrafagSalesExporter.Models;
|
||||||
|
using TrafagSalesExporter.Security;
|
||||||
|
|
||||||
namespace TrafagSalesExporter.Services;
|
namespace TrafagSalesExporter.Services;
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ public class DatabaseSeedService : IDatabaseSeedService
|
|||||||
EnsureBudgetExchangeRateDefaults(db);
|
EnsureBudgetExchangeRateDefaults(db);
|
||||||
EnsureFinanceIntercompanyRuleDefaults(db);
|
EnsureFinanceIntercompanyRuleDefaults(db);
|
||||||
EnsureFinanceRuleDefaults(db);
|
EnsureFinanceRuleDefaults(db);
|
||||||
|
EnsureNavigationMenuDefaults(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SeedIfEmpty(AppDbContext db)
|
private static void SeedIfEmpty(AppDbContext db)
|
||||||
@@ -115,6 +117,112 @@ public class DatabaseSeedService : IDatabaseSeedService
|
|||||||
db.SaveChanges();
|
db.SaveChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void EnsureNavigationMenuDefaults(AppDbContext db)
|
||||||
|
{
|
||||||
|
var defaults = BuildDefaultNavigationMenuItems();
|
||||||
|
var changed = false;
|
||||||
|
|
||||||
|
foreach (var item in defaults)
|
||||||
|
{
|
||||||
|
var existing = db.NavigationMenuItems.FirstOrDefault(x => x.Key == item.Key);
|
||||||
|
if (existing is null)
|
||||||
|
{
|
||||||
|
db.NavigationMenuItems.Add(item);
|
||||||
|
changed = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(existing.TitleDe)) existing.TitleDe = item.TitleDe;
|
||||||
|
if (string.IsNullOrWhiteSpace(existing.TitleEn)) existing.TitleEn = item.TitleEn;
|
||||||
|
if (string.IsNullOrWhiteSpace(existing.Icon)) existing.Icon = item.Icon;
|
||||||
|
if (string.IsNullOrWhiteSpace(existing.Href)) existing.Href = item.Href;
|
||||||
|
if (string.IsNullOrWhiteSpace(existing.ItemType)) existing.ItemType = item.ItemType;
|
||||||
|
if (string.IsNullOrWhiteSpace(existing.Match)) existing.Match = item.Match;
|
||||||
|
if (string.IsNullOrWhiteSpace(existing.RequiredPolicy)) existing.RequiredPolicy = item.RequiredPolicy;
|
||||||
|
existing.IsSystem = true;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed)
|
||||||
|
db.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<NavigationMenuItem> BuildDefaultNavigationMenuItems() =>
|
||||||
|
[
|
||||||
|
Group("finance", null, "Finance Cockpit", "Finance Cockpit", "Analytics", 10, expanded: true),
|
||||||
|
Link("export-dashboard", "finance", "Export Dashboard", "Export dashboard", "Dashboard", "export-dashboard", 10),
|
||||||
|
Group("management-analysis", "finance", "Management Analyse", "Management analysis", "QueryStats", 20),
|
||||||
|
Link("management-quick", "management-analysis", "Schnelluebersicht", "Quick overview", "Speed", "management-cockpit", 10, "All"),
|
||||||
|
Group("experts", "management-analysis", "Experten", "Experts", "Tune", 20),
|
||||||
|
Link("finance-summary", "experts", "Finance Summary", "Finance summary", "Dashboard", "management-cockpit?section=summary", 10, "All"),
|
||||||
|
Link("country-diagnostics", "experts", "Laender Diagnose", "Country diagnostics", "Public", "management-cockpit?section=countries", 20, "All"),
|
||||||
|
Link("data-status", "experts", "Datenstatus", "Data status", "FactCheck", "management-cockpit?section=status", 30, "All"),
|
||||||
|
Link("deviations", "experts", "Abweichungen", "Deviations", "WarningAmber", "management-cockpit?section=deviations", 40, "All"),
|
||||||
|
Link("credits", "experts", "Gutschriften", "Credit notes", "AssignmentReturn", "management-cockpit?section=credits", 50, "All"),
|
||||||
|
Link("data-quality", "experts", "Datenqualitaet", "Data quality", "Rule", "management-cockpit?section=quality", 60, "All"),
|
||||||
|
Link("division-finance", "experts", "Sparten-Finanzanalyse", "Division finance", "PieChart", "management-cockpit?section=division&division=finance", 70, "All"),
|
||||||
|
Link("division-central", "experts", "Zentrale Spartenzuordnung", "Central division mapping", "AccountTree", "management-cockpit?section=division&division=central", 80, "All"),
|
||||||
|
Link("finance-3d", "experts", "3D Datenanalyse", "3D data analysis", "ViewInAr", "management-cockpit?section=3d", 90, "All"),
|
||||||
|
Link("raw-diagnostics", "experts", "Rohdaten Diagnose", "Raw-data diagnostics", "QueryStats", "management-cockpit?section=raw", 100, "All"),
|
||||||
|
Link("finance-comparison", "finance", "Soll/Ist Vergleich", "Actual/reference comparison", "CompareArrows", "finance-cockpit/vergleich", 30),
|
||||||
|
Link("finance-training", "finance", "Finance Schulung", "Finance training", "School", "finance-cockpit/schulung", 40),
|
||||||
|
Link("manual-imports", "finance", "Manuelle Importe", "Manual imports", "UploadFile", "manual-imports", 50),
|
||||||
|
Group("finance-admin", "finance", "Admin", "Admin", "AdminPanelSettings", 60),
|
||||||
|
Link("sites", "finance-admin", "Standorte", "Sites", "LocationOn", "standorte", 10, requiredPolicy: SecurityPolicies.AdminOnly),
|
||||||
|
Link("transformations", "finance-admin", "Transformationen", "Transformations", "Transform", "transformations", 20, requiredPolicy: SecurityPolicies.AdminOnly),
|
||||||
|
Link("finance-rules", "finance-admin", "Finance Regeln", "Finance rules", "Rule", "finance-rules", 30, requiredPolicy: SecurityPolicies.AdminOnly),
|
||||||
|
Link("settings", "finance-admin", "Settings", "Settings", "Settings", "settings", 40, requiredPolicy: SecurityPolicies.AdminOnly),
|
||||||
|
Link("menu-structure", "finance-admin", "Menuestruktur", "Menu structure", "AccountTree", "admin/menu-structure", 45, requiredPolicy: SecurityPolicies.AdminOnly),
|
||||||
|
Link("logs", "finance-admin", "Logs", "Logs", "List", "logs", 50),
|
||||||
|
Action("finance-lock", "finance", "Finance sperren", "Lock finance", "Lock", 70),
|
||||||
|
Group("hr", null, "HR KPI (Login)", "HR KPI (login)", "Groups", 20),
|
||||||
|
Link("hr-dashboard", "hr", "HR Dashboard", "HR dashboard", "Dashboard", "hr-kpi", 10, "All"),
|
||||||
|
Link("hr-training", "hr", "HR KPI Schulung", "HR KPI training", "School", "hr-kpi/schulung", 20),
|
||||||
|
Group("purchasing", null, "Einkauf", "Purchasing", "ShoppingCart", 30),
|
||||||
|
Link("purchasing-dashboard", "purchasing", "Einkauf Dashboard", "Purchasing dashboard", "Dashboard", "einkauf", 10, "All"),
|
||||||
|
Link("admin-sessions", null, "Admin Bereich", "Admin area", "PeopleAlt", "admin/sessions", 90)
|
||||||
|
];
|
||||||
|
|
||||||
|
private static NavigationMenuItem Group(string key, string? parentKey, string titleDe, string titleEn, string icon, int sortOrder, bool expanded = false)
|
||||||
|
=> new()
|
||||||
|
{
|
||||||
|
Key = key,
|
||||||
|
ParentKey = parentKey,
|
||||||
|
TitleDe = titleDe,
|
||||||
|
TitleEn = titleEn,
|
||||||
|
Icon = icon,
|
||||||
|
ItemType = NavigationMenuItemTypes.Group,
|
||||||
|
IsExpanded = expanded,
|
||||||
|
SortOrder = sortOrder
|
||||||
|
};
|
||||||
|
|
||||||
|
private static NavigationMenuItem Link(string key, string? parentKey, string titleDe, string titleEn, string icon, string href, int sortOrder, string match = "Prefix", string requiredPolicy = "")
|
||||||
|
=> new()
|
||||||
|
{
|
||||||
|
Key = key,
|
||||||
|
ParentKey = parentKey,
|
||||||
|
TitleDe = titleDe,
|
||||||
|
TitleEn = titleEn,
|
||||||
|
Icon = icon,
|
||||||
|
Href = href,
|
||||||
|
Match = match,
|
||||||
|
RequiredPolicy = requiredPolicy,
|
||||||
|
ItemType = NavigationMenuItemTypes.Link,
|
||||||
|
SortOrder = sortOrder
|
||||||
|
};
|
||||||
|
|
||||||
|
private static NavigationMenuItem Action(string key, string? parentKey, string titleDe, string titleEn, string icon, int sortOrder)
|
||||||
|
=> new()
|
||||||
|
{
|
||||||
|
Key = key,
|
||||||
|
ParentKey = parentKey,
|
||||||
|
TitleDe = titleDe,
|
||||||
|
TitleEn = titleEn,
|
||||||
|
Icon = icon,
|
||||||
|
ItemType = NavigationMenuItemTypes.Action,
|
||||||
|
SortOrder = sortOrder
|
||||||
|
};
|
||||||
|
|
||||||
private static void EnsureCentralHanaServerRecords(AppDbContext db)
|
private static void EnsureCentralHanaServerRecords(AppDbContext db)
|
||||||
{
|
{
|
||||||
var centralSystems = db.SourceSystemDefinitions
|
var centralSystems = db.SourceSystemDefinitions
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using TrafagSalesExporter.Models;
|
||||||
|
|
||||||
|
namespace TrafagSalesExporter.Services;
|
||||||
|
|
||||||
|
public interface INavigationMenuService
|
||||||
|
{
|
||||||
|
Task<List<NavigationMenuItem>> GetItemsAsync();
|
||||||
|
Task SaveItemsAsync(IEnumerable<NavigationMenuItem> items);
|
||||||
|
Task ResetToDefaultsAsync();
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using MudBlazor;
|
||||||
|
|
||||||
|
namespace TrafagSalesExporter.Services;
|
||||||
|
|
||||||
|
public static class NavigationIconResolver
|
||||||
|
{
|
||||||
|
public static string Resolve(string icon) => icon switch
|
||||||
|
{
|
||||||
|
"AccountTree" => Icons.Material.Filled.AccountTree,
|
||||||
|
"AdminPanelSettings" => Icons.Material.Filled.AdminPanelSettings,
|
||||||
|
"Analytics" => Icons.Material.Filled.Analytics,
|
||||||
|
"AssignmentReturn" => Icons.Material.Filled.AssignmentReturn,
|
||||||
|
"CompareArrows" => Icons.Material.Filled.CompareArrows,
|
||||||
|
"Dashboard" => Icons.Material.Filled.Dashboard,
|
||||||
|
"FactCheck" => Icons.Material.Filled.FactCheck,
|
||||||
|
"Groups" => Icons.Material.Filled.Groups,
|
||||||
|
"List" => Icons.Material.Filled.List,
|
||||||
|
"LocationOn" => Icons.Material.Filled.LocationOn,
|
||||||
|
"Lock" => Icons.Material.Filled.Lock,
|
||||||
|
"PeopleAlt" => Icons.Material.Filled.PeopleAlt,
|
||||||
|
"PieChart" => Icons.Material.Filled.PieChart,
|
||||||
|
"Public" => Icons.Material.Filled.Public,
|
||||||
|
"QueryStats" => Icons.Material.Filled.QueryStats,
|
||||||
|
"Rule" => Icons.Material.Filled.Rule,
|
||||||
|
"School" => Icons.Material.Filled.School,
|
||||||
|
"Settings" => Icons.Material.Filled.Settings,
|
||||||
|
"ShoppingCart" => Icons.Material.Filled.ShoppingCart,
|
||||||
|
"Speed" => Icons.Material.Filled.Speed,
|
||||||
|
"Transform" => Icons.Material.Filled.Transform,
|
||||||
|
"Tune" => Icons.Material.Filled.Tune,
|
||||||
|
"UploadFile" => Icons.Material.Filled.UploadFile,
|
||||||
|
"ViewInAr" => Icons.Material.Filled.ViewInAr,
|
||||||
|
"WarningAmber" => Icons.Material.Filled.WarningAmber,
|
||||||
|
_ => Icons.Material.Filled.Circle
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TrafagSalesExporter.Data;
|
||||||
|
using TrafagSalesExporter.Models;
|
||||||
|
|
||||||
|
namespace TrafagSalesExporter.Services;
|
||||||
|
|
||||||
|
public sealed class NavigationMenuService : INavigationMenuService
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
||||||
|
|
||||||
|
public NavigationMenuService(IDbContextFactory<AppDbContext> dbFactory)
|
||||||
|
{
|
||||||
|
_dbFactory = dbFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<NavigationMenuItem>> GetItemsAsync()
|
||||||
|
{
|
||||||
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
|
return await db.NavigationMenuItems
|
||||||
|
.AsNoTracking()
|
||||||
|
.OrderBy(x => x.ParentKey ?? string.Empty)
|
||||||
|
.ThenBy(x => x.SortOrder)
|
||||||
|
.ThenBy(x => x.TitleDe)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SaveItemsAsync(IEnumerable<NavigationMenuItem> items)
|
||||||
|
{
|
||||||
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
|
var incoming = items.ToDictionary(x => x.Key, StringComparer.OrdinalIgnoreCase);
|
||||||
|
var existing = await db.NavigationMenuItems.ToListAsync();
|
||||||
|
|
||||||
|
foreach (var item in existing)
|
||||||
|
{
|
||||||
|
if (!incoming.TryGetValue(item.Key, out var source))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
item.ParentKey = string.IsNullOrWhiteSpace(source.ParentKey) ? null : source.ParentKey;
|
||||||
|
item.SortOrder = source.SortOrder;
|
||||||
|
item.IsVisible = source.IsVisible;
|
||||||
|
item.IsExpanded = source.IsExpanded;
|
||||||
|
item.TitleDe = source.TitleDe.Trim();
|
||||||
|
item.TitleEn = source.TitleEn.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ResetToDefaultsAsync()
|
||||||
|
{
|
||||||
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
|
db.NavigationMenuItems.RemoveRange(db.NavigationMenuItems);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
new DatabaseSeedService().SeedDefaults(db);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,8 @@ Stand: 2026-06-05
|
|||||||
- Validierung laut Doku: Finance-Sitzungsstand `82/82` Tests gruen; spaetere UI-/Deploy-Schritte wurden einzeln umgesetzt und deployed.
|
- Validierung laut Doku: Finance-Sitzungsstand `82/82` Tests gruen; spaetere UI-/Deploy-Schritte wurden einzeln umgesetzt und deployed.
|
||||||
- Letzter dokumentierter Finance-Deploy: 2026-06-05 auf `\\trch-webapp-bidashboard.trafagch.local\BiDashboard$\`.
|
- Letzter dokumentierter Finance-Deploy: 2026-06-05 auf `\\trch-webapp-bidashboard.trafagch.local\BiDashboard$\`.
|
||||||
- Neu im Finance/Management-Cockpit: einfache Schnelluebersicht links sichtbar; tiefere Funktionen bleiben unter `Experten`.
|
- Neu im Finance/Management-Cockpit: einfache Schnelluebersicht links sichtbar; tiefere Funktionen bleiben unter `Experten`.
|
||||||
|
- Neu in der Navigation: Menuebaum wird aus `NavigationMenuItems` gerendert; Admins koennen bestehende Punkte unter `Admin > Menuestruktur` umhaengen, sortieren und aus-/einblenden.
|
||||||
|
- Neu als Hauptbereich: `Einkauf` mit Einkaufswagen-Icon und Einstieg `Einkauf Dashboard`.
|
||||||
- Neu im Expertenbereich: `3D Datenanalyse` mit drehbarer 3D-Grafik, Achsen, Diagrammarten, Indikatorauswahl, Labelgroesse und Simulation per Schieberegler.
|
- Neu im Expertenbereich: `3D Datenanalyse` mit drehbarer 3D-Grafik, Achsen, Diagrammarten, Indikatorauswahl, Labelgroesse und Simulation per Schieberegler.
|
||||||
- Spanien: `Run-SpainRangeExportAndUpload-AllInOne.ps1` exportiert Sage-Range direkt und laedt CSV/Summary via rclone nach SharePoint `trafag-bi:Import/Finance/Spanien`.
|
- Spanien: `Run-SpainRangeExportAndUpload-AllInOne.ps1` exportiert Sage-Range direkt und laedt CSV/Summary via rclone nach SharePoint `trafag-bi:Import/Finance/Spanien`.
|
||||||
- Spanien: Default-Range ist heute minus 7 Tage bis heute; `ToDate` ist exklusiv.
|
- Spanien: Default-Range ist heute minus 7 Tage bis heute; `ToDate` ist exklusiv.
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ Diese Datei ist fuer tokenarme RAG-Nutzung komprimiert.
|
|||||||
- Letzter dokumentierter Finance-Deploy: 2026-06-04 nach 3D-Datenanalyse-/Schnelluebersicht-Anpassungen auf `\\trch-webapp-bidashboard.trafagch.local\BiDashboard$\`.
|
- Letzter dokumentierter Finance-Deploy: 2026-06-04 nach 3D-Datenanalyse-/Schnelluebersicht-Anpassungen auf `\\trch-webapp-bidashboard.trafagch.local\BiDashboard$\`.
|
||||||
- Aktueller Stand 2026-06-05: Spanien-Scriptfixes sind committed; Server muss die aktuelle All-in-one-PS1 verwenden, nicht alte Kopien mit `(1)` und nicht den alten Wrapper.
|
- Aktueller Stand 2026-06-05: Spanien-Scriptfixes sind committed; Server muss die aktuelle All-in-one-PS1 verwenden, nicht alte Kopien mit `(1)` und nicht den alten Wrapper.
|
||||||
- Spanien-Delta-Sync im Dashboard-Import wurde am 2026-06-05 publiziert. Publish brauchte kurz `app_offline.htm`, weil `BiDashboard.dll` gesperrt war; danach wurde `app_offline.htm` wieder entfernt.
|
- Spanien-Delta-Sync im Dashboard-Import wurde am 2026-06-05 publiziert. Publish brauchte kurz `app_offline.htm`, weil `BiDashboard.dll` gesperrt war; danach wurde `app_offline.htm` wieder entfernt.
|
||||||
|
- Neu umgesetzt: Linker Menuebaum ist datengetrieben und ueber `Admin > Menuestruktur` umhaengbar/sortierbar; bestehende Punkte koennen in andere Untermenues verschoben werden.
|
||||||
|
- Neu umgesetzt: Neuer Hauptpunkt `Einkauf` mit Einkaufswagen-Icon und vorbereiteter Einstiegseite `Einkauf Dashboard`.
|
||||||
- Letzte Validierung: `dotnet test TrafagSalesExporter.sln --verbosity minimal --artifacts-path C:\TMP\trafag-test-artifacts-finance-session-proof` mit `82/82` Tests gruen.
|
- Letzte Validierung: `dotnet test TrafagSalesExporter.sln --verbosity minimal --artifacts-path C:\TMP\trafag-test-artifacts-finance-session-proof` mit `82/82` Tests gruen.
|
||||||
|
|
||||||
## Nachtrag 2026-06-05 Spanien Sage / rclone Upload
|
## Nachtrag 2026-06-05 Spanien Sage / rclone Upload
|
||||||
|
|||||||
Reference in New Issue
Block a user