zentraler export
This commit is contained in:
@@ -19,6 +19,10 @@
|
|||||||
OnClick="ExportAll" Disabled="_anyRunning">
|
OnClick="ExportAll" Disabled="_anyRunning">
|
||||||
Alle exportieren
|
Alle exportieren
|
||||||
</MudButton>
|
</MudButton>
|
||||||
|
<MudButton Variant="Variant.Outlined" Color="Color.Secondary" StartIcon="@Icons.Material.Filled.TableView"
|
||||||
|
OnClick="ExportConsolidatedOnly" Disabled="_anyRunning">
|
||||||
|
Zentrale Datei neu erzeugen
|
||||||
|
</MudButton>
|
||||||
<MudText Typo="Typo.body1">
|
<MudText Typo="Typo.body1">
|
||||||
@if (TimerService.NextRun < DateTime.MaxValue)
|
@if (TimerService.NextRun < DateTime.MaxValue)
|
||||||
{
|
{
|
||||||
@@ -109,8 +113,49 @@
|
|||||||
</RowTemplate>
|
</RowTemplate>
|
||||||
</MudTable>
|
</MudTable>
|
||||||
|
|
||||||
|
<MudPaper Class="pa-4 mt-4" Elevation="1">
|
||||||
|
<MudText Typo="Typo.h6" Class="mb-3">Zentrale Datei</MudText>
|
||||||
|
<MudTable Items="_consolidatedRows" Dense Hover Striped>
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>Datei</MudTh>
|
||||||
|
<MudTh>Pfad</MudTh>
|
||||||
|
<MudTh>Letzte Änderung</MudTh>
|
||||||
|
<MudTh>Status</MudTh>
|
||||||
|
<MudTh>Aktion</MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd>@context.Label</MudTd>
|
||||||
|
<MudTd>@context.DisplayPath</MudTd>
|
||||||
|
<MudTd>@(context.LastModified.HasValue ? context.LastModified.Value.ToString("dd.MM.yyyy HH:mm:ss") : "-")</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
@if (Orchestrator.IsConsolidatedExporting())
|
||||||
|
{
|
||||||
|
<MudProgressCircular Size="Size.Small" Indeterminate Color="Color.Primary" Class="mr-1" />
|
||||||
|
<MudText Typo="Typo.caption">@Orchestrator.GetConsolidatedExportStatus()</MudText>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.caption" Color="Color.Default">-</MudText>
|
||||||
|
}
|
||||||
|
</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
<MudButton Size="Size.Small" Variant="Variant.Text" Color="Color.Info"
|
||||||
|
StartIcon="@Icons.Material.Filled.OpenInNew"
|
||||||
|
OnClick="() => OpenFile(context.FilePath)"
|
||||||
|
Disabled="@(!context.HasOpenableFile)">
|
||||||
|
Excel öffnen
|
||||||
|
</MudButton>
|
||||||
|
</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
<NoRecordsContent>
|
||||||
|
<MudText Typo="Typo.caption">Keine zentrale Excel-Datei gefunden.</MudText>
|
||||||
|
</NoRecordsContent>
|
||||||
|
</MudTable>
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private List<DashboardRow> _dashboardRows = new();
|
private List<DashboardRow> _dashboardRows = new();
|
||||||
|
private List<ConsolidatedDashboardRow> _consolidatedRows = new();
|
||||||
private bool _loading = true;
|
private bool _loading = true;
|
||||||
private bool _anyRunning;
|
private bool _anyRunning;
|
||||||
private CancellationTokenSource? _pollingCts;
|
private CancellationTokenSource? _pollingCts;
|
||||||
@@ -164,7 +209,9 @@
|
|||||||
};
|
};
|
||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
_anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId));
|
_consolidatedRows = BuildConsolidatedRows(settings: await db.ExportSettings.FirstOrDefaultAsync() ?? new());
|
||||||
|
|
||||||
|
_anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId)) || Orchestrator.IsConsolidatedExporting();
|
||||||
_loading = false;
|
_loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,6 +232,34 @@
|
|||||||
Snackbar.Add("Export für alle Standorte gestartet", Severity.Info);
|
Snackbar.Add("Export für alle Standorte gestartet", Severity.Info);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ExportConsolidatedOnly()
|
||||||
|
{
|
||||||
|
_anyRunning = true;
|
||||||
|
await LoadDataAsync();
|
||||||
|
StartPolling();
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var filePath = await Orchestrator.ExportConsolidatedOnlyAsync();
|
||||||
|
await InvokeAsync(async () =>
|
||||||
|
{
|
||||||
|
await LoadDataAsync();
|
||||||
|
StateHasChanged();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(filePath))
|
||||||
|
{
|
||||||
|
await InvokeAsync(() =>
|
||||||
|
Snackbar.Add($"Zentrale Datei erzeugt: {filePath}", Severity.Success));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await InvokeAsync(() =>
|
||||||
|
Snackbar.Add("Zentrale Datei konnte nicht erzeugt werden.", Severity.Warning));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Snackbar.Add("Zentrale Datei wird erzeugt", Severity.Info);
|
||||||
|
}
|
||||||
|
|
||||||
private void ExportSingle(int siteId)
|
private void ExportSingle(int siteId)
|
||||||
{
|
{
|
||||||
_anyRunning = true;
|
_anyRunning = true;
|
||||||
@@ -217,7 +292,7 @@
|
|||||||
{
|
{
|
||||||
await InvokeAsync(async () =>
|
await InvokeAsync(async () =>
|
||||||
{
|
{
|
||||||
_anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId)) || _dashboardRows.Count == 0;
|
_anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId)) || Orchestrator.IsConsolidatedExporting() || _dashboardRows.Count == 0;
|
||||||
if (_anyRunning)
|
if (_anyRunning)
|
||||||
{
|
{
|
||||||
StartPolling();
|
StartPolling();
|
||||||
@@ -240,7 +315,12 @@
|
|||||||
|
|
||||||
private void OpenExportFile(DashboardRow row)
|
private void OpenExportFile(DashboardRow row)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(row.FilePath) || !File.Exists(row.FilePath))
|
OpenFile(row.FilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenFile(string filePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
|
||||||
{
|
{
|
||||||
Snackbar.Add("Exportdatei nicht gefunden.", Severity.Warning);
|
Snackbar.Add("Exportdatei nicht gefunden.", Severity.Warning);
|
||||||
return;
|
return;
|
||||||
@@ -250,7 +330,7 @@
|
|||||||
{
|
{
|
||||||
Process.Start(new ProcessStartInfo
|
Process.Start(new ProcessStartInfo
|
||||||
{
|
{
|
||||||
FileName = row.FilePath,
|
FileName = filePath,
|
||||||
UseShellExecute = true
|
UseShellExecute = true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -284,7 +364,7 @@
|
|||||||
{
|
{
|
||||||
while (await timer.WaitForNextTickAsync(cancellationToken))
|
while (await timer.WaitForNextTickAsync(cancellationToken))
|
||||||
{
|
{
|
||||||
var anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId));
|
var anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId)) || Orchestrator.IsConsolidatedExporting();
|
||||||
if (!anyRunning)
|
if (!anyRunning)
|
||||||
{
|
{
|
||||||
await InvokeAsync(async () =>
|
await InvokeAsync(async () =>
|
||||||
@@ -321,10 +401,41 @@
|
|||||||
row.LiveDetails = string.Empty;
|
row.LiveDetails = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
_anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId));
|
_anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId)) || Orchestrator.IsConsolidatedExporting();
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<ConsolidatedDashboardRow> BuildConsolidatedRows(ExportSettings settings)
|
||||||
|
{
|
||||||
|
var outputDirectory = ResolveConsolidatedOutputDirectory(settings);
|
||||||
|
if (!Directory.Exists(outputDirectory))
|
||||||
|
return [];
|
||||||
|
|
||||||
|
return Directory.GetFiles(outputDirectory, "Sales_All_*.xlsx")
|
||||||
|
.Select(path => new FileInfo(path))
|
||||||
|
.OrderByDescending(file => file.LastWriteTime)
|
||||||
|
.Take(1)
|
||||||
|
.Select(file => new ConsolidatedDashboardRow
|
||||||
|
{
|
||||||
|
Label = "Konsolidierter Export",
|
||||||
|
FilePath = file.FullName,
|
||||||
|
DisplayPath = file.FullName,
|
||||||
|
LastModified = file.LastWriteTime
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveConsolidatedOutputDirectory(ExportSettings settings)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(settings.LocalConsolidatedExportFolder))
|
||||||
|
return settings.LocalConsolidatedExportFolder.Trim();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(settings.LocalSiteExportFolder))
|
||||||
|
return settings.LocalSiteExportFolder.Trim();
|
||||||
|
|
||||||
|
return Path.Combine(AppContext.BaseDirectory, "output");
|
||||||
|
}
|
||||||
|
|
||||||
private class DashboardRow
|
private class DashboardRow
|
||||||
{
|
{
|
||||||
public int SiteId { get; set; }
|
public int SiteId { get; set; }
|
||||||
@@ -342,4 +453,13 @@
|
|||||||
public string LiveDetails { get; set; } = "";
|
public string LiveDetails { get; set; } = "";
|
||||||
public bool HasOpenableFile => !string.IsNullOrWhiteSpace(FilePath) && File.Exists(FilePath);
|
public bool HasOpenableFile => !string.IsNullOrWhiteSpace(FilePath) && File.Exists(FilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class ConsolidatedDashboardRow
|
||||||
|
{
|
||||||
|
public string Label { get; set; } = "";
|
||||||
|
public string FilePath { get; set; } = "";
|
||||||
|
public string DisplayPath { get; set; } = "";
|
||||||
|
public DateTime? LastModified { get; set; }
|
||||||
|
public bool HasOpenableFile => !string.IsNullOrWhiteSpace(FilePath) && File.Exists(FilePath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@page "/standorte"
|
@page "/standorte"
|
||||||
|
@using Microsoft.AspNetCore.Components.Forms
|
||||||
@using Microsoft.EntityFrameworkCore
|
@using Microsoft.EntityFrameworkCore
|
||||||
@using System.Text.Json
|
@using System.Text.Json
|
||||||
@using System.Reflection
|
@using System.Reflection
|
||||||
@@ -341,6 +342,31 @@
|
|||||||
</RowTemplate>
|
</RowTemplate>
|
||||||
</MudTable>
|
</MudTable>
|
||||||
}
|
}
|
||||||
|
else if (IsManualExcelSite())
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.h6" Class="mb-2">Manueller Excel-Import</MudText>
|
||||||
|
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined" Class="mb-3">
|
||||||
|
Für diesen Standort wird keine SAP- oder HANA-Verbindung verwendet. Es wird die hier hinterlegte Excel-Datei gelesen und in `CentralSalesRecords` übernommen.
|
||||||
|
</MudAlert>
|
||||||
|
<InputFile OnChange="UploadManualImportFileAsync" accept=".xlsx" />
|
||||||
|
@if (_uploadingManualImport)
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.caption" Class="mt-2">Datei wird hochgeladen...</MudText>
|
||||||
|
}
|
||||||
|
@if (!string.IsNullOrWhiteSpace(_editingSite.ManualImportFilePath))
|
||||||
|
{
|
||||||
|
<MudPaper Class="pa-3 mt-3" Elevation="0">
|
||||||
|
<MudText Typo="Typo.body2">Datei: @_editingSite.ManualImportFilePath</MudText>
|
||||||
|
<MudText Typo="Typo.caption">
|
||||||
|
Letzter Upload: @(_editingSite.ManualImportLastUploadedAtUtc?.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss") ?? "-")
|
||||||
|
</MudText>
|
||||||
|
</MudPaper>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.caption" Class="mt-2">Noch keine Datei hinterlegt.</MudText>
|
||||||
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<MudText Typo="Typo.h6" Class="mb-2">HANA-Verbindung</MudText>
|
<MudText Typo="Typo.h6" Class="mb-2">HANA-Verbindung</MudText>
|
||||||
@@ -365,13 +391,13 @@
|
|||||||
}
|
}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<MudButton OnClick="CloseSiteDialog" Disabled="_savingSite">Abbrechen</MudButton>
|
<MudButton OnClick="CloseSiteDialog" Disabled="_savingSite || _uploadingManualImport">Abbrechen</MudButton>
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveSite" Disabled="_savingSite || _refreshingSapEntitySets">Speichern</MudButton>
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="SaveSite" Disabled="_savingSite || _refreshingSapEntitySets || _uploadingManualImport">Speichern</MudButton>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</MudDialog>
|
</MudDialog>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private readonly string[] _sourceSystems = ["SAP", "BI1", "SAGE"];
|
private readonly string[] _sourceSystems = ["SAP", "BI1", "SAGE", "MANUAL_EXCEL"];
|
||||||
private readonly Dictionary<int, ConnectionTestResult> _connectionStatus = new();
|
private readonly Dictionary<int, ConnectionTestResult> _connectionStatus = new();
|
||||||
private List<HanaServer> _servers = new();
|
private List<HanaServer> _servers = new();
|
||||||
private List<Site> _sites = new();
|
private List<Site> _sites = new();
|
||||||
@@ -394,6 +420,7 @@
|
|||||||
private bool _refreshingSapSourceFields;
|
private bool _refreshingSapSourceFields;
|
||||||
private bool _savingServer;
|
private bool _savingServer;
|
||||||
private bool _savingSite;
|
private bool _savingSite;
|
||||||
|
private bool _uploadingManualImport;
|
||||||
private readonly DialogOptions _dialogOptions = new() { MaxWidth = MaxWidth.Small, FullWidth = true };
|
private readonly DialogOptions _dialogOptions = new() { MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
@@ -514,7 +541,8 @@
|
|||||||
{
|
{
|
||||||
IsActive = true,
|
IsActive = true,
|
||||||
SourceSystem = "SAP",
|
SourceSystem = "SAP",
|
||||||
HanaServerId = null
|
HanaServerId = null,
|
||||||
|
ManualImportFilePath = string.Empty
|
||||||
};
|
};
|
||||||
_sapEntitySetsCache = [];
|
_sapEntitySetsCache = [];
|
||||||
_sapAvailableSourceExpressions = [];
|
_sapAvailableSourceExpressions = [];
|
||||||
@@ -539,6 +567,8 @@
|
|||||||
UsernameOverride = site.UsernameOverride,
|
UsernameOverride = site.UsernameOverride,
|
||||||
PasswordOverride = site.PasswordOverride,
|
PasswordOverride = site.PasswordOverride,
|
||||||
LocalExportFolderOverride = site.LocalExportFolderOverride,
|
LocalExportFolderOverride = site.LocalExportFolderOverride,
|
||||||
|
ManualImportFilePath = site.ManualImportFilePath,
|
||||||
|
ManualImportLastUploadedAtUtc = site.ManualImportLastUploadedAtUtc,
|
||||||
SapServiceUrl = site.SapServiceUrl,
|
SapServiceUrl = site.SapServiceUrl,
|
||||||
SapEntitySet = site.SapEntitySet,
|
SapEntitySet = site.SapEntitySet,
|
||||||
SapEntitySetsCache = site.SapEntitySetsCache,
|
SapEntitySetsCache = site.SapEntitySetsCache,
|
||||||
@@ -567,7 +597,7 @@
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var db = await DbFactory.CreateDbContextAsync();
|
using var db = await DbFactory.CreateDbContextAsync();
|
||||||
var serverId = IsSapSite() ? (int?)null : await SaveOrCreateSiteServerAsync(db);
|
var serverId = UsesHanaConnection() ? await SaveOrCreateSiteServerAsync(db) : (int?)null;
|
||||||
_editingSite.HanaServerId = serverId;
|
_editingSite.HanaServerId = serverId;
|
||||||
_editingSite.SapEntitySetsCache = SerializeSapEntitySets(_sapEntitySetsCache);
|
_editingSite.SapEntitySetsCache = SerializeSapEntitySets(_sapEntitySetsCache);
|
||||||
|
|
||||||
@@ -588,6 +618,8 @@
|
|||||||
existing.UsernameOverride = _editingSite.UsernameOverride;
|
existing.UsernameOverride = _editingSite.UsernameOverride;
|
||||||
existing.PasswordOverride = _editingSite.PasswordOverride;
|
existing.PasswordOverride = _editingSite.PasswordOverride;
|
||||||
existing.LocalExportFolderOverride = _editingSite.LocalExportFolderOverride;
|
existing.LocalExportFolderOverride = _editingSite.LocalExportFolderOverride;
|
||||||
|
existing.ManualImportFilePath = _editingSite.ManualImportFilePath;
|
||||||
|
existing.ManualImportLastUploadedAtUtc = _editingSite.ManualImportLastUploadedAtUtc;
|
||||||
existing.SapServiceUrl = _editingSite.SapServiceUrl;
|
existing.SapServiceUrl = _editingSite.SapServiceUrl;
|
||||||
existing.SapEntitySet = _editingSite.SapEntitySet;
|
existing.SapEntitySet = _editingSite.SapEntitySet;
|
||||||
existing.SapEntitySetsCache = _editingSite.SapEntitySetsCache;
|
existing.SapEntitySetsCache = _editingSite.SapEntitySetsCache;
|
||||||
@@ -654,6 +686,8 @@
|
|||||||
var sourceSystem = string.IsNullOrWhiteSpace(site.SourceSystem) ? "SAP" : site.SourceSystem;
|
var sourceSystem = string.IsNullOrWhiteSpace(site.SourceSystem) ? "SAP" : site.SourceSystem;
|
||||||
if (string.Equals(sourceSystem, "SAP", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(sourceSystem, "SAP", StringComparison.OrdinalIgnoreCase))
|
||||||
return string.IsNullOrWhiteSpace(site.SapServiceUrl) ? "-" : site.SapServiceUrl;
|
return string.IsNullOrWhiteSpace(site.SapServiceUrl) ? "-" : site.SapServiceUrl;
|
||||||
|
if (string.Equals(sourceSystem, "MANUAL_EXCEL", StringComparison.OrdinalIgnoreCase))
|
||||||
|
return string.IsNullOrWhiteSpace(site.ManualImportFilePath) ? "-" : Path.GetFileName(site.ManualImportFilePath);
|
||||||
|
|
||||||
return GetServerNode(site.HanaServer);
|
return GetServerNode(site.HanaServer);
|
||||||
}
|
}
|
||||||
@@ -696,6 +730,7 @@
|
|||||||
_editingSite.UsernameOverride = _editingSite.UsernameOverride.Trim();
|
_editingSite.UsernameOverride = _editingSite.UsernameOverride.Trim();
|
||||||
_editingSite.PasswordOverride = _editingSite.PasswordOverride.Trim();
|
_editingSite.PasswordOverride = _editingSite.PasswordOverride.Trim();
|
||||||
_editingSite.LocalExportFolderOverride = _editingSite.LocalExportFolderOverride.Trim();
|
_editingSite.LocalExportFolderOverride = _editingSite.LocalExportFolderOverride.Trim();
|
||||||
|
_editingSite.ManualImportFilePath = _editingSite.ManualImportFilePath.Trim();
|
||||||
_editingSite.SapServiceUrl = _editingSite.SapServiceUrl.Trim();
|
_editingSite.SapServiceUrl = _editingSite.SapServiceUrl.Trim();
|
||||||
_editingSite.SapEntitySet = _editingSite.SapEntitySet.Trim();
|
_editingSite.SapEntitySet = _editingSite.SapEntitySet.Trim();
|
||||||
_editingSiteServer.Host = _editingSiteServer.Host.Trim();
|
_editingSiteServer.Host = _editingSiteServer.Host.Trim();
|
||||||
@@ -745,6 +780,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
private bool IsSapSite() => string.Equals(_editingSite.SourceSystem, "SAP", StringComparison.OrdinalIgnoreCase);
|
private bool IsSapSite() => string.Equals(_editingSite.SourceSystem, "SAP", StringComparison.OrdinalIgnoreCase);
|
||||||
|
private bool IsManualExcelSite() => string.Equals(_editingSite.SourceSystem, "MANUAL_EXCEL", StringComparison.OrdinalIgnoreCase);
|
||||||
|
private bool UsesHanaConnection() => !IsSapSite() && !IsManualExcelSite();
|
||||||
|
|
||||||
private async Task RefreshSapEntitySets()
|
private async Task RefreshSapEntitySets()
|
||||||
{
|
{
|
||||||
@@ -804,12 +841,62 @@
|
|||||||
|
|
||||||
private void CloseSiteDialog()
|
private void CloseSiteDialog()
|
||||||
{
|
{
|
||||||
if (_savingSite || _refreshingSapEntitySets)
|
if (_savingSite || _refreshingSapEntitySets || _uploadingManualImport)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_siteDialogVisible = false;
|
_siteDialogVisible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task UploadManualImportFileAsync(InputFileChangeEventArgs args)
|
||||||
|
{
|
||||||
|
if (_uploadingManualImport)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var file = args.File;
|
||||||
|
if (file is null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_uploadingManualImport = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var extension = Path.GetExtension(file.Name);
|
||||||
|
if (!string.Equals(extension, ".xlsx", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Bitte eine Excel-Datei mit Endung .xlsx auswählen.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var uploadDirectory = Path.Combine(AppContext.BaseDirectory, "manual-imports");
|
||||||
|
Directory.CreateDirectory(uploadDirectory);
|
||||||
|
|
||||||
|
var safeBaseName = string.Concat(Path.GetFileNameWithoutExtension(file.Name).Select(ch =>
|
||||||
|
char.IsLetterOrDigit(ch) || ch == '-' || ch == '_' ? ch : '_'));
|
||||||
|
if (string.IsNullOrWhiteSpace(safeBaseName))
|
||||||
|
safeBaseName = "manual_import";
|
||||||
|
|
||||||
|
var targetPath = Path.Combine(uploadDirectory, $"{safeBaseName}_{Guid.NewGuid():N}{extension}");
|
||||||
|
|
||||||
|
await using (var sourceStream = file.OpenReadStream(maxAllowedSize: 50 * 1024 * 1024))
|
||||||
|
await using (var targetStream = File.Create(targetPath))
|
||||||
|
{
|
||||||
|
await sourceStream.CopyToAsync(targetStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
_editingSite.ManualImportFilePath = targetPath;
|
||||||
|
_editingSite.ManualImportLastUploadedAtUtc = DateTime.UtcNow;
|
||||||
|
Snackbar.Add("Excel-Datei hochgeladen.", Severity.Success);
|
||||||
|
await AppEventLogService.WriteAsync("ManualImport", "Excel-Datei hochgeladen", siteId: _editingSite.Id, land: _editingSite.Land, details: targetPath);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"Upload fehlgeschlagen: {ex.Message}", Severity.Error);
|
||||||
|
await AppEventLogService.WriteAsync("ManualImport", "Excel-Upload fehlgeschlagen", "Error", siteId: _editingSite.Id, land: _editingSite.Land, details: ex.ToString());
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_uploadingManualImport = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static List<string> ParseSapEntitySets(string json)
|
private static List<string> ParseSapEntitySets(string json)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(json))
|
if (string.IsNullOrWhiteSpace(json))
|
||||||
|
|||||||
@@ -7,34 +7,34 @@ Stand: 2026-04-15
|
|||||||
Die App wurde von einem reinen BI1/HANA-Exporter zu einer kombinierten Plattform erweitert:
|
Die App wurde von einem reinen BI1/HANA-Exporter zu einer kombinierten Plattform erweitert:
|
||||||
|
|
||||||
- `BI1` und `SAGE` bleiben auf direktem HANA-Zugriff
|
- `BI1` und `SAGE` bleiben auf direktem HANA-Zugriff
|
||||||
- `SAP` läuft separat über SAP Gateway / OData
|
- `SAP` laeuft separat ueber SAP Gateway / OData
|
||||||
- SAP-Quellen können gelesen, gejoint und auf das zentrale `SalesRecord`-Schema gemappt werden
|
- SAP-Quellen koennen gelesen, gejoint und auf das zentrale `SalesRecord`-Schema gemappt werden
|
||||||
- Standort-Exporte werden lokal als Excel geschrieben
|
- Standort-Exporte werden lokal als Excel geschrieben
|
||||||
- Zusätzlich werden Datensätze in eine zentrale SQLite-Tabelle geschrieben
|
- zusaetzlich werden Datensaetze in eine zentrale SQLite-Tabelle geschrieben
|
||||||
- Ein konsolidierter Export liest aus dieser zentralen Tabelle
|
- ein konsolidierter Export liest aus dieser zentralen Tabelle
|
||||||
|
|
||||||
## Wichtigste umgesetzte Funktionen
|
## Wichtigste umgesetzte Funktionen
|
||||||
|
|
||||||
### 1. Zentrale Credentials pro Quellsystem
|
### 1. Zentrale Credentials pro Quellsystem
|
||||||
|
|
||||||
Es gibt zentrale Zugangsdaten in `ExportSettings` für:
|
Es gibt zentrale Zugangsdaten in `ExportSettings` fuer:
|
||||||
|
|
||||||
- `SAP`
|
- `SAP`
|
||||||
- `BI1`
|
- `BI1`
|
||||||
- `SAGE`
|
- `SAGE`
|
||||||
|
|
||||||
Zusätzlich gibt es pro Standort optionale Overrides:
|
Zusaetzlich gibt es pro Standort optionale Overrides:
|
||||||
|
|
||||||
- `UsernameOverride`
|
- `UsernameOverride`
|
||||||
- `PasswordOverride`
|
- `PasswordOverride`
|
||||||
|
|
||||||
Auflösungsreihenfolge:
|
Aufloesungsreihenfolge:
|
||||||
|
|
||||||
1. Standort-Override
|
1. Standort-Override
|
||||||
2. zentrale Credentials des Quellsystems
|
2. zentrale Credentials des Quellsystems
|
||||||
3. bei HANA zusätzlich Fallback auf alten `HanaServer.Username/Password`
|
3. bei HANA zusaetzlich Fallback auf alten `HanaServer.Username/Password`
|
||||||
|
|
||||||
## 2. SAP von BI1/HANA getrennt
|
### 2. SAP von BI1/HANA getrennt
|
||||||
|
|
||||||
`SAP` nutzt nicht mehr den HANA-Pfad, sondern eine eigene Gateway/OData-Strecke.
|
`SAP` nutzt nicht mehr den HANA-Pfad, sondern eine eigene Gateway/OData-Strecke.
|
||||||
|
|
||||||
@@ -56,21 +56,21 @@ http://travt762.sap.trafag.com:8000/sap/opu/odata/sap/ZPOWERBI_EINKAUF_SRV/
|
|||||||
Wichtig:
|
Wichtig:
|
||||||
|
|
||||||
- Service URL immer nur bis zum Service
|
- Service URL immer nur bis zum Service
|
||||||
- Entity Set separat auswählen
|
- Entity Set separat auswaehlen
|
||||||
|
|
||||||
## 3. SAP-Quellen, Joins und Feldmappings
|
### 3. SAP-Quellen, Joins und Feldmappings
|
||||||
|
|
||||||
Für SAP gibt es mehrere neue Modelle:
|
Fuer SAP gibt es mehrere neue Modelle:
|
||||||
|
|
||||||
- `SapSourceDefinition`
|
- `SapSourceDefinition`
|
||||||
- `SapJoinDefinition`
|
- `SapJoinDefinition`
|
||||||
- `SapFieldMapping`
|
- `SapFieldMapping`
|
||||||
|
|
||||||
Unterstützt wird:
|
Unterstuetzt wird:
|
||||||
|
|
||||||
- mehrere SAP-Quellen pro Standort
|
- mehrere SAP-Quellen pro Standort
|
||||||
- Alias pro Quelle
|
- Alias pro Quelle
|
||||||
- Primärquelle
|
- Primaerquelle
|
||||||
- Join-Definitionen
|
- Join-Definitionen
|
||||||
- Mapping von `Alias.Feldname` auf zentrales Schema
|
- Mapping von `Alias.Feldname` auf zentrales Schema
|
||||||
|
|
||||||
@@ -79,9 +79,9 @@ UI-Erweiterungen:
|
|||||||
- `Quellen refreshen`
|
- `Quellen refreshen`
|
||||||
- `Felder aus Quellen laden`
|
- `Felder aus Quellen laden`
|
||||||
- Join-Key-Auswahl aus Metadaten
|
- Join-Key-Auswahl aus Metadaten
|
||||||
- `Auto-Match` für gleiche Feldnamen zwischen Primärquelle und anderen Quellen
|
- `Auto-Match` fuer gleiche Feldnamen zwischen Primaerquelle und anderen Quellen
|
||||||
|
|
||||||
## 4. Zentrale Datenspeicherung
|
### 4. Zentrale Datenspeicherung
|
||||||
|
|
||||||
Neue Tabelle:
|
Neue Tabelle:
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ Neue Tabelle:
|
|||||||
|
|
||||||
Verwendung:
|
Verwendung:
|
||||||
|
|
||||||
- pro Standort werden alte zentrale Sätze dieses Standorts ersetzt
|
- pro Standort werden alte zentrale Saetze dieses Standorts ersetzt
|
||||||
- konsolidierte Excel liest aus `CentralSalesRecords`
|
- konsolidierte Excel liest aus `CentralSalesRecords`
|
||||||
|
|
||||||
Wichtig:
|
Wichtig:
|
||||||
@@ -97,9 +97,9 @@ Wichtig:
|
|||||||
- zentrale Excel wird nicht appendet
|
- zentrale Excel wird nicht appendet
|
||||||
- sie wird aus dem aktuellen Zustand der zentralen Tabelle neu erstellt
|
- sie wird aus dem aktuellen Zustand der zentralen Tabelle neu erstellt
|
||||||
|
|
||||||
## 5. Exportpfade
|
### 5. Exportpfade
|
||||||
|
|
||||||
Neue Konfigurationsmöglichkeiten:
|
Neue Konfigurationsmoeglichkeiten:
|
||||||
|
|
||||||
Zentral in `Settings`:
|
Zentral in `Settings`:
|
||||||
|
|
||||||
@@ -118,16 +118,16 @@ Fallback wenn leer:
|
|||||||
|
|
||||||
relativ zum App-Verzeichnis.
|
relativ zum App-Verzeichnis.
|
||||||
|
|
||||||
## 6. SharePoint
|
### 6. SharePoint
|
||||||
|
|
||||||
SharePoint-Upload ist optional.
|
SharePoint-Upload ist optional.
|
||||||
|
|
||||||
Wenn keine vollständige SharePoint-Konfiguration vorhanden ist:
|
Wenn keine vollstaendige SharePoint-Konfiguration vorhanden ist:
|
||||||
|
|
||||||
- Excel wird trotzdem lokal erzeugt
|
- Excel wird trotzdem lokal erzeugt
|
||||||
- kein Upload nach SharePoint
|
- kein Upload nach SharePoint
|
||||||
|
|
||||||
Benötigte SharePoint-Werte:
|
Benoetigte SharePoint-Werte:
|
||||||
|
|
||||||
- `Tenant ID`
|
- `Tenant ID`
|
||||||
- `Client ID`
|
- `Client ID`
|
||||||
@@ -135,7 +135,7 @@ Benötigte SharePoint-Werte:
|
|||||||
|
|
||||||
Das sind Entra App Registration Werte, nicht normale Benutzer-Credentials.
|
Das sind Entra App Registration Werte, nicht normale Benutzer-Credentials.
|
||||||
|
|
||||||
## 7. Config Import/Export
|
### 7. Config Import/Export
|
||||||
|
|
||||||
Es gibt JSON-Import/Export der Konfiguration mit Checkbox:
|
Es gibt JSON-Import/Export der Konfiguration mit Checkbox:
|
||||||
|
|
||||||
@@ -153,9 +153,9 @@ Enthalten sind u. a.:
|
|||||||
- SAP-Joins
|
- SAP-Joins
|
||||||
- SAP-Mappings
|
- SAP-Mappings
|
||||||
|
|
||||||
## 8. Logging und Live-Status
|
### 8. Logging und Live-Status
|
||||||
|
|
||||||
Neue technische Logs über `AppEventLogs`.
|
Neue technische Logs ueber `AppEventLogs`.
|
||||||
|
|
||||||
Sichtbar:
|
Sichtbar:
|
||||||
|
|
||||||
@@ -172,11 +172,11 @@ Geloggt werden u. a.:
|
|||||||
- zentrale Tabellenspeicherung
|
- zentrale Tabellenspeicherung
|
||||||
- Export erfolgreich / fehlgeschlagen
|
- Export erfolgreich / fehlgeschlagen
|
||||||
|
|
||||||
## 9. Excel öffnen
|
### 9. Excel oeffnen
|
||||||
|
|
||||||
Im Dashboard gibt es neben `Export` den Button:
|
Im Dashboard gibt es neben `Export` den Button:
|
||||||
|
|
||||||
- `Excel öffnen`
|
- `Excel oeffnen`
|
||||||
|
|
||||||
Dieser nutzt `ExportLogs.FilePath`.
|
Dieser nutzt `ExportLogs.FilePath`.
|
||||||
|
|
||||||
@@ -186,9 +186,9 @@ Voraussetzungen:
|
|||||||
- `FilePath` gespeichert
|
- `FilePath` gespeichert
|
||||||
- Datei existiert lokal
|
- Datei existiert lokal
|
||||||
|
|
||||||
## 10. Management Cockpit
|
### 10. Management Cockpit
|
||||||
|
|
||||||
Es gibt einen neuen Menüpunkt:
|
Es gibt einen neuen Menuepunkt:
|
||||||
|
|
||||||
- `Management Cockpit`
|
- `Management Cockpit`
|
||||||
|
|
||||||
@@ -196,19 +196,19 @@ Funktion:
|
|||||||
|
|
||||||
- Auswahl vorhandener Excel-Dateien
|
- Auswahl vorhandener Excel-Dateien
|
||||||
- Analyse einer exportierten Standort-Datei
|
- Analyse einer exportierten Standort-Datei
|
||||||
- Kennzahlen für Geschäftsinhaber / Management
|
- Kennzahlen fuer Geschaeftsinhaber / Management
|
||||||
|
|
||||||
Aktuell enthalten:
|
Aktuell enthalten:
|
||||||
|
|
||||||
- Umsatz
|
- Umsatz
|
||||||
- geschätzte Kosten
|
- geschaetzte Kosten
|
||||||
- geschätzte Marge
|
- geschaetzte Marge
|
||||||
- Rechnungsanzahl
|
- Rechnungsanzahl
|
||||||
- Kundenanzahl
|
- Kundenanzahl
|
||||||
- Top Kunden
|
- Top Kunden
|
||||||
- Top Produktgruppen
|
- Top Produktgruppen
|
||||||
- Top Sales Owner
|
- Top Sales Owner
|
||||||
- Datenqualitätshinweise
|
- Datenqualitaetshinweise
|
||||||
- automatische Management-Aussagen
|
- automatische Management-Aussagen
|
||||||
|
|
||||||
## Wichtige Dateien
|
## Wichtige Dateien
|
||||||
@@ -249,7 +249,7 @@ Aktuell enthalten:
|
|||||||
|
|
||||||
## Datenbank / Migrationen
|
## Datenbank / Migrationen
|
||||||
|
|
||||||
Viele Änderungen laufen über `DatabaseInitializationService`.
|
Viele Aenderungen laufen ueber `DatabaseInitializationService`.
|
||||||
|
|
||||||
Wichtige neue oder erweiterte Tabellen/Felder:
|
Wichtige neue oder erweiterte Tabellen/Felder:
|
||||||
|
|
||||||
@@ -273,52 +273,46 @@ Wichtige neue oder erweiterte Tabellen/Felder:
|
|||||||
- `CentralSalesRecords`
|
- `CentralSalesRecords`
|
||||||
- SAP-Konfigtabellen
|
- SAP-Konfigtabellen
|
||||||
|
|
||||||
## Aktuell offenes Hauptproblem
|
## Letztes Hauptproblem und Loesung
|
||||||
|
|
||||||
### Zentrale Speicherung hängt noch
|
### Export hing nach zentraler Speicherung
|
||||||
|
|
||||||
Die große Problemstelle war die zentrale SQLite-Speicherung.
|
Der Export blieb zuletzt nach
|
||||||
|
|
||||||
Bereits probiert:
|
- `Zentrale Tabelle: 20106 Datensaetze gespeichert.`
|
||||||
|
|
||||||
- EF `RemoveRange + SaveChanges`
|
haengen.
|
||||||
- EF Batch-Speichern
|
|
||||||
- Dashboard-Polling reduziert
|
|
||||||
- SQLite WAL + busy timeout
|
|
||||||
- direkte SQLite-Inserts in einer großen Transaktion
|
|
||||||
- jetzt: kleine abgeschlossene Transaktionen pro Batch
|
|
||||||
|
|
||||||
Aktueller Stand:
|
Die eigentliche Ursache war am Ende nicht mehr der Batch-Insert selbst, sondern ein kaputter SQLite-Schemazustand:
|
||||||
|
|
||||||
- zentrale Excel ist jetzt sehr schnell
|
- mindestens eine Tabelle referenzierte per FK noch `main.Sites_old`
|
||||||
- das Hängen wurde stark eingegrenzt
|
- dadurch scheiterte `SaveChangesAsync()` spaeter beim Schreiben in `AppEventLogs` oder `ExportLogs`
|
||||||
- zuletzt wurde der Schreibpfad so umgebaut, dass:
|
- die alte Tabelle `Sites_old` existierte nicht mehr
|
||||||
- Löschen in eigener kurzer Transaktion läuft
|
|
||||||
- Inserts batchweise mit Commit pro Batch laufen
|
|
||||||
|
|
||||||
Datei:
|
Beobachteter Fehler:
|
||||||
|
|
||||||
|
- `SQLite Error 1: 'no such table: main.Sites_old'`
|
||||||
|
|
||||||
|
## Umgesetzte Korrekturen
|
||||||
|
|
||||||
|
- `Components/Pages/Dashboard.razor`
|
||||||
|
- Live-Status pollt waehrend laufendem Export nicht mehr permanent `AppEventLogs`
|
||||||
|
- stattdessen Anzeige ueber den In-Memory-Status aus `ExportOrchestrationService`
|
||||||
|
- `Program.cs`
|
||||||
|
- SQLite `Default Timeout` von `10` auf `60` erhoeht
|
||||||
- `Services/CentralSalesRecordService.cs`
|
- `Services/CentralSalesRecordService.cs`
|
||||||
|
- nach abgeschlossenem Batch-Insert wird explizit `Zentrale Tabelle aktualisiert` gesetzt
|
||||||
|
- `Services/DatabaseInitializationService.cs`
|
||||||
|
- automatische Reparaturlogik fuer Tabellen, deren `CREATE TABLE`-SQL noch `Sites_old` referenziert
|
||||||
|
- betroffene Tabellen werden beim Start neu aufgebaut und Daten rueberkopiert
|
||||||
|
|
||||||
Die nächste Session sollte genau dort weiter debuggen, falls es noch hängt.
|
Danach wurde der Export erfolgreich getestet und geht jetzt wieder durch.
|
||||||
|
|
||||||
Wichtig:
|
## Was bei einer naechsten Stoerung zuerst zu pruefen ist
|
||||||
|
|
||||||
- Das Problem ist nicht SAP
|
1. Tritt beim App-Start die Schema-Reparatur sauber durch?
|
||||||
- nicht SharePoint
|
2. Gibt es noch weitere Tabellen mit FK-Referenz auf `Sites_old`?
|
||||||
- nicht mehr der große EF-Insert
|
3. Erst danach wieder Insert-/Commit-Batches der zentralen Speicherung untersuchen
|
||||||
- sondern sehr wahrscheinlich SQLite-Commit/Lock-Verhalten rund um die zentrale Tabelle
|
|
||||||
|
|
||||||
## Letzte bekannte Beobachtung
|
|
||||||
|
|
||||||
Der User meldete zuletzt:
|
|
||||||
|
|
||||||
- vorher Hänger bei `Zentrale Tabelle: Abschluss speichern...`
|
|
||||||
- danach wurde auf Commit pro Batch umgestellt
|
|
||||||
- neue Session soll testen, ob es jetzt bei
|
|
||||||
- `Batch x/y speichern...`
|
|
||||||
- `Batch x/y abschliessen...`
|
|
||||||
- oder gar nicht mehr hängt
|
|
||||||
|
|
||||||
## Build-Status
|
## Build-Status
|
||||||
|
|
||||||
@@ -334,14 +328,3 @@ Ergebnis:
|
|||||||
- bekannte Warnungen bleiben:
|
- bekannte Warnungen bleiben:
|
||||||
- SAP HANA Architekturwarnung `MSB3270`
|
- SAP HANA Architekturwarnung `MSB3270`
|
||||||
- MudBlazor Analyzer `Dense`
|
- MudBlazor Analyzer `Dense`
|
||||||
|
|
||||||
## Hinweise für nächste Session
|
|
||||||
|
|
||||||
1. Zuerst aktuellen Export testen
|
|
||||||
2. Genaue letzte Live-Status-Meldung notieren
|
|
||||||
3. `Services/CentralSalesRecordService.cs` prüfen
|
|
||||||
4. Falls nötig:
|
|
||||||
- SQLite pragmas weiter anpassen
|
|
||||||
- zentrale Tabelle temporär ganz abschaltbar machen
|
|
||||||
- oder Schreiben über separate DB / Queue entkoppeln
|
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,8 @@ public class ConfigTransferSite
|
|||||||
public string? UsernameOverride { get; set; }
|
public string? UsernameOverride { get; set; }
|
||||||
public string? PasswordOverride { get; set; }
|
public string? PasswordOverride { get; set; }
|
||||||
public string LocalExportFolderOverride { get; set; } = string.Empty;
|
public string LocalExportFolderOverride { get; set; } = string.Empty;
|
||||||
|
public string ManualImportFilePath { get; set; } = string.Empty;
|
||||||
|
public DateTime? ManualImportLastUploadedAtUtc { get; set; }
|
||||||
public string SapServiceUrl { get; set; } = string.Empty;
|
public string SapServiceUrl { get; set; } = string.Empty;
|
||||||
public string SapEntitySet { get; set; } = string.Empty;
|
public string SapEntitySet { get; set; } = string.Empty;
|
||||||
public string SapEntitySetsCache { get; set; } = string.Empty;
|
public string SapEntitySetsCache { get; set; } = string.Empty;
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ public class Site
|
|||||||
|
|
||||||
public string PasswordOverride { get; set; } = string.Empty;
|
public string PasswordOverride { get; set; } = string.Empty;
|
||||||
public string LocalExportFolderOverride { get; set; } = string.Empty;
|
public string LocalExportFolderOverride { get; set; } = string.Empty;
|
||||||
|
public string ManualImportFilePath { get; set; } = string.Empty;
|
||||||
|
public DateTime? ManualImportLastUploadedAtUtc { get; set; }
|
||||||
|
|
||||||
public string SapServiceUrl { get; set; } = string.Empty;
|
public string SapServiceUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
|||||||
@@ -2,63 +2,51 @@
|
|||||||
|
|
||||||
Stand: 2026-04-15
|
Stand: 2026-04-15
|
||||||
|
|
||||||
## 1. Erstes Ziel
|
## 1. Status
|
||||||
|
|
||||||
Prüfen, ob die aktuelle Version beim Standort-Export noch in der zentralen SQLite-Speicherung hängen bleibt.
|
Der Export geht jetzt wieder durch.
|
||||||
|
|
||||||
Wichtig:
|
Die zuletzt gefundene Hauptursache war nicht mehr ein reiner SQLite-Lock beim Batch-Insert, sondern ein kaputter FK-Schemazustand in der bestehenden DB:
|
||||||
|
|
||||||
- App neu starten
|
- SQLite referenzierte in mindestens einer Tabelle noch `main.Sites_old`
|
||||||
- denselben Standort erneut exportieren
|
- dadurch scheiterte `SaveChangesAsync()` beim Schreiben z. B. in `AppEventLogs` oder `ExportLogs`
|
||||||
- letzte sichtbare `Live-Status`-Meldung exakt notieren
|
- sichtbarer Effekt: Export blieb nach `Zentrale Tabelle: ... Datensaetze gespeichert.` haengen
|
||||||
|
|
||||||
Interessant sind vor allem diese Fälle:
|
## 2. Umgesetzter Fix
|
||||||
|
|
||||||
- `Zentrale Tabelle: Batch x/y speichern...`
|
Umgesetzt wurde:
|
||||||
- `Zentrale Tabelle: Batch x/y abschliessen...`
|
|
||||||
- `Zentrale Tabelle aktualisiert`
|
|
||||||
- `Export erfolgreich`
|
|
||||||
|
|
||||||
## 2. Hauptverdächtiger
|
- Dashboard-Live-Status liest waehrend laufendem Export nicht mehr staendig aus `AppEventLogs`, sondern nutzt den In-Memory-Status des `ExportOrchestrationService`
|
||||||
|
- SQLite `Default Timeout` in `Program.cs` auf `60` erhoeht
|
||||||
|
- `CentralSalesRecordService` setzt nach den Batches explizit `Zentrale Tabelle aktualisiert`
|
||||||
|
- `DatabaseInitializationService` repariert beim App-Start automatisch Tabellen, deren FK-SQL noch `Sites_old` referenziert
|
||||||
|
|
||||||
Datei:
|
Betroffene Dateien:
|
||||||
|
|
||||||
- `Services/CentralSalesRecordService.cs`
|
|
||||||
|
|
||||||
Aktueller Stand:
|
|
||||||
|
|
||||||
- alte Sätze werden in eigener Transaktion gelöscht
|
|
||||||
- Inserts laufen in Batches von 25
|
|
||||||
- jeder Batch wird separat committed
|
|
||||||
|
|
||||||
Wenn es noch hängt, dort zuerst ansetzen.
|
|
||||||
|
|
||||||
## 3. Falls es weiter hängt
|
|
||||||
|
|
||||||
In dieser Reihenfolge prüfen:
|
|
||||||
|
|
||||||
1. Batchgröße weiter reduzieren
|
|
||||||
- z. B. `10` statt `25`
|
|
||||||
2. Direkt vor und direkt nach `transaction.CommitAsync()` zusätzlich technische Logs setzen
|
|
||||||
3. Prüfen, ob parallel noch andere SQLite-Zugriffe laufen
|
|
||||||
4. Optional zentrale Speicherung vorübergehend per Setting deaktivierbar machen
|
|
||||||
5. Falls nötig zentrale Speicherung in separate DB-Datei auslagern
|
|
||||||
|
|
||||||
## 4. Dashboard / UI prüfen
|
|
||||||
|
|
||||||
Zu testen:
|
|
||||||
|
|
||||||
- `Excel öffnen` wird nach neuem erfolgreichen Export aktiv
|
|
||||||
- `Export erfolgreich` zeigt `Pfad=...`
|
|
||||||
- Dashboard-Live-Status setzt sich nach Abschluss sauber zurück
|
|
||||||
|
|
||||||
Dateien:
|
|
||||||
|
|
||||||
|
- `Program.cs`
|
||||||
- `Components/Pages/Dashboard.razor`
|
- `Components/Pages/Dashboard.razor`
|
||||||
- `Services/SiteExportService.cs`
|
- `Services/CentralSalesRecordService.cs`
|
||||||
- `Models/ExportLog.cs`
|
- `Services/DatabaseInitializationService.cs`
|
||||||
|
|
||||||
## 5. SAP-Funktionalität kurz gegenprüfen
|
## 3. Was noch getestet werden sollte
|
||||||
|
|
||||||
|
Kurz gegenpruefen:
|
||||||
|
|
||||||
|
- Export eines Standorts erneut
|
||||||
|
- `Excel oeffnen` nach erfolgreichem Export
|
||||||
|
- `Export erfolgreich` inkl. `Pfad=...`
|
||||||
|
- Dashboard-Live-Status setzt sich nach Abschluss sauber zurueck
|
||||||
|
|
||||||
|
## 4. Falls wieder ein Fehler auftritt
|
||||||
|
|
||||||
|
In dieser Reihenfolge pruefen:
|
||||||
|
|
||||||
|
1. Exakte Fehlermeldung aus `AppEventLogs` bzw. Console notieren
|
||||||
|
2. Pruefen, ob die Reparaturlogik beim Start gelaufen ist
|
||||||
|
3. Pruefen, ob noch weitere Tabellen mit veralteter FK-Referenz existieren
|
||||||
|
4. Erst danach wieder am Batch-/Commit-Pfad der zentralen Speicherung arbeiten
|
||||||
|
|
||||||
|
## 5. SAP-Funktionalitaet kurz gegenpruefen
|
||||||
|
|
||||||
Zu testen:
|
Zu testen:
|
||||||
|
|
||||||
@@ -73,12 +61,12 @@ Dateien:
|
|||||||
- `Services/SapGatewayService.cs`
|
- `Services/SapGatewayService.cs`
|
||||||
- `Services/SapCompositionService.cs`
|
- `Services/SapCompositionService.cs`
|
||||||
|
|
||||||
## 6. Management Cockpit prüfen
|
## 6. Management Cockpit pruefen
|
||||||
|
|
||||||
Zu testen:
|
Zu testen:
|
||||||
|
|
||||||
- vorhandene Excel-Datei auswählbar
|
- vorhandene Excel-Datei auswaehlbar
|
||||||
- Analyse läuft
|
- Analyse laeuft
|
||||||
- Kennzahlen plausibel
|
- Kennzahlen plausibel
|
||||||
|
|
||||||
Dateien:
|
Dateien:
|
||||||
@@ -86,17 +74,8 @@ Dateien:
|
|||||||
- `Components/Pages/ManagementCockpit.razor`
|
- `Components/Pages/ManagementCockpit.razor`
|
||||||
- `Services/ManagementCockpitService.cs`
|
- `Services/ManagementCockpitService.cs`
|
||||||
|
|
||||||
## 7. Wenn Stabilität vor Funktion geht
|
## 7. Referenzdatei
|
||||||
|
|
||||||
Sinnvolle pragmatische Zwischenlösung:
|
Fuer den vollstaendigen Kontext zuerst lesen:
|
||||||
|
|
||||||
- zentrale SQLite-Speicherung per Setting abschaltbar machen
|
|
||||||
- Export lokal und zentral Excel weiter erlauben
|
|
||||||
- zentrale DB erst wieder aktivieren, wenn der Commit-Pfad stabil ist
|
|
||||||
|
|
||||||
## 8. Referenzdatei
|
|
||||||
|
|
||||||
Für den vollständigen Kontext zuerst lesen:
|
|
||||||
|
|
||||||
- `HANDOFF_2026-04-15.md`
|
- `HANDOFF_2026-04-15.md`
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ builder.Services.AddSingleton<ITransformationStrategy, ConstantTransformationStr
|
|||||||
builder.Services.AddSingleton<IRecordTransformationService, RecordTransformationService>();
|
builder.Services.AddSingleton<IRecordTransformationService, RecordTransformationService>();
|
||||||
builder.Services.AddSingleton<IAppEventLogService, AppEventLogService>();
|
builder.Services.AddSingleton<IAppEventLogService, AppEventLogService>();
|
||||||
builder.Services.AddSingleton<IManagementCockpitService, ManagementCockpitService>();
|
builder.Services.AddSingleton<IManagementCockpitService, ManagementCockpitService>();
|
||||||
|
builder.Services.AddSingleton<IManualExcelImportService, ManualExcelImportService>();
|
||||||
builder.Services.AddSingleton<ISiteExportService, SiteExportService>();
|
builder.Services.AddSingleton<ISiteExportService, SiteExportService>();
|
||||||
builder.Services.AddSingleton<IConsolidatedExportService, ConsolidatedExportService>();
|
builder.Services.AddSingleton<IConsolidatedExportService, ConsolidatedExportService>();
|
||||||
builder.Services.AddSingleton<IExportLogService, ExportLogService>();
|
builder.Services.AddSingleton<IExportLogService, ExportLogService>();
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ public class ConfigTransferService : IConfigTransferService
|
|||||||
UsernameOverride = includeSecrets ? site.UsernameOverride : null,
|
UsernameOverride = includeSecrets ? site.UsernameOverride : null,
|
||||||
PasswordOverride = includeSecrets ? site.PasswordOverride : null,
|
PasswordOverride = includeSecrets ? site.PasswordOverride : null,
|
||||||
LocalExportFolderOverride = site.LocalExportFolderOverride,
|
LocalExportFolderOverride = site.LocalExportFolderOverride,
|
||||||
|
ManualImportFilePath = site.ManualImportFilePath,
|
||||||
|
ManualImportLastUploadedAtUtc = site.ManualImportLastUploadedAtUtc,
|
||||||
SapServiceUrl = site.SapServiceUrl,
|
SapServiceUrl = site.SapServiceUrl,
|
||||||
SapEntitySet = site.SapEntitySet,
|
SapEntitySet = site.SapEntitySet,
|
||||||
SapEntitySetsCache = site.SapEntitySetsCache,
|
SapEntitySetsCache = site.SapEntitySetsCache,
|
||||||
@@ -242,6 +244,8 @@ public class ConfigTransferService : IConfigTransferService
|
|||||||
UsernameOverride = package.IncludesSecrets ? site.UsernameOverride ?? string.Empty : preserved.UsernameOverride ?? string.Empty,
|
UsernameOverride = package.IncludesSecrets ? site.UsernameOverride ?? string.Empty : preserved.UsernameOverride ?? string.Empty,
|
||||||
PasswordOverride = package.IncludesSecrets ? site.PasswordOverride ?? string.Empty : preserved.PasswordOverride ?? string.Empty,
|
PasswordOverride = package.IncludesSecrets ? site.PasswordOverride ?? string.Empty : preserved.PasswordOverride ?? string.Empty,
|
||||||
LocalExportFolderOverride = site.LocalExportFolderOverride,
|
LocalExportFolderOverride = site.LocalExportFolderOverride,
|
||||||
|
ManualImportFilePath = site.ManualImportFilePath,
|
||||||
|
ManualImportLastUploadedAtUtc = site.ManualImportLastUploadedAtUtc,
|
||||||
SapServiceUrl = site.SapServiceUrl,
|
SapServiceUrl = site.SapServiceUrl,
|
||||||
SapEntitySet = site.SapEntitySet,
|
SapEntitySet = site.SapEntitySet,
|
||||||
SapEntitySetsCache = site.SapEntitySetsCache,
|
SapEntitySetsCache = site.SapEntitySetsCache,
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ public class DatabaseInitializationService : IDatabaseInitializationService
|
|||||||
AddColumnIfMissing(db, "Sites", "UsernameOverride", "TEXT NOT NULL DEFAULT ''");
|
AddColumnIfMissing(db, "Sites", "UsernameOverride", "TEXT NOT NULL DEFAULT ''");
|
||||||
AddColumnIfMissing(db, "Sites", "PasswordOverride", "TEXT NOT NULL DEFAULT ''");
|
AddColumnIfMissing(db, "Sites", "PasswordOverride", "TEXT NOT NULL DEFAULT ''");
|
||||||
AddColumnIfMissing(db, "Sites", "LocalExportFolderOverride", "TEXT NOT NULL DEFAULT ''");
|
AddColumnIfMissing(db, "Sites", "LocalExportFolderOverride", "TEXT NOT NULL DEFAULT ''");
|
||||||
|
AddColumnIfMissing(db, "Sites", "ManualImportFilePath", "TEXT NOT NULL DEFAULT ''");
|
||||||
|
AddColumnIfMissing(db, "Sites", "ManualImportLastUploadedAtUtc", "TEXT NULL");
|
||||||
AddColumnIfMissing(db, "Sites", "SapServiceUrl", "TEXT NOT NULL DEFAULT ''");
|
AddColumnIfMissing(db, "Sites", "SapServiceUrl", "TEXT NOT NULL DEFAULT ''");
|
||||||
AddColumnIfMissing(db, "Sites", "SapEntitySet", "TEXT NOT NULL DEFAULT ''");
|
AddColumnIfMissing(db, "Sites", "SapEntitySet", "TEXT NOT NULL DEFAULT ''");
|
||||||
AddColumnIfMissing(db, "Sites", "SapEntitySetsCache", "TEXT NOT NULL DEFAULT ''");
|
AddColumnIfMissing(db, "Sites", "SapEntitySetsCache", "TEXT NOT NULL DEFAULT ''");
|
||||||
@@ -128,6 +130,8 @@ CREATE TABLE Sites (
|
|||||||
UsernameOverride TEXT NOT NULL DEFAULT '',
|
UsernameOverride TEXT NOT NULL DEFAULT '',
|
||||||
PasswordOverride TEXT NOT NULL DEFAULT '',
|
PasswordOverride TEXT NOT NULL DEFAULT '',
|
||||||
LocalExportFolderOverride TEXT NOT NULL DEFAULT '',
|
LocalExportFolderOverride TEXT NOT NULL DEFAULT '',
|
||||||
|
ManualImportFilePath TEXT NOT NULL DEFAULT '',
|
||||||
|
ManualImportLastUploadedAtUtc TEXT NULL,
|
||||||
SapServiceUrl TEXT NOT NULL DEFAULT '',
|
SapServiceUrl TEXT NOT NULL DEFAULT '',
|
||||||
SapEntitySet TEXT NOT NULL DEFAULT '',
|
SapEntitySet TEXT NOT NULL DEFAULT '',
|
||||||
SapEntitySetsCache TEXT NOT NULL DEFAULT '',
|
SapEntitySetsCache TEXT NOT NULL DEFAULT '',
|
||||||
@@ -145,7 +149,7 @@ CREATE TABLE Sites (
|
|||||||
INSERT INTO Sites (
|
INSERT INTO Sites (
|
||||||
Id, HanaServerId, Schema, TSC, Land, SourceSystem,
|
Id, HanaServerId, Schema, TSC, Land, SourceSystem,
|
||||||
UsernameOverride, PasswordOverride, LocalExportFolderOverride, SapServiceUrl, SapEntitySet,
|
UsernameOverride, PasswordOverride, LocalExportFolderOverride, SapServiceUrl, SapEntitySet,
|
||||||
SapEntitySetsCache, SapEntitySetsRefreshedAtUtc, IsActive
|
ManualImportFilePath, ManualImportLastUploadedAtUtc, SapEntitySetsCache, SapEntitySetsRefreshedAtUtc, IsActive
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
Id, HanaServerId, Schema, TSC, Land,
|
Id, HanaServerId, Schema, TSC, Land,
|
||||||
@@ -153,6 +157,8 @@ SELECT
|
|||||||
COALESCE(UsernameOverride, ''),
|
COALESCE(UsernameOverride, ''),
|
||||||
COALESCE(PasswordOverride, ''),
|
COALESCE(PasswordOverride, ''),
|
||||||
COALESCE(LocalExportFolderOverride, ''),
|
COALESCE(LocalExportFolderOverride, ''),
|
||||||
|
COALESCE(ManualImportFilePath, ''),
|
||||||
|
ManualImportLastUploadedAtUtc,
|
||||||
COALESCE(SapServiceUrl, ''),
|
COALESCE(SapServiceUrl, ''),
|
||||||
COALESCE(SapEntitySet, ''),
|
COALESCE(SapEntitySet, ''),
|
||||||
COALESCE(SapEntitySetsCache, ''),
|
COALESCE(SapEntitySetsCache, ''),
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ public class ExportOrchestrationService
|
|||||||
public event Action? OnExportStatusChanged;
|
public event Action? OnExportStatusChanged;
|
||||||
|
|
||||||
private readonly Dictionary<int, string> _runningExports = new();
|
private readonly Dictionary<int, string> _runningExports = new();
|
||||||
|
private bool _consolidatedExportRunning;
|
||||||
|
private string _consolidatedExportStatus = string.Empty;
|
||||||
private readonly object _lock = new();
|
private readonly object _lock = new();
|
||||||
|
|
||||||
public ExportOrchestrationService(
|
public ExportOrchestrationService(
|
||||||
@@ -44,6 +46,22 @@ public class ExportOrchestrationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsConsolidatedExporting()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _consolidatedExportRunning;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetConsolidatedExportStatus()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
return _consolidatedExportStatus;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task ExportAllAsync()
|
public async Task ExportAllAsync()
|
||||||
{
|
{
|
||||||
using var db = await _dbFactory.CreateDbContextAsync();
|
using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
@@ -57,7 +75,12 @@ public class ExportOrchestrationService
|
|||||||
consolidatedRecords.AddRange(result.Records);
|
consolidatedRecords.AddRange(result.Records);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _consolidatedExportService.ExportAsync(consolidatedRecords);
|
await RunConsolidatedExportAsync(consolidatedRecords);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<string?> ExportConsolidatedOnlyAsync()
|
||||||
|
{
|
||||||
|
return await RunConsolidatedExportAsync(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<SiteExportResult?> ExportSiteByIdAsync(int siteId)
|
public async Task<SiteExportResult?> ExportSiteByIdAsync(int siteId)
|
||||||
@@ -112,4 +135,31 @@ public class ExportOrchestrationService
|
|||||||
{
|
{
|
||||||
OnExportStatusChanged?.Invoke();
|
OnExportStatusChanged?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<string?> RunConsolidatedExportAsync(List<SalesRecord>? records)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (_consolidatedExportRunning)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
_consolidatedExportRunning = true;
|
||||||
|
_consolidatedExportStatus = "Zentrale Datei erzeugen...";
|
||||||
|
}
|
||||||
|
NotifyChanged();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _consolidatedExportService.ExportAsync(records ?? []);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_consolidatedExportRunning = false;
|
||||||
|
_consolidatedExportStatus = string.Empty;
|
||||||
|
}
|
||||||
|
NotifyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
using TrafagSalesExporter.Models;
|
||||||
|
|
||||||
|
namespace TrafagSalesExporter.Services;
|
||||||
|
|
||||||
|
public interface IManualExcelImportService
|
||||||
|
{
|
||||||
|
Task<List<SalesRecord>> ReadSalesRecordsAsync(string filePath, Site site);
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using ClosedXML.Excel;
|
||||||
|
using TrafagSalesExporter.Models;
|
||||||
|
|
||||||
|
namespace TrafagSalesExporter.Services;
|
||||||
|
|
||||||
|
public class ManualExcelImportService : IManualExcelImportService
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<string, string> HeaderMap = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["extractiondate"] = nameof(SalesRecord.ExtractionDate),
|
||||||
|
["tsc"] = nameof(SalesRecord.Tsc),
|
||||||
|
["invoicenumber"] = nameof(SalesRecord.InvoiceNumber),
|
||||||
|
["positiononinvoice"] = nameof(SalesRecord.PositionOnInvoice),
|
||||||
|
["material"] = nameof(SalesRecord.Material),
|
||||||
|
["name"] = nameof(SalesRecord.Name),
|
||||||
|
["productgroup"] = nameof(SalesRecord.ProductGroup),
|
||||||
|
["quantity"] = nameof(SalesRecord.Quantity),
|
||||||
|
["suppliernumber"] = nameof(SalesRecord.SupplierNumber),
|
||||||
|
["suppliername"] = nameof(SalesRecord.SupplierName),
|
||||||
|
["suppliercountry"] = nameof(SalesRecord.SupplierCountry),
|
||||||
|
["customernumber"] = nameof(SalesRecord.CustomerNumber),
|
||||||
|
["customername"] = nameof(SalesRecord.CustomerName),
|
||||||
|
["customercountry"] = nameof(SalesRecord.CustomerCountry),
|
||||||
|
["customerindustry"] = nameof(SalesRecord.CustomerIndustry),
|
||||||
|
["standardcost"] = nameof(SalesRecord.StandardCost),
|
||||||
|
["standardcostcurrency"] = nameof(SalesRecord.StandardCostCurrency),
|
||||||
|
["purchaseordernumber"] = nameof(SalesRecord.PurchaseOrderNumber),
|
||||||
|
["salespricevalue"] = nameof(SalesRecord.SalesPriceValue),
|
||||||
|
["salescurrency"] = nameof(SalesRecord.SalesCurrency),
|
||||||
|
["incoterms2020"] = nameof(SalesRecord.Incoterms2020),
|
||||||
|
["salesresponsibleemployee"] = nameof(SalesRecord.SalesResponsibleEmployee),
|
||||||
|
["invoicedate"] = nameof(SalesRecord.InvoiceDate),
|
||||||
|
["orderdate"] = nameof(SalesRecord.OrderDate),
|
||||||
|
["land"] = nameof(SalesRecord.Land),
|
||||||
|
["documenttype"] = nameof(SalesRecord.DocumentType)
|
||||||
|
};
|
||||||
|
|
||||||
|
public Task<List<SalesRecord>> ReadSalesRecordsAsync(string filePath, Site site)
|
||||||
|
{
|
||||||
|
using var workbook = new XLWorkbook(filePath);
|
||||||
|
var worksheet = workbook.Worksheets.FirstOrDefault()
|
||||||
|
?? throw new InvalidOperationException("Die Excel-Datei enthält kein Arbeitsblatt.");
|
||||||
|
var usedRange = worksheet.RangeUsed()
|
||||||
|
?? throw new InvalidOperationException("Die Excel-Datei enthält keine Daten.");
|
||||||
|
|
||||||
|
var headerRow = usedRange.FirstRow();
|
||||||
|
var headerIndexes = BuildHeaderIndexMap(headerRow);
|
||||||
|
var rows = new List<SalesRecord>();
|
||||||
|
|
||||||
|
foreach (var row in usedRange.RowsUsed().Skip(1))
|
||||||
|
{
|
||||||
|
if (IsRowEmpty(row))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
rows.Add(new SalesRecord
|
||||||
|
{
|
||||||
|
ExtractionDate = ReadDate(headerIndexes, row, nameof(SalesRecord.ExtractionDate)) ?? DateTime.UtcNow,
|
||||||
|
Tsc = ReadString(headerIndexes, row, nameof(SalesRecord.Tsc), site.TSC),
|
||||||
|
InvoiceNumber = ReadString(headerIndexes, row, nameof(SalesRecord.InvoiceNumber)),
|
||||||
|
PositionOnInvoice = (int)Math.Round(ReadDecimal(headerIndexes, row, nameof(SalesRecord.PositionOnInvoice))),
|
||||||
|
Material = ReadString(headerIndexes, row, nameof(SalesRecord.Material)),
|
||||||
|
Name = ReadString(headerIndexes, row, nameof(SalesRecord.Name)),
|
||||||
|
ProductGroup = ReadString(headerIndexes, row, nameof(SalesRecord.ProductGroup)),
|
||||||
|
Quantity = ReadDecimal(headerIndexes, row, nameof(SalesRecord.Quantity)),
|
||||||
|
SupplierNumber = ReadString(headerIndexes, row, nameof(SalesRecord.SupplierNumber)),
|
||||||
|
SupplierName = ReadString(headerIndexes, row, nameof(SalesRecord.SupplierName)),
|
||||||
|
SupplierCountry = ReadString(headerIndexes, row, nameof(SalesRecord.SupplierCountry)),
|
||||||
|
CustomerNumber = ReadString(headerIndexes, row, nameof(SalesRecord.CustomerNumber)),
|
||||||
|
CustomerName = ReadString(headerIndexes, row, nameof(SalesRecord.CustomerName)),
|
||||||
|
CustomerCountry = ReadString(headerIndexes, row, nameof(SalesRecord.CustomerCountry)),
|
||||||
|
CustomerIndustry = ReadString(headerIndexes, row, nameof(SalesRecord.CustomerIndustry)),
|
||||||
|
StandardCost = ReadDecimal(headerIndexes, row, nameof(SalesRecord.StandardCost)),
|
||||||
|
StandardCostCurrency = ReadString(headerIndexes, row, nameof(SalesRecord.StandardCostCurrency)),
|
||||||
|
PurchaseOrderNumber = ReadString(headerIndexes, row, nameof(SalesRecord.PurchaseOrderNumber)),
|
||||||
|
SalesPriceValue = ReadDecimal(headerIndexes, row, nameof(SalesRecord.SalesPriceValue)),
|
||||||
|
SalesCurrency = ReadString(headerIndexes, row, nameof(SalesRecord.SalesCurrency)),
|
||||||
|
Incoterms2020 = ReadString(headerIndexes, row, nameof(SalesRecord.Incoterms2020)),
|
||||||
|
SalesResponsibleEmployee = ReadString(headerIndexes, row, nameof(SalesRecord.SalesResponsibleEmployee)),
|
||||||
|
InvoiceDate = ReadDate(headerIndexes, row, nameof(SalesRecord.InvoiceDate)),
|
||||||
|
OrderDate = ReadDate(headerIndexes, row, nameof(SalesRecord.OrderDate)),
|
||||||
|
Land = ReadString(headerIndexes, row, nameof(SalesRecord.Land), site.Land),
|
||||||
|
DocumentType = ReadString(headerIndexes, row, nameof(SalesRecord.DocumentType))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, int> BuildHeaderIndexMap(IXLRangeRow headerRow)
|
||||||
|
{
|
||||||
|
var result = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var cell in headerRow.CellsUsed())
|
||||||
|
{
|
||||||
|
var normalizedHeader = NormalizeHeader(cell.GetString());
|
||||||
|
if (string.IsNullOrWhiteSpace(normalizedHeader))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (HeaderMap.TryGetValue(normalizedHeader, out var targetField))
|
||||||
|
result[targetField] = cell.Address.ColumnNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.ContainsKey(nameof(SalesRecord.InvoiceNumber)))
|
||||||
|
throw new InvalidOperationException("Die Excel-Datei hat nicht das erwartete Exportformat. Spalte 'Invoice Number' fehlt.");
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsRowEmpty(IXLRangeRow row)
|
||||||
|
=> row.CellsUsed().All(cell => string.IsNullOrWhiteSpace(cell.GetFormattedString()));
|
||||||
|
|
||||||
|
private static string ReadString(Dictionary<string, int> headerIndexes, IXLRangeRow row, string fieldName, string fallback = "")
|
||||||
|
{
|
||||||
|
if (!headerIndexes.TryGetValue(fieldName, out var index))
|
||||||
|
return fallback;
|
||||||
|
|
||||||
|
var value = row.Cell(index).GetFormattedString().Trim();
|
||||||
|
return string.IsNullOrWhiteSpace(value) ? fallback : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static decimal ReadDecimal(Dictionary<string, int> headerIndexes, IXLRangeRow row, string fieldName)
|
||||||
|
{
|
||||||
|
if (!headerIndexes.TryGetValue(fieldName, out var index))
|
||||||
|
return 0m;
|
||||||
|
|
||||||
|
var cell = row.Cell(index);
|
||||||
|
if (cell.TryGetValue<decimal>(out var decimalValue))
|
||||||
|
return decimalValue;
|
||||||
|
if (cell.TryGetValue<double>(out var doubleValue))
|
||||||
|
return Convert.ToDecimal(doubleValue, CultureInfo.InvariantCulture);
|
||||||
|
|
||||||
|
var text = cell.GetFormattedString().Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
return 0m;
|
||||||
|
|
||||||
|
if (decimal.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out decimalValue))
|
||||||
|
return decimalValue;
|
||||||
|
if (decimal.TryParse(text, NumberStyles.Any, CultureInfo.GetCultureInfo("de-CH"), out decimalValue))
|
||||||
|
return decimalValue;
|
||||||
|
if (decimal.TryParse(text, NumberStyles.Any, CultureInfo.GetCultureInfo("de-DE"), out decimalValue))
|
||||||
|
return decimalValue;
|
||||||
|
|
||||||
|
return 0m;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DateTime? ReadDate(Dictionary<string, int> headerIndexes, IXLRangeRow row, string fieldName)
|
||||||
|
{
|
||||||
|
if (!headerIndexes.TryGetValue(fieldName, out var index))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var cell = row.Cell(index);
|
||||||
|
if (cell.TryGetValue<DateTime>(out var dateValue))
|
||||||
|
return dateValue;
|
||||||
|
|
||||||
|
var text = cell.GetFormattedString().Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var formats = new[]
|
||||||
|
{
|
||||||
|
"dd.MM.yyyy HH:mm:ss",
|
||||||
|
"dd.MM.yyyy",
|
||||||
|
"yyyy-MM-dd HH:mm:ss",
|
||||||
|
"yyyy-MM-dd",
|
||||||
|
"O"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (DateTime.TryParseExact(text, formats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out dateValue))
|
||||||
|
return dateValue;
|
||||||
|
if (DateTime.TryParse(text, CultureInfo.GetCultureInfo("de-CH"), DateTimeStyles.AssumeLocal, out dateValue))
|
||||||
|
return dateValue;
|
||||||
|
if (DateTime.TryParse(text, CultureInfo.GetCultureInfo("de-DE"), DateTimeStyles.AssumeLocal, out dateValue))
|
||||||
|
return dateValue;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeHeader(string value)
|
||||||
|
{
|
||||||
|
var chars = value
|
||||||
|
.Where(char.IsLetterOrDigit)
|
||||||
|
.Select(char.ToLowerInvariant)
|
||||||
|
.ToArray();
|
||||||
|
return new string(chars);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ public class SiteExportService : ISiteExportService
|
|||||||
private readonly ISharePointUploadService _sharePointService;
|
private readonly ISharePointUploadService _sharePointService;
|
||||||
private readonly IRecordTransformationService _transformationService;
|
private readonly IRecordTransformationService _transformationService;
|
||||||
private readonly ICentralSalesRecordService _centralSalesRecordService;
|
private readonly ICentralSalesRecordService _centralSalesRecordService;
|
||||||
|
private readonly IManualExcelImportService _manualExcelImportService;
|
||||||
private readonly IAppEventLogService _appEventLogService;
|
private readonly IAppEventLogService _appEventLogService;
|
||||||
private readonly ILogger<SiteExportService> _logger;
|
private readonly ILogger<SiteExportService> _logger;
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ public class SiteExportService : ISiteExportService
|
|||||||
ISharePointUploadService sharePointService,
|
ISharePointUploadService sharePointService,
|
||||||
IRecordTransformationService transformationService,
|
IRecordTransformationService transformationService,
|
||||||
ICentralSalesRecordService centralSalesRecordService,
|
ICentralSalesRecordService centralSalesRecordService,
|
||||||
|
IManualExcelImportService manualExcelImportService,
|
||||||
IAppEventLogService appEventLogService,
|
IAppEventLogService appEventLogService,
|
||||||
ILogger<SiteExportService> logger)
|
ILogger<SiteExportService> logger)
|
||||||
{
|
{
|
||||||
@@ -38,6 +40,7 @@ public class SiteExportService : ISiteExportService
|
|||||||
_sharePointService = sharePointService;
|
_sharePointService = sharePointService;
|
||||||
_transformationService = transformationService;
|
_transformationService = transformationService;
|
||||||
_centralSalesRecordService = centralSalesRecordService;
|
_centralSalesRecordService = centralSalesRecordService;
|
||||||
|
_manualExcelImportService = manualExcelImportService;
|
||||||
_appEventLogService = appEventLogService;
|
_appEventLogService = appEventLogService;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
@@ -96,6 +99,30 @@ public class SiteExportService : ISiteExportService
|
|||||||
filePath = _excelService.CreateExcelFile(outputDir, site.TSC, DateTime.UtcNow.Date, records);
|
filePath = _excelService.CreateExcelFile(outputDir, site.TSC, DateTime.UtcNow.Date, records);
|
||||||
log.RowCount = records.Count;
|
log.RowCount = records.Count;
|
||||||
}
|
}
|
||||||
|
else if (sourceSystem == "MANUAL_EXCEL")
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(site.ManualImportFilePath))
|
||||||
|
throw new InvalidOperationException($"Standort '{site.Land}' hat keine manuelle Excel-Datei.");
|
||||||
|
if (!File.Exists(site.ManualImportFilePath))
|
||||||
|
throw new InvalidOperationException($"Die manuelle Excel-Datei wurde nicht gefunden: {site.ManualImportFilePath}");
|
||||||
|
|
||||||
|
updateStatus?.Invoke("Manuelle Excel lesen...");
|
||||||
|
await _appEventLogService.WriteAsync("Export", "Manuelle Excel lesen", siteId: site.Id, land: site.Land,
|
||||||
|
details: site.ManualImportFilePath);
|
||||||
|
records = await _manualExcelImportService.ReadSalesRecordsAsync(site.ManualImportFilePath, site);
|
||||||
|
|
||||||
|
updateStatus?.Invoke("Transformationen anwenden...");
|
||||||
|
await _appEventLogService.WriteAsync("Export", "Transformationen anwenden", siteId: site.Id, land: site.Land,
|
||||||
|
details: $"Records vor Transformation={records.Count}");
|
||||||
|
var rules = await db.FieldTransformationRules
|
||||||
|
.Where(r => r.IsActive && r.SourceSystem == sourceSystem)
|
||||||
|
.OrderBy(r => r.SortOrder)
|
||||||
|
.ToListAsync();
|
||||||
|
_transformationService.Apply(records, rules);
|
||||||
|
|
||||||
|
filePath = site.ManualImportFilePath;
|
||||||
|
log.RowCount = records.Count;
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var exportServer = BuildEffectiveServer(site, settings, sourceSystem);
|
var exportServer = BuildEffectiveServer(site, settings, sourceSystem);
|
||||||
|
|||||||
Reference in New Issue
Block a user