Regelsteuerung grafisch und per C# Templates
This commit is contained in:
@@ -3,18 +3,24 @@
|
||||
@using System.Reflection
|
||||
@using TrafagSalesExporter.Data
|
||||
@using TrafagSalesExporter.Models
|
||||
@using TrafagSalesExporter.Services
|
||||
@inject IDbContextFactory<AppDbContext> DbFactory
|
||||
@inject ITransformationCatalog TransformationCatalog
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<PageTitle>Transformationen</PageTitle>
|
||||
|
||||
<MudText Typo="Typo.h4" Class="mb-4">Transformer Ansicht</MudText>
|
||||
<MudText Typo="Typo.body1" Class="mb-4">Definiere pro Quellsystem (SAP, BI1, SAGE) Feld-Remapping und Transformationen.</MudText>
|
||||
<MudText Typo="Typo.body1" Class="mb-4">Definiere pro Quellsystem einfache Feldregeln und komplexe record-basierte Strategien.</MudText>
|
||||
|
||||
<MudPaper Class="pa-4" Elevation="1">
|
||||
<MudAlert Severity="Severity.Info" Dense="true" Variant="Variant.Outlined" Class="mb-3">
|
||||
`Value`-Regeln arbeiten feldweise. `Record`-Regeln rufen eine registrierte C#-Strategie auf und koennen mehrere Felder eines Datensatzes verwenden.
|
||||
</MudAlert>
|
||||
|
||||
<MudStack Row="true" Spacing="2" Class="mb-3">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add" OnClick="AddRule">
|
||||
Regel hinzufügen
|
||||
Regel hinzufuegen
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Secondary" StartIcon="@Icons.Material.Filled.Save" OnClick="SaveAllAsync">
|
||||
Alle speichern
|
||||
@@ -25,6 +31,7 @@
|
||||
<HeaderContent>
|
||||
<MudTh>Aktiv</MudTh>
|
||||
<MudTh>System</MudTh>
|
||||
<MudTh>Scope</MudTh>
|
||||
<MudTh>Source</MudTh>
|
||||
<MudTh>Target</MudTh>
|
||||
<MudTh>Typ</MudTh>
|
||||
@@ -38,15 +45,23 @@
|
||||
<MudSelect T="string" Value="@context.SourceSystem" ValueChanged="@(v => context.SourceSystem = v)" Dense>
|
||||
@foreach (var system in _systems)
|
||||
{
|
||||
<MudSelectItem Value="system">@system</MudSelectItem>
|
||||
<MudSelectItem Value="@system">@system</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudSelect T="string" Value="@context.SourceField" ValueChanged="@(v => context.SourceField = v)" Dense>
|
||||
<MudSelect T="string" Value="@context.RuleScope" ValueChanged="@(v => ChangeRuleScope(context, v))" Dense>
|
||||
@foreach (var scope in _ruleScopes)
|
||||
{
|
||||
<MudSelectItem Value="@scope">@scope</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudSelect T="string" Value="@context.SourceField" ValueChanged="@(v => context.SourceField = v)" Dense Disabled="@IsRecordScope(context)">
|
||||
@foreach (var field in _recordFields)
|
||||
{
|
||||
<MudSelectItem Value="field">@field</MudSelectItem>
|
||||
<MudSelectItem Value="@field">@field</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudTd>
|
||||
@@ -54,21 +69,21 @@
|
||||
<MudSelect T="string" Value="@context.TargetField" ValueChanged="@(v => context.TargetField = v)" Dense>
|
||||
@foreach (var field in _recordFields)
|
||||
{
|
||||
<MudSelectItem Value="field">@field</MudSelectItem>
|
||||
<MudSelectItem Value="@field">@field</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudSelect T="string" Value="@context.TransformationType" ValueChanged="@(v => context.TransformationType = v)" Dense>
|
||||
@foreach (var type in _types)
|
||||
@foreach (var type in GetTypesForScope(context.RuleScope))
|
||||
{
|
||||
<MudSelectItem Value="type">@type</MudSelectItem>
|
||||
<MudSelectItem Value="@type.Key">@type.Key</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudTextField T="string" Value="@context.Argument" ValueChanged="@(v => context.Argument = v)"
|
||||
HelperText="Replace: alt=>neu" />
|
||||
HelperText="@GetArgumentHelperText(context)" />
|
||||
</MudTd>
|
||||
<MudTd>
|
||||
<MudNumericField T="int" Value="@context.SortOrder" ValueChanged="@(v => context.SortOrder = v)" Dense />
|
||||
@@ -82,8 +97,8 @@
|
||||
</MudPaper>
|
||||
|
||||
@code {
|
||||
private readonly string[] _systems = ["SAP", "BI1", "SAGE"];
|
||||
private readonly string[] _types = ["Copy", "Uppercase", "Lowercase", "Prefix", "Suffix", "Replace", "Constant"];
|
||||
private readonly string[] _systems = ["SAP", "BI1", "SAGE", "MANUAL_EXCEL"];
|
||||
private readonly string[] _ruleScopes = ["Value", "Record"];
|
||||
private readonly string[] _recordFields = typeof(SalesRecord)
|
||||
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Select(p => p.Name)
|
||||
@@ -91,9 +106,11 @@
|
||||
.ToArray();
|
||||
|
||||
private List<FieldTransformationRule> _rules = new();
|
||||
private IReadOnlyList<TransformationCatalogItem> _catalogItems = [];
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_catalogItems = TransformationCatalog.GetAll();
|
||||
await LoadAsync();
|
||||
}
|
||||
|
||||
@@ -101,6 +118,15 @@
|
||||
{
|
||||
using var db = await DbFactory.CreateDbContextAsync();
|
||||
_rules = await db.FieldTransformationRules.OrderBy(r => r.SortOrder).ThenBy(r => r.Id).ToListAsync();
|
||||
|
||||
foreach (var rule in _rules)
|
||||
{
|
||||
rule.RuleScope = string.IsNullOrWhiteSpace(rule.RuleScope) ? "Value" : rule.RuleScope;
|
||||
if (!GetTypesForScope(rule.RuleScope).Any(x => string.Equals(x.Key, rule.TransformationType, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
rule.TransformationType = GetTypesForScope(rule.RuleScope).FirstOrDefault()?.Key ?? "Copy";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void AddRule()
|
||||
@@ -109,6 +135,7 @@
|
||||
_rules.Add(new FieldTransformationRule
|
||||
{
|
||||
SourceSystem = "SAP",
|
||||
RuleScope = "Value",
|
||||
SourceField = nameof(SalesRecord.Material),
|
||||
TargetField = nameof(SalesRecord.Material),
|
||||
TransformationType = "Copy",
|
||||
@@ -134,4 +161,35 @@
|
||||
Snackbar.Add("Transformationsregeln gespeichert.", Severity.Success);
|
||||
await LoadAsync();
|
||||
}
|
||||
|
||||
private IReadOnlyList<TransformationCatalogItem> GetTypesForScope(string? ruleScope)
|
||||
{
|
||||
var scope = string.IsNullOrWhiteSpace(ruleScope) ? "Value" : ruleScope;
|
||||
return TransformationCatalog.GetByScope(scope);
|
||||
}
|
||||
|
||||
private static bool IsRecordScope(FieldTransformationRule rule)
|
||||
=> string.Equals(rule.RuleScope, "Record", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private void ChangeRuleScope(FieldTransformationRule rule, string scope)
|
||||
{
|
||||
rule.RuleScope = scope;
|
||||
var firstType = GetTypesForScope(scope).FirstOrDefault()?.Key;
|
||||
if (!string.IsNullOrWhiteSpace(firstType))
|
||||
rule.TransformationType = firstType;
|
||||
|
||||
if (IsRecordScope(rule))
|
||||
rule.SourceField = string.Empty;
|
||||
else if (string.IsNullOrWhiteSpace(rule.SourceField))
|
||||
rule.SourceField = nameof(SalesRecord.Material);
|
||||
}
|
||||
|
||||
private string GetArgumentHelperText(FieldTransformationRule rule)
|
||||
{
|
||||
var item = _catalogItems.FirstOrDefault(x =>
|
||||
string.Equals(x.RuleScope, rule.RuleScope, StringComparison.OrdinalIgnoreCase) &&
|
||||
string.Equals(x.Key, rule.TransformationType, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
return item?.Description ?? "Optionales Argument.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ public class FieldTransformationRule
|
||||
[Required]
|
||||
public string TransformationType { get; set; } = "Copy";
|
||||
|
||||
[Required]
|
||||
public string RuleScope { get; set; } = "Value";
|
||||
|
||||
public string Argument { get; set; } = string.Empty;
|
||||
|
||||
public int SortOrder { get; set; }
|
||||
|
||||
@@ -25,6 +25,8 @@ builder.Services.AddSingleton<ITransformationStrategy, PrefixTransformationStrat
|
||||
builder.Services.AddSingleton<ITransformationStrategy, SuffixTransformationStrategy>();
|
||||
builder.Services.AddSingleton<ITransformationStrategy, ReplaceTransformationStrategy>();
|
||||
builder.Services.AddSingleton<ITransformationStrategy, ConstantTransformationStrategy>();
|
||||
builder.Services.AddSingleton<IRecordTransformationStrategy, FirstNonEmptyRecordTransformationStrategy>();
|
||||
builder.Services.AddSingleton<ITransformationCatalog, TransformationCatalog>();
|
||||
builder.Services.AddSingleton<IRecordTransformationService, RecordTransformationService>();
|
||||
builder.Services.AddSingleton<IAppEventLogService, AppEventLogService>();
|
||||
builder.Services.AddSingleton<IManagementCockpitService, ManagementCockpitService>();
|
||||
|
||||
@@ -95,6 +95,7 @@ public class ConfigTransferService : IConfigTransferService
|
||||
SourceField = r.SourceField,
|
||||
TargetField = r.TargetField,
|
||||
TransformationType = r.TransformationType,
|
||||
RuleScope = r.RuleScope,
|
||||
Argument = r.Argument,
|
||||
SortOrder = r.SortOrder,
|
||||
IsActive = r.IsActive
|
||||
@@ -265,6 +266,7 @@ public class ConfigTransferService : IConfigTransferService
|
||||
SourceField = r.SourceField,
|
||||
TargetField = r.TargetField,
|
||||
TransformationType = r.TransformationType,
|
||||
RuleScope = r.RuleScope,
|
||||
Argument = r.Argument,
|
||||
SortOrder = r.SortOrder,
|
||||
IsActive = r.IsActive
|
||||
|
||||
@@ -71,6 +71,7 @@ public class DatabaseInitializationService : IDatabaseInitializationService
|
||||
AddColumnIfMissing(db, "ExportSettings", "LocalConsolidatedExportFolder", "TEXT NOT NULL DEFAULT ''");
|
||||
AddColumnIfMissing(db, "ExportLogs", "FilePath", "TEXT NOT NULL DEFAULT ''");
|
||||
EnsureTransformationTable(db);
|
||||
AddColumnIfMissing(db, "FieldTransformationRules", "RuleScope", "TEXT NOT NULL DEFAULT 'Value'");
|
||||
EnsureSapSourceTable(db);
|
||||
EnsureSapJoinTable(db);
|
||||
EnsureSapFieldMappingTable(db);
|
||||
@@ -440,6 +441,7 @@ CREATE TABLE IF NOT EXISTS FieldTransformationRules (
|
||||
SourceField TEXT NOT NULL,
|
||||
TargetField TEXT NOT NULL,
|
||||
TransformationType TEXT NOT NULL,
|
||||
RuleScope TEXT NOT NULL DEFAULT 'Value',
|
||||
Argument TEXT NOT NULL DEFAULT '',
|
||||
SortOrder INTEGER NOT NULL DEFAULT 0,
|
||||
IsActive INTEGER NOT NULL DEFAULT 1
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using TrafagSalesExporter.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public interface IRecordTransformationStrategy
|
||||
{
|
||||
string TransformationType { get; }
|
||||
string Description => string.Empty;
|
||||
void Transform(SalesRecord record, FieldTransformationRule rule);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public interface ITransformationCatalog
|
||||
{
|
||||
IReadOnlyList<TransformationCatalogItem> GetAll();
|
||||
IReadOnlyList<TransformationCatalogItem> GetByScope(string ruleScope);
|
||||
}
|
||||
|
||||
public sealed class TransformationCatalogItem
|
||||
{
|
||||
public string Key { get; init; } = string.Empty;
|
||||
public string RuleScope { get; init; } = string.Empty;
|
||||
public string Description { get; init; } = string.Empty;
|
||||
}
|
||||
@@ -3,5 +3,6 @@ namespace TrafagSalesExporter.Services;
|
||||
public interface ITransformationStrategy
|
||||
{
|
||||
string TransformationType { get; }
|
||||
string Description => string.Empty;
|
||||
object? Transform(object? sourceValue, string? argument);
|
||||
}
|
||||
|
||||
@@ -5,15 +5,17 @@ namespace TrafagSalesExporter.Services;
|
||||
|
||||
public class RecordTransformationService : IRecordTransformationService
|
||||
{
|
||||
private static readonly Dictionary<string, PropertyInfo> PropertyMap = typeof(SalesRecord)
|
||||
internal static readonly Dictionary<string, PropertyInfo> PropertyMap = typeof(SalesRecord)
|
||||
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.ToDictionary(p => p.Name, p => p, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private readonly IReadOnlyDictionary<string, ITransformationStrategy> _strategies;
|
||||
private readonly IReadOnlyDictionary<string, IRecordTransformationStrategy> _recordStrategies;
|
||||
|
||||
public RecordTransformationService(IEnumerable<ITransformationStrategy> strategies)
|
||||
public RecordTransformationService(IEnumerable<ITransformationStrategy> strategies, IEnumerable<IRecordTransformationStrategy> recordStrategies)
|
||||
{
|
||||
_strategies = strategies.ToDictionary(s => s.TransformationType, StringComparer.OrdinalIgnoreCase);
|
||||
_recordStrategies = recordStrategies.ToDictionary(s => s.TransformationType, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
public void Apply(List<SalesRecord> records, IEnumerable<FieldTransformationRule> rules)
|
||||
@@ -32,6 +34,13 @@ public class RecordTransformationService : IRecordTransformationService
|
||||
|
||||
private void ApplyRule(SalesRecord record, FieldTransformationRule rule)
|
||||
{
|
||||
if (string.Equals(rule.RuleScope, "Record", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (_recordStrategies.TryGetValue(rule.TransformationType, out var recordStrategy))
|
||||
recordStrategy.Transform(record, rule);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!PropertyMap.TryGetValue(rule.SourceField, out var sourceProp)) return;
|
||||
if (!PropertyMap.TryGetValue(rule.TargetField, out var targetProp)) return;
|
||||
|
||||
@@ -43,7 +52,7 @@ public class RecordTransformationService : IRecordTransformationService
|
||||
SetPropertyValue(record, targetProp, result);
|
||||
}
|
||||
|
||||
private static void SetPropertyValue(SalesRecord record, PropertyInfo property, object? value)
|
||||
internal static void SetPropertyValue(SalesRecord record, PropertyInfo property, object? value)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public class TransformationCatalog : ITransformationCatalog
|
||||
{
|
||||
private readonly IReadOnlyList<TransformationCatalogItem> _items;
|
||||
|
||||
public TransformationCatalog(IEnumerable<ITransformationStrategy> valueStrategies, IEnumerable<IRecordTransformationStrategy> recordStrategies)
|
||||
{
|
||||
_items = valueStrategies
|
||||
.Select(x => new TransformationCatalogItem
|
||||
{
|
||||
Key = x.TransformationType,
|
||||
RuleScope = "Value",
|
||||
Description = x.Description
|
||||
})
|
||||
.Concat(recordStrategies.Select(x => new TransformationCatalogItem
|
||||
{
|
||||
Key = x.TransformationType,
|
||||
RuleScope = "Record",
|
||||
Description = x.Description
|
||||
}))
|
||||
.OrderBy(x => x.RuleScope, StringComparer.OrdinalIgnoreCase)
|
||||
.ThenBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public IReadOnlyList<TransformationCatalogItem> GetAll() => _items;
|
||||
|
||||
public IReadOnlyList<TransformationCatalogItem> GetByScope(string ruleScope)
|
||||
=> _items
|
||||
.Where(x => string.Equals(x.RuleScope, ruleScope, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
}
|
||||
@@ -1,38 +1,46 @@
|
||||
using TrafagSalesExporter.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public sealed class CopyTransformationStrategy : ITransformationStrategy
|
||||
{
|
||||
public string TransformationType => "Copy";
|
||||
public string Description => "Kopiert Source nach Target.";
|
||||
public object? Transform(object? sourceValue, string? argument) => sourceValue;
|
||||
}
|
||||
|
||||
public sealed class UppercaseTransformationStrategy : ITransformationStrategy
|
||||
{
|
||||
public string TransformationType => "Uppercase";
|
||||
public string Description => "Wandelt Text in Grossbuchstaben.";
|
||||
public object? Transform(object? sourceValue, string? argument) => sourceValue?.ToString()?.ToUpperInvariant();
|
||||
}
|
||||
|
||||
public sealed class LowercaseTransformationStrategy : ITransformationStrategy
|
||||
{
|
||||
public string TransformationType => "Lowercase";
|
||||
public string Description => "Wandelt Text in Kleinbuchstaben.";
|
||||
public object? Transform(object? sourceValue, string? argument) => sourceValue?.ToString()?.ToLowerInvariant();
|
||||
}
|
||||
|
||||
public sealed class PrefixTransformationStrategy : ITransformationStrategy
|
||||
{
|
||||
public string TransformationType => "Prefix";
|
||||
public string Description => "Stellt Argument vor den Source-Wert.";
|
||||
public object? Transform(object? sourceValue, string? argument) => $"{argument}{sourceValue}";
|
||||
}
|
||||
|
||||
public sealed class SuffixTransformationStrategy : ITransformationStrategy
|
||||
{
|
||||
public string TransformationType => "Suffix";
|
||||
public string Description => "Haengt Argument an den Source-Wert.";
|
||||
public object? Transform(object? sourceValue, string? argument) => $"{sourceValue}{argument}";
|
||||
}
|
||||
|
||||
public sealed class ReplaceTransformationStrategy : ITransformationStrategy
|
||||
{
|
||||
public string TransformationType => "Replace";
|
||||
public string Description => "Ersetzt in Text mit Syntax alt=>neu.";
|
||||
|
||||
public object? Transform(object? sourceValue, string? argument)
|
||||
{
|
||||
@@ -54,5 +62,54 @@ public sealed class ReplaceTransformationStrategy : ITransformationStrategy
|
||||
public sealed class ConstantTransformationStrategy : ITransformationStrategy
|
||||
{
|
||||
public string TransformationType => "Constant";
|
||||
public string Description => "Setzt das Target auf einen konstanten Wert aus Argument.";
|
||||
public object? Transform(object? sourceValue, string? argument) => argument;
|
||||
}
|
||||
|
||||
public sealed class FirstNonEmptyRecordTransformationStrategy : IRecordTransformationStrategy
|
||||
{
|
||||
public string TransformationType => "FirstNonEmpty";
|
||||
public string Description => "Record-Strategie: setzt Target aus dem ersten nicht-leeren Feld aus Argument, z.B. CustomerName|SupplierName|Name.";
|
||||
|
||||
public void Transform(SalesRecord record, FieldTransformationRule rule)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rule.TargetField) || string.IsNullOrWhiteSpace(rule.Argument))
|
||||
return;
|
||||
|
||||
var propertyMap = RecordTransformationService.PropertyMap;
|
||||
if (!propertyMap.TryGetValue(rule.TargetField, out var targetProperty))
|
||||
return;
|
||||
|
||||
var sourceFields = rule.Argument
|
||||
.Split(['|', ',', ';'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
|
||||
foreach (var sourceField in sourceFields)
|
||||
{
|
||||
if (!propertyMap.TryGetValue(sourceField, out var sourceProperty))
|
||||
continue;
|
||||
|
||||
var value = sourceProperty.GetValue(record);
|
||||
if (IsMeaningfulValue(value))
|
||||
{
|
||||
RecordTransformationService.SetPropertyValue(record, targetProperty, value);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsMeaningfulValue(object? value)
|
||||
{
|
||||
if (value is null)
|
||||
return false;
|
||||
if (value is string text)
|
||||
return !string.IsNullOrWhiteSpace(text);
|
||||
if (value is DateTime date)
|
||||
return date != default;
|
||||
if (value is decimal decimalNumber)
|
||||
return decimalNumber != 0m;
|
||||
if (value is int intNumber)
|
||||
return intNumber != 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user