@page "/results/load" @using Microsoft.Extensions.DependencyInjection @using Marathon.Application.UseCases @using Marathon.Domain.Enums @using Marathon.UI.Components @using Marathon.UI.Services @using AppDateRange = Marathon.Application.Storage.DateRange @inject IStringLocalizer L @inject IResultsBrowsingService Browsing @inject NavigationManager Nav @inject IServiceProvider Sp @implements IDisposable @L["App.Title"] · @L["Results.Loader.Title"]
@L["Results.Loader.Kicker"]

@L["Results.Loader.Title"]

@L["Results.Loader.Lede"]

@L["Results.Loader.Action.Back"]
@if (_running || _completed) {
@string.Format(L["Results.Loader.Progress.Format"], _processed, _total) @if (_completed) { @string.Format(L["Results.Loader.Summary.Format"], _summaryLoaded, _summarySkipped, _summaryProcessed) }
@if (_progressLog.Count > 0) { @foreach (var entry in _progressLog) { }
@L["Results.Column.Match"] @L["Results.Column.Score"] @L["Results.Column.Winner"] Outcome
@entry.MatchLabel @if (entry.Loaded is { } r) { @($"{r.Side1Score}:{r.Side2Score}") } else { @("—") } @if (entry.Loaded is { WinnerSide: var w }) { @WinnerLabel(w) } @OutcomeLabel(entry.Outcome)
}
}
@code { private DateTimeOffset _from; private DateTimeOffset _to; private LoaderMode _mode = LoaderMode.AllInRange; private List _candidates = new(); private HashSet _selectedIds = new(StringComparer.Ordinal); private bool _running; private bool _completed; private int _processed; private int _total; private int _summaryLoaded; private int _summarySkipped; private int _summaryProcessed; private List _progressLog = new(); private CancellationTokenSource? _runCts; private enum LoaderMode { AllInRange, Selected } private sealed record ProgressEntry( string MatchLabel, ResultLoadOutcome Outcome, Domain.Entities.EventResult? Loaded); protected override async Task OnInitializedAsync() { var todayMoscow = new DateTimeOffset(DateTime.UtcNow.Date, TimeSpan.Zero).ToOffset(MoscowTime.Offset); _from = todayMoscow.AddDays(-7); _to = todayMoscow.AddDays(1).AddSeconds(-1); await ReloadCandidatesAsync(); } private async Task ReloadCandidatesAsync() { try { var range = new AppDateRange(_from, _to); var fresh = await Browsing.ListLoadCandidatesAsync(range, CancellationToken.None); _candidates = fresh.ToList(); // Keep selections that are still valid; drop the rest. var stillValid = new HashSet(fresh.Select(c => c.Id.Value), StringComparer.Ordinal); _selectedIds.IntersectWith(stillValid); } catch { _candidates = new(); _selectedIds.Clear(); } } private bool CanLoad() { if (_running) return false; if (_mode == LoaderMode.AllInRange) return _candidates.Count > 0; return _selectedIds.Count > 0; } private async Task StartLoad() { _running = true; _completed = false; _progressLog.Clear(); _processed = 0; IReadOnlyList? selection = _mode == LoaderMode.Selected ? _selectedIds.Select(s => new Domain.ValueObjects.EventId(s)).ToList() : null; // Pre-set Total so the progress bar has the right scale before the first tick. _total = _mode == LoaderMode.Selected ? _selectedIds.Count : _candidates.Count; StateHasChanged(); _runCts = new CancellationTokenSource(); var ct = _runCts.Token; // Resolve a fresh use-case scope so the EF DbContext is owned by this run. await using var scope = Sp.CreateAsyncScope(); var useCase = scope.ServiceProvider.GetRequiredService(); var progress = new Progress(OnProgress); try { var range = new AppDateRange(_from, _to); var (inspected, loaded, skipped) = await useCase.ExecuteAsync(range, selection, progress, ct); _summaryProcessed = inspected; _summaryLoaded = loaded; _summarySkipped = skipped; _completed = true; } catch (OperationCanceledException) { // Cancelled — leave the partial log visible. _completed = true; } finally { _running = false; _runCts?.Dispose(); _runCts = null; await ReloadCandidatesAsync(); StateHasChanged(); } } private void OnProgress(PullResultsProgress update) { _processed = update.Processed; _total = Math.Max(_total, update.Total); var label = LabelForEvent(update.EventId.Value); _progressLog.Add(new ProgressEntry( MatchLabel: label, Outcome: update.Outcome, Loaded: update.Result)); InvokeAsync(StateHasChanged); } private string LabelForEvent(string id) { var c = _candidates.FirstOrDefault(c => c.Id.Value == id); return c is null ? id : $"{c.Side1Name} vs {c.Side2Name}"; } private void CancelLoad() { _runCts?.Cancel(); } private void GoBack() => Nav.NavigateTo("/results"); private async Task OnFromChanged(ChangeEventArgs e) { if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v)) { _from = new DateTimeOffset(v.Date, MoscowTime.Offset); await ReloadCandidatesAsync(); } } private async Task OnToChanged(ChangeEventArgs e) { if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v)) { _to = new DateTimeOffset(v.Date, MoscowTime.Offset).AddDays(1).AddSeconds(-1); await ReloadCandidatesAsync(); } } private void OnModeChanged(ChangeEventArgs e) { var raw = e.Value?.ToString(); _mode = string.Equals(raw, nameof(LoaderMode.Selected), StringComparison.OrdinalIgnoreCase) ? LoaderMode.Selected : LoaderMode.AllInRange; } private void ToggleSelected(string id, bool isChecked) { if (isChecked) _selectedIds.Add(id); else _selectedIds.Remove(id); } private string WinnerLabel(Side s) => s switch { Side.Side1 => L["Results.Filter.Winner.Side1"], Side.Side2 => L["Results.Filter.Winner.Side2"], Side.Draw => L["Results.Filter.Winner.Draw"], _ => s.ToString(), }; private static string WinnerCss(Side s) => s switch { Side.Side1 => "side1", Side.Side2 => "side2", Side.Draw => "draw", _ => "draw", }; private string OutcomeLabel(ResultLoadOutcome o) => o switch { ResultLoadOutcome.Loaded => L["Results.Loader.Progress.Loaded"], ResultLoadOutcome.AlreadyLoaded => L["Results.Loader.Progress.AlreadyLoaded"], ResultLoadOutcome.NotYetComplete => L["Results.Loader.Progress.NotYetComplete"], ResultLoadOutcome.Failed => L["Results.Loader.Progress.Failed"], _ => o.ToString(), }; private string SportLabel(int code) => SportLabels.Resolve(L, code); private static string FormatDate(DateTimeOffset value) => value.ToString("yyyy-MM-dd"); public void Dispose() { _runCts?.Cancel(); _runCts?.Dispose(); } }