Add purchasing data sources and 3D simulation
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
@page "/einkauf"
|
||||
@using System.Globalization
|
||||
@using TrafagSalesExporter.Models
|
||||
@inject TrafagSalesExporter.Services.IUiTextService UiText
|
||||
@inject IJSRuntime JsRuntime
|
||||
|
||||
<PageTitle>@T("Einkauf", "Purchasing")</PageTitle>
|
||||
|
||||
@@ -119,9 +121,76 @@
|
||||
</MudTable>
|
||||
</MudPaper>
|
||||
</MudTabPanel>
|
||||
|
||||
<MudTabPanel Text="@T("3D Simulation", "3D simulation")" Icon="@Icons.Material.Filled.ViewInAr">
|
||||
<MudPaper Class="pa-3 mb-3" Outlined="true">
|
||||
<MudGrid Spacing="2">
|
||||
<MudItem xs="12" md="3">
|
||||
<MudSelect T="string" Value="_purchasing3dIndicator" ValueChanged="SetPurchasing3dIndicator" Label="@T("Indikator", "Indicator")" Dense="true">
|
||||
@foreach (var option in Purchasing3dIndicators)
|
||||
{
|
||||
<MudSelectItem Value="@option.Key">@T(option.TitleDe, option.TitleEn)</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="2">
|
||||
<MudSelect T="string" Value="_purchasing3dChartType" ValueChanged="SetPurchasing3dChartType" Label="@T("Grafik", "Chart")" Dense="true">
|
||||
<MudSelectItem Value="@("bar")">@T("Balken", "Bars")</MudSelectItem>
|
||||
<MudSelectItem Value="@("line")">@T("Linie", "Line")</MudSelectItem>
|
||||
<MudSelectItem Value="@("surface")">@T("Flaeche", "Surface")</MudSelectItem>
|
||||
<MudSelectItem Value="@("pie")">@T("Kreis", "Pie")</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="3">
|
||||
<MudText Typo="Typo.caption">@T("Preis-/Wechselkurs-Szenario", "Price/exchange-rate scenario")</MudText>
|
||||
<div class="finance-3d-range-row">
|
||||
<MudButton Variant="Variant.Outlined" Size="Size.Small" OnClick="@(() => SetPurchasing3dFactorPreset(0.9d))">-10%</MudButton>
|
||||
<input class="finance-3d-range"
|
||||
type="range"
|
||||
min="0.5"
|
||||
max="1.5"
|
||||
step="0.01"
|
||||
value="@_purchasing3dFactor.ToString("0.00", CultureInfo.InvariantCulture)"
|
||||
@oninput="SetPurchasing3dFactor" />
|
||||
<MudText Typo="Typo.body2" Class="finance-3d-factor">@_purchasing3dFactor.ToString("0.00", CultureInfo.InvariantCulture)x</MudText>
|
||||
</div>
|
||||
<MudText Typo="Typo.caption">
|
||||
@T("Delta", "Delta"): @FormatScenarioDelta()
|
||||
</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="2">
|
||||
<MudText Typo="Typo.caption">@T("Beschriftung", "Labels")</MudText>
|
||||
<div class="finance-3d-range-row">
|
||||
<input class="finance-3d-range"
|
||||
type="range"
|
||||
min="0.8"
|
||||
max="2.5"
|
||||
step="0.1"
|
||||
value="@_purchasing3dLabelScale.ToString("0.0", CultureInfo.InvariantCulture)"
|
||||
@oninput="SetPurchasing3dLabelScale" />
|
||||
<MudText Typo="Typo.body2" Class="finance-3d-factor">@_purchasing3dLabelScale.ToString("0.0", CultureInfo.InvariantCulture)x</MudText>
|
||||
</div>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="2">
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Info" StartIcon="@Icons.Material.Filled.Refresh" OnClick="RenderPurchasing3dAsync">
|
||||
@T("Neu zeichnen", "Redraw")
|
||||
</MudButton>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudPaper>
|
||||
<MudPaper Class="pa-0 finance-3d-surface purchasing-3d-surface" Elevation="1">
|
||||
<canvas @ref="_purchasing3dCanvas" class="finance-3d-canvas" style="display:block;width:100%;height:100%;touch-action:none;"></canvas>
|
||||
</MudPaper>
|
||||
</MudTabPanel>
|
||||
</MudTabs>
|
||||
|
||||
@code {
|
||||
private ElementReference _purchasing3dCanvas;
|
||||
private string _purchasing3dIndicator = "spend";
|
||||
private string _purchasing3dChartType = "bar";
|
||||
private double _purchasing3dFactor = 1d;
|
||||
private double _purchasing3dLabelScale = 1.5d;
|
||||
|
||||
private readonly List<PurchasingKpiCard> KpiCards =
|
||||
[
|
||||
new("Spend total", "Total spend", "-", "Netwr CHF historisch", "Historic Netwr CHF", Icons.Material.Filled.Payments, Color.Primary),
|
||||
@@ -189,12 +258,134 @@
|
||||
new("Matrix Vol./WG", "Pivot, Slicer", "Sum(EKPOSet.Netwr CHF)", "Warengruppe, Lieferant, Artikel")
|
||||
];
|
||||
|
||||
private readonly List<Purchasing3dIndicator> Purchasing3dIndicators =
|
||||
[
|
||||
new("spend", "Spend CHF", "Spend CHF", "CHF"),
|
||||
new("openValue", "Offener Bestellwert", "Open order value", "CHF"),
|
||||
new("openQuantity", "Offene Menge", "Open quantity", "Qty"),
|
||||
new("contractValue", "Kontrakt-Restwert", "Contract remaining value", "CHF"),
|
||||
new("supplierScore", "Lieferantenperformance", "Supplier performance", "%")
|
||||
];
|
||||
|
||||
private readonly List<Purchasing3dBaseRow> Purchasing3dBaseRows =
|
||||
[
|
||||
new("Lieferant A", 2024, 1450000d, 260000d, 11800d, 420000d, 91d),
|
||||
new("Lieferant A", 2025, 1680000d, 310000d, 13200d, 460000d, 93d),
|
||||
new("Lieferant A", 2026, 1820000d, 335000d, 14100d, 490000d, 92d),
|
||||
new("Lieferant B", 2024, 980000d, 190000d, 9300d, 260000d, 86d),
|
||||
new("Lieferant B", 2025, 1120000d, 225000d, 10100d, 315000d, 88d),
|
||||
new("Lieferant B", 2026, 1240000d, 250000d, 10800d, 350000d, 89d),
|
||||
new("Warengruppe Sensorik", 2024, 760000d, 120000d, 6400d, 210000d, 94d),
|
||||
new("Warengruppe Sensorik", 2025, 890000d, 145000d, 7100d, 230000d, 95d),
|
||||
new("Warengruppe Sensorik", 2026, 940000d, 155000d, 7400d, 245000d, 94d),
|
||||
new("Artikel Top 10", 2024, 520000d, 83000d, 2800d, 160000d, 90d),
|
||||
new("Artikel Top 10", 2025, 610000d, 97000d, 3100d, 180000d, 91d),
|
||||
new("Artikel Top 10", 2026, 680000d, 105000d, 3350d, 195000d, 92d)
|
||||
];
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
await RenderPurchasing3dAsync();
|
||||
}
|
||||
|
||||
private string T(string german, string english) => UiText.Text(german, english);
|
||||
|
||||
private async Task SetPurchasing3dIndicator(string value)
|
||||
{
|
||||
_purchasing3dIndicator = value;
|
||||
await RenderPurchasing3dAsync();
|
||||
}
|
||||
|
||||
private async Task SetPurchasing3dChartType(string value)
|
||||
{
|
||||
_purchasing3dChartType = value;
|
||||
await RenderPurchasing3dAsync();
|
||||
}
|
||||
|
||||
private async Task SetPurchasing3dFactor(ChangeEventArgs args)
|
||||
{
|
||||
if (double.TryParse(Convert.ToString(args.Value, CultureInfo.InvariantCulture), NumberStyles.Number, CultureInfo.InvariantCulture, out var value))
|
||||
{
|
||||
_purchasing3dFactor = Math.Clamp(value, 0.5d, 1.5d);
|
||||
await JsRuntime.InvokeVoidAsync("trafagFinance3d.updateFactor", _purchasing3dCanvas, ScenarioAffectsPurchasingValue ? _purchasing3dFactor : 1d);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SetPurchasing3dFactorPreset(double value)
|
||||
{
|
||||
_purchasing3dFactor = Math.Clamp(value, 0.5d, 1.5d);
|
||||
await JsRuntime.InvokeVoidAsync("trafagFinance3d.updateFactor", _purchasing3dCanvas, ScenarioAffectsPurchasingValue ? _purchasing3dFactor : 1d);
|
||||
}
|
||||
|
||||
private async Task SetPurchasing3dLabelScale(ChangeEventArgs args)
|
||||
{
|
||||
if (double.TryParse(Convert.ToString(args.Value, CultureInfo.InvariantCulture), NumberStyles.Number, CultureInfo.InvariantCulture, out var value))
|
||||
{
|
||||
_purchasing3dLabelScale = Math.Clamp(value, 0.8d, 2.5d);
|
||||
await RenderPurchasing3dAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RenderPurchasing3dAsync()
|
||||
{
|
||||
await JsRuntime.InvokeVoidAsync("trafagFinance3d.render", _purchasing3dCanvas, BuildPurchasing3dRows(), new
|
||||
{
|
||||
indicator = _purchasing3dIndicator,
|
||||
title = ResolvePurchasing3dIndicatorLabel(),
|
||||
chartType = _purchasing3dChartType,
|
||||
xAxis = T("X: Lieferant / Warengruppe / Artikel", "X: supplier / material group / article"),
|
||||
yAxis = T("Y: Wert / Menge / Score", "Y: value / quantity / score"),
|
||||
zAxis = T("Z: Jahr / Zeit", "Z: year / time"),
|
||||
pieAxis = T("Kreis: Anteil am aktuellen Indikator", "Pie: share of current indicator"),
|
||||
labelScale = _purchasing3dLabelScale,
|
||||
scenarioFactor = ScenarioAffectsPurchasingValue ? _purchasing3dFactor : 1d
|
||||
});
|
||||
}
|
||||
|
||||
private IReadOnlyList<object> BuildPurchasing3dRows()
|
||||
=> Purchasing3dBaseRows
|
||||
.Select(row => new
|
||||
{
|
||||
country = row.Axis,
|
||||
year = row.Year,
|
||||
value = ResolvePurchasing3dValue(row)
|
||||
})
|
||||
.Cast<object>()
|
||||
.ToList();
|
||||
|
||||
private double ResolvePurchasing3dValue(Purchasing3dBaseRow row) => _purchasing3dIndicator switch
|
||||
{
|
||||
"openValue" => row.OpenValue,
|
||||
"openQuantity" => row.OpenQuantity,
|
||||
"contractValue" => row.ContractValue,
|
||||
"supplierScore" => row.SupplierScore,
|
||||
_ => row.Spend
|
||||
};
|
||||
|
||||
private bool ScenarioAffectsPurchasingValue => _purchasing3dIndicator is "spend" or "openValue" or "contractValue";
|
||||
|
||||
private string ResolvePurchasing3dIndicatorLabel()
|
||||
=> T(
|
||||
Purchasing3dIndicators.FirstOrDefault(x => x.Key == _purchasing3dIndicator)?.TitleDe ?? "Spend CHF",
|
||||
Purchasing3dIndicators.FirstOrDefault(x => x.Key == _purchasing3dIndicator)?.TitleEn ?? "Spend CHF");
|
||||
|
||||
private string FormatScenarioDelta()
|
||||
{
|
||||
if (!ScenarioAffectsPurchasingValue)
|
||||
return T("nicht auf diesen Indikator angewendet", "not applied to this indicator");
|
||||
|
||||
var baseTotal = Purchasing3dBaseRows.Sum(ResolvePurchasing3dValue);
|
||||
var delta = baseTotal * _purchasing3dFactor - baseTotal;
|
||||
return $"{delta:N0} {Purchasing3dIndicators.First(x => x.Key == _purchasing3dIndicator).Unit}";
|
||||
}
|
||||
|
||||
private sealed record PurchasingKpiCard(string TitleDe, string TitleEn, string Value, string DetailDe, string DetailEn, string Icon, Color Color);
|
||||
private sealed record PurchasingAxis(string LabelDe, string LabelEn, string Field, string UsageDe, string UsageEn);
|
||||
private sealed record PurchasingSource(string Name, string Description);
|
||||
private sealed record PowerBiPageInfo(string Page, string Visuals, string Measure, string Dimensions);
|
||||
private sealed record Purchasing3dIndicator(string Key, string TitleDe, string TitleEn, string Unit);
|
||||
private sealed record Purchasing3dBaseRow(string Axis, int Year, double Spend, double OpenValue, double OpenQuantity, double ContractValue, double SupplierScore);
|
||||
}
|
||||
|
||||
<style>
|
||||
@@ -218,4 +409,10 @@
|
||||
.purchasing-source-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.purchasing-3d-surface {
|
||||
height: calc(100vh - 300px);
|
||||
min-height: 620px;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
@page "/einkauf/verbindungen"
|
||||
@using TrafagSalesExporter.Services
|
||||
@inject IPurchasingDataSourcePageService DataSourceService
|
||||
@inject TrafagSalesExporter.Services.IUiTextService UiText
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<PageTitle>@T("Einkauf Datenquellen", "Purchasing data sources")</PageTitle>
|
||||
|
||||
<MudText Typo="Typo.h4" Class="mb-1">@T("Einkauf Datenquellen", "Purchasing data sources")</MudText>
|
||||
<MudText Typo="Typo.body2" Class="mb-4 purchasing-muted">
|
||||
@T("Grafische SAP/OData-Anbindung fuer das Einkaufsdashboard, analog zur Finance-Quellenpflege.",
|
||||
"Graphical SAP/OData connection for the purchasing dashboard, following the finance source configuration pattern.")
|
||||
</MudText>
|
||||
|
||||
@if (_loading)
|
||||
{
|
||||
<MudProgressLinear Indeterminate="true" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudGrid Spacing="2" Class="mb-4">
|
||||
<MudItem xs="12" md="7">
|
||||
<MudPaper Class="pa-3" Outlined="true">
|
||||
<MudText Typo="Typo.h6" Class="mb-3">@T("Verbindung", "Connection")</MudText>
|
||||
<MudGrid Spacing="2">
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudTextField Value="_state.Site.TSC" Label="TSC" ReadOnly="true" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudTextField Value="_state.Site.SourceSystem" Label="@T("Quellsystem", "Source system")" ReadOnly="true" />
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudTextField @bind-Value="_state.Site.SapServiceUrl"
|
||||
Label="SAP Service URL Override"
|
||||
HelperText="@T("Leer lassen = zentrale SAP OData URL aus Settings verwenden.", "Leave empty = use central SAP OData URL from settings.")" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudTextField @bind-Value="_state.Site.UsernameOverride"
|
||||
Label="Username Override"
|
||||
HelperText="@T("Leer lassen = zentraler SAP User.", "Leave empty = central SAP user.")" />
|
||||
</MudItem>
|
||||
<MudItem xs="12" sm="6">
|
||||
<MudTextField @bind-Value="_state.Site.PasswordOverride"
|
||||
Label="Password Override"
|
||||
InputType="InputType.Password"
|
||||
HelperText="@T("Leer lassen = zentrales SAP Passwort.", "Leave empty = central SAP password.")" />
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudCheckBox @bind-Value="_state.Site.IsActive"
|
||||
Label="@T("Einkaufsquelle fuer Import aktivieren", "Activate purchasing source for import")" />
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
<MudStack Row="true" Spacing="2" Class="mt-3">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Save" OnClick="SaveAsync" Disabled="_busy">
|
||||
@T("Speichern", "Save")
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Info" StartIcon="@Icons.Material.Filled.NetworkCheck" OnClick="TestConnectionAsync" Disabled="_busy">
|
||||
@T("Verbindung testen", "Test connection")
|
||||
</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Restore" OnClick="ResetDefaultsAsync" Disabled="_busy">
|
||||
@T("Defaults wiederherstellen", "Restore defaults")
|
||||
</MudButton>
|
||||
</MudStack>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="12" md="5">
|
||||
<MudPaper Class="pa-3" Outlined="true">
|
||||
<MudText Typo="Typo.h6" Class="mb-2">@T("Aktuelle Basis", "Current basis")</MudText>
|
||||
<div class="purchasing-source-row">
|
||||
<MudIcon Icon="@Icons.Material.Filled.CloudQueue" Size="Size.Small" />
|
||||
<span>@T("Zentrale URL", "Central URL"): <code>@Display(_state.SourceSystem?.CentralServiceUrl)</code></span>
|
||||
</div>
|
||||
<div class="purchasing-source-row">
|
||||
<MudIcon Icon="@Icons.Material.Filled.TableChart" Size="Size.Small" />
|
||||
<span>@T("Quellen", "Sources"): @_state.Sources.Count(x => x.IsActive)</span>
|
||||
</div>
|
||||
<div class="purchasing-source-row">
|
||||
<MudIcon Icon="@Icons.Material.Filled.AccountTree" Size="Size.Small" />
|
||||
<span>@T("Joins", "Joins"): @_state.Joins.Count(x => x.IsActive)</span>
|
||||
</div>
|
||||
<div class="purchasing-source-row">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Schema" Size="Size.Small" />
|
||||
<span>@T("Mappings", "Mappings"): @_state.Mappings.Count(x => x.IsActive)</span>
|
||||
</div>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudTabs Elevation="1" Rounded="false" PanelClass="pt-4">
|
||||
<MudTabPanel Text="@T("Quellen", "Sources")" Icon="@Icons.Material.Filled.Hub">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mb-2">
|
||||
<MudText Typo="Typo.h6">@T("OData Entity Sets", "OData entity sets")</MudText>
|
||||
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Add" OnClick="AddSource">@T("Quelle", "Source")</MudButton>
|
||||
</MudStack>
|
||||
<MudTable Items="_state.Sources" Dense="true" Hover="true" Striped="true">
|
||||
<HeaderContent>
|
||||
<MudTh>Alias</MudTh>
|
||||
<MudTh>Entity Set</MudTh>
|
||||
<MudTh>@T("Primaer", "Primary")</MudTh>
|
||||
<MudTh>@T("Aktiv", "Active")</MudTh>
|
||||
<MudTh></MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd><MudTextField @bind-Value="context.Alias" Dense="true" /></MudTd>
|
||||
<MudTd><MudTextField @bind-Value="context.EntitySet" Dense="true" /></MudTd>
|
||||
<MudTd><MudCheckBox @bind-Value="context.IsPrimary" Dense="true" /></MudTd>
|
||||
<MudTd><MudCheckBox @bind-Value="context.IsActive" Dense="true" /></MudTd>
|
||||
<MudTd><MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" Size="Size.Small" OnClick="() => RemoveSource(context)" /></MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
</MudTabPanel>
|
||||
|
||||
<MudTabPanel Text="@T("Join-Fluss", "Join flow")" Icon="@Icons.Material.Filled.AccountTree">
|
||||
<MudGrid Spacing="2" Class="mb-3">
|
||||
@foreach (var join in _state.Joins.Where(x => x.IsActive))
|
||||
{
|
||||
<MudItem xs="12" sm="6" lg="3">
|
||||
<MudPaper Class="pa-3 purchasing-flow" Outlined="true">
|
||||
<MudText Typo="Typo.subtitle2">@join.LeftAlias -> @join.RightAlias</MudText>
|
||||
<MudText Typo="Typo.caption">@join.LeftKeys = @join.RightKeys</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
}
|
||||
</MudGrid>
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mb-2">
|
||||
<MudText Typo="Typo.h6">@T("Joins", "Joins")</MudText>
|
||||
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Add" OnClick="AddJoin">@T("Join", "Join")</MudButton>
|
||||
</MudStack>
|
||||
<MudTable Items="_state.Joins" Dense="true" Hover="true" Striped="true">
|
||||
<HeaderContent>
|
||||
<MudTh>@T("Links", "Left")</MudTh>
|
||||
<MudTh>Left Keys</MudTh>
|
||||
<MudTh>@T("Rechts", "Right")</MudTh>
|
||||
<MudTh>Right Keys</MudTh>
|
||||
<MudTh>@T("Typ", "Type")</MudTh>
|
||||
<MudTh>@T("Aktiv", "Active")</MudTh>
|
||||
<MudTh></MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd><MudTextField @bind-Value="context.LeftAlias" Dense="true" /></MudTd>
|
||||
<MudTd><MudTextField @bind-Value="context.LeftKeys" Dense="true" /></MudTd>
|
||||
<MudTd><MudTextField @bind-Value="context.RightAlias" Dense="true" /></MudTd>
|
||||
<MudTd><MudTextField @bind-Value="context.RightKeys" Dense="true" /></MudTd>
|
||||
<MudTd><MudTextField @bind-Value="context.JoinType" Dense="true" /></MudTd>
|
||||
<MudTd><MudCheckBox @bind-Value="context.IsActive" Dense="true" /></MudTd>
|
||||
<MudTd><MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" Size="Size.Small" OnClick="() => RemoveJoin(context)" /></MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
</MudTabPanel>
|
||||
|
||||
<MudTabPanel Text="@T("Mapping", "Mapping")" Icon="@Icons.Material.Filled.Schema">
|
||||
<MudStack Row="true" Justify="Justify.SpaceBetween" AlignItems="AlignItems.Center" Class="mb-2">
|
||||
<MudText Typo="Typo.h6">@T("Zielfelder", "Target fields")</MudText>
|
||||
<MudButton Variant="Variant.Outlined" StartIcon="@Icons.Material.Filled.Add" OnClick="AddMapping">@T("Mapping", "Mapping")</MudButton>
|
||||
</MudStack>
|
||||
<MudTable Items="_state.Mappings" Dense="true" Hover="true" Striped="true">
|
||||
<HeaderContent>
|
||||
<MudTh>@T("Ziel", "Target")</MudTh>
|
||||
<MudTh>@T("Quelle", "Source")</MudTh>
|
||||
<MudTh>@T("Pflicht", "Required")</MudTh>
|
||||
<MudTh>@T("Aktiv", "Active")</MudTh>
|
||||
<MudTh></MudTh>
|
||||
</HeaderContent>
|
||||
<RowTemplate>
|
||||
<MudTd><MudTextField @bind-Value="context.TargetField" Dense="true" /></MudTd>
|
||||
<MudTd><MudTextField @bind-Value="context.SourceExpression" Dense="true" /></MudTd>
|
||||
<MudTd><MudCheckBox @bind-Value="context.IsRequired" Dense="true" /></MudTd>
|
||||
<MudTd><MudCheckBox @bind-Value="context.IsActive" Dense="true" /></MudTd>
|
||||
<MudTd><MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" Size="Size.Small" OnClick="() => RemoveMapping(context)" /></MudTd>
|
||||
</RowTemplate>
|
||||
</MudTable>
|
||||
</MudTabPanel>
|
||||
</MudTabs>
|
||||
}
|
||||
|
||||
@code {
|
||||
private PurchasingDataSourceState _state = new();
|
||||
private bool _loading = true;
|
||||
private bool _busy;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_state = await DataSourceService.LoadAsync();
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
private async Task SaveAsync()
|
||||
{
|
||||
await RunAsync(async () =>
|
||||
{
|
||||
_state = await DataSourceService.SaveAsync(_state);
|
||||
Snackbar.Add(T("Einkaufsdatenquellen gespeichert.", "Purchasing data sources saved."), Severity.Success);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task ResetDefaultsAsync()
|
||||
{
|
||||
await RunAsync(async () =>
|
||||
{
|
||||
_state = await DataSourceService.ResetDefaultsAsync();
|
||||
Snackbar.Add(T("Einkaufsdatenquellen auf Defaults gesetzt.", "Purchasing data sources restored to defaults."), Severity.Info);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task TestConnectionAsync()
|
||||
{
|
||||
await RunAsync(async () =>
|
||||
{
|
||||
var result = await DataSourceService.TestConnectionAsync(_state);
|
||||
Snackbar.Add(result.Message, result.Success ? Severity.Success : result.Warning ? Severity.Warning : Severity.Error);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task RunAsync(Func<Task> action)
|
||||
{
|
||||
if (_busy)
|
||||
return;
|
||||
|
||||
_busy = true;
|
||||
try
|
||||
{
|
||||
await action();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Snackbar.Add(ex.Message, Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddSource()
|
||||
=> _state.Sources.Add(new SapSourceDefinition { Alias = "NEW", EntitySet = "NewEntitySet", IsActive = true, SortOrder = (_state.Sources.Count + 1) * 10 });
|
||||
|
||||
private void AddJoin()
|
||||
=> _state.Joins.Add(new SapJoinDefinition { LeftAlias = "EKKO", RightAlias = "NEW", LeftKeys = "Key", RightKeys = "Key", JoinType = "Left", IsActive = true, SortOrder = (_state.Joins.Count + 1) * 10 });
|
||||
|
||||
private void AddMapping()
|
||||
=> _state.Mappings.Add(new SapFieldMapping { TargetField = "NewField", SourceExpression = "Alias.Field", IsActive = true, SortOrder = (_state.Mappings.Count + 1) * 10 });
|
||||
|
||||
private void RemoveSource(SapSourceDefinition source) => _state.Sources.Remove(source);
|
||||
private void RemoveJoin(SapJoinDefinition join) => _state.Joins.Remove(join);
|
||||
private void RemoveMapping(SapFieldMapping mapping) => _state.Mappings.Remove(mapping);
|
||||
|
||||
private string T(string german, string english) => UiText.Text(german, english);
|
||||
private static string Display(string? value) => string.IsNullOrWhiteSpace(value) ? "-" : value;
|
||||
}
|
||||
|
||||
<style>
|
||||
.purchasing-muted {
|
||||
color: var(--mud-palette-text-secondary);
|
||||
}
|
||||
|
||||
.purchasing-flow {
|
||||
min-height: 86px;
|
||||
}
|
||||
|
||||
.purchasing-source-row {
|
||||
display: grid;
|
||||
grid-template-columns: 26px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 7px 0;
|
||||
border-bottom: 1px solid var(--mud-palette-lines-default);
|
||||
}
|
||||
|
||||
.purchasing-source-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -114,6 +114,7 @@ builder.Services.AddScoped<IDashboardPageService, DashboardPageService>();
|
||||
builder.Services.AddScoped<ILogsPageService, LogsPageService>();
|
||||
builder.Services.AddScoped<ITransformationsPageService, TransformationsPageService>();
|
||||
builder.Services.AddScoped<IFinanceRulesPageService, FinanceRulesPageService>();
|
||||
builder.Services.AddScoped<IPurchasingDataSourcePageService, PurchasingDataSourcePageService>();
|
||||
builder.Services.AddScoped<IHrKpiAccessService, HrKpiAccessService>();
|
||||
builder.Services.AddScoped<IFinanceCockpitAccessService, FinanceCockpitAccessService>();
|
||||
builder.Services.AddScoped<IAdminAccessService, AdminAccessService>();
|
||||
|
||||
@@ -17,6 +17,7 @@ public class DatabaseSeedService : IDatabaseSeedService
|
||||
EnsureGermanyManualExcelSite(db);
|
||||
EnsureUkManualExcelFolder(db);
|
||||
EnsureSapODataDachSite(db);
|
||||
EnsurePurchasingSapSite(db);
|
||||
EnsureFinanceReferenceDefaults(db);
|
||||
EnsureBudgetExchangeRateDefaults(db);
|
||||
EnsureFinanceIntercompanyRuleDefaults(db);
|
||||
@@ -180,6 +181,7 @@ public class DatabaseSeedService : IDatabaseSeedService
|
||||
Link("hr-training", "hr", "HR KPI Schulung", "HR KPI training", "School", "hr-kpi/schulung", 20),
|
||||
Group("purchasing", null, "Einkauf", "Purchasing", "ShoppingCart", 30),
|
||||
Link("purchasing-dashboard", "purchasing", "Einkauf Dashboard", "Purchasing dashboard", "Dashboard", "einkauf", 10, "All"),
|
||||
Link("purchasing-data-sources", "purchasing", "Datenquellen", "Data sources", "Hub", "einkauf/verbindungen", 20, "All"),
|
||||
Link("admin-sessions", null, "Admin Bereich", "Admin area", "PeopleAlt", "admin/sessions", 90)
|
||||
];
|
||||
|
||||
@@ -968,6 +970,89 @@ public class DatabaseSeedService : IDatabaseSeedService
|
||||
db.SaveChanges();
|
||||
}
|
||||
|
||||
private static void EnsurePurchasingSapSite(AppDbContext db)
|
||||
{
|
||||
if (db.Sites.Count() <= 1)
|
||||
return;
|
||||
|
||||
var site = db.Sites
|
||||
.OrderBy(x => x.Id)
|
||||
.FirstOrDefault(x => x.TSC == PurchasingDataSourcePageService.PurchasingTsc);
|
||||
|
||||
var changed = false;
|
||||
if (site is null)
|
||||
{
|
||||
site = new Site
|
||||
{
|
||||
Schema = string.Empty,
|
||||
TSC = PurchasingDataSourcePageService.PurchasingTsc,
|
||||
Land = "Einkauf SAP",
|
||||
SourceSystem = "SAP",
|
||||
IsActive = false
|
||||
};
|
||||
db.Sites.Add(site);
|
||||
db.SaveChanges();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (site.SourceSystem != "SAP")
|
||||
{
|
||||
site.SourceSystem = "SAP";
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(site.Land))
|
||||
{
|
||||
site.Land = "Einkauf SAP";
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!db.SapSourceDefinitions.Any(x => x.SiteId == site.Id))
|
||||
{
|
||||
db.SapSourceDefinitions.AddRange(
|
||||
new SapSourceDefinition { SiteId = site.Id, Alias = "EKKO", EntitySet = "EKKOSet", IsPrimary = true, IsActive = true, SortOrder = 10 },
|
||||
new SapSourceDefinition { SiteId = site.Id, Alias = "EKPO", EntitySet = "EKPOSet", IsPrimary = false, IsActive = true, SortOrder = 20 },
|
||||
new SapSourceDefinition { SiteId = site.Id, Alias = "EKET", EntitySet = "eketSet", IsPrimary = false, IsActive = true, SortOrder = 30 },
|
||||
new SapSourceDefinition { SiteId = site.Id, Alias = "LIEF", EntitySet = "Data", IsPrimary = false, IsActive = true, SortOrder = 40 },
|
||||
new SapSourceDefinition { SiteId = site.Id, Alias = "WG", EntitySet = "Data2", IsPrimary = false, IsActive = true, SortOrder = 50 });
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!db.SapJoinDefinitions.Any(x => x.SiteId == site.Id))
|
||||
{
|
||||
db.SapJoinDefinitions.AddRange(
|
||||
new SapJoinDefinition { SiteId = site.Id, LeftAlias = "EKKO", RightAlias = "EKPO", LeftKeys = "Ebeln", RightKeys = "Ebeln", JoinType = "Left", IsActive = true, SortOrder = 10 },
|
||||
new SapJoinDefinition { SiteId = site.Id, LeftAlias = "EKPO", RightAlias = "EKET", LeftKeys = "Ebeln,Ebelp", RightKeys = "Ebeln,Ebelp", JoinType = "Left", IsActive = true, SortOrder = 20 },
|
||||
new SapJoinDefinition { SiteId = site.Id, LeftAlias = "EKKO", RightAlias = "LIEF", LeftKeys = "Lifnr", RightKeys = "Lifnr", JoinType = "Left", IsActive = true, SortOrder = 30 },
|
||||
new SapJoinDefinition { SiteId = site.Id, LeftAlias = "EKPO", RightAlias = "WG", LeftKeys = "Matkl", RightKeys = "Matkl", JoinType = "Left", IsActive = true, SortOrder = 40 });
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!db.SapFieldMappings.Any(x => x.SiteId == site.Id))
|
||||
{
|
||||
db.SapFieldMappings.AddRange(
|
||||
new SapFieldMapping { SiteId = site.Id, TargetField = "PurchaseOrder", SourceExpression = "EKKO.Ebeln", IsRequired = true, IsActive = true, SortOrder = 10 },
|
||||
new SapFieldMapping { SiteId = site.Id, TargetField = "PurchaseOrderDate", SourceExpression = "EKKO.Bedat", IsRequired = true, IsActive = true, SortOrder = 20 },
|
||||
new SapFieldMapping { SiteId = site.Id, TargetField = "SupplierNumber", SourceExpression = "EKKO.Lifnr", IsRequired = false, IsActive = true, SortOrder = 30 },
|
||||
new SapFieldMapping { SiteId = site.Id, TargetField = "SupplierName", SourceExpression = "LIEF.Name", IsRequired = false, IsActive = true, SortOrder = 40 },
|
||||
new SapFieldMapping { SiteId = site.Id, TargetField = "Position", SourceExpression = "EKPO.Ebelp", IsRequired = true, IsActive = true, SortOrder = 50 },
|
||||
new SapFieldMapping { SiteId = site.Id, TargetField = "Material", SourceExpression = "EKPO.Matnr", IsRequired = false, IsActive = true, SortOrder = 60 },
|
||||
new SapFieldMapping { SiteId = site.Id, TargetField = "MaterialText", SourceExpression = "EKPO.Txz01", IsRequired = false, IsActive = true, SortOrder = 70 },
|
||||
new SapFieldMapping { SiteId = site.Id, TargetField = "MaterialGroup", SourceExpression = "EKPO.Matkl", IsRequired = false, IsActive = true, SortOrder = 80 },
|
||||
new SapFieldMapping { SiteId = site.Id, TargetField = "MaterialGroupText", SourceExpression = "WG.WgKomplett", IsRequired = false, IsActive = true, SortOrder = 90 },
|
||||
new SapFieldMapping { SiteId = site.Id, TargetField = "NetValueChf", SourceExpression = "EKPO.NetwrChf", IsRequired = false, IsActive = true, SortOrder = 100 },
|
||||
new SapFieldMapping { SiteId = site.Id, TargetField = "NetValueChfPerPiece", SourceExpression = "EKPO.NetwrChfStk", IsRequired = false, IsActive = true, SortOrder = 110 },
|
||||
new SapFieldMapping { SiteId = site.Id, TargetField = "OrderQuantity", SourceExpression = "EKPO.Menge", IsRequired = false, IsActive = true, SortOrder = 120 },
|
||||
new SapFieldMapping { SiteId = site.Id, TargetField = "ScheduleDate", SourceExpression = "EKET.Eindt", IsRequired = false, IsActive = true, SortOrder = 130 },
|
||||
new SapFieldMapping { SiteId = site.Id, TargetField = "ScheduleQuantity", SourceExpression = "EKET.Menge", IsRequired = false, IsActive = true, SortOrder = 140 });
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed)
|
||||
db.SaveChanges();
|
||||
}
|
||||
|
||||
private static void EnsureFinanceReferenceDefaults(AppDbContext db)
|
||||
{
|
||||
var defaults = new[]
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
using TrafagSalesExporter.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public interface IPurchasingDataSourcePageService
|
||||
{
|
||||
Task<PurchasingDataSourceState> LoadAsync();
|
||||
Task<PurchasingDataSourceState> SaveAsync(PurchasingDataSourceState state);
|
||||
Task<PurchasingDataSourceState> ResetDefaultsAsync();
|
||||
Task<PageActionResult> TestConnectionAsync(PurchasingDataSourceState state);
|
||||
}
|
||||
|
||||
public sealed class PurchasingDataSourceState
|
||||
{
|
||||
public Site Site { get; set; } = new();
|
||||
public SourceSystemDefinition? SourceSystem { get; set; }
|
||||
public List<SapSourceDefinition> Sources { get; set; } = [];
|
||||
public List<SapJoinDefinition> Joins { get; set; } = [];
|
||||
public List<SapFieldMapping> Mappings { get; set; } = [];
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TrafagSalesExporter.Data;
|
||||
using TrafagSalesExporter.Models;
|
||||
|
||||
namespace TrafagSalesExporter.Services;
|
||||
|
||||
public sealed class PurchasingDataSourcePageService : IPurchasingDataSourcePageService
|
||||
{
|
||||
public const string PurchasingTsc = "PURCHASING_SAP";
|
||||
|
||||
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
||||
private readonly ISapGatewayService _sapGatewayService;
|
||||
|
||||
public PurchasingDataSourcePageService(IDbContextFactory<AppDbContext> dbFactory, ISapGatewayService sapGatewayService)
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
_sapGatewayService = sapGatewayService;
|
||||
}
|
||||
|
||||
public async Task<PurchasingDataSourceState> LoadAsync()
|
||||
{
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
await EnsureDefaultsAsync(db);
|
||||
return await LoadStateAsync(db);
|
||||
}
|
||||
|
||||
public async Task<PurchasingDataSourceState> SaveAsync(PurchasingDataSourceState state)
|
||||
{
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var site = await GetOrCreateSiteAsync(db);
|
||||
|
||||
site.SapServiceUrl = state.Site.SapServiceUrl.Trim();
|
||||
site.UsernameOverride = state.Site.UsernameOverride.Trim();
|
||||
site.PasswordOverride = state.Site.PasswordOverride;
|
||||
site.IsActive = state.Site.IsActive;
|
||||
|
||||
Replace(db, db.SapSourceDefinitions.Where(x => x.SiteId == site.Id), state.Sources.Select((x, i) => new SapSourceDefinition
|
||||
{
|
||||
SiteId = site.Id,
|
||||
Alias = x.Alias.Trim(),
|
||||
EntitySet = x.EntitySet.Trim(),
|
||||
IsPrimary = x.IsPrimary,
|
||||
IsActive = x.IsActive,
|
||||
SortOrder = x.SortOrder == 0 ? i * 10 : x.SortOrder
|
||||
}));
|
||||
|
||||
Replace(db, db.SapJoinDefinitions.Where(x => x.SiteId == site.Id), state.Joins.Select((x, i) => new SapJoinDefinition
|
||||
{
|
||||
SiteId = site.Id,
|
||||
LeftAlias = x.LeftAlias.Trim(),
|
||||
RightAlias = x.RightAlias.Trim(),
|
||||
LeftKeys = x.LeftKeys.Trim(),
|
||||
RightKeys = x.RightKeys.Trim(),
|
||||
JoinType = string.IsNullOrWhiteSpace(x.JoinType) ? "Left" : x.JoinType.Trim(),
|
||||
IsActive = x.IsActive,
|
||||
SortOrder = x.SortOrder == 0 ? i * 10 : x.SortOrder
|
||||
}));
|
||||
|
||||
Replace(db, db.SapFieldMappings.Where(x => x.SiteId == site.Id), state.Mappings.Select((x, i) => new SapFieldMapping
|
||||
{
|
||||
SiteId = site.Id,
|
||||
TargetField = x.TargetField.Trim(),
|
||||
SourceExpression = x.SourceExpression.Trim(),
|
||||
IsRequired = x.IsRequired,
|
||||
IsActive = x.IsActive,
|
||||
SortOrder = x.SortOrder == 0 ? i * 10 : x.SortOrder
|
||||
}));
|
||||
|
||||
await db.SaveChangesAsync();
|
||||
return await LoadStateAsync(db);
|
||||
}
|
||||
|
||||
public async Task<PurchasingDataSourceState> ResetDefaultsAsync()
|
||||
{
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var site = await GetOrCreateSiteAsync(db);
|
||||
|
||||
db.SapSourceDefinitions.RemoveRange(db.SapSourceDefinitions.Where(x => x.SiteId == site.Id));
|
||||
db.SapJoinDefinitions.RemoveRange(db.SapJoinDefinitions.Where(x => x.SiteId == site.Id));
|
||||
db.SapFieldMappings.RemoveRange(db.SapFieldMappings.Where(x => x.SiteId == site.Id));
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
AddDefaultSources(db, site.Id);
|
||||
AddDefaultJoins(db, site.Id);
|
||||
AddDefaultMappings(db, site.Id);
|
||||
await db.SaveChangesAsync();
|
||||
|
||||
return await LoadStateAsync(db);
|
||||
}
|
||||
|
||||
public async Task<PageActionResult> TestConnectionAsync(PurchasingDataSourceState state)
|
||||
{
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var sourceSystem = await db.SourceSystemDefinitions
|
||||
.AsNoTracking()
|
||||
.OrderBy(x => x.Id)
|
||||
.FirstOrDefaultAsync(x => x.Code == "SAP");
|
||||
|
||||
var serviceUrl = string.IsNullOrWhiteSpace(state.Site.SapServiceUrl)
|
||||
? sourceSystem?.CentralServiceUrl ?? string.Empty
|
||||
: state.Site.SapServiceUrl;
|
||||
var username = string.IsNullOrWhiteSpace(state.Site.UsernameOverride)
|
||||
? sourceSystem?.CentralUsername ?? string.Empty
|
||||
: state.Site.UsernameOverride;
|
||||
var password = string.IsNullOrWhiteSpace(state.Site.PasswordOverride)
|
||||
? sourceSystem?.CentralPassword ?? string.Empty
|
||||
: state.Site.PasswordOverride;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(serviceUrl))
|
||||
return PageActionResult.WarningResult("Keine SAP Service URL gepflegt.");
|
||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
||||
return PageActionResult.WarningResult("Keine SAP Gateway Zugangsdaten gepflegt.");
|
||||
|
||||
try
|
||||
{
|
||||
await _sapGatewayService.TestConnectionAsync(serviceUrl.Trim(), username.Trim(), password);
|
||||
return PageActionResult.SuccessResult("SAP OData Verbindung erfolgreich.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return PageActionResult.ErrorResult($"SAP OData Verbindung fehlgeschlagen: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<PurchasingDataSourceState> LoadStateAsync(AppDbContext db)
|
||||
{
|
||||
var site = await GetOrCreateSiteAsync(db);
|
||||
var sourceSystem = await db.SourceSystemDefinitions
|
||||
.AsNoTracking()
|
||||
.OrderBy(x => x.Id)
|
||||
.FirstOrDefaultAsync(x => x.Code == "SAP");
|
||||
|
||||
return new PurchasingDataSourceState
|
||||
{
|
||||
Site = Clone(site),
|
||||
SourceSystem = sourceSystem,
|
||||
Sources = await db.SapSourceDefinitions.AsNoTracking().Where(x => x.SiteId == site.Id).OrderBy(x => x.SortOrder).ThenBy(x => x.Id).ToListAsync(),
|
||||
Joins = await db.SapJoinDefinitions.AsNoTracking().Where(x => x.SiteId == site.Id).OrderBy(x => x.SortOrder).ThenBy(x => x.Id).ToListAsync(),
|
||||
Mappings = await db.SapFieldMappings.AsNoTracking().Where(x => x.SiteId == site.Id).OrderBy(x => x.SortOrder).ThenBy(x => x.Id).ToListAsync()
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task EnsureDefaultsAsync(AppDbContext db)
|
||||
{
|
||||
var site = await GetOrCreateSiteAsync(db);
|
||||
var hasSources = await db.SapSourceDefinitions.AnyAsync(x => x.SiteId == site.Id);
|
||||
if (hasSources)
|
||||
return;
|
||||
|
||||
AddDefaultSources(db, site.Id);
|
||||
AddDefaultJoins(db, site.Id);
|
||||
AddDefaultMappings(db, site.Id);
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private static async Task<Site> GetOrCreateSiteAsync(AppDbContext db)
|
||||
{
|
||||
var site = await db.Sites.OrderBy(x => x.Id).FirstOrDefaultAsync(x => x.TSC == PurchasingTsc);
|
||||
if (site is not null)
|
||||
return site;
|
||||
|
||||
site = new Site
|
||||
{
|
||||
Schema = string.Empty,
|
||||
TSC = PurchasingTsc,
|
||||
Land = "Einkauf SAP",
|
||||
SourceSystem = "SAP",
|
||||
IsActive = false
|
||||
};
|
||||
db.Sites.Add(site);
|
||||
await db.SaveChangesAsync();
|
||||
return site;
|
||||
}
|
||||
|
||||
private static void AddDefaultSources(AppDbContext db, int siteId)
|
||||
{
|
||||
db.SapSourceDefinitions.AddRange(
|
||||
new SapSourceDefinition { SiteId = siteId, Alias = "EKKO", EntitySet = "EKKOSet", IsPrimary = true, IsActive = true, SortOrder = 10 },
|
||||
new SapSourceDefinition { SiteId = siteId, Alias = "EKPO", EntitySet = "EKPOSet", IsPrimary = false, IsActive = true, SortOrder = 20 },
|
||||
new SapSourceDefinition { SiteId = siteId, Alias = "EKET", EntitySet = "eketSet", IsPrimary = false, IsActive = true, SortOrder = 30 },
|
||||
new SapSourceDefinition { SiteId = siteId, Alias = "LIEF", EntitySet = "Data", IsPrimary = false, IsActive = true, SortOrder = 40 },
|
||||
new SapSourceDefinition { SiteId = siteId, Alias = "WG", EntitySet = "Data2", IsPrimary = false, IsActive = true, SortOrder = 50 });
|
||||
}
|
||||
|
||||
private static void AddDefaultJoins(AppDbContext db, int siteId)
|
||||
{
|
||||
db.SapJoinDefinitions.AddRange(
|
||||
new SapJoinDefinition { SiteId = siteId, LeftAlias = "EKKO", RightAlias = "EKPO", LeftKeys = "Ebeln", RightKeys = "Ebeln", JoinType = "Left", IsActive = true, SortOrder = 10 },
|
||||
new SapJoinDefinition { SiteId = siteId, LeftAlias = "EKPO", RightAlias = "EKET", LeftKeys = "Ebeln,Ebelp", RightKeys = "Ebeln,Ebelp", JoinType = "Left", IsActive = true, SortOrder = 20 },
|
||||
new SapJoinDefinition { SiteId = siteId, LeftAlias = "EKKO", RightAlias = "LIEF", LeftKeys = "Lifnr", RightKeys = "Lifnr", JoinType = "Left", IsActive = true, SortOrder = 30 },
|
||||
new SapJoinDefinition { SiteId = siteId, LeftAlias = "EKPO", RightAlias = "WG", LeftKeys = "Matkl", RightKeys = "Matkl", JoinType = "Left", IsActive = true, SortOrder = 40 });
|
||||
}
|
||||
|
||||
private static void AddDefaultMappings(AppDbContext db, int siteId)
|
||||
{
|
||||
db.SapFieldMappings.AddRange(
|
||||
new SapFieldMapping { SiteId = siteId, TargetField = "PurchaseOrder", SourceExpression = "EKKO.Ebeln", IsRequired = true, IsActive = true, SortOrder = 10 },
|
||||
new SapFieldMapping { SiteId = siteId, TargetField = "PurchaseOrderDate", SourceExpression = "EKKO.Bedat", IsRequired = true, IsActive = true, SortOrder = 20 },
|
||||
new SapFieldMapping { SiteId = siteId, TargetField = "SupplierNumber", SourceExpression = "EKKO.Lifnr", IsRequired = false, IsActive = true, SortOrder = 30 },
|
||||
new SapFieldMapping { SiteId = siteId, TargetField = "SupplierName", SourceExpression = "LIEF.Name", IsRequired = false, IsActive = true, SortOrder = 40 },
|
||||
new SapFieldMapping { SiteId = siteId, TargetField = "Position", SourceExpression = "EKPO.Ebelp", IsRequired = true, IsActive = true, SortOrder = 50 },
|
||||
new SapFieldMapping { SiteId = siteId, TargetField = "Material", SourceExpression = "EKPO.Matnr", IsRequired = false, IsActive = true, SortOrder = 60 },
|
||||
new SapFieldMapping { SiteId = siteId, TargetField = "MaterialText", SourceExpression = "EKPO.Txz01", IsRequired = false, IsActive = true, SortOrder = 70 },
|
||||
new SapFieldMapping { SiteId = siteId, TargetField = "MaterialGroup", SourceExpression = "EKPO.Matkl", IsRequired = false, IsActive = true, SortOrder = 80 },
|
||||
new SapFieldMapping { SiteId = siteId, TargetField = "MaterialGroupText", SourceExpression = "WG.WgKomplett", IsRequired = false, IsActive = true, SortOrder = 90 },
|
||||
new SapFieldMapping { SiteId = siteId, TargetField = "NetValueChf", SourceExpression = "EKPO.NetwrChf", IsRequired = false, IsActive = true, SortOrder = 100 },
|
||||
new SapFieldMapping { SiteId = siteId, TargetField = "NetValueChfPerPiece", SourceExpression = "EKPO.NetwrChfStk", IsRequired = false, IsActive = true, SortOrder = 110 },
|
||||
new SapFieldMapping { SiteId = siteId, TargetField = "OrderQuantity", SourceExpression = "EKPO.Menge", IsRequired = false, IsActive = true, SortOrder = 120 },
|
||||
new SapFieldMapping { SiteId = siteId, TargetField = "ScheduleDate", SourceExpression = "EKET.Eindt", IsRequired = false, IsActive = true, SortOrder = 130 },
|
||||
new SapFieldMapping { SiteId = siteId, TargetField = "ScheduleQuantity", SourceExpression = "EKET.Menge", IsRequired = false, IsActive = true, SortOrder = 140 });
|
||||
}
|
||||
|
||||
private static void Replace<TEntity>(AppDbContext db, IQueryable<TEntity> oldRows, IEnumerable<TEntity> newRows)
|
||||
where TEntity : class
|
||||
{
|
||||
var set = db.Set<TEntity>();
|
||||
set.RemoveRange(oldRows);
|
||||
set.AddRange(newRows);
|
||||
}
|
||||
|
||||
private static Site Clone(Site site) => new()
|
||||
{
|
||||
Id = site.Id,
|
||||
HanaServerId = site.HanaServerId,
|
||||
Schema = site.Schema,
|
||||
TSC = site.TSC,
|
||||
Land = site.Land,
|
||||
SourceSystem = site.SourceSystem,
|
||||
UsernameOverride = site.UsernameOverride,
|
||||
PasswordOverride = site.PasswordOverride,
|
||||
LocalExportFolderOverride = site.LocalExportFolderOverride,
|
||||
ManualImportFilePath = site.ManualImportFilePath,
|
||||
ManualImportLastUploadedAtUtc = site.ManualImportLastUploadedAtUtc,
|
||||
SapServiceUrl = site.SapServiceUrl,
|
||||
SapEntitySet = site.SapEntitySet,
|
||||
SapEntitySetsCache = site.SapEntitySetsCache,
|
||||
SapEntitySetsRefreshedAtUtc = site.SapEntitySetsRefreshedAtUtc,
|
||||
IsActive = site.IsActive
|
||||
};
|
||||
}
|
||||
@@ -102,6 +102,14 @@ public class DatabaseInitializationServiceTests : IDisposable
|
||||
x.TargetField == nameof(SalesRecord.DocumentType) &&
|
||||
x.SourceHeader == "=Alphaplan Excel");
|
||||
Assert.Equal(2, db.FieldTransformationRules.Count(x => x.SourceSystem == "MANUAL_EXCEL"));
|
||||
|
||||
var purchasing = Assert.Single(db.Sites, x => x.TSC == PurchasingDataSourcePageService.PurchasingTsc);
|
||||
Assert.Equal("SAP", purchasing.SourceSystem);
|
||||
Assert.Contains(db.SapSourceDefinitions, x => x.SiteId == purchasing.Id && x.Alias == "EKKO" && x.EntitySet == "EKKOSet" && x.IsPrimary);
|
||||
Assert.Contains(db.SapSourceDefinitions, x => x.SiteId == purchasing.Id && x.Alias == "EKPO" && x.EntitySet == "EKPOSet");
|
||||
Assert.Contains(db.SapSourceDefinitions, x => x.SiteId == purchasing.Id && x.Alias == "EKET" && x.EntitySet == "eketSet");
|
||||
Assert.Contains(db.SapJoinDefinitions, x => x.SiteId == purchasing.Id && x.LeftAlias == "EKKO" && x.RightAlias == "EKPO");
|
||||
Assert.Contains(db.SapFieldMappings, x => x.SiteId == purchasing.Id && x.TargetField == "NetValueChf" && x.SourceExpression == "EKPO.NetwrChf");
|
||||
}
|
||||
|
||||
private async Task PrepareLegacySitesTableAsync()
|
||||
|
||||
@@ -46,9 +46,42 @@ Das Dashboard wurde fachlich um diese Bereiche erweitert:
|
||||
- `Kontrakte`
|
||||
- `Lieferanten`
|
||||
- `PBIX Vorlage`
|
||||
- `3D Simulation`
|
||||
- Unterpunkt `Einkauf > Datenquellen` fuer SAP/OData-Verbindung, Quellen, Join-Fluss und Zielmappings.
|
||||
- Die Seite ist als Cockpit-Struktur umgesetzt und zweisprachig ueber den vorhandenen UI-Sprachservice vorbereitet.
|
||||
- Die Kennzahlen sind noch nicht live an SAP gebunden.
|
||||
|
||||
## SAP/OData-Konfiguration
|
||||
|
||||
Vorbefuellte Quellen:
|
||||
|
||||
- `EKKO -> EKKOSet`
|
||||
- `EKPO -> EKPOSet`
|
||||
- `EKET -> eketSet`
|
||||
- `LIEF -> Data`
|
||||
- `WG -> Data2`
|
||||
|
||||
Vorbefuellte Joins:
|
||||
|
||||
- `EKKO.Ebeln = EKPO.Ebeln`
|
||||
- `EKPO.Ebeln,Ebelp = EKET.Ebeln,Ebelp`
|
||||
- `EKKO.Lifnr = LIEF.Lifnr`
|
||||
- `EKPO.Matkl = WG.Matkl`
|
||||
|
||||
Die Seite verwendet dieselben Grundtabellen wie die Finance-/Standorte-Quellenpflege: `Sites`, `SapSourceDefinitions`, `SapJoinDefinitions`, `SapFieldMappings`.
|
||||
|
||||
## 3D Simulation
|
||||
|
||||
Das Einkaufsdashboard hat eine eigene 3D-Simulation fuer wichtige Einkaufsindikatoren:
|
||||
|
||||
- Spend CHF.
|
||||
- Offener Bestellwert.
|
||||
- Offene Menge.
|
||||
- Kontrakt-Restwert.
|
||||
- Lieferantenperformance.
|
||||
|
||||
Die Simulation nutzt feste Canvas-Groessen, sichtbare Achsen, waehlbare Diagrammarten, Labelgroesse und einen Szenario-Slider fuer Preis-/Wechselkurswirkung.
|
||||
|
||||
## Naechster Schritt fuer Live-Daten
|
||||
|
||||
Fuer echte Werte muessen die Einkaufsquellen sauber gemappt werden:
|
||||
|
||||
@@ -11,7 +11,8 @@ Stand: 2026-06-05
|
||||
- Neu im Finance/Management-Cockpit: einfache Schnelluebersicht links sichtbar; tiefere Funktionen bleiben unter `Experten`.
|
||||
- Neu in der Navigation: Menuebaum wird aus `NavigationMenuItems` gerendert; Admins koennen bestehende Punkte unter `Admin > Menuestruktur` umhaengen, sortieren und aus-/einblenden.
|
||||
- Neu als Hauptbereich: `Einkauf` mit Einkaufswagen-Icon und erweitertem `Einkauf Dashboard`.
|
||||
- Einkauf: `x.pbix` wurde als Vorlage analysiert; `/einkauf` enthaelt jetzt Struktur fuer Spend, offene Bestellungen, Mengenkontrakte, Lieferantenperformance und die PBIX-Reportseiten. Live-SAP-Anbindung ist noch offen.
|
||||
- Einkauf: `x.pbix` wurde als Vorlage analysiert; `/einkauf` enthaelt jetzt Struktur fuer Spend, offene Bestellungen, Mengenkontrakte, Lieferantenperformance, PBIX-Reportseiten und 3D-Simulation.
|
||||
- Einkauf: `Einkauf > Datenquellen` pflegt die SAP/OData-Konfiguration grafisch und ist mit `EKKOSet`, `EKPOSet`, `eketSet`, `Data`, `Data2`, Joins und Zielmappings vorbefuellt. Realer Kennzahlenimport ist noch offen.
|
||||
- Neu im Expertenbereich: `3D Datenanalyse` mit drehbarer 3D-Grafik, Achsen, Diagrammarten, Indikatorauswahl, Labelgroesse und Simulation per Schieberegler.
|
||||
- Spanien: `Run-SpainRangeExportAndUpload-AllInOne.ps1` exportiert Sage-Range direkt und laedt CSV/Summary via rclone nach SharePoint `trafag-bi:Import/Finance/Spanien`.
|
||||
- Spanien: Default-Range ist heute minus 7 Tage bis heute; `ToDate` ist exklusiv.
|
||||
|
||||
@@ -52,7 +52,9 @@ Diese Datei ist fuer tokenarme RAG-Nutzung komprimiert.
|
||||
- Neu umgesetzt: Neuer Hauptpunkt `Einkauf` mit Einkaufswagen-Icon und vorbereiteter Einstiegseite `Einkauf Dashboard`.
|
||||
- Neu umgesetzt: `x.pbix` als Einkaufs-/SAP-Vorlage analysiert und `Einkauf Dashboard` auf Spend, offene Bestellungen, Kontrakte, Lieferantenperformance und PBIX-Vorlagenstruktur erweitert.
|
||||
- Wichtig Einkauf: Aktuell ist die Seite fachlich strukturiert, aber noch nicht live an SAP/OData angebunden; fuer Echtwerte muessen Einkaufsquellen wie `EKKOSet`, `EKPOSet`, ggf. Termin-/Kontrakt- und Lieferantenbewertungsdaten gemappt werden.
|
||||
- Letzte Validierung: `dotnet test TrafagSalesExporter.sln --verbosity minimal` mit `83/83` Tests gruen.
|
||||
- Neu umgesetzt: `Einkauf > Datenquellen` als grafische SAP/OData-Quellenpflege analog Finance/Standorte; vorbefuellt mit `EKKOSet`, `EKPOSet`, `eketSet`, Lieferanten- und Warengruppen-Mapping, Joins und Zielmappings.
|
||||
- Neu umgesetzt: `Einkauf Dashboard > 3D Simulation` mit festen Canvas-Abmessungen, Achsenbeschriftung, Diagrammarten, Labelgroesse und Szenario-Slider fuer Preis-/Wechselkurswirkung.
|
||||
- Letzte Validierung: `dotnet test TrafagSalesExporter.sln --verbosity minimal` mit `83/83` Tests gruen; Test prueft auch Einkaufs-SAP-Seed mit Quellen/Joins/Mappings.
|
||||
|
||||
## Nachtrag 2026-06-05 Einkauf / PBIX
|
||||
|
||||
@@ -65,7 +67,10 @@ Quelle:
|
||||
Umgesetzt:
|
||||
|
||||
- Einkaufsseite `/einkauf` von Platzhalter zu fachlichem Cockpit erweitert.
|
||||
- Tabs: `Uebersicht`, `Spend`, `Offene Bestellungen`, `Kontrakte`, `Lieferanten`, `PBIX Vorlage`.
|
||||
- Tabs: `Uebersicht`, `Spend`, `Offene Bestellungen`, `Kontrakte`, `Lieferanten`, `PBIX Vorlage`, `3D Simulation`.
|
||||
- Neuer Unterpunkt `Einkauf > Datenquellen` fuer die grafische SAP/OData-Konfiguration.
|
||||
- Standardquellen: `EKKO -> EKKOSet`, `EKPO -> EKPOSet`, `EKET -> eketSet`, `LIEF -> Data`, `WG -> Data2`.
|
||||
- Standardjoins: `EKKO.Ebeln = EKPO.Ebeln`, `EKPO.Ebeln,Ebelp = EKET.Ebeln,Ebelp`, `EKKO.Lifnr = LIEF.Lifnr`, `EKPO.Matkl = WG.Matkl`.
|
||||
- Zusaetzlich zu den PBIX-Sichten wurden die vom Einkauf genannten SAP-Themen aufgenommen:
|
||||
- Spend total vergangen nach Jahr, Lieferant, Warengruppe, Artikel.
|
||||
- Offene Bestellwerte und Mengen nach Lieferant, Warengruppe, Artikel.
|
||||
|
||||
Reference in New Issue
Block a user