diff --git a/TrafagSalesExporter/Components/App.razor b/TrafagSalesExporter/Components/App.razor
new file mode 100644
index 0000000..9ec705e
--- /dev/null
+++ b/TrafagSalesExporter/Components/App.razor
@@ -0,0 +1,18 @@
+
+
+
+
+
+ Trafag Sales Exporter
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/TrafagSalesExporter/Components/Layout/MainLayout.razor b/TrafagSalesExporter/Components/Layout/MainLayout.razor
new file mode 100644
index 0000000..ed0b75a
--- /dev/null
+++ b/TrafagSalesExporter/Components/Layout/MainLayout.razor
@@ -0,0 +1,38 @@
+@inherits LayoutComponentBase
+
+
+
+
+
+
+
+
+
+ Trafag Sales Exporter
+
+
+
+
+
+
+
+ @Body
+
+
+
+@code {
+ private bool _drawerOpen = true;
+
+ private readonly MudTheme _theme = new()
+ {
+ PaletteLight = new PaletteLight
+ {
+ Primary = "#1565C0",
+ Secondary = "#00897B",
+ AppbarBackground = "#1565C0"
+ }
+ };
+
+ private void ToggleDrawer() => _drawerOpen = !_drawerOpen;
+}
diff --git a/TrafagSalesExporter/Components/Layout/NavMenu.razor b/TrafagSalesExporter/Components/Layout/NavMenu.razor
new file mode 100644
index 0000000..27b1476
--- /dev/null
+++ b/TrafagSalesExporter/Components/Layout/NavMenu.razor
@@ -0,0 +1,14 @@
+
+
+ Dashboard
+
+
+ Standorte
+
+
+ Settings
+
+
+ Logs
+
+
diff --git a/TrafagSalesExporter/Components/Pages/Dashboard.razor b/TrafagSalesExporter/Components/Pages/Dashboard.razor
new file mode 100644
index 0000000..d8241d5
--- /dev/null
+++ b/TrafagSalesExporter/Components/Pages/Dashboard.razor
@@ -0,0 +1,193 @@
+@page "/"
+@using Microsoft.EntityFrameworkCore
+@using TrafagSalesExporter.Data
+@using TrafagSalesExporter.Services
+@inject IDbContextFactory DbFactory
+@inject ExportOrchestrationService Orchestrator
+@inject TimerBackgroundService TimerService
+@inject ISnackbar Snackbar
+@implements IDisposable
+
+Dashboard
+
+Dashboard
+
+
+
+
+ Alle exportieren
+
+
+ @if (TimerService.NextRun < DateTime.MaxValue)
+ {
+
+ @($"Nächster automatischer Lauf: {TimerService.NextRun:dd.MM.yyyy HH:mm}")
+ }
+ else
+ {
+
+ @("Timer deaktiviert")
+ }
+
+
+
+
+
+
+ Land
+ TSC
+ Schema
+ Server
+ Status
+ Zeilen
+ Letzter Lauf
+ Dauer
+ Aktion
+
+
+ @context.Land
+ @context.TSC
+ @context.Schema
+ @context.ServerName
+
+ @if (Orchestrator.IsExporting(context.SiteId))
+ {
+
+ @Orchestrator.GetExportStatus(context.SiteId)
+ }
+ else if (context.LastStatus == "OK")
+ {
+
+ }
+ else if (context.LastStatus == "Error")
+ {
+
+
+
+ }
+ else
+ {
+ -
+ }
+
+ @(context.RowCount > 0 ? context.RowCount.ToString("N0") : "-")
+ @(context.LastRun.HasValue ? context.LastRun.Value.ToString("dd.MM.yyyy HH:mm:ss") : "-")
+ @(context.DurationSeconds > 0 ? $"{context.DurationSeconds:F1}s" : "-")
+
+
+ Export
+
+
+
+
+
+@code {
+ private List _dashboardRows = new();
+ private bool _loading = true;
+ private bool _anyRunning;
+
+ protected override async Task OnInitializedAsync()
+ {
+ Orchestrator.OnExportStatusChanged += HandleStatusChanged;
+ await LoadDataAsync();
+ }
+
+ private async Task LoadDataAsync()
+ {
+ _loading = true;
+ using var db = await DbFactory.CreateDbContextAsync();
+
+ var sites = await db.Sites.Include(s => s.HanaServer).Where(s => s.IsActive).ToListAsync();
+ var logs = await db.ExportLogs
+ .GroupBy(l => l.SiteId)
+ .Select(g => g.OrderByDescending(l => l.Timestamp).First())
+ .ToListAsync();
+
+ _dashboardRows = sites.Select(s =>
+ {
+ var log = logs.FirstOrDefault(l => l.SiteId == s.Id);
+ return new DashboardRow
+ {
+ SiteId = s.Id,
+ Land = s.Land,
+ TSC = s.TSC,
+ Schema = s.Schema,
+ ServerName = s.HanaServer?.Name ?? "",
+ LastStatus = log?.Status ?? "",
+ RowCount = log?.RowCount ?? 0,
+ LastRun = log?.Timestamp,
+ DurationSeconds = log?.DurationSeconds ?? 0,
+ ErrorMessage = log?.ErrorMessage ?? ""
+ };
+ }).ToList();
+
+ _anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId));
+ _loading = false;
+ }
+
+ private async Task ExportAll()
+ {
+ _anyRunning = true;
+ _ = Task.Run(async () =>
+ {
+ await Orchestrator.ExportAllAsync();
+ await InvokeAsync(async () =>
+ {
+ await LoadDataAsync();
+ StateHasChanged();
+ });
+ });
+ Snackbar.Add("Export für alle Standorte gestartet", Severity.Info);
+ }
+
+ private void ExportSingle(int siteId)
+ {
+ _ = Task.Run(async () =>
+ {
+ await Orchestrator.ExportSiteByIdAsync(siteId);
+ await InvokeAsync(async () =>
+ {
+ await LoadDataAsync();
+ StateHasChanged();
+ });
+ });
+ Snackbar.Add("Export gestartet", Severity.Info);
+ }
+
+ private async void HandleStatusChanged()
+ {
+ await InvokeAsync(async () =>
+ {
+ _anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId));
+ StateHasChanged();
+ if (!_anyRunning)
+ {
+ await LoadDataAsync();
+ StateHasChanged();
+ }
+ });
+ }
+
+ public void Dispose()
+ {
+ Orchestrator.OnExportStatusChanged -= HandleStatusChanged;
+ }
+
+ private class DashboardRow
+ {
+ public int SiteId { get; set; }
+ public string Land { get; set; } = "";
+ public string TSC { get; set; } = "";
+ public string Schema { get; set; } = "";
+ public string ServerName { get; set; } = "";
+ public string LastStatus { get; set; } = "";
+ public int RowCount { get; set; }
+ public DateTime? LastRun { get; set; }
+ public double DurationSeconds { get; set; }
+ public string ErrorMessage { get; set; } = "";
+ }
+}
diff --git a/TrafagSalesExporter/Components/Pages/Logs.razor b/TrafagSalesExporter/Components/Pages/Logs.razor
new file mode 100644
index 0000000..f97b51f
--- /dev/null
+++ b/TrafagSalesExporter/Components/Pages/Logs.razor
@@ -0,0 +1,134 @@
+@page "/logs"
+@using Microsoft.EntityFrameworkCore
+@using TrafagSalesExporter.Data
+@inject IDbContextFactory DbFactory
+@inject ISnackbar Snackbar
+@inject IDialogService DialogService
+
+Logs
+
+Export Logs
+
+
+
+
+ @foreach (var land in _availableLands)
+ {
+ @land
+ }
+
+
+ OK
+ Error
+
+
+
+ Filtern
+
+
+
+ Alte Logs löschen
+
+
+
+
+
+
+ Zeitpunkt
+ Land
+ TSC
+ Status
+ Zeilen
+ Dauer
+ Dateiname
+ Fehler
+
+
+ @context.Timestamp.ToString("dd.MM.yyyy HH:mm:ss")
+ @context.Land
+ @context.TSC
+
+ @if (context.Status == "OK")
+ {
+ OK
+ }
+ else
+ {
+ Error
+ }
+
+ @context.RowCount.ToString("N0")
+ @($"{context.DurationSeconds:F1}s")
+ @context.FileName
+
+ @if (!string.IsNullOrEmpty(context.ErrorMessage))
+ {
+
+
+ @context.ErrorMessage
+
+
+ }
+
+
+
+
+@code {
+ private List _logs = new();
+ private List _availableLands = new();
+ private string? _filterLand;
+ private string? _filterStatus;
+ private DateTime? _filterDate;
+ private bool _loading = true;
+
+ protected override async Task OnInitializedAsync()
+ {
+ using var db = await DbFactory.CreateDbContextAsync();
+ _availableLands = await db.ExportLogs.Select(l => l.Land).Distinct().OrderBy(l => l).ToListAsync();
+ await LoadLogsAsync();
+ }
+
+ private async Task LoadLogsAsync()
+ {
+ _loading = true;
+ using var db = await DbFactory.CreateDbContextAsync();
+ IQueryable query = db.ExportLogs.OrderByDescending(l => l.Timestamp);
+
+ if (!string.IsNullOrEmpty(_filterLand))
+ query = query.Where(l => l.Land == _filterLand);
+
+ if (!string.IsNullOrEmpty(_filterStatus))
+ query = query.Where(l => l.Status == _filterStatus);
+
+ if (_filterDate.HasValue)
+ query = query.Where(l => l.Timestamp.Date == _filterDate.Value.Date);
+
+ _logs = await query.Take(500).ToListAsync();
+ _loading = false;
+ }
+
+ private async Task ApplyFilter()
+ {
+ await LoadLogsAsync();
+ }
+
+ private async Task DeleteOldLogs()
+ {
+ var result = await DialogService.ShowMessageBox(
+ "Alte Logs löschen",
+ "Logs älter als 90 Tage löschen?",
+ yesText: "Löschen", cancelText: "Abbrechen");
+
+ if (result != true) return;
+
+ using var db = await DbFactory.CreateDbContextAsync();
+ var cutoff = DateTime.Now.AddDays(-90);
+ var oldLogs = await db.ExportLogs.Where(l => l.Timestamp < cutoff).ToListAsync();
+ db.ExportLogs.RemoveRange(oldLogs);
+ var count = await db.SaveChangesAsync();
+ await LoadLogsAsync();
+ Snackbar.Add($"{oldLogs.Count} alte Logs gelöscht", Severity.Info);
+ }
+}
diff --git a/TrafagSalesExporter/Components/Pages/Settings.razor b/TrafagSalesExporter/Components/Pages/Settings.razor
new file mode 100644
index 0000000..d40e958
--- /dev/null
+++ b/TrafagSalesExporter/Components/Pages/Settings.razor
@@ -0,0 +1,164 @@
+@page "/settings"
+@using Microsoft.EntityFrameworkCore
+@using TrafagSalesExporter.Data
+@using TrafagSalesExporter.Services
+@inject IDbContextFactory DbFactory
+@inject SharePointUploadService SpService
+@inject TimerBackgroundService TimerService
+@inject ISnackbar Snackbar
+
+Settings
+
+Settings
+
+@* SharePoint Config *@
+SharePoint Konfiguration
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Speichern
+
+
+ @if (_testingSp)
+ {
+
+ @("Teste...")
+ }
+ else
+ {
+ @("SharePoint Verbindung testen")
+ }
+
+
+
+
+
+
+@* Export Settings *@
+Export Einstellungen
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Speichern
+
+
+
+
+
+@* Filename Preview *@
+Dateiname Vorschau
+
+
+
+ Sales_{"{TSC}"}_{DateTime.Now:yyyy-MM-dd}.xlsx
+
+
+ Beispiel: Sales_TRFR_@(DateTime.Now.ToString("yyyy-MM-dd")).xlsx
+
+
+
+@code {
+ private SharePointConfig _spConfig = new();
+ private ExportSettings _exportSettings = new();
+ private bool _testingSp;
+
+ protected override async Task OnInitializedAsync()
+ {
+ using var db = await DbFactory.CreateDbContextAsync();
+ _spConfig = await db.SharePointConfigs.FirstOrDefaultAsync() ?? new SharePointConfig();
+ _exportSettings = await db.ExportSettings.FirstOrDefaultAsync() ?? new ExportSettings();
+ }
+
+ private async Task SaveSharePoint()
+ {
+ using var db = await DbFactory.CreateDbContextAsync();
+ var existing = await db.SharePointConfigs.FirstOrDefaultAsync();
+ if (existing is null)
+ {
+ db.SharePointConfigs.Add(_spConfig);
+ }
+ else
+ {
+ existing.SiteUrl = _spConfig.SiteUrl;
+ existing.ExportFolder = _spConfig.ExportFolder;
+ existing.TenantId = _spConfig.TenantId;
+ existing.ClientId = _spConfig.ClientId;
+ existing.ClientSecret = _spConfig.ClientSecret;
+ }
+ await db.SaveChangesAsync();
+ Snackbar.Add("SharePoint Konfiguration gespeichert", Severity.Success);
+ }
+
+ private async Task TestSharePoint()
+ {
+ _testingSp = true;
+ try
+ {
+ await SpService.TestConnectionAsync(
+ _spConfig.TenantId, _spConfig.ClientId, _spConfig.ClientSecret, _spConfig.SiteUrl);
+ Snackbar.Add("SharePoint Verbindung erfolgreich!", Severity.Success);
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"Verbindung fehlgeschlagen: {ex.Message}", Severity.Error);
+ }
+ finally
+ {
+ _testingSp = false;
+ }
+ }
+
+ private async Task SaveExportSettings()
+ {
+ using var db = await DbFactory.CreateDbContextAsync();
+ var existing = await db.ExportSettings.FirstOrDefaultAsync();
+ if (existing is null)
+ {
+ db.ExportSettings.Add(_exportSettings);
+ }
+ else
+ {
+ existing.DateFilter = _exportSettings.DateFilter;
+ existing.TimerHour = _exportSettings.TimerHour;
+ existing.TimerMinute = _exportSettings.TimerMinute;
+ existing.TimerEnabled = _exportSettings.TimerEnabled;
+ }
+ await db.SaveChangesAsync();
+ TimerService.Recalculate();
+ Snackbar.Add("Export Einstellungen gespeichert", Severity.Success);
+ }
+}
diff --git a/TrafagSalesExporter/Components/Pages/Standorte.razor b/TrafagSalesExporter/Components/Pages/Standorte.razor
new file mode 100644
index 0000000..b524454
--- /dev/null
+++ b/TrafagSalesExporter/Components/Pages/Standorte.razor
@@ -0,0 +1,299 @@
+@page "/standorte"
+@using Microsoft.EntityFrameworkCore
+@using TrafagSalesExporter.Data
+@using TrafagSalesExporter.Services
+@inject IDbContextFactory DbFactory
+@inject HanaQueryService HanaService
+@inject ISnackbar Snackbar
+@inject IDialogService DialogService
+
+Standorte
+
+Standorte
+
+@* HANA Server Section *@
+HANA Server
+
+
+ Server hinzufügen
+
+
+
+
+ Name
+ Host
+ Port
+ Username
+ Aktionen
+
+
+ @context.Name
+ @context.Host
+ @context.Port
+ @context.Username
+
+
+
+
+
+
+
+
+
+@* Sites Section *@
+Standorte (Sites)
+
+
+ Neuen Standort hinzufügen
+
+
+
+
+ Land
+ TSC
+ Schema
+ Server
+ Aktiv
+ Aktionen
+
+
+ @context.Land
+ @context.TSC
+ @context.Schema
+ @(context.HanaServer?.Name ?? "-")
+
+ @if (context.IsActive)
+ {
+
+ }
+ else
+ {
+
+ }
+
+
+
+
+
+
+
+
+
+@* Server Dialog *@
+
+
+ @(_editingServer.Id == 0 ? "Server hinzufügen" : "Server bearbeiten")
+
+
+
+
+
+
+
+
+
+ Abbrechen
+ Speichern
+
+
+
+@* Site Dialog *@
+
+
+ @(_editingSite.Id == 0 ? "Standort hinzufügen" : "Standort bearbeiten")
+
+
+
+ @foreach (var s in _servers)
+ {
+ @s.Name
+ }
+
+
+
+
+
+
+
+ Abbrechen
+ Speichern
+
+
+
+@code {
+ private List _servers = new();
+ private List _sites = new();
+ private HanaServer _editingServer = new();
+ private Site _editingSite = new();
+ private bool _serverDialogVisible;
+ private bool _siteDialogVisible;
+ private readonly DialogOptions _dialogOptions = new() { MaxWidth = MaxWidth.Small, FullWidth = true };
+
+ protected override async Task OnInitializedAsync()
+ {
+ await LoadDataAsync();
+ }
+
+ private async Task LoadDataAsync()
+ {
+ using var db = await DbFactory.CreateDbContextAsync();
+ _servers = await db.HanaServers.OrderBy(s => s.Name).ToListAsync();
+ _sites = await db.Sites.Include(s => s.HanaServer).OrderBy(s => s.Land).ToListAsync();
+ }
+
+ // Server CRUD
+ private void AddServer()
+ {
+ _editingServer = new HanaServer { Port = 30015 };
+ _serverDialogVisible = true;
+ }
+
+ private void EditServer(HanaServer server)
+ {
+ _editingServer = new HanaServer
+ {
+ Id = server.Id,
+ Name = server.Name,
+ Host = server.Host,
+ Port = server.Port,
+ Username = server.Username,
+ Password = server.Password
+ };
+ _serverDialogVisible = true;
+ }
+
+ private async Task SaveServer()
+ {
+ using var db = await DbFactory.CreateDbContextAsync();
+ if (_editingServer.Id == 0)
+ {
+ db.HanaServers.Add(_editingServer);
+ }
+ else
+ {
+ var existing = await db.HanaServers.FindAsync(_editingServer.Id);
+ if (existing is not null)
+ {
+ existing.Name = _editingServer.Name;
+ existing.Host = _editingServer.Host;
+ existing.Port = _editingServer.Port;
+ existing.Username = _editingServer.Username;
+ existing.Password = _editingServer.Password;
+ }
+ }
+ await db.SaveChangesAsync();
+ _serverDialogVisible = false;
+ await LoadDataAsync();
+ Snackbar.Add("Server gespeichert", Severity.Success);
+ }
+
+ private async Task DeleteServer(HanaServer server)
+ {
+ var result = await DialogService.ShowMessageBox(
+ "Server löschen",
+ $"Server '{server.Name}' wirklich löschen?",
+ yesText: "Löschen", cancelText: "Abbrechen");
+
+ if (result != true) return;
+
+ using var db = await DbFactory.CreateDbContextAsync();
+ var entity = await db.HanaServers.FindAsync(server.Id);
+ if (entity is not null)
+ {
+ db.HanaServers.Remove(entity);
+ await db.SaveChangesAsync();
+ }
+ await LoadDataAsync();
+ Snackbar.Add("Server gelöscht", Severity.Info);
+ }
+
+ private async Task TestServerConnection(HanaServer server)
+ {
+ try
+ {
+ await Task.Run(() => HanaService.TestConnection(server.Host, server.Port, server.Username, server.Password));
+ Snackbar.Add($"Verbindung zu '{server.Name}' erfolgreich!", Severity.Success);
+ }
+ catch (Exception ex)
+ {
+ Snackbar.Add($"Verbindung fehlgeschlagen: {ex.Message}", Severity.Error);
+ }
+ }
+
+ // Site CRUD
+ private void AddSite()
+ {
+ _editingSite = new Site
+ {
+ IsActive = true,
+ HanaServerId = _servers.FirstOrDefault()?.Id ?? 0
+ };
+ _siteDialogVisible = true;
+ }
+
+ private void EditSite(Site site)
+ {
+ _editingSite = new Site
+ {
+ Id = site.Id,
+ HanaServerId = site.HanaServerId,
+ Schema = site.Schema,
+ TSC = site.TSC,
+ Land = site.Land,
+ IsActive = site.IsActive
+ };
+ _siteDialogVisible = true;
+ }
+
+ private async Task SaveSite()
+ {
+ using var db = await DbFactory.CreateDbContextAsync();
+ if (_editingSite.Id == 0)
+ {
+ db.Sites.Add(_editingSite);
+ }
+ else
+ {
+ var existing = await db.Sites.FindAsync(_editingSite.Id);
+ if (existing is not null)
+ {
+ existing.HanaServerId = _editingSite.HanaServerId;
+ existing.Schema = _editingSite.Schema;
+ existing.TSC = _editingSite.TSC;
+ existing.Land = _editingSite.Land;
+ existing.IsActive = _editingSite.IsActive;
+ }
+ }
+ await db.SaveChangesAsync();
+ _siteDialogVisible = false;
+ await LoadDataAsync();
+ Snackbar.Add("Standort gespeichert", Severity.Success);
+ }
+
+ private async Task DeleteSite(Site site)
+ {
+ var result = await DialogService.ShowMessageBox(
+ "Standort löschen",
+ $"Standort '{site.Land}' wirklich löschen?",
+ yesText: "Löschen", cancelText: "Abbrechen");
+
+ if (result != true) return;
+
+ using var db = await DbFactory.CreateDbContextAsync();
+ var entity = await db.Sites.FindAsync(site.Id);
+ if (entity is not null)
+ {
+ db.Sites.Remove(entity);
+ await db.SaveChangesAsync();
+ }
+ await LoadDataAsync();
+ Snackbar.Add("Standort gelöscht", Severity.Info);
+ }
+}
diff --git a/TrafagSalesExporter/Components/Routes.razor b/TrafagSalesExporter/Components/Routes.razor
new file mode 100644
index 0000000..faa2a8c
--- /dev/null
+++ b/TrafagSalesExporter/Components/Routes.razor
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/TrafagSalesExporter/Components/_Imports.razor b/TrafagSalesExporter/Components/_Imports.razor
new file mode 100644
index 0000000..5bef312
--- /dev/null
+++ b/TrafagSalesExporter/Components/_Imports.razor
@@ -0,0 +1,9 @@
+@using System.Net.Http
+@using Microsoft.AspNetCore.Components.Forms
+@using Microsoft.AspNetCore.Components.Routing
+@using Microsoft.AspNetCore.Components.Web
+@using Microsoft.JSInterop
+@using MudBlazor
+@using TrafagSalesExporter.Components
+@using TrafagSalesExporter.Components.Layout
+@using TrafagSalesExporter.Models
diff --git a/TrafagSalesExporter/Data/AppDbContext.cs b/TrafagSalesExporter/Data/AppDbContext.cs
new file mode 100644
index 0000000..c045206
--- /dev/null
+++ b/TrafagSalesExporter/Data/AppDbContext.cs
@@ -0,0 +1,51 @@
+using Microsoft.EntityFrameworkCore;
+using TrafagSalesExporter.Models;
+
+namespace TrafagSalesExporter.Data;
+
+public class AppDbContext : DbContext
+{
+ public AppDbContext(DbContextOptions options) : base(options) { }
+
+ public DbSet HanaServers => Set();
+ public DbSet Sites => Set();
+ public DbSet SharePointConfigs => Set();
+ public DbSet ExportSettings => Set();
+ public DbSet ExportLogs => Set();
+
+ public static void SeedIfEmpty(AppDbContext db)
+ {
+ if (db.HanaServers.Any()) return;
+
+ var serverInternal = new HanaServer { Name = "Internal", Host = "travtrp0", Port = 30015, Username = "", Password = "" };
+ var serverIndia = new HanaServer { Name = "India", Host = "20.197.20.60", Port = 30015, Username = "", Password = "" };
+ db.HanaServers.AddRange(serverInternal, serverIndia);
+ db.SaveChanges();
+
+ db.Sites.AddRange(
+ new Site { HanaServerId = serverInternal.Id, Schema = "fr01_p", TSC = "TRFR", Land = "Frankreich", IsActive = true },
+ new Site { HanaServerId = serverInternal.Id, Schema = "it01_p", TSC = "TRIT", Land = "Italien", IsActive = true },
+ new Site { HanaServerId = serverInternal.Id, Schema = "us01_p", TSC = "TRUS", Land = "USA", IsActive = true },
+ new Site { HanaServerId = serverIndia.Id, Schema = "TRAFAG_LIVE", TSC = "TRIN", Land = "Indien", IsActive = true }
+ );
+
+ db.SharePointConfigs.Add(new SharePointConfig
+ {
+ SiteUrl = "https://trafagag.sharepoint.com/sites/WorldwideBIPlatform",
+ ExportFolder = "/Shared Documents/Exports/",
+ TenantId = "",
+ ClientId = "",
+ ClientSecret = ""
+ });
+
+ db.ExportSettings.Add(new ExportSettings
+ {
+ DateFilter = "2025-01-01",
+ TimerHour = 3,
+ TimerMinute = 0,
+ TimerEnabled = true
+ });
+
+ db.SaveChanges();
+ }
+}
diff --git a/TrafagSalesExporter/Models/ExportLog.cs b/TrafagSalesExporter/Models/ExportLog.cs
new file mode 100644
index 0000000..7a9e88b
--- /dev/null
+++ b/TrafagSalesExporter/Models/ExportLog.cs
@@ -0,0 +1,21 @@
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace TrafagSalesExporter.Models;
+
+public class ExportLog
+{
+ public int Id { get; set; }
+ public DateTime Timestamp { get; set; }
+ public int SiteId { get; set; }
+
+ [ForeignKey(nameof(SiteId))]
+ public Site? Site { get; set; }
+
+ public string Land { get; set; } = string.Empty;
+ public string TSC { get; set; } = string.Empty;
+ public string Status { get; set; } = string.Empty;
+ public int RowCount { get; set; }
+ public string? ErrorMessage { get; set; }
+ public string FileName { get; set; } = string.Empty;
+ public double DurationSeconds { get; set; }
+}
diff --git a/TrafagSalesExporter/Models/ExportSettings.cs b/TrafagSalesExporter/Models/ExportSettings.cs
new file mode 100644
index 0000000..4fdd204
--- /dev/null
+++ b/TrafagSalesExporter/Models/ExportSettings.cs
@@ -0,0 +1,10 @@
+namespace TrafagSalesExporter.Models;
+
+public class ExportSettings
+{
+ public int Id { get; set; }
+ public string DateFilter { get; set; } = "2025-01-01";
+ public int TimerHour { get; set; } = 3;
+ public int TimerMinute { get; set; }
+ public bool TimerEnabled { get; set; } = true;
+}
diff --git a/TrafagSalesExporter/Models/HanaServer.cs b/TrafagSalesExporter/Models/HanaServer.cs
new file mode 100644
index 0000000..157953d
--- /dev/null
+++ b/TrafagSalesExporter/Models/HanaServer.cs
@@ -0,0 +1,20 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace TrafagSalesExporter.Models;
+
+public class HanaServer
+{
+ public int Id { get; set; }
+
+ [Required]
+ public string Name { get; set; } = string.Empty;
+
+ [Required]
+ public string Host { get; set; } = string.Empty;
+
+ public int Port { get; set; } = 30015;
+
+ public string Username { get; set; } = string.Empty;
+
+ public string Password { get; set; } = string.Empty;
+}
diff --git a/TrafagSalesExporter/Models/SharePointConfig.cs b/TrafagSalesExporter/Models/SharePointConfig.cs
new file mode 100644
index 0000000..f973215
--- /dev/null
+++ b/TrafagSalesExporter/Models/SharePointConfig.cs
@@ -0,0 +1,11 @@
+namespace TrafagSalesExporter.Models;
+
+public class SharePointConfig
+{
+ public int Id { get; set; }
+ public string SiteUrl { get; set; } = string.Empty;
+ public string ExportFolder { get; set; } = string.Empty;
+ public string TenantId { get; set; } = string.Empty;
+ public string ClientId { get; set; } = string.Empty;
+ public string ClientSecret { get; set; } = string.Empty;
+}
diff --git a/TrafagSalesExporter/Models/Site.cs b/TrafagSalesExporter/Models/Site.cs
new file mode 100644
index 0000000..42df2d3
--- /dev/null
+++ b/TrafagSalesExporter/Models/Site.cs
@@ -0,0 +1,25 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace TrafagSalesExporter.Models;
+
+public class Site
+{
+ public int Id { get; set; }
+
+ public int HanaServerId { get; set; }
+
+ [ForeignKey(nameof(HanaServerId))]
+ public HanaServer? HanaServer { get; set; }
+
+ [Required]
+ public string Schema { get; set; } = string.Empty;
+
+ [Required]
+ public string TSC { get; set; } = string.Empty;
+
+ [Required]
+ public string Land { get; set; } = string.Empty;
+
+ public bool IsActive { get; set; } = true;
+}
diff --git a/TrafagSalesExporter/Program.cs b/TrafagSalesExporter/Program.cs
index 5ca6023..054df35 100644
--- a/TrafagSalesExporter/Program.cs
+++ b/TrafagSalesExporter/Program.cs
@@ -1,100 +1,44 @@
-using Microsoft.Extensions.Configuration;
+using Microsoft.EntityFrameworkCore;
+using MudBlazor.Services;
+using TrafagSalesExporter.Data;
using TrafagSalesExporter.Services;
-namespace TrafagSalesExporter;
+var builder = WebApplication.CreateBuilder(args);
-internal static class Program
+builder.Services.AddRazorComponents()
+ .AddInteractiveServerComponents();
+
+builder.Services.AddMudServices();
+
+builder.Services.AddDbContextFactory(options =>
+ options.UseSqlite("Data Source=trafag_exporter.db"));
+
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+builder.Services.AddHostedService(sp => sp.GetRequiredService());
+
+var app = builder.Build();
+
+using (var scope = app.Services.CreateScope())
{
- private static async Task Main()
- {
- var config = new ConfigurationBuilder()
- .SetBasePath(AppContext.BaseDirectory)
- .AddJsonFile("appsettings.json", optional: false, reloadOnChange: false)
- .Build();
-
- var appConfig = config.Get() ?? throw new InvalidOperationException("Konfiguration konnte nicht geladen werden.");
-
- var hanaService = new HanaQueryService();
- var excelService = new ExcelExportService();
- var sharePointService = new SharePointUploadService(
- appConfig.SharePoint.TenantId,
- appConfig.SharePoint.ClientId,
- appConfig.SharePoint.ClientSecret,
- appConfig.SharePoint.SiteUrl,
- appConfig.SharePoint.ExportFolder);
-
- var outputDir = Path.Combine(AppContext.BaseDirectory, "output");
-
- foreach (var site in appConfig.Sites)
- {
- try
- {
- Log($"Starte Standort: {site.Land} ({site.Schema})");
-
- if (!appConfig.HanaServers.TryGetValue(site.Server, out var serverConfig))
- {
- throw new InvalidOperationException($"HANA Server-Konfiguration '{site.Server}' nicht gefunden.");
- }
-
- var records = hanaService.GetSalesRecords(
- serverConfig.Host,
- serverConfig.Port,
- serverConfig.Username,
- serverConfig.Password,
- site.Schema,
- site.TSC,
- site.Land);
-
- var filePath = excelService.CreateExcelFile(outputDir, site.TSC, DateTime.UtcNow.Date, records);
- Log($"Excel erzeugt: {filePath}");
-
- await sharePointService.UploadAsync(site.Land, filePath);
- Log($"Upload abgeschlossen: {site.Land}");
- }
- catch (Exception ex)
- {
- Log($"Fehler bei Standort {site.Land}: {ex.Message}");
- }
- }
-
- Log("Export beendet.");
- }
-
- private static void Log(string message)
- {
- Console.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {message}");
- }
+ var dbFactory = scope.ServiceProvider.GetRequiredService>();
+ using var db = await dbFactory.CreateDbContextAsync();
+ await db.Database.EnsureCreatedAsync();
+ AppDbContext.SeedIfEmpty(db);
}
-public class AppConfig
+if (!app.Environment.IsDevelopment())
{
- public Dictionary HanaServers { get; set; } = new();
- public List Sites { get; set; } = new();
- public SharePointConfig SharePoint { get; set; } = new();
- public string DateFilter { get; set; } = "2025-01-01";
+ app.UseHsts();
}
-public class HanaServerConfig
-{
- public string Host { get; set; } = string.Empty;
- public int Port { get; set; }
- public string Username { get; set; } = string.Empty;
- public string Password { get; set; } = string.Empty;
-}
+app.UseStaticFiles();
+app.UseAntiforgery();
-public class SiteConfig
-{
- public string Schema { get; set; } = string.Empty;
- public string Server { get; set; } = string.Empty;
- public string TSC { get; set; } = string.Empty;
- public string Land { get; set; } = string.Empty;
-}
+app.MapRazorComponents()
+ .AddInteractiveServerRenderMode();
-public class SharePointConfig
-{
- public string SiteUrl { get; set; } = string.Empty;
- public string ExportFolder { get; set; } = string.Empty;
- public string TenantId { get; set; } = string.Empty;
- public string ClientId { get; set; } = string.Empty;
- public string ClientSecret { get; set; } = string.Empty;
-}
+app.Run();
diff --git a/TrafagSalesExporter/Services/ExportOrchestrationService.cs b/TrafagSalesExporter/Services/ExportOrchestrationService.cs
new file mode 100644
index 0000000..6fddbb5
--- /dev/null
+++ b/TrafagSalesExporter/Services/ExportOrchestrationService.cs
@@ -0,0 +1,163 @@
+using Microsoft.EntityFrameworkCore;
+using System.Diagnostics;
+using TrafagSalesExporter.Data;
+using TrafagSalesExporter.Models;
+
+namespace TrafagSalesExporter.Services;
+
+public class ExportOrchestrationService
+{
+ private readonly IDbContextFactory _dbFactory;
+ private readonly HanaQueryService _hanaService;
+ private readonly ExcelExportService _excelService;
+ private readonly SharePointUploadService _sharePointService;
+ private readonly ILogger _logger;
+
+ public event Action? OnExportStatusChanged;
+
+ private readonly Dictionary _runningExports = new();
+ private readonly object _lock = new();
+
+ public ExportOrchestrationService(
+ IDbContextFactory dbFactory,
+ HanaQueryService hanaService,
+ ExcelExportService excelService,
+ SharePointUploadService sharePointService,
+ ILogger logger)
+ {
+ _dbFactory = dbFactory;
+ _hanaService = hanaService;
+ _excelService = excelService;
+ _sharePointService = sharePointService;
+ _logger = logger;
+ }
+
+ public bool IsExporting(int siteId)
+ {
+ lock (_lock)
+ {
+ return _runningExports.ContainsKey(siteId);
+ }
+ }
+
+ public string GetExportStatus(int siteId)
+ {
+ lock (_lock)
+ {
+ return _runningExports.TryGetValue(siteId, out var status) ? status : string.Empty;
+ }
+ }
+
+ public async Task ExportAllAsync()
+ {
+ using var db = await _dbFactory.CreateDbContextAsync();
+ var sites = await db.Sites.Include(s => s.HanaServer).Where(s => s.IsActive).ToListAsync();
+ foreach (var site in sites)
+ {
+ await ExportSiteAsync(site);
+ }
+ }
+
+ public async Task ExportSiteByIdAsync(int siteId)
+ {
+ using var db = await _dbFactory.CreateDbContextAsync();
+ var site = await db.Sites.Include(s => s.HanaServer).FirstOrDefaultAsync(s => s.Id == siteId);
+ if (site is null) return;
+ await ExportSiteAsync(site);
+ }
+
+ private async Task ExportSiteAsync(Site site)
+ {
+ if (site.HanaServer is null) return;
+
+ lock (_lock)
+ {
+ if (_runningExports.ContainsKey(site.Id)) return;
+ _runningExports[site.Id] = "HANA Abfrage...";
+ }
+ NotifyChanged();
+
+ var sw = Stopwatch.StartNew();
+ var log = new ExportLog
+ {
+ Timestamp = DateTime.Now,
+ SiteId = site.Id,
+ Land = site.Land,
+ TSC = site.TSC
+ };
+
+ try
+ {
+ using var db = await _dbFactory.CreateDbContextAsync();
+ var settings = await db.ExportSettings.FirstOrDefaultAsync() ?? new ExportSettings();
+ var spConfig = await db.SharePointConfigs.FirstOrDefaultAsync();
+
+ UpdateStatus(site.Id, "HANA Abfrage...");
+ var records = await Task.Run(() => _hanaService.GetSalesRecords(
+ site.HanaServer.Host, site.HanaServer.Port,
+ site.HanaServer.Username, site.HanaServer.Password,
+ site.Schema, site.TSC, site.Land, settings.DateFilter));
+
+ UpdateStatus(site.Id, "Excel erstellen...");
+ var outputDir = Path.Combine(AppContext.BaseDirectory, "output");
+ var filePath = _excelService.CreateExcelFile(outputDir, site.TSC, DateTime.UtcNow.Date, records);
+ var fileName = Path.GetFileName(filePath);
+
+ if (spConfig is not null &&
+ !string.IsNullOrWhiteSpace(spConfig.TenantId) &&
+ !string.IsNullOrWhiteSpace(spConfig.ClientId) &&
+ !string.IsNullOrWhiteSpace(spConfig.ClientSecret))
+ {
+ UpdateStatus(site.Id, "SharePoint Upload...");
+ await _sharePointService.UploadAsync(
+ spConfig.TenantId, spConfig.ClientId, spConfig.ClientSecret,
+ spConfig.SiteUrl, spConfig.ExportFolder, site.Land, filePath);
+ }
+
+ sw.Stop();
+ log.Status = "OK";
+ log.RowCount = records.Count;
+ log.FileName = fileName;
+ log.DurationSeconds = sw.Elapsed.TotalSeconds;
+
+ _logger.LogInformation("Export OK: {Land} ({TSC}) - {Rows} Zeilen in {Duration:F1}s",
+ site.Land, site.TSC, records.Count, sw.Elapsed.TotalSeconds);
+ }
+ catch (Exception ex)
+ {
+ sw.Stop();
+ log.Status = "Error";
+ log.ErrorMessage = ex.Message;
+ log.FileName = string.Empty;
+ log.DurationSeconds = sw.Elapsed.TotalSeconds;
+
+ _logger.LogError(ex, "Export Fehler: {Land} ({TSC})", site.Land, site.TSC);
+ }
+ finally
+ {
+ using var db = await _dbFactory.CreateDbContextAsync();
+ db.ExportLogs.Add(log);
+ await db.SaveChangesAsync();
+
+ lock (_lock)
+ {
+ _runningExports.Remove(site.Id);
+ }
+ NotifyChanged();
+ }
+ }
+
+ private void UpdateStatus(int siteId, string status)
+ {
+ lock (_lock)
+ {
+ _runningExports[siteId] = status;
+ }
+ NotifyChanged();
+ }
+
+ private void NotifyChanged()
+ {
+ OnExportStatusChanged?.Invoke();
+ }
+}
diff --git a/TrafagSalesExporter/Services/HanaQueryService.cs b/TrafagSalesExporter/Services/HanaQueryService.cs
index 125c2ad..8872d2e 100644
--- a/TrafagSalesExporter/Services/HanaQueryService.cs
+++ b/TrafagSalesExporter/Services/HanaQueryService.cs
@@ -5,7 +5,8 @@ namespace TrafagSalesExporter.Services;
public class HanaQueryService
{
- public List GetSalesRecords(string host, int port, string username, string password, string schema, string tsc, string land)
+ public List GetSalesRecords(string host, int port, string username, string password,
+ string schema, string tsc, string land, string dateFilter)
{
var connectionString = $"ServerNode={host}:{port};UserName={username};Password={password}";
var result = new List();
@@ -13,8 +14,8 @@ public class HanaQueryService
using var connection = new HanaConnection(connectionString);
connection.Open();
- var invoiceQuery = GetInvoiceQuery(schema, tsc);
- var creditNoteQuery = GetCreditNoteQuery(schema, tsc);
+ var invoiceQuery = GetInvoiceQuery(schema, tsc, dateFilter);
+ var creditNoteQuery = GetCreditNoteQuery(schema, tsc, dateFilter);
result.AddRange(ReadRecords(connection, invoiceQuery, land));
result.AddRange(ReadRecords(connection, creditNoteQuery, land));
@@ -31,6 +32,13 @@ public class HanaQueryService
return result;
}
+ public void TestConnection(string host, int port, string username, string password)
+ {
+ var connectionString = $"ServerNode={host}:{port};UserName={username};Password={password}";
+ using var connection = new HanaConnection(connectionString);
+ connection.Open();
+ }
+
private static List ReadRecords(HanaConnection connection, string query, string land)
{
var records = new List();
@@ -74,7 +82,7 @@ public class HanaQueryService
return records;
}
- private static string GetInvoiceQuery(string schema, string tsc) => $@"
+ private static string GetInvoiceQuery(string schema, string tsc, string dateFilter) => $@"
SELECT
CURRENT_TIMESTAMP AS extraction_date,
'{tsc}' AS tsc,
@@ -119,10 +127,10 @@ LEFT JOIN {schema}.""OCRD"" sup ON itm.""CardCode"" = sup.""CardCode""
LEFT JOIN {schema}.""CRD1"" sup_adr ON itm.""CardCode"" = sup_adr.""CardCode""
AND sup_adr.""AdresType"" = 'B'
LEFT JOIN {schema}.""OSLP"" emp ON h.""SlpCode"" = emp.""SlpCode""
-WHERE h.""CANCELED"" = 'N' AND h.""DocDate"" >= '2025-01-01'
+WHERE h.""CANCELED"" = 'N' AND h.""DocDate"" >= '{dateFilter}'
ORDER BY h.""DocDate"" DESC, h.""DocNum"", p.""LineNum""";
- private static string GetCreditNoteQuery(string schema, string tsc) => $@"
+ private static string GetCreditNoteQuery(string schema, string tsc, string dateFilter) => $@"
SELECT
CURRENT_TIMESTAMP AS extraction_date,
'{tsc}' AS tsc,
@@ -162,6 +170,6 @@ LEFT JOIN {schema}.""OCRD"" sup ON itm.""CardCode"" = sup.""CardCode""
LEFT JOIN {schema}.""CRD1"" sup_adr ON itm.""CardCode"" = sup_adr.""CardCode""
AND sup_adr.""AdresType"" = 'B'
LEFT JOIN {schema}.""OSLP"" emp ON h.""SlpCode"" = emp.""SlpCode""
-WHERE h.""CANCELED"" = 'N' AND h.""DocDate"" >= '2025-01-01'
+WHERE h.""CANCELED"" = 'N' AND h.""DocDate"" >= '{dateFilter}'
ORDER BY h.""DocDate"" DESC, h.""DocNum"", p.""LineNum""";
}
diff --git a/TrafagSalesExporter/Services/SharePointUploadService.cs b/TrafagSalesExporter/Services/SharePointUploadService.cs
index 12aae78..fdfb578 100644
--- a/TrafagSalesExporter/Services/SharePointUploadService.cs
+++ b/TrafagSalesExporter/Services/SharePointUploadService.cs
@@ -5,40 +5,41 @@ namespace TrafagSalesExporter.Services;
public class SharePointUploadService
{
- private readonly GraphServiceClient _graphClient;
- private readonly string _siteUrl;
- private readonly string _exportFolder;
-
- public SharePointUploadService(string tenantId, string clientId, string clientSecret, string siteUrl, string exportFolder)
+ public async Task UploadAsync(string tenantId, string clientId, string clientSecret,
+ string siteUrl, string exportFolder, string land, string localFilePath)
{
var credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
- _graphClient = new GraphServiceClient(credential, ["https://graph.microsoft.com/.default"]);
- _siteUrl = siteUrl;
- _exportFolder = exportFolder;
- }
+ var graphClient = new GraphServiceClient(credential, ["https://graph.microsoft.com/.default"]);
- public async Task UploadAsync(string land, string localFilePath)
- {
- var uri = new Uri(_siteUrl);
+ var uri = new Uri(siteUrl);
var sitePath = uri.AbsolutePath;
- var site = await _graphClient.Sites[$"{uri.Host}:{sitePath}"].GetAsync();
+ var site = await graphClient.Sites[$"{uri.Host}:{sitePath}"].GetAsync();
if (site?.Id is null)
- {
throw new InvalidOperationException("SharePoint Site konnte nicht gefunden werden.");
- }
- var drive = await _graphClient.Sites[site.Id].Drive.GetAsync();
+ var drive = await graphClient.Sites[site.Id].Drive.GetAsync();
if (drive?.Id is null)
- {
throw new InvalidOperationException("SharePoint Dokumentenbibliothek konnte nicht gefunden werden.");
- }
var fileName = Path.GetFileName(localFilePath);
- var folderPath = $"{_exportFolder.Trim('/').Trim()}";
+ var folderPath = exportFolder.Trim('/').Trim();
var remotePath = $"{folderPath}/{land}/{fileName}";
await using var stream = File.OpenRead(localFilePath);
- await _graphClient.Drives[drive.Id].Root.ItemWithPath(remotePath).Content.PutAsync(stream);
+ await graphClient.Drives[drive.Id].Root.ItemWithPath(remotePath).Content.PutAsync(stream);
+ }
+
+ public async Task TestConnectionAsync(string tenantId, string clientId, string clientSecret, string siteUrl)
+ {
+ var credential = new ClientSecretCredential(tenantId, clientId, clientSecret);
+ var graphClient = new GraphServiceClient(credential, ["https://graph.microsoft.com/.default"]);
+
+ var uri = new Uri(siteUrl);
+ var sitePath = uri.AbsolutePath;
+ var site = await graphClient.Sites[$"{uri.Host}:{sitePath}"].GetAsync();
+
+ if (site?.Id is null)
+ throw new InvalidOperationException("SharePoint Site konnte nicht gefunden werden.");
}
}
diff --git a/TrafagSalesExporter/Services/TimerBackgroundService.cs b/TrafagSalesExporter/Services/TimerBackgroundService.cs
new file mode 100644
index 0000000..22d9463
--- /dev/null
+++ b/TrafagSalesExporter/Services/TimerBackgroundService.cs
@@ -0,0 +1,67 @@
+using Microsoft.EntityFrameworkCore;
+using TrafagSalesExporter.Data;
+
+namespace TrafagSalesExporter.Services;
+
+public class TimerBackgroundService : BackgroundService
+{
+ private readonly IServiceProvider _serviceProvider;
+ private readonly ILogger _logger;
+ private DateTime _nextRun = DateTime.MaxValue;
+
+ public DateTime NextRun => _nextRun;
+
+ public TimerBackgroundService(IServiceProvider serviceProvider, ILogger logger)
+ {
+ _serviceProvider = serviceProvider;
+ _logger = logger;
+ }
+
+ public void Recalculate()
+ {
+ _ = RecalculateNextRunAsync();
+ }
+
+ private async Task RecalculateNextRunAsync()
+ {
+ var dbFactory = _serviceProvider.GetRequiredService>();
+ using var db = await dbFactory.CreateDbContextAsync();
+ var settings = await db.ExportSettings.FirstOrDefaultAsync();
+
+ if (settings is null || !settings.TimerEnabled)
+ {
+ _nextRun = DateTime.MaxValue;
+ return;
+ }
+
+ var now = DateTime.Now;
+ var todayRun = new DateTime(now.Year, now.Month, now.Day, settings.TimerHour, settings.TimerMinute, 0);
+ _nextRun = todayRun <= now ? todayRun.AddDays(1) : todayRun;
+ }
+
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ await RecalculateNextRunAsync();
+
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
+
+ if (DateTime.Now < _nextRun) continue;
+
+ _logger.LogInformation("Timer-Export gestartet um {Time}", DateTime.Now);
+
+ try
+ {
+ var orchestrator = _serviceProvider.GetRequiredService();
+ await orchestrator.ExportAllAsync();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Fehler beim Timer-Export");
+ }
+
+ await RecalculateNextRunAsync();
+ }
+ }
+}
diff --git a/TrafagSalesExporter/TrafagSalesExporter.csproj b/TrafagSalesExporter/TrafagSalesExporter.csproj
index 4d9d45a..ddd1092 100644
--- a/TrafagSalesExporter/TrafagSalesExporter.csproj
+++ b/TrafagSalesExporter/TrafagSalesExporter.csproj
@@ -1,6 +1,5 @@
-
+
- Exe
net8.0
enable
enable
@@ -8,17 +7,14 @@
-
-
-
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
-
-
-
- PreserveNewest
-
-
diff --git a/TrafagSalesExporter/appsettings.json b/TrafagSalesExporter/appsettings.json
index 0a5463d..0c208ae 100644
--- a/TrafagSalesExporter/appsettings.json
+++ b/TrafagSalesExporter/appsettings.json
@@ -1,30 +1,8 @@
{
- "HanaServers": {
- "Internal": {
- "Host": "travtrp0",
- "Port": 30015,
- "Username": "",
- "Password": ""
- },
- "India": {
- "Host": "20.197.20.60",
- "Port": 30015,
- "Username": "",
- "Password": ""
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
}
- },
- "Sites": [
- { "Schema": "fr01_p", "Server": "Internal", "TSC": "TRFR", "Land": "Frankreich" },
- { "Schema": "it01_p", "Server": "Internal", "TSC": "TRIT", "Land": "Italien" },
- { "Schema": "us01_p", "Server": "Internal", "TSC": "TRUS", "Land": "USA" },
- { "Schema": "TRAFAG_LIVE", "Server": "India", "TSC": "TRIN", "Land": "Indien" }
- ],
- "SharePoint": {
- "SiteUrl": "https://trafagag.sharepoint.com/sites/WorldwideBIPlatform",
- "ExportFolder": "/Shared Documents/Exports/",
- "TenantId": "",
- "ClientId": "",
- "ClientSecret": ""
- },
- "DateFilter": "2025-01-01"
+ }
}
diff --git a/TrafagSalesExporter/wwwroot/css/app.css b/TrafagSalesExporter/wwwroot/css/app.css
new file mode 100644
index 0000000..43376d2
--- /dev/null
+++ b/TrafagSalesExporter/wwwroot/css/app.css
@@ -0,0 +1,3 @@
+html, body {
+ font-family: 'Roboto', sans-serif;
+}