474 lines
19 KiB
Plaintext
474 lines
19 KiB
Plaintext
@page "/"
|
|
@using System.Diagnostics
|
|
@using TrafagSalesExporter.Services
|
|
@inject IDashboardPageService DashboardPageActions
|
|
@inject ExportOrchestrationService Orchestrator
|
|
@inject TimerBackgroundService TimerService
|
|
@inject ISnackbar Snackbar
|
|
@inject IUiTextService UiText
|
|
@implements IDisposable
|
|
|
|
<PageTitle>@T("Dashboard", "Dashboard")</PageTitle>
|
|
|
|
<MudText Typo="Typo.h4" Class="mb-4">@T("Dashboard", "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">
|
|
<MudStack Row AlignItems="AlignItems.Center" Spacing="4">
|
|
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.PlayArrow"
|
|
OnClick="ExportAll" Disabled="_anyRunning">
|
|
@T("Alle exportieren", "Export all")
|
|
</MudButton>
|
|
<MudButton Variant="Variant.Outlined" Color="Color.Secondary" StartIcon="@Icons.Material.Filled.TableView"
|
|
OnClick="ExportConsolidatedOnly" Disabled="_anyRunning">
|
|
@T("Zentrale Datei neu erzeugen", "Rebuild consolidated file")
|
|
</MudButton>
|
|
<MudText Typo="Typo.body1">
|
|
@if (TimerService.NextRun < DateTime.MaxValue)
|
|
{
|
|
<MudIcon Icon="@Icons.Material.Filled.Schedule" Size="Size.Small" Class="mr-1" />
|
|
@(string.Format(T("Naechster automatischer Lauf: {0}", "Next automatic run: {0}"), TimerService.NextRun.ToString("dd.MM.yyyy HH:mm")))
|
|
}
|
|
else
|
|
{
|
|
<MudIcon Icon="@Icons.Material.Filled.TimerOff" Size="Size.Small" Class="mr-1" />
|
|
@T("Timer deaktiviert", "Timer disabled")
|
|
}
|
|
</MudText>
|
|
</MudStack>
|
|
</MudPaper>
|
|
|
|
<MudTable Items="_dashboardRows" Dense Hover Striped Loading="_loading">
|
|
<HeaderContent>
|
|
<MudTh>@T("Land", "Country")</MudTh>
|
|
<MudTh>TSC</MudTh>
|
|
<MudTh>@T("Schema", "Schema")</MudTh>
|
|
<MudTh>@T("Server", "Server")</MudTh>
|
|
<MudTh>@T("Status", "Status")</MudTh>
|
|
<MudTh>@T("Live-Status", "Live status")</MudTh>
|
|
<MudTh>@T("Zeilen", "Rows")</MudTh>
|
|
<MudTh>@T("Letzter Lauf", "Last run")</MudTh>
|
|
<MudTh>@T("Dauer", "Duration")</MudTh>
|
|
<MudTh>@T("Aktion", "Action")</MudTh>
|
|
</HeaderContent>
|
|
<RowTemplate>
|
|
<MudTd>@context.Land</MudTd>
|
|
<MudTd>@context.TSC</MudTd>
|
|
<MudTd>@context.Schema</MudTd>
|
|
<MudTd>@context.ServerName</MudTd>
|
|
<MudTd>
|
|
@if (Orchestrator.IsExporting(context.SiteId))
|
|
{
|
|
<MudProgressCircular Size="Size.Small" Indeterminate Color="Color.Primary" Class="mr-1" />
|
|
<MudText Typo="Typo.caption">@Orchestrator.GetExportStatus(context.SiteId)</MudText>
|
|
}
|
|
else if (context.LastStatus == "OK")
|
|
{
|
|
<MudIcon Icon="@Icons.Material.Filled.CheckCircle" Color="Color.Success" Size="Size.Small" />
|
|
}
|
|
else if (context.LastStatus == "Error")
|
|
{
|
|
<MudTooltip Text="@context.ErrorMessage">
|
|
<MudIcon Icon="@Icons.Material.Filled.Error" Color="Color.Error" Size="Size.Small" />
|
|
</MudTooltip>
|
|
}
|
|
else
|
|
{
|
|
<MudText Typo="Typo.caption" Color="Color.Default">-</MudText>
|
|
}
|
|
</MudTd>
|
|
<MudTd>
|
|
@if (!string.IsNullOrWhiteSpace(context.LiveMessage))
|
|
{
|
|
<MudTooltip Text="@context.LiveDetails">
|
|
<MudText Typo="Typo.caption" Style="max-width:360px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; display:block;">
|
|
@context.LiveMessage
|
|
</MudText>
|
|
</MudTooltip>
|
|
}
|
|
else
|
|
{
|
|
<MudText Typo="Typo.caption" Color="Color.Default">-</MudText>
|
|
}
|
|
</MudTd>
|
|
<MudTd>@(context.RowCount > 0 ? context.RowCount.ToString("N0") : "-")</MudTd>
|
|
<MudTd>@(context.LastRun.HasValue ? context.LastRun.Value.ToString("dd.MM.yyyy HH:mm:ss") : "-")</MudTd>
|
|
<MudTd>@(context.DurationSeconds > 0 ? $"{context.DurationSeconds:F1}s" : "-")</MudTd>
|
|
<MudTd>
|
|
<MudStack Row Spacing="1">
|
|
<MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary"
|
|
StartIcon="@Icons.Material.Filled.FileDownload"
|
|
OnClick="() => ExportSingle(context.SiteId)"
|
|
Disabled="Orchestrator.IsExporting(context.SiteId)">
|
|
Export
|
|
</MudButton>
|
|
<MudButton Size="Size.Small" Variant="Variant.Text" Color="Color.Info"
|
|
StartIcon="@Icons.Material.Filled.OpenInNew"
|
|
OnClick="() => OpenExportFile(context)"
|
|
Disabled="@(!context.HasOpenableFile || Orchestrator.IsExporting(context.SiteId))">
|
|
@T("Excel oeffnen", "Open Excel")
|
|
</MudButton>
|
|
</MudStack>
|
|
</MudTd>
|
|
</RowTemplate>
|
|
</MudTable>
|
|
|
|
<MudPaper Class="pa-4 mt-4" Elevation="1">
|
|
<MudText Typo="Typo.h6" Class="mb-3">@T("Zentrale Datei", "Consolidated file")</MudText>
|
|
<MudTable Items="_consolidatedRows" Dense Hover Striped>
|
|
<HeaderContent>
|
|
<MudTh>@T("Datei", "File")</MudTh>
|
|
<MudTh>Pfad</MudTh>
|
|
<MudTh>Letzte Änderung</MudTh>
|
|
<MudTh>@T("Status", "Status")</MudTh>
|
|
<MudTh>@T("Aktion", "Action")</MudTh>
|
|
</HeaderContent>
|
|
<RowTemplate>
|
|
<MudTd>@context.Label</MudTd>
|
|
<MudTd>@context.DisplayPath</MudTd>
|
|
<MudTd>@(context.LastModified.HasValue ? context.LastModified.Value.ToString("dd.MM.yyyy HH:mm:ss") : "-")</MudTd>
|
|
<MudTd>
|
|
@if (Orchestrator.IsConsolidatedExporting())
|
|
{
|
|
<MudProgressCircular Size="Size.Small" Indeterminate Color="Color.Primary" Class="mr-1" />
|
|
<MudText Typo="Typo.caption">@Orchestrator.GetConsolidatedExportStatus()</MudText>
|
|
}
|
|
else
|
|
{
|
|
<MudText Typo="Typo.caption" Color="Color.Default">-</MudText>
|
|
}
|
|
</MudTd>
|
|
<MudTd>
|
|
<MudButton Size="Size.Small" Variant="Variant.Text" Color="Color.Info"
|
|
StartIcon="@Icons.Material.Filled.OpenInNew"
|
|
OnClick="() => OpenFile(context.FilePath)"
|
|
Disabled="@(!context.HasOpenableFile)">
|
|
@T("Excel oeffnen", "Open Excel")
|
|
</MudButton>
|
|
</MudTd>
|
|
</RowTemplate>
|
|
<NoRecordsContent>
|
|
<MudText Typo="Typo.caption">@T("Keine zentrale Excel-Datei gefunden.", "No consolidated Excel file found.")</MudText>
|
|
</NoRecordsContent>
|
|
</MudTable>
|
|
</MudPaper>
|
|
|
|
@code {
|
|
private List<DashboardRow> _dashboardRows = new();
|
|
private List<ConsolidatedDashboardRow> _consolidatedRows = new();
|
|
private List<NetSalesReferenceRow> _netSalesReferenceRows = new();
|
|
private bool _loading = true;
|
|
private bool _anyRunning;
|
|
private CancellationTokenSource? _pollingCts;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
Orchestrator.OnExportStatusChanged += HandleStatusChanged;
|
|
await LoadDataAsync();
|
|
}
|
|
|
|
private async Task LoadDataAsync()
|
|
{
|
|
_loading = true;
|
|
var state = await DashboardPageActions.LoadAsync();
|
|
_dashboardRows = state.DashboardRows;
|
|
_consolidatedRows = state.ConsolidatedRows;
|
|
_netSalesReferenceRows = state.NetSalesReferenceRows;
|
|
|
|
_anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId)) || Orchestrator.IsConsolidatedExporting();
|
|
_loading = false;
|
|
}
|
|
|
|
private async Task ExportAll()
|
|
{
|
|
_anyRunning = true;
|
|
await LoadDataAsync();
|
|
StartPolling();
|
|
_ = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
await Orchestrator.ExportAllAsync();
|
|
await InvokeAsync(() =>
|
|
Snackbar.Add(T("Export fuer alle Standorte beendet", "Export completed for all sites"), Severity.Success));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await InvokeAsync(() =>
|
|
Snackbar.Add(string.Format(T("Export fuer alle Standorte fehlgeschlagen: {0}", "Export for all sites failed: {0}"), FormatException(ex)), Severity.Error));
|
|
}
|
|
finally
|
|
{
|
|
await InvokeAsync(async () =>
|
|
{
|
|
await LoadDataAsync();
|
|
StateHasChanged();
|
|
});
|
|
}
|
|
});
|
|
Snackbar.Add(T("Export fuer alle Standorte gestartet", "Export started for all sites"), Severity.Info);
|
|
}
|
|
|
|
private async Task ExportConsolidatedOnly()
|
|
{
|
|
_anyRunning = true;
|
|
await LoadDataAsync();
|
|
StartPolling();
|
|
_ = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
var filePath = await Orchestrator.ExportConsolidatedOnlyAsync();
|
|
|
|
if (!string.IsNullOrWhiteSpace(filePath))
|
|
{
|
|
await InvokeAsync(() =>
|
|
Snackbar.Add(string.Format(T("Zentrale Datei erzeugt: {0}", "Consolidated file created: {0}"), filePath), Severity.Success));
|
|
}
|
|
else
|
|
{
|
|
await InvokeAsync(() =>
|
|
Snackbar.Add(T("Zentrale Datei konnte nicht erzeugt werden. Details stehen in den Logs.", "Consolidated file could not be created. Details are in the logs."), Severity.Warning));
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await InvokeAsync(() =>
|
|
Snackbar.Add(string.Format(T("Zentrale Datei fehlgeschlagen: {0}", "Consolidated file failed: {0}"), FormatException(ex)), Severity.Error));
|
|
}
|
|
finally
|
|
{
|
|
await InvokeAsync(async () =>
|
|
{
|
|
await LoadDataAsync();
|
|
StateHasChanged();
|
|
});
|
|
}
|
|
});
|
|
Snackbar.Add(T("Zentrale Datei wird erzeugt", "Building consolidated file"), Severity.Info);
|
|
}
|
|
|
|
private void ExportSingle(int siteId)
|
|
{
|
|
_anyRunning = true;
|
|
_ = InvokeAsync(async () => await LoadDataAsync());
|
|
StartPolling();
|
|
_ = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
var result = await Orchestrator.ExportSiteByIdAsync(siteId);
|
|
|
|
if (result?.Log.Status == "OK" && !string.IsNullOrWhiteSpace(result.FilePath))
|
|
{
|
|
await InvokeAsync(() =>
|
|
Snackbar.Add(string.Format(T("Export gespeichert: {0}", "Export saved: {0}"), result.FilePath), Severity.Success));
|
|
}
|
|
else if (result?.Log.Status == "Error" && !string.IsNullOrWhiteSpace(result.Log.ErrorMessage))
|
|
{
|
|
await InvokeAsync(() =>
|
|
Snackbar.Add(string.Format(T("Export fehlgeschlagen: {0}", "Export failed: {0}"), result.Log.ErrorMessage), Severity.Error));
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
await InvokeAsync(() =>
|
|
Snackbar.Add(string.Format(T("Export fehlgeschlagen: {0}", "Export failed: {0}"), FormatException(ex)), Severity.Error));
|
|
}
|
|
finally
|
|
{
|
|
await InvokeAsync(async () =>
|
|
{
|
|
await LoadDataAsync();
|
|
StateHasChanged();
|
|
});
|
|
}
|
|
});
|
|
Snackbar.Add(T("Export gestartet", "Export started"), Severity.Info);
|
|
}
|
|
|
|
private async void HandleStatusChanged()
|
|
{
|
|
await InvokeAsync(async () =>
|
|
{
|
|
_anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId)) || Orchestrator.IsConsolidatedExporting() || _dashboardRows.Count == 0;
|
|
if (_anyRunning)
|
|
{
|
|
StartPolling();
|
|
await RefreshLiveDataAsync();
|
|
StateHasChanged();
|
|
return;
|
|
}
|
|
|
|
StopPolling();
|
|
await LoadDataAsync();
|
|
StateHasChanged();
|
|
});
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
StopPolling();
|
|
Orchestrator.OnExportStatusChanged -= HandleStatusChanged;
|
|
}
|
|
|
|
private void OpenExportFile(DashboardRow row)
|
|
{
|
|
OpenFile(row.FilePath);
|
|
}
|
|
|
|
private void OpenFile(string filePath)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(filePath) || !File.Exists(filePath))
|
|
{
|
|
Snackbar.Add(T("Exportdatei nicht gefunden.", "Export file not found."), Severity.Warning);
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
Process.Start(new ProcessStartInfo
|
|
{
|
|
FileName = filePath,
|
|
UseShellExecute = true
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Snackbar.Add(string.Format(T("Datei konnte nicht geoeffnet werden: {0}", "Could not open file: {0}"), ex.Message), Severity.Error);
|
|
}
|
|
}
|
|
|
|
private void StartPolling()
|
|
{
|
|
if (_pollingCts is not null && !_pollingCts.IsCancellationRequested)
|
|
return;
|
|
|
|
_pollingCts = new CancellationTokenSource();
|
|
_ = PollDashboardAsync(_pollingCts.Token);
|
|
}
|
|
|
|
private void StopPolling()
|
|
{
|
|
_pollingCts?.Cancel();
|
|
_pollingCts?.Dispose();
|
|
_pollingCts = null;
|
|
}
|
|
|
|
private async Task PollDashboardAsync(CancellationToken cancellationToken)
|
|
{
|
|
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(3));
|
|
|
|
try
|
|
{
|
|
while (await timer.WaitForNextTickAsync(cancellationToken))
|
|
{
|
|
var anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId)) || Orchestrator.IsConsolidatedExporting();
|
|
if (!anyRunning)
|
|
{
|
|
await InvokeAsync(async () =>
|
|
{
|
|
_anyRunning = false;
|
|
await LoadDataAsync();
|
|
StateHasChanged();
|
|
});
|
|
StopPolling();
|
|
break;
|
|
}
|
|
|
|
await InvokeAsync(async () =>
|
|
{
|
|
_anyRunning = true;
|
|
await RefreshLiveDataAsync();
|
|
StateHasChanged();
|
|
});
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
}
|
|
}
|
|
|
|
private Task RefreshLiveDataAsync()
|
|
{
|
|
foreach (var row in _dashboardRows)
|
|
{
|
|
if (!Orchestrator.IsExporting(row.SiteId))
|
|
continue;
|
|
|
|
row.LiveMessage = Orchestrator.GetExportStatus(row.SiteId);
|
|
row.LiveDetails = string.Empty;
|
|
}
|
|
|
|
_anyRunning = _dashboardRows.Any(r => Orchestrator.IsExporting(r.SiteId)) || Orchestrator.IsConsolidatedExporting();
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private static string FormatAmount(decimal? value)
|
|
=> value.HasValue ? value.Value.ToString("N2") : "-";
|
|
|
|
private static string FormatException(Exception ex)
|
|
=> ex.InnerException is null ? ex.Message : $"{ex.Message} Inner: {ex.InnerException.Message}";
|
|
|
|
}
|
|
|
|
@code {
|
|
private string T(string german, string english) => UiText.Text(german, english);
|
|
}
|