+ @T("Einkauf Dashboard", "Purchasing dashboard")
+
+ @T("Dieser Bereich ist als Einstiegspunkt fuer Einkaufsauswertungen vorbereitet.",
+ "This area is prepared as the entry point for purchasing analytics.")
+
+
+
+
+
+@code {
+ private string T(string german, string english) => UiText.Text(german, english);
+}
diff --git a/TrafagSalesExporter/Data/AppDbContext.cs b/TrafagSalesExporter/Data/AppDbContext.cs
index 64e1f58..3f7504b 100644
--- a/TrafagSalesExporter/Data/AppDbContext.cs
+++ b/TrafagSalesExporter/Data/AppDbContext.cs
@@ -24,4 +24,5 @@ public class AppDbContext : DbContext
public DbSet SapFieldMappings => Set();
public DbSet ManualExcelColumnMappings => Set();
public DbSet CentralSalesRecords => Set();
+ public DbSet NavigationMenuItems => Set();
}
diff --git a/TrafagSalesExporter/Models/NavigationMenuItem.cs b/TrafagSalesExporter/Models/NavigationMenuItem.cs
new file mode 100644
index 0000000..887b0cd
--- /dev/null
+++ b/TrafagSalesExporter/Models/NavigationMenuItem.cs
@@ -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";
+}
diff --git a/TrafagSalesExporter/Program.cs b/TrafagSalesExporter/Program.cs
index df0211a..47a907d 100644
--- a/TrafagSalesExporter/Program.cs
+++ b/TrafagSalesExporter/Program.cs
@@ -91,6 +91,7 @@ builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
+builder.Services.AddSingleton();
// Datenquellen-Adapter (Strategy per ConnectionKind).
builder.Services.AddSingleton();
diff --git a/TrafagSalesExporter/Services/DatabaseInitializationService.SchemaSql.cs b/TrafagSalesExporter/Services/DatabaseInitializationService.SchemaSql.cs
index dfab745..b081b57 100644
--- a/TrafagSalesExporter/Services/DatabaseInitializationService.SchemaSql.cs
+++ b/TrafagSalesExporter/Services/DatabaseInitializationService.SchemaSql.cs
@@ -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
+);";
}
diff --git a/TrafagSalesExporter/Services/DatabaseSchemaMaintenanceService.cs b/TrafagSalesExporter/Services/DatabaseSchemaMaintenanceService.cs
index 561fab4..9a3c70a 100644
--- a/TrafagSalesExporter/Services/DatabaseSchemaMaintenanceService.cs
+++ b/TrafagSalesExporter/Services/DatabaseSchemaMaintenanceService.cs
@@ -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();
diff --git a/TrafagSalesExporter/Services/DatabaseSeedService.cs b/TrafagSalesExporter/Services/DatabaseSeedService.cs
index c059320..610db70 100644
--- a/TrafagSalesExporter/Services/DatabaseSeedService.cs
+++ b/TrafagSalesExporter/Services/DatabaseSeedService.cs
@@ -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 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
diff --git a/TrafagSalesExporter/Services/INavigationMenuService.cs b/TrafagSalesExporter/Services/INavigationMenuService.cs
new file mode 100644
index 0000000..1de1f70
--- /dev/null
+++ b/TrafagSalesExporter/Services/INavigationMenuService.cs
@@ -0,0 +1,10 @@
+using TrafagSalesExporter.Models;
+
+namespace TrafagSalesExporter.Services;
+
+public interface INavigationMenuService
+{
+ Task> GetItemsAsync();
+ Task SaveItemsAsync(IEnumerable items);
+ Task ResetToDefaultsAsync();
+}
diff --git a/TrafagSalesExporter/Services/NavigationIconResolver.cs b/TrafagSalesExporter/Services/NavigationIconResolver.cs
new file mode 100644
index 0000000..aa050be
--- /dev/null
+++ b/TrafagSalesExporter/Services/NavigationIconResolver.cs
@@ -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
+ };
+}
diff --git a/TrafagSalesExporter/Services/NavigationMenuService.cs b/TrafagSalesExporter/Services/NavigationMenuService.cs
new file mode 100644
index 0000000..a20c81c
--- /dev/null
+++ b/TrafagSalesExporter/Services/NavigationMenuService.cs
@@ -0,0 +1,57 @@
+using Microsoft.EntityFrameworkCore;
+using TrafagSalesExporter.Data;
+using TrafagSalesExporter.Models;
+
+namespace TrafagSalesExporter.Services;
+
+public sealed class NavigationMenuService : INavigationMenuService
+{
+ private readonly IDbContextFactory _dbFactory;
+
+ public NavigationMenuService(IDbContextFactory dbFactory)
+ {
+ _dbFactory = dbFactory;
+ }
+
+ public async Task> 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 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);
+ }
+}
diff --git a/TrafagSalesExporter/docs/rag/PROJECT.md b/TrafagSalesExporter/docs/rag/PROJECT.md
index b04479d..2a92e23 100644
--- a/TrafagSalesExporter/docs/rag/PROJECT.md
+++ b/TrafagSalesExporter/docs/rag/PROJECT.md
@@ -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.
- 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 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.
- 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.
diff --git a/TrafagSalesExporter/lastchange.md b/TrafagSalesExporter/lastchange.md
index 1999888..91eaf71 100644
--- a/TrafagSalesExporter/lastchange.md
+++ b/TrafagSalesExporter/lastchange.md
@@ -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$\`.
- 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.
+- 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.
## Nachtrag 2026-06-05 Spanien Sage / rclone Upload