Add configurable menu structure and purchasing area

This commit is contained in:
2026-06-05 07:03:08 +02:00
parent bed1f5f0ba
commit 6f094fcac6
15 changed files with 710 additions and 101 deletions
@@ -215,4 +215,22 @@ CREATE TABLE FinanceRules (
SortOrder INTEGER NOT NULL DEFAULT 0,
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);
EnsureManualExcelColumnMappingTable(db);
EnsureCentralSalesRecordTable(db);
EnsureNavigationMenuItemTable(db);
AddColumnIfMissing(db, "CentralSalesRecords", "DocumentEntry", "INTEGER NOT NULL DEFAULT 0");
AddColumnIfMissing(db, "CentralSalesRecords", "DocumentCurrency", "TEXT NOT NULL DEFAULT ''");
AddColumnIfMissing(db, "CentralSalesRecords", "DocumentTotalForeignCurrency", "TEXT NOT NULL DEFAULT '0'");
@@ -272,6 +273,17 @@ CREATE TABLE IF NOT EXISTS FieldTransformationRules (
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)
{
var conn = db.Database.GetDbConnection();
@@ -1,6 +1,7 @@
using Microsoft.EntityFrameworkCore;
using TrafagSalesExporter.Data;
using TrafagSalesExporter.Models;
using TrafagSalesExporter.Security;
namespace TrafagSalesExporter.Services;
@@ -20,6 +21,7 @@ public class DatabaseSeedService : IDatabaseSeedService
EnsureBudgetExchangeRateDefaults(db);
EnsureFinanceIntercompanyRuleDefaults(db);
EnsureFinanceRuleDefaults(db);
EnsureNavigationMenuDefaults(db);
}
private static void SeedIfEmpty(AppDbContext db)
@@ -115,6 +117,112 @@ public class DatabaseSeedService : IDatabaseSeedService
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)
{
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);
}
}