Add finance summary view and HR guide
This commit is contained in:
@@ -123,6 +123,10 @@
|
|||||||
<MudTabPanel Text="@T("Datenstatus", "Data status")" Icon="@Icons.Material.Filled.FactCheck">
|
<MudTabPanel Text="@T("Datenstatus", "Data status")" Icon="@Icons.Material.Filled.FactCheck">
|
||||||
@FileStatusTable(Result.FileStatuses)
|
@FileStatusTable(Result.FileStatuses)
|
||||||
</MudTabPanel>
|
</MudTabPanel>
|
||||||
|
|
||||||
|
<MudTabPanel Text="@T("Anleitung", "Guide")" Icon="@Icons.Material.Filled.HelpOutline">
|
||||||
|
@GuidePanel()
|
||||||
|
</MudTabPanel>
|
||||||
</MudTabs>
|
</MudTabs>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
@@ -279,6 +283,75 @@
|
|||||||
</MudTable>
|
</MudTable>
|
||||||
</MudPaper>;
|
</MudPaper>;
|
||||||
|
|
||||||
|
private RenderFragment GuidePanel() => @<MudGrid>
|
||||||
|
<MudItem xs="12" md="8">
|
||||||
|
<MudPaper Class="pa-4" Elevation="1">
|
||||||
|
<MudText Typo="Typo.h6" Class="mb-2">@T("Ablauf fuer HR", "HR workflow")</MudText>
|
||||||
|
<div class="hr-guide-steps">
|
||||||
|
<div class="hr-guide-step">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.Download" Size="Size.Large" />
|
||||||
|
<span>1</span>
|
||||||
|
<strong>@T("Rexx exportieren", "Export from Rexx")</strong>
|
||||||
|
<p>@T("Die benoetigten Rexx-Abfragen manuell herunterladen. Excel/XLSX verwenden, nicht PDF.", "Download the required Rexx queries manually. Use Excel/XLSX, not PDF.")</p>
|
||||||
|
</div>
|
||||||
|
<div class="hr-guide-step">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.FolderCopy" Size="Size.Large" />
|
||||||
|
<span>2</span>
|
||||||
|
<strong>@T("Dateien ablegen", "Place files")</strong>
|
||||||
|
<p>@T("Downloads in den Datenordner kopieren und exakt wie unten benennen.", "Copy downloads into the data folder and name them exactly as listed below.")</p>
|
||||||
|
</div>
|
||||||
|
<div class="hr-guide-step">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.Refresh" Size="Size.Large" />
|
||||||
|
<span>3</span>
|
||||||
|
<strong>@T("Cockpit laden", "Load cockpit")</strong>
|
||||||
|
<p>@T("Im HR-KPI-Cockpit den Datenordner kontrollieren und Laden klicken.", "Check the data folder in the HR KPI cockpit and click Load.")</p>
|
||||||
|
</div>
|
||||||
|
<div class="hr-guide-step">
|
||||||
|
<MudIcon Icon="@Icons.Material.Filled.FactCheck" Size="Size.Large" />
|
||||||
|
<span>4</span>
|
||||||
|
<strong>@T("Datenstatus pruefen", "Check data status")</strong>
|
||||||
|
<p>@T("Im Reiter Datenstatus muessen die erwarteten Dateien gruen erscheinen.", "In the Data status tab, the expected files should be green.")</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" md="4">
|
||||||
|
<MudPaper Class="pa-4" Elevation="1">
|
||||||
|
<MudText Typo="Typo.h6" Class="mb-2">@T("Datenordner", "Data folder")</MudText>
|
||||||
|
<MudText Typo="Typo.body1">@Result.Options.DataFolder</MudText>
|
||||||
|
<MudAlert Severity="Severity.Info" Dense Variant="Variant.Outlined" Class="mt-3">
|
||||||
|
@T("Der Standardordner ist konfigurierbar. Fuer einen anderen Ordner oben im HR-KPI-Filter den Datenordner anpassen und neu laden.",
|
||||||
|
"The default folder is configurable. To use another folder, change the data folder in the HR KPI filter above and reload.")
|
||||||
|
</MudAlert>
|
||||||
|
<MudAlert Severity="Severity.Warning" Dense Variant="Variant.Outlined" Class="mt-2">
|
||||||
|
@T("HR-Dateien enthalten Personendaten. Nicht per E-Mail weiterleiten und keine Kopien in ungeschuetzten Ordnern liegen lassen.",
|
||||||
|
"HR files contain personal data. Do not forward them by email and do not leave copies in unprotected folders.")
|
||||||
|
</MudAlert>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<MudPaper Class="pa-4" Elevation="1">
|
||||||
|
<MudText Typo="Typo.h6" Class="mb-2">@T("Erwartete Dateien", "Expected files")</MudText>
|
||||||
|
<MudTable Items="Result.FileStatuses" Dense Hover Striped>
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>@T("Inhalt", "Content")</MudTh>
|
||||||
|
<MudTh>@T("Datei/Pfad", "File/path")</MudTh>
|
||||||
|
<MudTh>@T("Status", "Status")</MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd>@context.Label</MudTd>
|
||||||
|
<MudTd>@context.Path</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
<MudChip T="string" Size="Size.Small" Color="@(context.Exists ? Color.Success : Color.Error)" Variant="Variant.Outlined">
|
||||||
|
@(context.Exists ? T("gefunden", "found") : T("fehlt", "missing"))
|
||||||
|
</MudChip>
|
||||||
|
</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
</MudTable>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>;
|
||||||
|
|
||||||
private static IEnumerable<HrKpiGroupValue> BuildLeaverExclusionRows(IReadOnlyList<HrLeaverRow> items)
|
private static IEnumerable<HrKpiGroupValue> BuildLeaverExclusionRows(IReadOnlyList<HrLeaverRow> items)
|
||||||
=> items
|
=> items
|
||||||
.GroupBy(x => x.FluktuationAusschlussgrund ?? "Relevant")
|
.GroupBy(x => x.FluktuationAusschlussgrund ?? "Relevant")
|
||||||
@@ -391,6 +464,51 @@
|
|||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hr-guide-steps {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(150px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hr-guide-step {
|
||||||
|
min-height: 175px;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid var(--mud-palette-lines-default);
|
||||||
|
border-top: 5px solid var(--mud-palette-primary);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
background: var(--mud-palette-surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hr-guide-step span {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-grid;
|
||||||
|
place-items: center;
|
||||||
|
color: var(--mud-palette-primary-text);
|
||||||
|
background: var(--mud-palette-primary);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hr-guide-step p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--mud-palette-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
@@media (max-width: 1100px) {
|
||||||
|
.hr-guide-steps {
|
||||||
|
grid-template-columns: repeat(2, minmax(150px, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@@media (max-width: 700px) {
|
||||||
|
.hr-guide-steps {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.hr-gauge {
|
.hr-gauge {
|
||||||
--gauge-color: #2e7d32;
|
--gauge-color: #2e7d32;
|
||||||
--gauge-deg: 0deg;
|
--gauge-deg: 0deg;
|
||||||
|
|||||||
@@ -39,7 +39,9 @@ else
|
|||||||
<MudPaper Class="pa-4 mb-4" Elevation="1">
|
<MudPaper Class="pa-4 mb-4" Elevation="1">
|
||||||
<MudGrid>
|
<MudGrid>
|
||||||
<MudItem xs="12" md="5">
|
<MudItem xs="12" md="5">
|
||||||
<MudTextField @bind-Value="_dataFolder" Label="@T("Datenordner", "Data folder")" />
|
<MudTextField @bind-Value="_dataFolder"
|
||||||
|
Label="@T("Datenordner fuer Rexx/SAP-Dateien", "Data folder for Rexx/SAP files")"
|
||||||
|
HelperText="@T("Standard ist C:\\temp. Der Ordner kann hier fuer den aktuellen Lauf angepasst oder dauerhaft in appsettings.json unter HrKpi:DataFolder geaendert werden.", "Default is C:\\temp. The folder can be changed here for the current run or permanently in appsettings.json under HrKpi:DataFolder.")" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="6" md="2">
|
<MudItem xs="6" md="2">
|
||||||
<MudSelect T="int?" @bind-Value="_year" Label="@T("Austrittsjahr", "Leaver year")" Dense Clearable>
|
<MudSelect T="int?" @bind-Value="_year" Label="@T("Austrittsjahr", "Leaver year")" Dense Clearable>
|
||||||
|
|||||||
@@ -9,6 +9,138 @@
|
|||||||
|
|
||||||
<MudText Typo="Typo.h4" Class="mb-4">@T("Management Analyse", "Management analysis")</MudText>
|
<MudText Typo="Typo.h4" Class="mb-4">@T("Management Analyse", "Management analysis")</MudText>
|
||||||
|
|
||||||
|
<MudPaper Class="pa-4 mb-4" Elevation="1">
|
||||||
|
<MudGrid>
|
||||||
|
<MudItem xs="12" md="3">
|
||||||
|
<MudSelect T="int" @bind-Value="_selectedFinanceYear" Label="@T("Finance-Jahr", "Finance year")" Dense>
|
||||||
|
@foreach (var year in _financeYearOptions)
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@year">@year</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" md="3">
|
||||||
|
<MudSelect T="string" @bind-Value="_selectedFinanceCountryKey" Label="@T("Land", "Country")" Dense Clearable>
|
||||||
|
@foreach (var option in _financeCountryOptions)
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@option">@option</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" md="3">
|
||||||
|
<MudSelect T="string" @bind-Value="_selectedFinanceCurrency" Label="@T("Waehrung", "Currency")" Dense Clearable>
|
||||||
|
@foreach (var option in _financeCurrencyOptions)
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@option">@option</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" md="3">
|
||||||
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="AnalyzeFinanceSummary"
|
||||||
|
StartIcon="@Icons.Material.Filled.FactCheck" Disabled="_analyzingFinance" FullWidth>
|
||||||
|
@(_analyzingFinance ? T("Lade...", "Loading...") : T("Finance Summary laden", "Load finance summary"))
|
||||||
|
</MudButton>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
|
@if (_financeResult is not null)
|
||||||
|
{
|
||||||
|
<MudTabs Elevation="1" Rounded="false" PanelClass="pt-4">
|
||||||
|
<MudTabPanel Text="@T("Finance Summary", "Finance summary")" Icon="@Icons.Material.Filled.Dashboard">
|
||||||
|
<MudGrid Class="mb-4">
|
||||||
|
<MudItem xs="12" sm="6" md="3">
|
||||||
|
<MudPaper Class="pa-4" Elevation="1">
|
||||||
|
<MudText Typo="Typo.caption">@T("Net Sales Actual", "Net sales actual")</MudText>
|
||||||
|
<MudText Typo="Typo.h5">@FormatValue(_financeResult.NetSalesActual, _financeResult.DisplayCurrency)</MudText>
|
||||||
|
<MudText Typo="Typo.body2">@T("gefiltertes Endergebnis", "filtered final result")</MudText>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6" md="3">
|
||||||
|
<MudPaper Class="pa-4" Elevation="1">
|
||||||
|
<MudText Typo="Typo.caption">@T("Enthaltene Zeilen", "Included rows")</MudText>
|
||||||
|
<MudText Typo="Typo.h5">@_financeResult.IncludedRows.ToString("N0")</MudText>
|
||||||
|
<MudText Typo="Typo.body2">@T("Finance Include = TRUE", "Finance Include = TRUE")</MudText>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6" md="3">
|
||||||
|
<MudPaper Class="pa-4" Elevation="1">
|
||||||
|
<MudText Typo="Typo.caption">@T("Ausgeschlossen", "Excluded")</MudText>
|
||||||
|
<MudText Typo="Typo.h5">@_financeResult.ExcludedRows.ToString("N0")</MudText>
|
||||||
|
<MudText Typo="Typo.body2">@T("Finance-Regeln", "Finance rules")</MudText>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6" md="3">
|
||||||
|
<MudPaper Class="pa-4" Elevation="1">
|
||||||
|
<MudText Typo="Typo.caption">@T("Laender / Waehrungen", "Countries / currencies")</MudText>
|
||||||
|
<MudText Typo="Typo.h5">@($"{_financeResult.CountryCount:N0} / {_financeResult.CurrencyCount:N0}")</MudText>
|
||||||
|
<MudText Typo="Typo.body2">@($"{_financeResult.Filter.Year}")</MudText>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
|
||||||
|
<MudGrid Class="mb-4">
|
||||||
|
<MudItem xs="12" md="8">
|
||||||
|
<MudPaper Class="pa-4" Elevation="1">
|
||||||
|
<MudText Typo="Typo.h6" Class="mb-2">@T("Summen wie im Excel-Blatt Finance Summary", "Totals matching the Finance Summary Excel sheet")</MudText>
|
||||||
|
<MudTable Items="_financeResult.Rows" Dense Hover Striped>
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>@T("Jahr", "Year")</MudTh>
|
||||||
|
<MudTh>@T("Land", "Country")</MudTh>
|
||||||
|
<MudTh>@T("Waehrung", "Currency")</MudTh>
|
||||||
|
<MudTh>@T("Net Sales Actual", "Net sales actual")</MudTh>
|
||||||
|
<MudTh>@T("Enthalten", "Included")</MudTh>
|
||||||
|
<MudTh>@T("Ausgeschlossen", "Excluded")</MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd>@context.Year</MudTd>
|
||||||
|
<MudTd>@context.CountryKey</MudTd>
|
||||||
|
<MudTd>@context.Currency</MudTd>
|
||||||
|
<MudTd>@FormatValue(context.NetSalesActual, context.Currency)</MudTd>
|
||||||
|
<MudTd>@context.IncludedRows.ToString("N0")</MudTd>
|
||||||
|
<MudTd>@context.ExcludedRows.ToString("N0")</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
<NoRecordsContent>
|
||||||
|
<MudText Typo="Typo.body2">
|
||||||
|
@T("Keine Finance-Summary-Daten fuer diese Filter.", "No finance summary data for these filters.")
|
||||||
|
</MudText>
|
||||||
|
</NoRecordsContent>
|
||||||
|
</MudTable>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" md="4">
|
||||||
|
<MudPaper Class="pa-4" Elevation="1">
|
||||||
|
<MudText Typo="Typo.h6" Class="mb-2">@T("Hinweise", "Notes")</MudText>
|
||||||
|
@foreach (var notice in _financeResult.Notices)
|
||||||
|
{
|
||||||
|
<MudAlert Severity="Severity.Info" Dense Variant="Variant.Outlined" Class="mb-2">@notice</MudAlert>
|
||||||
|
}
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
|
||||||
|
<MudPaper Class="pa-4" Elevation="1">
|
||||||
|
<MudText Typo="Typo.h6" Class="mb-2">@T("Jahresvergleich mit aktuellem Filter", "Year comparison with current filter")</MudText>
|
||||||
|
<MudTable Items="_financeResult.YearRows" Dense Hover Striped>
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>@T("Jahr", "Year")</MudTh>
|
||||||
|
<MudTh>@T("Waehrung", "Currency")</MudTh>
|
||||||
|
<MudTh>@T("Net Sales Actual", "Net sales actual")</MudTh>
|
||||||
|
<MudTh>@T("Enthalten", "Included")</MudTh>
|
||||||
|
<MudTh>@T("Ausgeschlossen", "Excluded")</MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd>@context.Year</MudTd>
|
||||||
|
<MudTd>@context.Currency</MudTd>
|
||||||
|
<MudTd>@FormatValue(context.NetSalesActual, context.Currency)</MudTd>
|
||||||
|
<MudTd>@context.IncludedRows.ToString("N0")</MudTd>
|
||||||
|
<MudTd>@context.ExcludedRows.ToString("N0")</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
</MudTable>
|
||||||
|
</MudPaper>
|
||||||
|
</MudTabPanel>
|
||||||
|
<MudTabPanel Text="@T("Rohdaten Diagnose", "Raw-data diagnostics")" Icon="@Icons.Material.Filled.QueryStats">
|
||||||
|
|
||||||
<MudPaper Class="pa-4 mb-4" Elevation="1">
|
<MudPaper Class="pa-4 mb-4" Elevation="1">
|
||||||
<MudGrid>
|
<MudGrid>
|
||||||
<MudItem xs="12" md="6">
|
<MudItem xs="12" md="6">
|
||||||
@@ -339,9 +471,16 @@
|
|||||||
</MudPaper>
|
</MudPaper>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
</MudTabPanel>
|
||||||
|
</MudTabs>
|
||||||
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private List<ManagementCockpitFileOption> _files = [];
|
private List<ManagementCockpitFileOption> _files = [];
|
||||||
private List<int> _centralYears = [];
|
private List<int> _centralYears = [];
|
||||||
|
private List<int> _financeYearOptions = [];
|
||||||
|
private List<string> _financeCountryOptions = [];
|
||||||
|
private List<string> _financeCurrencyOptions = [];
|
||||||
private List<ManagementCockpitValueFieldOption> _valueFieldOptions = [];
|
private List<ManagementCockpitValueFieldOption> _valueFieldOptions = [];
|
||||||
private readonly List<CurrencySelectOption> _currencyOptions =
|
private readonly List<CurrencySelectOption> _currencyOptions =
|
||||||
[
|
[
|
||||||
@@ -352,6 +491,10 @@
|
|||||||
private string? _selectedFilePath;
|
private string? _selectedFilePath;
|
||||||
private ManagementCockpitResult? _result;
|
private ManagementCockpitResult? _result;
|
||||||
private ManagementCockpitCentralResult? _centralResult;
|
private ManagementCockpitCentralResult? _centralResult;
|
||||||
|
private ManagementFinanceSummaryResult? _financeResult;
|
||||||
|
private int _selectedFinanceYear;
|
||||||
|
private string? _selectedFinanceCountryKey;
|
||||||
|
private string? _selectedFinanceCurrency;
|
||||||
private int _selectedCentralYear;
|
private int _selectedCentralYear;
|
||||||
private int? _selectedCentralMonth;
|
private int? _selectedCentralMonth;
|
||||||
private string? _centralLandFilter;
|
private string? _centralLandFilter;
|
||||||
@@ -360,10 +503,11 @@
|
|||||||
private string _selectedCentralValueField = ManagementCockpitValueFieldKeys.SalesPriceValue;
|
private string _selectedCentralValueField = ManagementCockpitValueFieldKeys.SalesPriceValue;
|
||||||
private IEnumerable<string> _selectedCentralAdditionalValueFields = [];
|
private IEnumerable<string> _selectedCentralAdditionalValueFields = [];
|
||||||
private string _selectedFileTargetCurrency = ManagementCockpitCurrencyOptions.Eur;
|
private string _selectedFileTargetCurrency = ManagementCockpitCurrencyOptions.Eur;
|
||||||
private string _selectedCentralTargetCurrency = ManagementCockpitCurrencyOptions.Eur;
|
private string _selectedCentralTargetCurrency = ManagementCockpitCurrencyOptions.Native;
|
||||||
private bool _loadingFiles;
|
private bool _loadingFiles;
|
||||||
private bool _analyzing;
|
private bool _analyzing;
|
||||||
private bool _analyzingCentral;
|
private bool _analyzingCentral;
|
||||||
|
private bool _analyzingFinance;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
@@ -373,6 +517,8 @@
|
|||||||
_centralYears = state.CentralYears;
|
_centralYears = state.CentralYears;
|
||||||
_selectedFilePath = state.SelectedFilePath;
|
_selectedFilePath = state.SelectedFilePath;
|
||||||
_selectedCentralYear = state.SelectedCentralYear;
|
_selectedCentralYear = state.SelectedCentralYear;
|
||||||
|
_selectedFinanceYear = _selectedCentralYear;
|
||||||
|
await AnalyzeFinanceSummary();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ReloadFiles()
|
private async Task ReloadFiles()
|
||||||
@@ -449,6 +595,31 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task AnalyzeFinanceSummary()
|
||||||
|
{
|
||||||
|
_analyzingFinance = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_financeResult = await CockpitPageService.AnalyzeFinanceSummaryAsync(
|
||||||
|
_selectedFinanceYear,
|
||||||
|
_selectedFinanceCountryKey,
|
||||||
|
_selectedFinanceCurrency);
|
||||||
|
|
||||||
|
_financeYearOptions = _financeResult.YearOptions;
|
||||||
|
_financeCountryOptions = _financeResult.CountryOptions;
|
||||||
|
_financeCurrencyOptions = _financeResult.CurrencyOptions;
|
||||||
|
_selectedFinanceYear = _financeResult.Filter.Year;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Snackbar.Add(string.Format(T("Finance Summary konnte nicht erzeugt werden: {0}", "Could not build finance summary: {0}"), ex.Message), Severity.Error);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_analyzingFinance = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void ClearCentralScope()
|
private void ClearCentralScope()
|
||||||
{
|
{
|
||||||
_centralLandFilter = null;
|
_centralLandFilter = null;
|
||||||
|
|||||||
@@ -153,3 +153,37 @@ public class ManagementCockpitCentralResult
|
|||||||
public List<ManagementCockpitDimensionValueRow> SourceSystemTotals { get; set; } = [];
|
public List<ManagementCockpitDimensionValueRow> SourceSystemTotals { get; set; } = [];
|
||||||
public List<ManagementCockpitDimensionValueRow> CountryTotals { get; set; } = [];
|
public List<ManagementCockpitDimensionValueRow> CountryTotals { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class ManagementFinanceSummaryFilter
|
||||||
|
{
|
||||||
|
public int Year { get; set; }
|
||||||
|
public string? CountryKey { get; set; }
|
||||||
|
public string? Currency { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ManagementFinanceSummaryRow
|
||||||
|
{
|
||||||
|
public int Year { get; set; }
|
||||||
|
public string CountryKey { get; set; } = string.Empty;
|
||||||
|
public string Currency { get; set; } = string.Empty;
|
||||||
|
public int IncludedRows { get; set; }
|
||||||
|
public int ExcludedRows { get; set; }
|
||||||
|
public decimal NetSalesActual { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ManagementFinanceSummaryResult
|
||||||
|
{
|
||||||
|
public ManagementFinanceSummaryFilter Filter { get; set; } = new();
|
||||||
|
public List<string> Notices { get; set; } = [];
|
||||||
|
public List<int> YearOptions { get; set; } = [];
|
||||||
|
public List<string> CountryOptions { get; set; } = [];
|
||||||
|
public List<string> CurrencyOptions { get; set; } = [];
|
||||||
|
public List<ManagementFinanceSummaryRow> Rows { get; set; } = [];
|
||||||
|
public List<ManagementFinanceSummaryRow> YearRows { get; set; } = [];
|
||||||
|
public int IncludedRows { get; set; }
|
||||||
|
public int ExcludedRows { get; set; }
|
||||||
|
public int CountryCount { get; set; }
|
||||||
|
public int CurrencyCount { get; set; }
|
||||||
|
public decimal NetSalesActual { get; set; }
|
||||||
|
public string DisplayCurrency { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,4 +11,5 @@ public interface IManagementCockpitService
|
|||||||
Task<List<int>> GetAvailableCentralYearsAsync();
|
Task<List<int>> GetAvailableCentralYearsAsync();
|
||||||
Task<ManagementCockpitCentralResult> AnalyzeCentralAsync(int year, int? month);
|
Task<ManagementCockpitCentralResult> AnalyzeCentralAsync(int year, int? month);
|
||||||
Task<ManagementCockpitCentralResult> AnalyzeCentralAsync(int year, int? month, ManagementCockpitAnalysisOptions? options);
|
Task<ManagementCockpitCentralResult> AnalyzeCentralAsync(int year, int? month, ManagementCockpitAnalysisOptions? options);
|
||||||
|
Task<ManagementFinanceSummaryResult> AnalyzeFinanceSummaryAsync(int year, string? countryKey, string? currency);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ public interface IManagementCockpitPageService
|
|||||||
Task<List<int>> LoadCentralYearsAsync();
|
Task<List<int>> LoadCentralYearsAsync();
|
||||||
Task<ManagementCockpitResult> AnalyzeAsync(string filePath, ManagementCockpitAnalysisOptions options);
|
Task<ManagementCockpitResult> AnalyzeAsync(string filePath, ManagementCockpitAnalysisOptions options);
|
||||||
Task<ManagementCockpitCentralResult> AnalyzeCentralAsync(int year, int? month, ManagementCockpitAnalysisOptions options);
|
Task<ManagementCockpitCentralResult> AnalyzeCentralAsync(int year, int? month, ManagementCockpitAnalysisOptions options);
|
||||||
|
Task<ManagementFinanceSummaryResult> AnalyzeFinanceSummaryAsync(int year, string? countryKey, string? currency);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class ManagementCockpitPageService : IManagementCockpitPageService
|
public sealed class ManagementCockpitPageService : IManagementCockpitPageService
|
||||||
@@ -46,6 +47,9 @@ public sealed class ManagementCockpitPageService : IManagementCockpitPageService
|
|||||||
|
|
||||||
public Task<ManagementCockpitCentralResult> AnalyzeCentralAsync(int year, int? month, ManagementCockpitAnalysisOptions options)
|
public Task<ManagementCockpitCentralResult> AnalyzeCentralAsync(int year, int? month, ManagementCockpitAnalysisOptions options)
|
||||||
=> _cockpitService.AnalyzeCentralAsync(year, month, options);
|
=> _cockpitService.AnalyzeCentralAsync(year, month, options);
|
||||||
|
|
||||||
|
public Task<ManagementFinanceSummaryResult> AnalyzeFinanceSummaryAsync(int year, string? countryKey, string? currency)
|
||||||
|
=> _cockpitService.AnalyzeFinanceSummaryAsync(year, countryKey, currency);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class ManagementCockpitPageState
|
public sealed class ManagementCockpitPageState
|
||||||
|
|||||||
@@ -296,6 +296,149 @@ public class ManagementCockpitService : IManagementCockpitService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<ManagementFinanceSummaryResult> AnalyzeFinanceSummaryAsync(int year, string? countryKey, string? currency)
|
||||||
|
{
|
||||||
|
using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
|
var financeRules = await db.FinanceRules
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(rule => rule.IsActive)
|
||||||
|
.OrderBy(rule => rule.SortOrder)
|
||||||
|
.ThenBy(rule => rule.Id)
|
||||||
|
.ToListAsync();
|
||||||
|
if (financeRules.Count == 0)
|
||||||
|
financeRules = FinanceRuleEngine.CreateDefaultRules().ToList();
|
||||||
|
|
||||||
|
var financeRuleEngine = new FinanceRuleEngine(financeRules);
|
||||||
|
var records = await db.CentralSalesRecords
|
||||||
|
.AsNoTracking()
|
||||||
|
.Select(r => new SalesRecord
|
||||||
|
{
|
||||||
|
Land = r.Land,
|
||||||
|
Tsc = r.Tsc,
|
||||||
|
DocumentEntry = r.DocumentEntry,
|
||||||
|
InvoiceNumber = r.InvoiceNumber,
|
||||||
|
PositionOnInvoice = r.PositionOnInvoice,
|
||||||
|
Material = r.Material,
|
||||||
|
Name = r.Name,
|
||||||
|
Quantity = r.Quantity,
|
||||||
|
SupplierCountry = r.SupplierCountry,
|
||||||
|
CustomerNumber = r.CustomerNumber,
|
||||||
|
CustomerName = r.CustomerName,
|
||||||
|
SalesCurrency = r.SalesCurrency,
|
||||||
|
DocumentCurrency = r.DocumentCurrency,
|
||||||
|
CompanyCurrency = r.CompanyCurrency,
|
||||||
|
SalesPriceValue = r.SalesPriceValue,
|
||||||
|
DocumentType = r.DocumentType,
|
||||||
|
PostingDate = r.PostingDate,
|
||||||
|
InvoiceDate = r.InvoiceDate,
|
||||||
|
ExtractionDate = r.ExtractionDate
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (records.Count == 0)
|
||||||
|
throw new InvalidOperationException("Die zentrale Tabelle enthaelt noch keine Datensaetze.");
|
||||||
|
|
||||||
|
var allRows = records
|
||||||
|
.Select(record =>
|
||||||
|
{
|
||||||
|
var resolvedCountryKey = ResolveFinanceCountryKey(record.Land, record.Tsc);
|
||||||
|
var financeDate = financeRuleEngine.ResolveFinanceDate(record, resolvedCountryKey);
|
||||||
|
var rawInclude = financeRuleEngine.ShouldInclude(record, resolvedCountryKey);
|
||||||
|
var value = financeRuleEngine.ResolveNetSalesActual(record, resolvedCountryKey, rawInclude);
|
||||||
|
var include = rawInclude && value != 0m;
|
||||||
|
return new FinanceAggregationRow
|
||||||
|
{
|
||||||
|
Year = financeDate.Year,
|
||||||
|
CountryKey = resolvedCountryKey,
|
||||||
|
Currency = ResolveFinanceCurrency(record),
|
||||||
|
Include = include,
|
||||||
|
Value = value
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var yearOptions = allRows
|
||||||
|
.Select(row => row.Year)
|
||||||
|
.Distinct()
|
||||||
|
.OrderBy(yearValue => yearValue)
|
||||||
|
.ToList();
|
||||||
|
if (year == 0)
|
||||||
|
year = yearOptions.LastOrDefault();
|
||||||
|
|
||||||
|
var countryFilter = NormalizeOptionalFilter(countryKey);
|
||||||
|
var currencyFilter = NormalizeOptionalFilter(currency);
|
||||||
|
var scopedRows = allRows
|
||||||
|
.Where(row => row.Year == year)
|
||||||
|
.Where(row => countryFilter is null || row.CountryKey.Equals(countryFilter, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Where(row => currencyFilter is null || row.Currency.Equals(currencyFilter, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var summaryRows = scopedRows
|
||||||
|
.GroupBy(row => new { row.Year, row.CountryKey, row.Currency })
|
||||||
|
.OrderBy(group => group.Key.CountryKey, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ThenBy(group => group.Key.Currency, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Select(group => BuildFinanceSummaryRow(group.Key.Year, group.Key.CountryKey, group.Key.Currency, group))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var yearRows = allRows
|
||||||
|
.Where(row => countryFilter is null || row.CountryKey.Equals(countryFilter, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Where(row => currencyFilter is null || row.Currency.Equals(currencyFilter, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.GroupBy(row => new { row.Year, row.Currency })
|
||||||
|
.OrderBy(group => group.Key.Year)
|
||||||
|
.ThenBy(group => group.Key.Currency, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Select(group => BuildFinanceSummaryRow(group.Key.Year, "Alle", group.Key.Currency, group))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var includedRows = scopedRows.Count(row => row.Include);
|
||||||
|
var excludedRows = scopedRows.Count(row => !row.Include);
|
||||||
|
var resultCurrencies = summaryRows
|
||||||
|
.Select(row => row.Currency)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.OrderBy(value => value, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
var notices = new List<string>
|
||||||
|
{
|
||||||
|
"Diese Sicht verwendet dieselbe FinanceRuleEngine wie das zentrale Excel-Blatt Finance Summary.",
|
||||||
|
"Jahr, Land und Waehrung werden auf das Endergebnis angewendet.",
|
||||||
|
"Finance-Jahr basiert auf PostingDate, danach InvoiceDate, danach ExtractionDate; DE-Regeln koennen das Jahr erzwingen.",
|
||||||
|
"Include/Exclude, Gutschriften-Negierung und IT-Deduplizierung folgen den gepflegten Finance Regeln."
|
||||||
|
};
|
||||||
|
if (scopedRows.Count == 0)
|
||||||
|
{
|
||||||
|
notices.Insert(0, "Fuer die gewaehlten Finance-Filter gibt es keine Datensaetze im aktuellen Zentraldatenbestand.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ManagementFinanceSummaryResult
|
||||||
|
{
|
||||||
|
Filter = new ManagementFinanceSummaryFilter
|
||||||
|
{
|
||||||
|
Year = year,
|
||||||
|
CountryKey = countryFilter,
|
||||||
|
Currency = currencyFilter
|
||||||
|
},
|
||||||
|
YearOptions = yearOptions,
|
||||||
|
CountryOptions = allRows
|
||||||
|
.Select(row => row.CountryKey)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.OrderBy(value => value, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList(),
|
||||||
|
CurrencyOptions = allRows
|
||||||
|
.Select(row => row.Currency)
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.OrderBy(value => value, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList(),
|
||||||
|
Rows = summaryRows,
|
||||||
|
YearRows = yearRows,
|
||||||
|
IncludedRows = includedRows,
|
||||||
|
ExcludedRows = excludedRows,
|
||||||
|
CountryCount = summaryRows.Select(row => row.CountryKey).Distinct(StringComparer.OrdinalIgnoreCase).Count(),
|
||||||
|
CurrencyCount = resultCurrencies.Count,
|
||||||
|
NetSalesActual = summaryRows.Sum(row => row.NetSalesActual),
|
||||||
|
DisplayCurrency = BuildDisplayCurrencyLabel(resultCurrencies),
|
||||||
|
Notices = notices
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static IEnumerable<CentralAggregationRow> ApplyCentralDimensionFilters(
|
private static IEnumerable<CentralAggregationRow> ApplyCentralDimensionFilters(
|
||||||
IEnumerable<CentralAggregationRow> rows,
|
IEnumerable<CentralAggregationRow> rows,
|
||||||
ManagementCockpitAnalysisOptions? options)
|
ManagementCockpitAnalysisOptions? options)
|
||||||
@@ -308,6 +451,57 @@ public class ManagementCockpitService : IManagementCockpitService
|
|||||||
(tscFilter is null || string.Equals(row.Tsc, tscFilter, StringComparison.OrdinalIgnoreCase)));
|
(tscFilter is null || string.Equals(row.Tsc, tscFilter, StringComparison.OrdinalIgnoreCase)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ManagementFinanceSummaryRow BuildFinanceSummaryRow(
|
||||||
|
int year,
|
||||||
|
string countryKey,
|
||||||
|
string currency,
|
||||||
|
IEnumerable<FinanceAggregationRow> rows)
|
||||||
|
{
|
||||||
|
var rowList = rows.ToList();
|
||||||
|
return new ManagementFinanceSummaryRow
|
||||||
|
{
|
||||||
|
Year = year,
|
||||||
|
CountryKey = countryKey,
|
||||||
|
Currency = currency,
|
||||||
|
IncludedRows = rowList.Count(row => row.Include),
|
||||||
|
ExcludedRows = rowList.Count(row => !row.Include),
|
||||||
|
NetSalesActual = rowList.Sum(row => row.Value)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveFinanceCurrency(SalesRecord record)
|
||||||
|
=> ResolveFinanceCountryKey(record.Land, record.Tsc) switch
|
||||||
|
{
|
||||||
|
"CH" => "CHF",
|
||||||
|
"AT" => "EUR",
|
||||||
|
"DE" => "EUR",
|
||||||
|
"ES" => "EUR",
|
||||||
|
"FR" => "EUR",
|
||||||
|
"IN" => "INR",
|
||||||
|
"IT" => "EUR",
|
||||||
|
"UK" => "GBP",
|
||||||
|
"US" => "USD",
|
||||||
|
_ => string.IsNullOrWhiteSpace(record.CompanyCurrency) ? record.SalesCurrency : record.CompanyCurrency
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string ResolveFinanceCountryKey(string land, string tsc)
|
||||||
|
{
|
||||||
|
var normalizedLand = (land ?? string.Empty).Trim().ToUpperInvariant();
|
||||||
|
var normalizedTsc = (tsc ?? string.Empty).Trim().ToUpperInvariant();
|
||||||
|
|
||||||
|
if (normalizedLand is "AT" or "AUT" || normalizedLand.Contains("OESTER") || normalizedLand.Contains("OSTER") || normalizedLand.Contains("AUSTRIA")) return "AT";
|
||||||
|
if (normalizedLand is "CH" or "CHE" || normalizedLand.Contains("SCHWE") || normalizedLand.Contains("SWITZER")) return "CH";
|
||||||
|
if (normalizedLand.Contains("FRANK") || normalizedTsc.Contains("FR")) return "FR";
|
||||||
|
if (normalizedLand.Contains("IND") || normalizedTsc.Contains("IN")) return "IN";
|
||||||
|
if (normalizedLand.Contains("ITAL") || normalizedTsc.Contains("IT")) return "IT";
|
||||||
|
if (normalizedLand.Contains("ENGL") || normalizedLand.Contains("KINGDOM") || normalizedTsc.Contains("UK") || normalizedTsc.Contains("GB")) return "UK";
|
||||||
|
if (normalizedLand.Contains("USA") || normalizedLand.Contains("UNITED STATES") || normalizedTsc.Contains("US")) return "US";
|
||||||
|
if (normalizedLand.Contains("DEUT") || normalizedTsc.Contains("DE")) return "DE";
|
||||||
|
if (normalizedLand.Contains("SPAN") || normalizedTsc is "SE" or "ES") return "ES";
|
||||||
|
|
||||||
|
return normalizedTsc.Replace("TR", string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
private static IEnumerable<string> GetCandidateDirectories(ExportSettings settings)
|
private static IEnumerable<string> GetCandidateDirectories(ExportSettings settings)
|
||||||
{
|
{
|
||||||
yield return Path.Combine(AppContext.BaseDirectory, "output");
|
yield return Path.Combine(AppContext.BaseDirectory, "output");
|
||||||
@@ -892,6 +1086,15 @@ public class ManagementCockpitService : IManagementCockpitService
|
|||||||
public Dictionary<string, ConvertedValue> AdditionalValues { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
public Dictionary<string, ConvertedValue> AdditionalValues { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class FinanceAggregationRow
|
||||||
|
{
|
||||||
|
public int Year { get; set; }
|
||||||
|
public string CountryKey { get; set; } = string.Empty;
|
||||||
|
public string Currency { get; set; } = string.Empty;
|
||||||
|
public bool Include { get; set; }
|
||||||
|
public decimal Value { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
private sealed record AggregationSelection(
|
private sealed record AggregationSelection(
|
||||||
ValueFieldDefinition ValueField,
|
ValueFieldDefinition ValueField,
|
||||||
IReadOnlyList<ValueFieldDefinition> AdditionalValueFields,
|
IReadOnlyList<ValueFieldDefinition> AdditionalValueFields,
|
||||||
|
|||||||
@@ -238,6 +238,23 @@ public class ManagementCockpitServiceTests : IDisposable
|
|||||||
Assert.Contains("gewählten Zeitraum", ex.Message);
|
Assert.Contains("gewählten Zeitraum", ex.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task AnalyzeFinanceSummaryAsync_Returns_Empty_Result_For_Filter_With_No_Rows()
|
||||||
|
{
|
||||||
|
await SeedCentralRowsAsync(
|
||||||
|
CreateRow("MANUAL_EXCEL", "Deutschland", "TRDE", "INV-1", "EUR", 100m, new DateTime(2025, 1, 10)));
|
||||||
|
|
||||||
|
var result = await _service.AnalyzeFinanceSummaryAsync(2026, "DE", null);
|
||||||
|
|
||||||
|
Assert.Equal(2026, result.Filter.Year);
|
||||||
|
Assert.Equal("DE", result.Filter.CountryKey);
|
||||||
|
Assert.Empty(result.Rows);
|
||||||
|
Assert.Equal(0m, result.NetSalesActual);
|
||||||
|
Assert.Contains("keine Datensaetze", result.Notices[0]);
|
||||||
|
Assert.Contains(2025, result.YearOptions);
|
||||||
|
Assert.Contains("DE", result.CountryOptions);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task SeedCentralRowsAsync(params CentralSalesRecord[] rows)
|
private async Task SeedCentralRowsAsync(params CentralSalesRecord[] rows)
|
||||||
{
|
{
|
||||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||||
|
|||||||
Reference in New Issue
Block a user