Refine cockpit navigation and HR access
This commit is contained in:
@@ -2,35 +2,38 @@
|
|||||||
@inject TrafagSalesExporter.Services.IUiTextService UiText
|
@inject TrafagSalesExporter.Services.IUiTextService UiText
|
||||||
|
|
||||||
<MudNavMenu>
|
<MudNavMenu>
|
||||||
<MudNavLink Href="/" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Dashboard">
|
<MudNavGroup Title="@T("Finance Cockpit", "Finance Cockpit")" Icon="@Icons.Material.Filled.Analytics" Expanded="true">
|
||||||
@T("Dashboard", "Dashboard")
|
<MudNavLink Href="/" Match="NavLinkMatch.All" Icon="@Icons.Material.Filled.Dashboard">
|
||||||
</MudNavLink>
|
@T("Export Dashboard", "Export dashboard")
|
||||||
<AuthorizeView Policy="@SecurityPolicies.AdminOnly">
|
</MudNavLink>
|
||||||
<Authorized>
|
<MudNavLink Href="/management-cockpit" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.QueryStats">
|
||||||
<MudNavLink Href="/standorte" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.LocationOn">
|
@T("Management Analyse", "Management analysis")
|
||||||
@T("Standorte", "Sites")
|
</MudNavLink>
|
||||||
</MudNavLink>
|
<MudNavLink Href="/finance-cockpit/vergleich" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.CompareArrows">
|
||||||
<MudNavLink Href="/transformations" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Transform">
|
@T("Soll/Ist Vergleich", "Actual/reference comparison")
|
||||||
@T("Transformationen", "Transformations")
|
</MudNavLink>
|
||||||
</MudNavLink>
|
</MudNavGroup>
|
||||||
</Authorized>
|
|
||||||
</AuthorizeView>
|
|
||||||
<MudNavLink Href="/management-cockpit" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Analytics">
|
|
||||||
@T("Management Cockpit", "Management Cockpit")
|
|
||||||
</MudNavLink>
|
|
||||||
<MudNavLink Href="/hr-kpi" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Groups">
|
<MudNavLink Href="/hr-kpi" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Groups">
|
||||||
@T("HR KPI", "HR KPI")
|
@T("HR KPI (Login)", "HR KPI (login)")
|
||||||
</MudNavLink>
|
|
||||||
<AuthorizeView Policy="@SecurityPolicies.AdminOnly">
|
|
||||||
<Authorized>
|
|
||||||
<MudNavLink Href="/settings" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Settings">
|
|
||||||
@T("Settings", "Settings")
|
|
||||||
</MudNavLink>
|
|
||||||
</Authorized>
|
|
||||||
</AuthorizeView>
|
|
||||||
<MudNavLink Href="/logs" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.List">
|
|
||||||
@T("Logs", "Logs")
|
|
||||||
</MudNavLink>
|
</MudNavLink>
|
||||||
|
<MudNavGroup Title="@T("Admin", "Admin")" Icon="@Icons.Material.Filled.AdminPanelSettings">
|
||||||
|
<AuthorizeView Policy="@SecurityPolicies.AdminOnly">
|
||||||
|
<Authorized>
|
||||||
|
<MudNavLink Href="/standorte" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.LocationOn">
|
||||||
|
@T("Standorte", "Sites")
|
||||||
|
</MudNavLink>
|
||||||
|
<MudNavLink Href="/transformations" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Transform">
|
||||||
|
@T("Transformationen", "Transformations")
|
||||||
|
</MudNavLink>
|
||||||
|
<MudNavLink Href="/settings" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.Settings">
|
||||||
|
@T("Settings", "Settings")
|
||||||
|
</MudNavLink>
|
||||||
|
</Authorized>
|
||||||
|
</AuthorizeView>
|
||||||
|
<MudNavLink Href="/logs" Match="NavLinkMatch.Prefix" Icon="@Icons.Material.Filled.List">
|
||||||
|
@T("Logs", "Logs")
|
||||||
|
</MudNavLink>
|
||||||
|
</MudNavGroup>
|
||||||
</MudNavMenu>
|
</MudNavMenu>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
|||||||
@@ -8,66 +8,9 @@
|
|||||||
@inject IUiTextService UiText
|
@inject IUiTextService UiText
|
||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
|
|
||||||
<PageTitle>@T("Dashboard", "Dashboard")</PageTitle>
|
<PageTitle>@T("Export Dashboard", "Export dashboard")</PageTitle>
|
||||||
|
|
||||||
<MudText Typo="Typo.h4" Class="mb-4">@T("Dashboard", "Dashboard")</MudText>
|
<MudText Typo="Typo.h4" Class="mb-4">@T("Export Dashboard", "Export dashboard")</MudText>
|
||||||
|
|
||||||
<MudPaper Class="pa-4 mb-4" Elevation="1">
|
|
||||||
<MudStack Row AlignItems="AlignItems.Center" Class="mb-3">
|
|
||||||
<MudText Typo="Typo.h6">@T("Net Sales Actuals 2025 Referenz", "Net sales actuals 2025 reference")</MudText>
|
|
||||||
<MudSpacer />
|
|
||||||
<MudText Typo="Typo.caption">check.xlsx / Power BI Stand 29.04.2026</MudText>
|
|
||||||
</MudStack>
|
|
||||||
<MudTable Items="_netSalesReferenceRows" Dense Hover Striped>
|
|
||||||
<HeaderContent>
|
|
||||||
<MudTh>@T("Firma", "Company")</MudTh>
|
|
||||||
<MudTh>@T("Ist 2025", "Actual 2025")</MudTh>
|
|
||||||
<MudTh>@T("IC-Abzug", "IC deduction")</MudTh>
|
|
||||||
<MudTh>@T("Ist exkl. IC", "Actual excl. IC")</MudTh>
|
|
||||||
<MudTh>@T("Referenz", "Reference")</MudTh>
|
|
||||||
<MudTh>@T("Summenfeld", "Value field")</MudTh>
|
|
||||||
<MudTh>@T("Quelle", "Source")</MudTh>
|
|
||||||
<MudTh>@T("Differenz", "Difference")</MudTh>
|
|
||||||
<MudTh>@T("Diff exkl. IC", "Diff excl. IC")</MudTh>
|
|
||||||
<MudTh>@T("Waehrung", "Currency")</MudTh>
|
|
||||||
<MudTh>@T("Zeilen", "Rows")</MudTh>
|
|
||||||
<MudTh>@T("Status", "Status")</MudTh>
|
|
||||||
</HeaderContent>
|
|
||||||
<RowTemplate>
|
|
||||||
<MudTd>@context.Label</MudTd>
|
|
||||||
<MudTd>@FormatAmount(context.ActualValue)</MudTd>
|
|
||||||
<MudTd>@FormatAmount(context.IntercompanyDeduction)</MudTd>
|
|
||||||
<MudTd>@FormatAmount(context.ActualValueExcludingIntercompany)</MudTd>
|
|
||||||
<MudTd>@FormatAmount(context.ReferenceValue)</MudTd>
|
|
||||||
<MudTd>@(string.IsNullOrWhiteSpace(context.ValueField) ? "-" : context.ValueField)</MudTd>
|
|
||||||
<MudTd>@context.ReferenceSource</MudTd>
|
|
||||||
<MudTd>@FormatAmount(context.Difference)</MudTd>
|
|
||||||
<MudTd>@FormatAmount(context.DifferenceExcludingIntercompany)</MudTd>
|
|
||||||
<MudTd>@(string.IsNullOrWhiteSpace(context.Currencies) ? "-" : context.Currencies)</MudTd>
|
|
||||||
<MudTd>@(context.RowCount > 0 ? context.RowCount.ToString("N0") : "-")</MudTd>
|
|
||||||
<MudTd>
|
|
||||||
@if (context.Status == "OK")
|
|
||||||
{
|
|
||||||
<MudChip T="string" Size="Size.Small" Color="Color.Success" Variant="Variant.Filled">OK</MudChip>
|
|
||||||
}
|
|
||||||
else if (context.Status == "Pruefen")
|
|
||||||
{
|
|
||||||
<MudChip T="string" Size="Size.Small" Color="Color.Warning" Variant="Variant.Filled">@T("Pruefen", "Check")</MudChip>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<MudChip T="string" Size="Size.Small" Color="Color.Default" Variant="Variant.Outlined">@T("Keine Daten", "No data")</MudChip>
|
|
||||||
}
|
|
||||||
</MudTd>
|
|
||||||
</RowTemplate>
|
|
||||||
<NoRecordsContent>
|
|
||||||
<MudText Typo="Typo.caption">@T("Keine Referenzdaten fuer aktive Standorte gefunden.", "No reference data found for active sites.")</MudText>
|
|
||||||
</NoRecordsContent>
|
|
||||||
</MudTable>
|
|
||||||
<MudAlert Severity="Severity.Info" Dense Variant="Variant.Outlined" Class="mt-3">
|
|
||||||
@T("Vergleich: Jahr 2025 aus Invoice Date, sonst Extraction Date. Das Summenfeld wird automatisch aus Sales Price/Value, DocTotalFC - VatSumFC oder DocTotal - VatSum gewaehlt; Belegkopfwerte werden pro DocEntry nur einmal gezaehlt. IC-Abzug ist eine Diagnose fuer den aktuellen Trafag-IT-Abgleich und veraendert die Originaldaten nicht.", "Comparison: year 2025 from Invoice Date, otherwise Extraction Date. The value field is selected automatically from Sales Price/Value, DocTotalFC - VatSumFC, or DocTotal - VatSum; document header values are counted only once per DocEntry. IC deduction is a diagnostic value for the current Trafag IT reconciliation and does not change the original data.")
|
|
||||||
</MudAlert>
|
|
||||||
</MudPaper>
|
|
||||||
|
|
||||||
<MudPaper Class="pa-4 mb-4" Elevation="1">
|
<MudPaper Class="pa-4 mb-4" Elevation="1">
|
||||||
<MudStack Row AlignItems="AlignItems.Center" Spacing="4">
|
<MudStack Row AlignItems="AlignItems.Center" Spacing="4">
|
||||||
@@ -212,7 +155,6 @@
|
|||||||
@code {
|
@code {
|
||||||
private List<DashboardRow> _dashboardRows = new();
|
private List<DashboardRow> _dashboardRows = new();
|
||||||
private List<ConsolidatedDashboardRow> _consolidatedRows = new();
|
private List<ConsolidatedDashboardRow> _consolidatedRows = new();
|
||||||
private List<NetSalesReferenceRow> _netSalesReferenceRows = new();
|
|
||||||
private bool _loading = true;
|
private bool _loading = true;
|
||||||
private bool _anyRunning;
|
private bool _anyRunning;
|
||||||
private CancellationTokenSource? _pollingCts;
|
private CancellationTokenSource? _pollingCts;
|
||||||
@@ -229,7 +171,6 @@
|
|||||||
var state = await DashboardPageActions.LoadAsync();
|
var state = await DashboardPageActions.LoadAsync();
|
||||||
_dashboardRows = state.DashboardRows;
|
_dashboardRows = state.DashboardRows;
|
||||||
_consolidatedRows = state.ConsolidatedRows;
|
_consolidatedRows = state.ConsolidatedRows;
|
||||||
_netSalesReferenceRows = state.NetSalesReferenceRows;
|
|
||||||
|
|
||||||
_anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId)) || Orchestrator.IsConsolidatedExporting();
|
_anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId)) || Orchestrator.IsConsolidatedExporting();
|
||||||
_loading = false;
|
_loading = false;
|
||||||
@@ -460,9 +401,6 @@
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string FormatAmount(decimal? value)
|
|
||||||
=> value.HasValue ? value.Value.ToString("N2") : "-";
|
|
||||||
|
|
||||||
private static string FormatException(Exception ex)
|
private static string FormatException(Exception ex)
|
||||||
=> ex.InnerException is null ? ex.Message : $"{ex.Message} Inner: {ex.InnerException.Message}";
|
=> ex.InnerException is null ? ex.Message : $"{ex.Message} Inner: {ex.InnerException.Message}";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
@page "/finance-cockpit/vergleich"
|
||||||
|
@using TrafagSalesExporter.Models
|
||||||
|
@using TrafagSalesExporter.Services
|
||||||
|
@inject IFinanceReconciliationService FinanceReconciliationService
|
||||||
|
@inject IUiTextService UiText
|
||||||
|
|
||||||
|
<PageTitle>@T("Soll/Ist Vergleich", "Actual/reference comparison")</PageTitle>
|
||||||
|
|
||||||
|
<MudText Typo="Typo.h4" Class="mb-4">@T("Soll/Ist Vergleich", "Actual/reference comparison")</MudText>
|
||||||
|
|
||||||
|
<MudPaper Class="pa-4 mb-4" Elevation="1">
|
||||||
|
<MudStack Row AlignItems="AlignItems.Center" Class="mb-3">
|
||||||
|
<div>
|
||||||
|
<MudText Typo="Typo.h6">@T("Net Sales Actuals 2025 Referenz", "Net sales actuals 2025 reference")</MudText>
|
||||||
|
<MudText Typo="Typo.caption">check.xlsx / Power BI Stand 29.04.2026</MudText>
|
||||||
|
</div>
|
||||||
|
<MudSpacer />
|
||||||
|
<MudButton Variant="Variant.Outlined" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Refresh"
|
||||||
|
OnClick="LoadAsync" Disabled="_loading">
|
||||||
|
@(_loading ? T("Lade...", "Loading...") : T("Aktualisieren", "Refresh"))
|
||||||
|
</MudButton>
|
||||||
|
</MudStack>
|
||||||
|
|
||||||
|
<MudTable Items="_netSalesReferenceRows" Dense Hover Striped Loading="_loading">
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>@T("Firma", "Company")</MudTh>
|
||||||
|
<MudTh>@T("Ist 2025", "Actual 2025")</MudTh>
|
||||||
|
<MudTh>@T("IC-Abzug", "IC deduction")</MudTh>
|
||||||
|
<MudTh>@T("Ist exkl. IC", "Actual excl. IC")</MudTh>
|
||||||
|
<MudTh>@T("Referenz", "Reference")</MudTh>
|
||||||
|
<MudTh>@T("Summenfeld", "Value field")</MudTh>
|
||||||
|
<MudTh>@T("Quelle", "Source")</MudTh>
|
||||||
|
<MudTh>@T("Differenz", "Difference")</MudTh>
|
||||||
|
<MudTh>@T("Diff exkl. IC", "Diff excl. IC")</MudTh>
|
||||||
|
<MudTh>@T("Waehrung", "Currency")</MudTh>
|
||||||
|
<MudTh>@T("Zeilen", "Rows")</MudTh>
|
||||||
|
<MudTh>@T("Status", "Status")</MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd>@context.Label</MudTd>
|
||||||
|
<MudTd>@FormatAmount(context.ActualValue)</MudTd>
|
||||||
|
<MudTd>@FormatAmount(context.IntercompanyDeduction)</MudTd>
|
||||||
|
<MudTd>@FormatAmount(context.ActualValueExcludingIntercompany)</MudTd>
|
||||||
|
<MudTd>@FormatAmount(context.ReferenceValue)</MudTd>
|
||||||
|
<MudTd>@(string.IsNullOrWhiteSpace(context.ValueField) ? "-" : context.ValueField)</MudTd>
|
||||||
|
<MudTd>@context.ReferenceSource</MudTd>
|
||||||
|
<MudTd>@FormatAmount(context.Difference)</MudTd>
|
||||||
|
<MudTd>@FormatAmount(context.DifferenceExcludingIntercompany)</MudTd>
|
||||||
|
<MudTd>@(string.IsNullOrWhiteSpace(context.Currencies) ? "-" : context.Currencies)</MudTd>
|
||||||
|
<MudTd>@(context.RowCount > 0 ? context.RowCount.ToString("N0") : "-")</MudTd>
|
||||||
|
<MudTd>
|
||||||
|
@if (context.Status == "OK")
|
||||||
|
{
|
||||||
|
<MudChip T="string" Size="Size.Small" Color="Color.Success" Variant="Variant.Filled">OK</MudChip>
|
||||||
|
}
|
||||||
|
else if (context.Status == "Pruefen")
|
||||||
|
{
|
||||||
|
<MudChip T="string" Size="Size.Small" Color="Color.Warning" Variant="Variant.Filled">@T("Pruefen", "Check")</MudChip>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudChip T="string" Size="Size.Small" Color="Color.Default" Variant="Variant.Outlined">@T("Keine Daten", "No data")</MudChip>
|
||||||
|
}
|
||||||
|
</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
<NoRecordsContent>
|
||||||
|
<MudText Typo="Typo.caption">@T("Keine Referenzdaten fuer aktive Standorte gefunden.", "No reference data found for active sites.")</MudText>
|
||||||
|
</NoRecordsContent>
|
||||||
|
</MudTable>
|
||||||
|
|
||||||
|
<MudAlert Severity="Severity.Info" Dense Variant="Variant.Outlined" Class="mt-3">
|
||||||
|
@T("Vergleich: Jahr 2025 aus Buchungsdatum, sonst Invoice Date, sonst Extraction Date. Das Summenfeld wird automatisch aus Sales Price/Value, DocTotalFC - VatSumFC oder DocTotal - VatSum gewaehlt; Belegkopfwerte werden pro DocEntry nur einmal gezaehlt. IC-Abzug ist eine Diagnose fuer den aktuellen Abgleich und veraendert die Originaldaten nicht.", "Comparison: year 2025 from posting date, otherwise invoice date, otherwise extraction date. The value field is selected automatically from Sales Price/Value, DocTotalFC - VatSumFC, or DocTotal - VatSum; document header values are counted only once per DocEntry. IC deduction is a diagnostic value for the current reconciliation and does not change the original data.")
|
||||||
|
</MudAlert>
|
||||||
|
</MudPaper>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private List<NetSalesReferenceRow> _netSalesReferenceRows = new();
|
||||||
|
private bool _loading = true;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await LoadAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadAsync()
|
||||||
|
{
|
||||||
|
_loading = true;
|
||||||
|
_netSalesReferenceRows = await FinanceReconciliationService.BuildNetSalesReferenceRowsAsync(2025);
|
||||||
|
_loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatAmount(decimal? value)
|
||||||
|
=> value.HasValue ? value.Value.ToString("N2") : "-";
|
||||||
|
|
||||||
|
private string T(string german, string english) => UiText.Text(german, english);
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
@using TrafagSalesExporter.Services
|
@using TrafagSalesExporter.Services
|
||||||
@inject IHrKpiService HrKpiService
|
@inject IHrKpiService HrKpiService
|
||||||
@inject IOptions<HrKpiDataSourceOptions> DataSourceOptions
|
@inject IOptions<HrKpiDataSourceOptions> DataSourceOptions
|
||||||
|
@inject IHrKpiAccessService HrKpiAccess
|
||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
@inject IUiTextService UiText
|
@inject IUiTextService UiText
|
||||||
|
|
||||||
@@ -11,94 +12,125 @@
|
|||||||
|
|
||||||
<MudText Typo="Typo.h4" Class="mb-4">@T("HR KPI", "HR KPI")</MudText>
|
<MudText Typo="Typo.h4" Class="mb-4">@T("HR KPI", "HR KPI")</MudText>
|
||||||
|
|
||||||
<MudPaper Class="pa-4 mb-4" Elevation="1">
|
@if (!CanShowHrKpi)
|
||||||
<MudGrid>
|
{
|
||||||
<MudItem xs="12" md="5">
|
<MudPaper Class="pa-4 mb-4" Elevation="1" Style="max-width:520px;">
|
||||||
<MudTextField @bind-Value="_dataFolder" Label="@T("Datenordner", "Data folder")" />
|
<MudStack Spacing="3">
|
||||||
</MudItem>
|
<MudAlert Severity="Severity.Warning" Variant="Variant.Outlined">
|
||||||
<MudItem xs="6" md="2">
|
@T("HR KPI enthaelt sensible Personaldaten. Bitte separat anmelden.", "HR KPI contains sensitive HR data. Please sign in separately.")
|
||||||
<MudSelect T="int?" @bind-Value="_year" Label="@T("Austrittsjahr", "Leaver year")" Dense Clearable>
|
</MudAlert>
|
||||||
@foreach (var option in _result?.ExitYearOptions ?? [])
|
@if (!HrKpiAccess.IsConfigured)
|
||||||
{
|
{
|
||||||
<MudSelectItem Value="@((int?)option)">@option</MudSelectItem>
|
<MudAlert Severity="Severity.Error" Variant="Variant.Filled">
|
||||||
}
|
@T("HR-KPI-Zugang ist noch nicht konfiguriert. Bitte Username und PasswordHash in HrKpiAccess konfigurieren.", "HR KPI access is not configured yet. Please configure Username and PasswordHash in HrKpiAccess.")
|
||||||
</MudSelect>
|
</MudAlert>
|
||||||
</MudItem>
|
}
|
||||||
<MudItem xs="12" md="3">
|
<MudTextField @bind-Value="_hrUsername" Label="@T("Name", "Name")" Disabled="@(!HrKpiAccess.IsConfigured)" />
|
||||||
<MudSelect T="string" @bind-Value="_organisation" Label="@T("Organisation", "Organisation")" Dense Clearable>
|
<MudTextField @bind-Value="_hrPassword" Label="@T("Passwort", "Password")" InputType="InputType.Password" Disabled="@(!HrKpiAccess.IsConfigured)" />
|
||||||
@foreach (var option in _result?.OrganisationOptions ?? [])
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="UnlockHrKpiAsync"
|
||||||
{
|
StartIcon="@Icons.Material.Filled.LockOpen" Disabled="@(!HrKpiAccess.IsConfigured)">
|
||||||
<MudSelectItem Value="@option">@option</MudSelectItem>
|
@T("HR KPI entsperren", "Unlock HR KPI")
|
||||||
}
|
|
||||||
</MudSelect>
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12" md="2">
|
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="LoadAsync"
|
|
||||||
StartIcon="@Icons.Material.Filled.Refresh" Disabled="_loading" FullWidth>
|
|
||||||
@(_loading ? T("Lade...", "Loading...") : T("Laden", "Load"))
|
|
||||||
</MudButton>
|
</MudButton>
|
||||||
</MudItem>
|
</MudStack>
|
||||||
<MudItem xs="12" md="3">
|
</MudPaper>
|
||||||
<MudDatePicker @bind-Date="_fromDate" Label="@T("Von Austritt", "Exit from")" Clearable DateFormat="dd.MM.yyyy" />
|
}
|
||||||
</MudItem>
|
else
|
||||||
<MudItem xs="12" md="3">
|
{
|
||||||
<MudDatePicker @bind-Date="_toDate" Label="@T("Bis Austritt", "Exit to")" Clearable DateFormat="dd.MM.yyyy" />
|
<MudPaper Class="pa-4 mb-4" Elevation="1">
|
||||||
</MudItem>
|
<MudGrid>
|
||||||
<MudItem xs="12" md="2">
|
<MudItem xs="12" md="5">
|
||||||
<MudSelect T="int?" @bind-Value="_entryYear" Label="@T("Eintrittsjahr", "Entry year")" Dense Clearable>
|
<MudTextField @bind-Value="_dataFolder" Label="@T("Datenordner", "Data folder")" />
|
||||||
@foreach (var option in _result?.EntryYearOptions ?? [])
|
</MudItem>
|
||||||
{
|
<MudItem xs="6" md="2">
|
||||||
<MudSelectItem Value="@((int?)option)">@option</MudSelectItem>
|
<MudSelect T="int?" @bind-Value="_year" Label="@T("Austrittsjahr", "Leaver year")" Dense Clearable>
|
||||||
}
|
@foreach (var option in _result?.ExitYearOptions ?? [])
|
||||||
</MudSelect>
|
{
|
||||||
</MudItem>
|
<MudSelectItem Value="@((int?)option)">@option</MudSelectItem>
|
||||||
<MudItem xs="12" md="4">
|
}
|
||||||
<MudTextField @bind-Value="_searchText" Label="@T("Suche Name / Personalnr.", "Search name / personnel no.")" />
|
</MudSelect>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="12" md="3">
|
<MudItem xs="12" md="3">
|
||||||
<MudSelect T="string" @bind-Value="_kostenstelle" Label="@T("Kostenstelle", "Cost center")" Dense Clearable>
|
<MudSelect T="string" @bind-Value="_organisation" Label="@T("Organisation", "Organisation")" Dense Clearable>
|
||||||
@foreach (var option in _result?.KostenstelleOptions ?? [])
|
@foreach (var option in _result?.OrganisationOptions ?? [])
|
||||||
{
|
{
|
||||||
<MudSelectItem Value="@option">@option</MudSelectItem>
|
<MudSelectItem Value="@option">@option</MudSelectItem>
|
||||||
}
|
}
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="12" md="3">
|
<MudItem xs="12" md="2">
|
||||||
<MudSelect T="string" @bind-Value="_mitarbeitertyp" Label="@T("Mitarbeitertyp", "Employee type")" Dense Clearable>
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="LoadAsync"
|
||||||
@foreach (var option in _result?.MitarbeitertypOptions ?? [])
|
StartIcon="@Icons.Material.Filled.Refresh" Disabled="_loading" FullWidth>
|
||||||
{
|
@(_loading ? T("Lade...", "Loading...") : T("Laden", "Load"))
|
||||||
<MudSelectItem Value="@option">@option</MudSelectItem>
|
</MudButton>
|
||||||
}
|
</MudItem>
|
||||||
</MudSelect>
|
<MudItem xs="12" md="3">
|
||||||
</MudItem>
|
<MudDatePicker @bind-Date="_fromDate" Label="@T("Von Austritt", "Exit from")" Clearable DateFormat="dd.MM.yyyy" />
|
||||||
<MudItem xs="12" md="2">
|
</MudItem>
|
||||||
<MudSelect T="string" @bind-Value="_fluktuationFilter" Label="@T("Fluktuation", "Turnover")" Dense>
|
<MudItem xs="12" md="3">
|
||||||
@foreach (var option in _fluktuationOptions)
|
<MudDatePicker @bind-Date="_toDate" Label="@T("Bis Austritt", "Exit to")" Clearable DateFormat="dd.MM.yyyy" />
|
||||||
{
|
</MudItem>
|
||||||
<MudSelectItem Value="@option.Key">@option.Label</MudSelectItem>
|
<MudItem xs="12" md="2">
|
||||||
}
|
<MudSelect T="int?" @bind-Value="_entryYear" Label="@T("Eintrittsjahr", "Entry year")" Dense Clearable>
|
||||||
</MudSelect>
|
@foreach (var option in _result?.EntryYearOptions ?? [])
|
||||||
</MudItem>
|
{
|
||||||
<MudItem xs="6" md="2">
|
<MudSelectItem Value="@((int?)option)">@option</MudSelectItem>
|
||||||
<MudSelect T="string" @bind-Value="_glzAmpel" Label="@T("GLZ", "Time")" Dense Clearable>
|
}
|
||||||
@foreach (var option in _ampelOptions)
|
</MudSelect>
|
||||||
{
|
</MudItem>
|
||||||
<MudSelectItem Value="@option">@option</MudSelectItem>
|
<MudItem xs="12" md="4">
|
||||||
}
|
<MudTextField @bind-Value="_searchText" Label="@T("Suche Name / Personalnr.", "Search name / personnel no.")" />
|
||||||
</MudSelect>
|
</MudItem>
|
||||||
</MudItem>
|
<MudItem xs="12" md="3">
|
||||||
<MudItem xs="6" md="2">
|
<MudSelect T="string" @bind-Value="_kostenstelle" Label="@T("Kostenstelle", "Cost center")" Dense Clearable>
|
||||||
<MudSelect T="string" @bind-Value="_restferienAmpel" Label="@T("Restferien", "Vacation")" Dense Clearable>
|
@foreach (var option in _result?.KostenstelleOptions ?? [])
|
||||||
@foreach (var option in _restferienOptions)
|
{
|
||||||
{
|
<MudSelectItem Value="@option">@option</MudSelectItem>
|
||||||
<MudSelectItem Value="@option">@option</MudSelectItem>
|
}
|
||||||
}
|
</MudSelect>
|
||||||
</MudSelect>
|
</MudItem>
|
||||||
</MudItem>
|
<MudItem xs="12" md="3">
|
||||||
</MudGrid>
|
<MudSelect T="string" @bind-Value="_mitarbeitertyp" Label="@T("Mitarbeitertyp", "Employee type")" Dense Clearable>
|
||||||
</MudPaper>
|
@foreach (var option in _result?.MitarbeitertypOptions ?? [])
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@option">@option</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" md="2">
|
||||||
|
<MudSelect T="string" @bind-Value="_fluktuationFilter" Label="@T("Fluktuation", "Turnover")" Dense>
|
||||||
|
@foreach (var option in _fluktuationOptions)
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@option.Key">@option.Label</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="6" md="2">
|
||||||
|
<MudSelect T="string" @bind-Value="_glzAmpel" Label="@T("GLZ", "Time")" Dense Clearable>
|
||||||
|
@foreach (var option in _ampelOptions)
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@option">@option</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="6" md="2">
|
||||||
|
<MudSelect T="string" @bind-Value="_restferienAmpel" Label="@T("Restferien", "Vacation")" Dense Clearable>
|
||||||
|
@foreach (var option in _restferienOptions)
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@option">@option</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" md="2">
|
||||||
|
<MudButton Variant="Variant.Outlined" Color="Color.Secondary" OnClick="LockHrKpi"
|
||||||
|
StartIcon="@Icons.Material.Filled.Lock" FullWidth>
|
||||||
|
@T("Sperren", "Lock")
|
||||||
|
</MudButton>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
</MudPaper>
|
||||||
|
}
|
||||||
|
|
||||||
@if (_result is not null)
|
@if (CanShowHrKpi && _result is not null)
|
||||||
{
|
{
|
||||||
@if (_result.Notices.Count > 0)
|
@if (_result.Notices.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -127,6 +159,8 @@
|
|||||||
private string? _glzAmpel;
|
private string? _glzAmpel;
|
||||||
private string? _restferienAmpel;
|
private string? _restferienAmpel;
|
||||||
private string? _searchText;
|
private string? _searchText;
|
||||||
|
private string? _hrUsername;
|
||||||
|
private string? _hrPassword;
|
||||||
private bool _loading;
|
private bool _loading;
|
||||||
private HrKpiResult? _result;
|
private HrKpiResult? _result;
|
||||||
private readonly List<(string Key, string Label)> _fluktuationOptions =
|
private readonly List<(string Key, string Label)> _fluktuationOptions =
|
||||||
@@ -142,11 +176,19 @@
|
|||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
_dataFolder = DataSourceOptions.Value.Normalize().DataFolder;
|
_dataFolder = DataSourceOptions.Value.Normalize().DataFolder;
|
||||||
await LoadAsync();
|
if (CanShowHrKpi)
|
||||||
|
{
|
||||||
|
await LoadAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadAsync()
|
private async Task LoadAsync()
|
||||||
{
|
{
|
||||||
|
if (!CanShowHrKpi)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_loading = true;
|
_loading = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -176,5 +218,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task UnlockHrKpiAsync()
|
||||||
|
{
|
||||||
|
if (!HrKpiAccess.TryUnlock(_hrUsername ?? string.Empty, _hrPassword ?? string.Empty))
|
||||||
|
{
|
||||||
|
Snackbar.Add(T("HR-KPI-Anmeldung fehlgeschlagen.", "HR KPI sign-in failed."), Severity.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_hrPassword = string.Empty;
|
||||||
|
await LoadAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LockHrKpi()
|
||||||
|
{
|
||||||
|
HrKpiAccess.Lock();
|
||||||
|
_result = null;
|
||||||
|
_hrPassword = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanShowHrKpi => !HrKpiAccess.IsEnabled || HrKpiAccess.IsUnlocked;
|
||||||
|
|
||||||
private string T(string german, string english) => UiText.Text(german, english);
|
private string T(string german, string english) => UiText.Text(german, english);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,9 @@
|
|||||||
@inject ISnackbar Snackbar
|
@inject ISnackbar Snackbar
|
||||||
@inject IUiTextService UiText
|
@inject IUiTextService UiText
|
||||||
|
|
||||||
<PageTitle>@T("Management Cockpit", "Management Cockpit")</PageTitle>
|
<PageTitle>@T("Management Analyse", "Management analysis")</PageTitle>
|
||||||
|
|
||||||
<MudText Typo="Typo.h4" Class="mb-4">@T("Management Cockpit", "Management Cockpit")</MudText>
|
<MudText Typo="Typo.h4" Class="mb-4">@T("Management Analyse", "Management analysis")</MudText>
|
||||||
|
|
||||||
<MudPaper Class="pa-4 mb-4" Elevation="1">
|
<MudPaper Class="pa-4 mb-4" Elevation="1">
|
||||||
<MudGrid>
|
<MudGrid>
|
||||||
@@ -64,6 +64,12 @@
|
|||||||
}
|
}
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
|
<MudItem xs="12" md="2">
|
||||||
|
<MudTextField @bind-Value="_centralLandFilter" Label="@T("Landfilter", "Country filter")" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" md="2">
|
||||||
|
<MudTextField @bind-Value="_centralTscFilter" Label="TSC" />
|
||||||
|
</MudItem>
|
||||||
<MudItem xs="12" md="2">
|
<MudItem xs="12" md="2">
|
||||||
<MudSelect T="int?" @bind-Value="_selectedCentralMonth" Label='@T("Monat (optional)", "Month (optional)")' Dense Clearable>
|
<MudSelect T="int?" @bind-Value="_selectedCentralMonth" Label='@T("Monat (optional)", "Month (optional)")' Dense Clearable>
|
||||||
@foreach (var month in Enumerable.Range(1, 12))
|
@foreach (var month in Enumerable.Range(1, 12))
|
||||||
@@ -102,10 +108,22 @@
|
|||||||
</MudSelect>
|
</MudSelect>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="12">
|
<MudItem xs="12">
|
||||||
<MudButton Variant="Variant.Filled" Color="Color.Secondary" OnClick="AnalyzeCentral"
|
<MudStack Row Spacing="2" AlignItems="AlignItems.Center">
|
||||||
StartIcon="@Icons.Material.Filled.QueryStats" Disabled="_analyzingCentral || _selectedCentralYear == 0">
|
<MudButton Variant="Variant.Filled" Color="Color.Secondary" OnClick="AnalyzeCentral"
|
||||||
@(_analyzingCentral ? T("Analysiere...", "Analyzing...") : T("Zentrale Auswertung laden", "Load central analysis"))
|
StartIcon="@Icons.Material.Filled.QueryStats" Disabled="_analyzingCentral || _selectedCentralYear == 0">
|
||||||
</MudButton>
|
@(_analyzingCentral ? T("Analysiere...", "Analyzing...") : T("Zentrale Auswertung laden", "Load central analysis"))
|
||||||
|
</MudButton>
|
||||||
|
<MudButton Variant="Variant.Outlined" Color="Color.Default" OnClick="ClearCentralScope"
|
||||||
|
StartIcon="@Icons.Material.Filled.FilterAltOff">
|
||||||
|
@T("Global", "Global")
|
||||||
|
</MudButton>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(_centralLandFilter) || !string.IsNullOrWhiteSpace(_centralTscFilter))
|
||||||
|
{
|
||||||
|
<MudChip T="string" Size="Size.Small" Color="Color.Info" Variant="Variant.Outlined">
|
||||||
|
@T("Gefiltert", "Filtered"): @($"{(_centralLandFilter ?? "-")} / {(_centralTscFilter ?? "-")}")
|
||||||
|
</MudChip>
|
||||||
|
}
|
||||||
|
</MudStack>
|
||||||
</MudItem>
|
</MudItem>
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
</MudPaper>
|
</MudPaper>
|
||||||
@@ -332,6 +350,8 @@
|
|||||||
private ManagementCockpitCentralResult? _centralResult;
|
private ManagementCockpitCentralResult? _centralResult;
|
||||||
private int _selectedCentralYear;
|
private int _selectedCentralYear;
|
||||||
private int? _selectedCentralMonth;
|
private int? _selectedCentralMonth;
|
||||||
|
private string? _centralLandFilter;
|
||||||
|
private string? _centralTscFilter;
|
||||||
private string _selectedFileValueField = ManagementCockpitValueFieldKeys.SalesPriceValue;
|
private string _selectedFileValueField = ManagementCockpitValueFieldKeys.SalesPriceValue;
|
||||||
private string _selectedCentralValueField = ManagementCockpitValueFieldKeys.SalesPriceValue;
|
private string _selectedCentralValueField = ManagementCockpitValueFieldKeys.SalesPriceValue;
|
||||||
private IEnumerable<string> _selectedCentralAdditionalValueFields = [];
|
private IEnumerable<string> _selectedCentralAdditionalValueFields = [];
|
||||||
@@ -385,6 +405,8 @@
|
|||||||
ValueField = _selectedFileValueField,
|
ValueField = _selectedFileValueField,
|
||||||
TargetCurrency = _selectedFileTargetCurrency
|
TargetCurrency = _selectedFileTargetCurrency
|
||||||
});
|
});
|
||||||
|
_centralLandFilter = _result.Summary.Land;
|
||||||
|
_centralTscFilter = _result.Summary.Tsc;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -408,7 +430,9 @@
|
|||||||
{
|
{
|
||||||
ValueField = _selectedCentralValueField,
|
ValueField = _selectedCentralValueField,
|
||||||
AdditionalValueFields = _selectedCentralAdditionalValueFields.ToList(),
|
AdditionalValueFields = _selectedCentralAdditionalValueFields.ToList(),
|
||||||
TargetCurrency = _selectedCentralTargetCurrency
|
TargetCurrency = _selectedCentralTargetCurrency,
|
||||||
|
LandFilter = _centralLandFilter,
|
||||||
|
TscFilter = _centralTscFilter
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -421,6 +445,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ClearCentralScope()
|
||||||
|
{
|
||||||
|
_centralLandFilter = null;
|
||||||
|
_centralTscFilter = null;
|
||||||
|
}
|
||||||
|
|
||||||
private static Severity MapSeverity(string severity) => severity switch
|
private static Severity MapSeverity(string severity) => severity switch
|
||||||
{
|
{
|
||||||
"Warning" => Severity.Warning,
|
"Warning" => Severity.Warning,
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ public class ManagementCockpitAnalysisOptions
|
|||||||
public string ValueField { get; set; } = ManagementCockpitValueFieldKeys.SalesPriceValue;
|
public string ValueField { get; set; } = ManagementCockpitValueFieldKeys.SalesPriceValue;
|
||||||
public List<string> AdditionalValueFields { get; set; } = [];
|
public List<string> AdditionalValueFields { get; set; } = [];
|
||||||
public string TargetCurrency { get; set; } = ManagementCockpitCurrencyOptions.Native;
|
public string TargetCurrency { get; set; } = ManagementCockpitCurrencyOptions.Native;
|
||||||
|
public string? LandFilter { get; set; }
|
||||||
|
public string? TscFilter { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ManagementCockpitSummary
|
public class ManagementCockpitSummary
|
||||||
@@ -89,6 +91,8 @@ public class ManagementCockpitCentralFilter
|
|||||||
public int? Month { get; set; }
|
public int? Month { get; set; }
|
||||||
public string ValueField { get; set; } = ManagementCockpitValueFieldKeys.SalesPriceValue;
|
public string ValueField { get; set; } = ManagementCockpitValueFieldKeys.SalesPriceValue;
|
||||||
public string TargetCurrency { get; set; } = ManagementCockpitCurrencyOptions.Native;
|
public string TargetCurrency { get; set; } = ManagementCockpitCurrencyOptions.Native;
|
||||||
|
public string? Land { get; set; }
|
||||||
|
public string? Tsc { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ManagementCockpitCentralSummary
|
public class ManagementCockpitCentralSummary
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ builder.Services.AddAuthorization(options =>
|
|||||||
builder.Services.AddMudServices();
|
builder.Services.AddMudServices();
|
||||||
builder.Services.AddHttpClient(nameof(ExchangeRateImportService));
|
builder.Services.AddHttpClient(nameof(ExchangeRateImportService));
|
||||||
builder.Services.Configure<HrKpiDataSourceOptions>(builder.Configuration.GetSection(HrKpiDataSourceOptions.SectionName));
|
builder.Services.Configure<HrKpiDataSourceOptions>(builder.Configuration.GetSection(HrKpiDataSourceOptions.SectionName));
|
||||||
|
builder.Services.Configure<HrKpiAccessOptions>(builder.Configuration.GetSection(HrKpiAccessOptions.SectionName));
|
||||||
|
|
||||||
builder.Services.AddDbContextFactory<AppDbContext>(options =>
|
builder.Services.AddDbContextFactory<AppDbContext>(options =>
|
||||||
options.UseSqlite("Data Source=trafag_exporter.db;Default Timeout=60"));
|
options.UseSqlite("Data Source=trafag_exporter.db;Default Timeout=60"));
|
||||||
@@ -104,6 +105,7 @@ builder.Services.AddScoped<IManagementCockpitPageService, ManagementCockpitPageS
|
|||||||
builder.Services.AddScoped<IDashboardPageService, DashboardPageService>();
|
builder.Services.AddScoped<IDashboardPageService, DashboardPageService>();
|
||||||
builder.Services.AddScoped<ILogsPageService, LogsPageService>();
|
builder.Services.AddScoped<ILogsPageService, LogsPageService>();
|
||||||
builder.Services.AddScoped<ITransformationsPageService, TransformationsPageService>();
|
builder.Services.AddScoped<ITransformationsPageService, TransformationsPageService>();
|
||||||
|
builder.Services.AddScoped<IHrKpiAccessService, HrKpiAccessService>();
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace TrafagSalesExporter.Security;
|
||||||
|
|
||||||
|
public sealed class HrKpiAccessOptions
|
||||||
|
{
|
||||||
|
public const string SectionName = "HrKpiAccess";
|
||||||
|
|
||||||
|
public bool Enabled { get; set; } = true;
|
||||||
|
public string Username { get; set; } = "hr";
|
||||||
|
public string PasswordHash { get; set; } = string.Empty;
|
||||||
|
public string Password { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -12,14 +12,10 @@ public interface IDashboardPageService
|
|||||||
public sealed class DashboardPageService : IDashboardPageService
|
public sealed class DashboardPageService : IDashboardPageService
|
||||||
{
|
{
|
||||||
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
private readonly IDbContextFactory<AppDbContext> _dbFactory;
|
||||||
private readonly IFinanceReconciliationService _financeReconciliationService;
|
|
||||||
|
|
||||||
public DashboardPageService(
|
public DashboardPageService(IDbContextFactory<AppDbContext> dbFactory)
|
||||||
IDbContextFactory<AppDbContext> dbFactory,
|
|
||||||
IFinanceReconciliationService financeReconciliationService)
|
|
||||||
{
|
{
|
||||||
_dbFactory = dbFactory;
|
_dbFactory = dbFactory;
|
||||||
_financeReconciliationService = financeReconciliationService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<DashboardPageState> LoadAsync()
|
public async Task<DashboardPageState> LoadAsync()
|
||||||
@@ -69,8 +65,7 @@ public sealed class DashboardPageService : IDashboardPageService
|
|||||||
return new DashboardPageState
|
return new DashboardPageState
|
||||||
{
|
{
|
||||||
DashboardRows = rows,
|
DashboardRows = rows,
|
||||||
ConsolidatedRows = BuildConsolidatedRows(await db.ExportSettings.FirstOrDefaultAsync() ?? new()),
|
ConsolidatedRows = BuildConsolidatedRows(await db.ExportSettings.FirstOrDefaultAsync() ?? new())
|
||||||
NetSalesReferenceRows = await _financeReconciliationService.BuildNetSalesReferenceRowsAsync(2025)
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +114,6 @@ public sealed class DashboardPageState
|
|||||||
{
|
{
|
||||||
public List<DashboardRow> DashboardRows { get; set; } = [];
|
public List<DashboardRow> DashboardRows { get; set; } = [];
|
||||||
public List<ConsolidatedDashboardRow> ConsolidatedRows { get; set; } = [];
|
public List<ConsolidatedDashboardRow> ConsolidatedRows { get; set; } = [];
|
||||||
public List<NetSalesReferenceRow> NetSalesReferenceRows { get; set; } = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class DashboardRow
|
public sealed class DashboardRow
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using TrafagSalesExporter.Security;
|
||||||
|
|
||||||
|
namespace TrafagSalesExporter.Services;
|
||||||
|
|
||||||
|
public interface IHrKpiAccessService
|
||||||
|
{
|
||||||
|
bool IsEnabled { get; }
|
||||||
|
bool IsConfigured { get; }
|
||||||
|
bool IsUnlocked { get; }
|
||||||
|
bool TryUnlock(string username, string password);
|
||||||
|
void Lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class HrKpiAccessService : IHrKpiAccessService
|
||||||
|
{
|
||||||
|
private readonly HrKpiAccessOptions _options;
|
||||||
|
|
||||||
|
public HrKpiAccessService(IOptions<HrKpiAccessOptions> options)
|
||||||
|
{
|
||||||
|
_options = options.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsEnabled => _options.Enabled;
|
||||||
|
|
||||||
|
public bool IsConfigured =>
|
||||||
|
!IsEnabled ||
|
||||||
|
!string.IsNullOrWhiteSpace(_options.Username) &&
|
||||||
|
(!string.IsNullOrWhiteSpace(_options.PasswordHash) || !string.IsNullOrEmpty(_options.Password));
|
||||||
|
|
||||||
|
public bool IsUnlocked { get; private set; }
|
||||||
|
|
||||||
|
public bool TryUnlock(string username, string password)
|
||||||
|
{
|
||||||
|
if (!IsEnabled)
|
||||||
|
{
|
||||||
|
IsUnlocked = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsConfigured ||
|
||||||
|
string.IsNullOrWhiteSpace(username) ||
|
||||||
|
string.IsNullOrEmpty(password) ||
|
||||||
|
!FixedEquals(username.Trim(), _options.Username.Trim()))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var valid = !string.IsNullOrWhiteSpace(_options.PasswordHash)
|
||||||
|
? VerifyPasswordHash(password, _options.PasswordHash)
|
||||||
|
: FixedEquals(password, _options.Password);
|
||||||
|
|
||||||
|
IsUnlocked = valid;
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Lock() => IsUnlocked = false;
|
||||||
|
|
||||||
|
private static bool VerifyPasswordHash(string password, string configuredHash)
|
||||||
|
{
|
||||||
|
var passwordHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(password)));
|
||||||
|
return FixedEquals(passwordHash, configuredHash.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool FixedEquals(string left, string right)
|
||||||
|
{
|
||||||
|
var leftBytes = Encoding.UTF8.GetBytes(left);
|
||||||
|
var rightBytes = Encoding.UTF8.GetBytes(right);
|
||||||
|
return leftBytes.Length == rightBytes.Length &&
|
||||||
|
CryptographicOperations.FixedTimeEquals(leftBytes, rightBytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -199,14 +199,17 @@ public class ManagementCockpitService : IManagementCockpitService
|
|||||||
.Select(row => BuildCentralAggregationRow(row, aggregation))
|
.Select(row => BuildCentralAggregationRow(row, aggregation))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var selectedRows = aggregatedRows
|
var scopedRows = ApplyCentralDimensionFilters(aggregatedRows, options)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var selectedRows = scopedRows
|
||||||
.Where(r => r.PeriodDate.Year == year && (!month.HasValue || r.PeriodDate.Month == month.Value))
|
.Where(r => r.PeriodDate.Year == year && (!month.HasValue || r.PeriodDate.Month == month.Value))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
if (selectedRows.Count == 0)
|
if (selectedRows.Count == 0)
|
||||||
throw new InvalidOperationException("Für den gewählten Zeitraum gibt es keine Datensätze in der zentralen Tabelle.");
|
throw new InvalidOperationException("Für den gewählten Zeitraum gibt es keine Datensätze in der zentralen Tabelle.");
|
||||||
|
|
||||||
var yearlyRows = aggregatedRows;
|
var yearlyRows = scopedRows;
|
||||||
|
|
||||||
var dailyBaseRows = selectedRows
|
var dailyBaseRows = selectedRows
|
||||||
.Where(r => month.HasValue)
|
.Where(r => month.HasValue)
|
||||||
@@ -219,7 +222,9 @@ public class ManagementCockpitService : IManagementCockpitService
|
|||||||
Year = year,
|
Year = year,
|
||||||
Month = month,
|
Month = month,
|
||||||
ValueField = aggregation.ValueField.Key,
|
ValueField = aggregation.ValueField.Key,
|
||||||
TargetCurrency = aggregation.TargetCurrency
|
TargetCurrency = aggregation.TargetCurrency,
|
||||||
|
Land = NormalizeOptionalFilter(options?.LandFilter),
|
||||||
|
Tsc = NormalizeOptionalFilter(options?.TscFilter)
|
||||||
},
|
},
|
||||||
Summary = new ManagementCockpitCentralSummary
|
Summary = new ManagementCockpitCentralSummary
|
||||||
{
|
{
|
||||||
@@ -239,7 +244,7 @@ public class ManagementCockpitService : IManagementCockpitService
|
|||||||
AdditionalValueFields = aggregation.AdditionalValueFields
|
AdditionalValueFields = aggregation.AdditionalValueFields
|
||||||
.Select(ToValueFieldOption)
|
.Select(ToValueFieldOption)
|
||||||
.ToList(),
|
.ToList(),
|
||||||
Notices = BuildCentralNotices(aggregation, selectedRows.Count(x => x.MissingExchangeRate)),
|
Notices = BuildCentralNotices(aggregation, selectedRows.Count(x => x.MissingExchangeRate), options),
|
||||||
YearlyTotals = yearlyRows
|
YearlyTotals = yearlyRows
|
||||||
.GroupBy(x => new { x.PeriodDate.Year, x.DisplayCurrency })
|
.GroupBy(x => new { x.PeriodDate.Year, x.DisplayCurrency })
|
||||||
.OrderBy(g => g.Key.Year)
|
.OrderBy(g => g.Key.Year)
|
||||||
@@ -291,6 +296,18 @@ public class ManagementCockpitService : IManagementCockpitService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<CentralAggregationRow> ApplyCentralDimensionFilters(
|
||||||
|
IEnumerable<CentralAggregationRow> rows,
|
||||||
|
ManagementCockpitAnalysisOptions? options)
|
||||||
|
{
|
||||||
|
var landFilter = NormalizeOptionalFilter(options?.LandFilter);
|
||||||
|
var tscFilter = NormalizeOptionalFilter(options?.TscFilter);
|
||||||
|
|
||||||
|
return rows.Where(row =>
|
||||||
|
(landFilter is null || string.Equals(row.Land, landFilter, StringComparison.OrdinalIgnoreCase)) &&
|
||||||
|
(tscFilter is null || string.Equals(row.Tsc, tscFilter, StringComparison.OrdinalIgnoreCase)));
|
||||||
|
}
|
||||||
|
|
||||||
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");
|
||||||
@@ -456,7 +473,10 @@ public class ManagementCockpitService : IManagementCockpitService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<string> BuildCentralNotices(AggregationSelection aggregation, int missingExchangeRateCount)
|
private static List<string> BuildCentralNotices(
|
||||||
|
AggregationSelection aggregation,
|
||||||
|
int missingExchangeRateCount,
|
||||||
|
ManagementCockpitAnalysisOptions? options)
|
||||||
{
|
{
|
||||||
var notices = new List<string>
|
var notices = new List<string>
|
||||||
{
|
{
|
||||||
@@ -467,6 +487,13 @@ public class ManagementCockpitService : IManagementCockpitService
|
|||||||
"Periodenlogik basiert auf Invoice Date, falls vorhanden, sonst auf Extraction Date."
|
"Periodenlogik basiert auf Invoice Date, falls vorhanden, sonst auf Extraction Date."
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var landFilter = NormalizeOptionalFilter(options?.LandFilter);
|
||||||
|
var tscFilter = NormalizeOptionalFilter(options?.TscFilter);
|
||||||
|
if (landFilter is not null || tscFilter is not null)
|
||||||
|
{
|
||||||
|
notices.Add($"Filter aus Auswahl: Land {(landFilter ?? "alle")}, TSC {(tscFilter ?? "alle")}.");
|
||||||
|
}
|
||||||
|
|
||||||
if (aggregation.AdditionalValueFields.Count > 0)
|
if (aggregation.AdditionalValueFields.Count > 0)
|
||||||
notices.Add($"Weitere Summenfelder: {string.Join(", ", aggregation.AdditionalValueFields.Select(x => x.Label))}.");
|
notices.Add($"Weitere Summenfelder: {string.Join(", ", aggregation.AdditionalValueFields.Select(x => x.Label))}.");
|
||||||
|
|
||||||
@@ -488,6 +515,9 @@ public class ManagementCockpitService : IManagementCockpitService
|
|||||||
return notices;
|
return notices;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string? NormalizeOptionalFilter(string? value)
|
||||||
|
=> string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||||
|
|
||||||
private static ManagementCockpitTimeValueRow BuildTimeValueRow(
|
private static ManagementCockpitTimeValueRow BuildTimeValueRow(
|
||||||
IEnumerable<CentralAggregationRow> groupRows,
|
IEnumerable<CentralAggregationRow> groupRows,
|
||||||
AggregationSelection aggregation,
|
AggregationSelection aggregation,
|
||||||
|
|||||||
@@ -24,5 +24,10 @@
|
|||||||
"SapFile": "HR_KPI_Export.xlsx",
|
"SapFile": "HR_KPI_Export.xlsx",
|
||||||
"AbsenceFile": "Abwesenheitinstunden.xlsx",
|
"AbsenceFile": "Abwesenheitinstunden.xlsx",
|
||||||
"LeaverFile": "Personalausgeschieden.xlsx"
|
"LeaverFile": "Personalausgeschieden.xlsx"
|
||||||
|
},
|
||||||
|
"HrKpiAccess": {
|
||||||
|
"Enabled": true,
|
||||||
|
"Username": "hr",
|
||||||
|
"PasswordHash": "A8AF253007750E0C2986CBD0BC570530B4AE2417AAC59067591E708547834AE4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1313,3 +1313,111 @@ Inhalt:
|
|||||||
|
|
||||||
- Todo-Liste fuer Group Sales Reporting Intranet-Dashboard.
|
- Todo-Liste fuer Group Sales Reporting Intranet-Dashboard.
|
||||||
- Priorisierte Punkte fuer CFO-Dokument, offene Laenderabweichungen, Intercompany, Budgetkurse und Berechtigungskonzept.
|
- Priorisierte Punkte fuer CFO-Dokument, offene Laenderabweichungen, Intercompany, Budgetkurse und Berechtigungskonzept.
|
||||||
|
|
||||||
|
## Navigation und HR-KPI-Zugriff 2026-05-15
|
||||||
|
|
||||||
|
Geaendert:
|
||||||
|
|
||||||
|
- Linke Navigation reduziert:
|
||||||
|
- Hauptgruppe `Finance Cockpit`
|
||||||
|
- eigener Hauptpunkt `HR KPI (Login)`
|
||||||
|
- Bisherige Finance-Seiten liegen als Unterpunkte unter `Finance Cockpit`:
|
||||||
|
- Dashboard
|
||||||
|
- Management Cockpit
|
||||||
|
- Standorte
|
||||||
|
- Transformationen
|
||||||
|
- Settings
|
||||||
|
- Logs
|
||||||
|
- HR KPI hat eine separate zweite Zugriffssperre mit Name und Passwort.
|
||||||
|
- HR-Daten werden erst geladen und angezeigt, wenn die HR-KPI-Sperre erfolgreich entsperrt wurde.
|
||||||
|
|
||||||
|
Konfiguration:
|
||||||
|
|
||||||
|
- Abschnitt `HrKpiAccess` in `appsettings.json`
|
||||||
|
- Benutzer: `hr`
|
||||||
|
- Passwortvorschlag: `Trafag-HR-KPI-2026!`
|
||||||
|
- Im Repo ist nur der SHA-256-Hash gespeichert, nicht das Klartextpasswort.
|
||||||
|
|
||||||
|
Verifikation:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dotnet build .\TrafagSalesExporter.csproj --no-restore -p:UseAppHost=false -p:OutDir=.\obj\verify_hrlogin\ --verbosity minimal
|
||||||
|
```
|
||||||
|
|
||||||
|
Ergebnis:
|
||||||
|
|
||||||
|
- Build erfolgreich.
|
||||||
|
- 3 bestehende MudBlazor-Analyzer-Warnungen in `Logs.razor`, `Transformations.razor` und `Standorte.razor`.
|
||||||
|
|
||||||
|
## Navigation in Finance/HR/Admin gegliedert 2026-05-15
|
||||||
|
|
||||||
|
Geaendert:
|
||||||
|
|
||||||
|
- Linke Navigation neu gegliedert:
|
||||||
|
- `Finance Cockpit`
|
||||||
|
- `HR KPI (Login)`
|
||||||
|
- `Admin`
|
||||||
|
- Unter `Finance Cockpit` stehen:
|
||||||
|
- `Export Dashboard`
|
||||||
|
- `Management Analyse`
|
||||||
|
- `Soll/Ist Vergleich`
|
||||||
|
- Unter `Admin` stehen:
|
||||||
|
- `Standorte`
|
||||||
|
- `Transformationen`
|
||||||
|
- `Settings`
|
||||||
|
- `Logs`
|
||||||
|
- Seitentitel wurden an die neuen Menuebezeichnungen angepasst.
|
||||||
|
|
||||||
|
Verifikation:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dotnet build .\TrafagSalesExporter.csproj --no-restore -p:UseAppHost=false -p:OutDir=.\obj\verify_nav_groups\ --verbosity minimal
|
||||||
|
```
|
||||||
|
|
||||||
|
Ergebnis:
|
||||||
|
|
||||||
|
- Build erfolgreich.
|
||||||
|
- 3 bestehende MudBlazor-Analyzer-Warnungen in `Logs.razor`, `Transformations.razor` und `Standorte.razor`.
|
||||||
|
|
||||||
|
## Management Cockpit zentrale Filterkopplung 2026-05-15
|
||||||
|
|
||||||
|
Geaendert:
|
||||||
|
|
||||||
|
- Die untere `Zentrale Roh-Auswertung` im Management Cockpit ist nicht mehr nur global.
|
||||||
|
- Neue Filterfelder: `Landfilter` und `TSC`.
|
||||||
|
- Wenn oben eine Einzeldatei analysiert wird, uebernimmt die zentrale Auswertung automatisch Land und TSC aus dieser Datei.
|
||||||
|
- Beispiel: Auswahl `USA | TRUS | Sales_TRUS_2026-05-08.xlsx` setzt unten automatisch `USA / TRUS`.
|
||||||
|
- Button `Global` leert die Filter, falls wieder alle Laender/Standorte ausgewertet werden sollen.
|
||||||
|
- Jahres-, Monats-, Jahreswerte-, Monatswerte-, Tageswerte-, Quellen- und Laendertabellen verwenden denselben Land/TSC-Filter.
|
||||||
|
|
||||||
|
Verifikation:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dotnet build .\TrafagSalesExporter.csproj --no-restore -p:UseAppHost=false -p:OutDir=.\obj\verify_management_scope2\ --verbosity minimal
|
||||||
|
```
|
||||||
|
|
||||||
|
Ergebnis:
|
||||||
|
|
||||||
|
- Build erfolgreich.
|
||||||
|
- 3 bestehende MudBlazor-Analyzer-Warnungen in `Logs.razor`, `Transformations.razor` und `Standorte.razor`.
|
||||||
|
|
||||||
|
## Finance Vergleich als eigener Reiter 2026-05-15
|
||||||
|
|
||||||
|
Geaendert:
|
||||||
|
|
||||||
|
- `Net Sales Actuals 2025 Referenz` aus dem Start-Dashboard entfernt.
|
||||||
|
- Neue Seite `Finance Vergleich` unter `Finance Cockpit` angelegt.
|
||||||
|
- Route: `/finance-cockpit/vergleich`
|
||||||
|
- Die Seite zeigt den Soll/Ist-Vergleich gegen `check.xlsx` separat, inklusive IC-Abzug, Referenzwert, Summenfeld, Differenz, Waehrung, Zeilen und Status.
|
||||||
|
- `DashboardPageService` laedt die Finance-Referenzdaten nicht mehr automatisch mit dem operativen Dashboard.
|
||||||
|
|
||||||
|
Verifikation:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dotnet build .\TrafagSalesExporter.csproj --no-restore -p:UseAppHost=false -p:OutDir=.\obj\verify_finance_compare_tab\ --verbosity minimal
|
||||||
|
```
|
||||||
|
|
||||||
|
Ergebnis:
|
||||||
|
|
||||||
|
- Build erfolgreich.
|
||||||
|
- 3 bestehende MudBlazor-Analyzer-Warnungen in `Logs.razor`, `Transformations.razor` und `Standorte.razor`.
|
||||||
|
|||||||
Reference in New Issue
Block a user