Add manual Excel column mapping

This commit is contained in:
2026-05-04 16:08:56 +02:00
parent 749a3209d9
commit c862a559f6
23 changed files with 1523 additions and 182 deletions
@@ -1,4 +1,5 @@
using System.Text.Json;
using ClosedXML.Excel;
using Microsoft.EntityFrameworkCore;
using TrafagSalesExporter.Data;
using TrafagSalesExporter.Models;
@@ -12,11 +13,12 @@ public interface IStandortePageService
Task DeleteServerAsync(HanaServer server);
Task<ConnectionTestResult> TestServerConnectionAsync(HanaServer server);
Task<StandortEditorState> LoadSiteEditorAsync(Site site, IEnumerable<SourceSystemDefinition> sourceSystems);
Task SaveSiteAsync(Site site, bool usesHanaConnection, bool isSapSite, List<SapSourceDefinition> sapSources, List<SapJoinDefinition> sapJoins, List<SapFieldMapping> sapMappings, List<string> sapEntitySetsCache);
Task SaveSiteAsync(Site site, bool usesHanaConnection, bool isSapSite, bool isManualExcelSite, List<SapSourceDefinition> sapSources, List<SapJoinDefinition> sapJoins, List<SapFieldMapping> sapMappings, List<ManualExcelColumnMapping> manualExcelMappings, List<string> sapEntitySetsCache);
Task DeleteSiteAsync(Site site);
Task<List<string>> LoadAvailableSchemasAsync(Site site);
Task<SapEntitySetRefreshResult> RefreshSapEntitySetsAsync(Site site);
Task<SapSourceFieldRefreshResult> RefreshSapSourceFieldsAsync(Site site, List<SapSourceDefinition> sapSources, List<SapFieldMapping> sapMappings);
Task<List<string>> LoadManualExcelHeadersAsync(string manualImportFilePath);
Task<DateTime> ValidateManualImportPathAsync(string manualImportFilePath);
}
@@ -163,6 +165,7 @@ public sealed class StandortePageService : IStandortePageService
var sapSources = await db.SapSourceDefinitions.Where(s => s.SiteId == site.Id).OrderBy(s => s.SortOrder).ThenBy(s => s.Id).ToListAsync();
var sapJoins = await db.SapJoinDefinitions.Where(j => j.SiteId == site.Id).OrderBy(j => j.SortOrder).ThenBy(j => j.Id).ToListAsync();
var sapMappings = await db.SapFieldMappings.Where(m => m.SiteId == site.Id).OrderBy(m => m.SortOrder).ThenBy(m => m.Id).ToListAsync();
var manualExcelMappings = await db.ManualExcelColumnMappings.Where(m => m.SiteId == site.Id).OrderBy(m => m.SortOrder).ThenBy(m => m.Id).ToListAsync();
return new StandortEditorState
{
@@ -188,11 +191,12 @@ public sealed class StandortePageService : IStandortePageService
SapEntitySets = ParseSapEntitySets(site.SapEntitySetsCache),
SapSources = sapSources,
SapJoins = sapJoins,
SapMappings = sapMappings
SapMappings = sapMappings,
ManualExcelMappings = manualExcelMappings
};
}
public async Task SaveSiteAsync(Site site, bool usesHanaConnection, bool isSapSite, List<SapSourceDefinition> sapSources, List<SapJoinDefinition> sapJoins, List<SapFieldMapping> sapMappings, List<string> sapEntitySetsCache)
public async Task SaveSiteAsync(Site site, bool usesHanaConnection, bool isSapSite, bool isManualExcelSite, List<SapSourceDefinition> sapSources, List<SapJoinDefinition> sapJoins, List<SapFieldMapping> sapMappings, List<ManualExcelColumnMapping> manualExcelMappings, List<string> sapEntitySetsCache)
{
await using var db = await _dbFactory.CreateDbContextAsync();
var serverId = usesHanaConnection ? await ResolveCentralHanaServerIdAsync(db, site) : (int?)null;
@@ -212,6 +216,7 @@ public sealed class StandortePageService : IStandortePageService
await db.SaveChangesAsync();
await SaveSapConfigurationAsync(db, site.Id, isSapSite, sapSources, sapJoins, sapMappings);
await SaveManualExcelConfigurationAsync(db, site.Id, isManualExcelSite, manualExcelMappings);
}
public async Task DeleteSiteAsync(Site site)
@@ -224,10 +229,12 @@ public sealed class StandortePageService : IStandortePageService
var sources = await db.SapSourceDefinitions.Where(s => s.SiteId == site.Id).ToListAsync();
var joins = await db.SapJoinDefinitions.Where(j => j.SiteId == site.Id).ToListAsync();
var mappings = await db.SapFieldMappings.Where(m => m.SiteId == site.Id).ToListAsync();
var manualMappings = await db.ManualExcelColumnMappings.Where(m => m.SiteId == site.Id).ToListAsync();
var centralRows = await db.CentralSalesRecords.Where(r => r.SiteId == site.Id).ToListAsync();
if (sources.Count > 0) db.SapSourceDefinitions.RemoveRange(sources);
if (joins.Count > 0) db.SapJoinDefinitions.RemoveRange(joins);
if (mappings.Count > 0) db.SapFieldMappings.RemoveRange(mappings);
if (manualMappings.Count > 0) db.ManualExcelColumnMappings.RemoveRange(manualMappings);
if (centralRows.Count > 0) db.CentralSalesRecords.RemoveRange(centralRows);
db.Sites.Remove(entity);
await db.SaveChangesAsync();
@@ -381,6 +388,59 @@ public sealed class StandortePageService : IStandortePageService
}
}
public async Task<List<string>> LoadManualExcelHeadersAsync(string manualImportFilePath)
{
var filePath = await ResolveManualImportFilePathAsync(manualImportFilePath);
var deleteAfterRead = !string.Equals(filePath, manualImportFilePath?.Trim(), StringComparison.OrdinalIgnoreCase);
try
{
using var workbook = new XLWorkbook(filePath);
var worksheet = workbook.Worksheets.FirstOrDefault()
?? throw new InvalidOperationException("Die Excel-Datei enthaelt kein Arbeitsblatt.");
var usedRange = worksheet.RangeUsed()
?? throw new InvalidOperationException("Die Excel-Datei enthaelt keine Daten.");
return usedRange.FirstRow().CellsUsed()
.Select(cell => cell.GetString().Trim())
.Where(x => !string.IsNullOrWhiteSpace(x))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)
.ToList();
}
finally
{
if (deleteAfterRead && File.Exists(filePath))
File.Delete(filePath);
}
}
private async Task<string> ResolveManualImportFilePathAsync(string manualImportFilePath)
{
var trimmedPath = manualImportFilePath.Trim();
if (string.IsNullOrWhiteSpace(trimmedPath))
throw new InvalidOperationException("Bitte zuerst einen Dateipfad eintragen.");
if (File.Exists(trimmedPath))
return trimmedPath;
if (!LooksLikeSharePointReference(trimmedPath))
throw new InvalidOperationException($"Datei nicht gefunden oder nicht erreichbar: {trimmedPath}");
await using var db = await _dbFactory.CreateDbContextAsync();
var spConfig = await db.SharePointConfigs.FirstOrDefaultAsync();
if (spConfig is null ||
string.IsNullOrWhiteSpace(spConfig.TenantId) ||
string.IsNullOrWhiteSpace(spConfig.ClientId) ||
string.IsNullOrWhiteSpace(spConfig.ClientSecret) ||
string.IsNullOrWhiteSpace(spConfig.SiteUrl))
{
throw new InvalidOperationException("Fuer SharePoint-Pruefung fehlt eine vollstaendige SharePoint-Konfiguration in Settings.");
}
return await _sharePointService.DownloadToTempFileAsync(
spConfig.TenantId, spConfig.ClientId, spConfig.ClientSecret, spConfig.SiteUrl, trimmedPath);
}
private static void ApplyServer(HanaServer target, HanaServer source)
{
target.SourceSystem = source.SourceSystem;
@@ -452,6 +512,12 @@ public sealed class StandortePageService : IStandortePageService
sapSources[0].IsPrimary = true;
}
private static void NormalizeManualExcelMappings(List<ManualExcelColumnMapping> manualExcelMappings)
{
for (var i = 0; i < manualExcelMappings.Count; i++)
manualExcelMappings[i].SortOrder = i;
}
private static async Task SaveSapConfigurationAsync(AppDbContext db, int siteId, bool isSapSite, List<SapSourceDefinition> sapSources, List<SapJoinDefinition> sapJoins, List<SapFieldMapping> sapMappings)
{
var oldSources = await db.SapSourceDefinitions.Where(s => s.SiteId == siteId).ToListAsync();
@@ -475,6 +541,22 @@ public sealed class StandortePageService : IStandortePageService
await db.SaveChangesAsync();
}
private static async Task SaveManualExcelConfigurationAsync(AppDbContext db, int siteId, bool isManualExcelSite, List<ManualExcelColumnMapping> manualExcelMappings)
{
var oldMappings = await db.ManualExcelColumnMappings.Where(m => m.SiteId == siteId).ToListAsync();
if (oldMappings.Count > 0) db.ManualExcelColumnMappings.RemoveRange(oldMappings);
if (isManualExcelSite)
{
NormalizeManualExcelMappings(manualExcelMappings);
foreach (var mapping in manualExcelMappings)
mapping.SiteId = siteId;
db.ManualExcelColumnMappings.AddRange(manualExcelMappings);
}
await db.SaveChangesAsync();
}
private static async Task<int> ResolveCentralHanaServerIdAsync(AppDbContext db, Site site)
{
site.UsernameOverride = site.UsernameOverride.Trim();
@@ -507,6 +589,7 @@ public sealed class StandortEditorState
public List<SapSourceDefinition> SapSources { get; set; } = [];
public List<SapJoinDefinition> SapJoins { get; set; } = [];
public List<SapFieldMapping> SapMappings { get; set; } = [];
public List<ManualExcelColumnMapping> ManualExcelMappings { get; set; } = [];
}
public sealed class SapEntitySetRefreshResult