fed3a09695
* New Marathon.Domain.ValueObjects.MoscowTime with Offset, Now, and EndOfMoscowDay(DateOnly) — replaces ~15 inline TimeSpan.FromHours(3) literals across Domain/Application/Infrastructure/UI. * New Marathon.UI.Services.SportLabels.Resolve(IStringLocalizer, int) — replaces 6 near-identical SportLabel switch bodies in EventListShell, Events/Detail, Anomalies/AnomalyFeed, Results/ResultsList, Results/ResultsLoader, and AnomalyCard. Single source of truth for the 6/11/22723/43658 sport-code mapping. Pages keep a one-liner wrapper so the call sites stay terse.
174 lines
5.7 KiB
Plaintext
174 lines
5.7 KiB
Plaintext
@*
|
|
ExportDialog — modal that collects a DateRange + ExportKind, then calls
|
|
ExportToExcelUseCase. Reports back via DialogResult so the caller can
|
|
show a snackbar with the file path. Keyboard: Esc to cancel, Enter to
|
|
submit.
|
|
*@
|
|
|
|
@using Marathon.Application.UseCases
|
|
@using AppDateRange = Marathon.Application.Storage.DateRange
|
|
@using ExportKind = Marathon.Application.Storage.ExportKind
|
|
@inject IStringLocalizer<SharedResource> L
|
|
@inject ExportToExcelUseCase ExportUseCase
|
|
@inject ILogger<ExportDialog> Logger
|
|
|
|
<MudDialog @key="@(_kind.ToString())" Class="m-export-dialog">
|
|
<TitleContent>
|
|
<span class="m-kicker">@L["Export.Kicker"]</span>
|
|
<h2 style="font-family: var(--m-font-display); font-weight: 400; font-size: 1.5rem; margin: var(--m-space-2) 0 0;">
|
|
@L["Export.Title"]
|
|
</h2>
|
|
</TitleContent>
|
|
<DialogContent>
|
|
<div style="display: grid; gap: var(--m-space-4); padding: var(--m-space-2) 0;" @onkeydown="HandleKeyDown">
|
|
<div class="m-field-row">
|
|
<div>
|
|
<label style="font-weight: 500;">@L["Export.DateRange.From"]</label>
|
|
</div>
|
|
<div>
|
|
<MudDatePicker
|
|
@bind-Date="_from"
|
|
Label="@L["Export.DateRange.From"]"
|
|
DateFormat="yyyy-MM-dd"
|
|
Variant="Variant.Outlined" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="m-field-row">
|
|
<div>
|
|
<label style="font-weight: 500;">@L["Export.DateRange.To"]</label>
|
|
</div>
|
|
<div>
|
|
<MudDatePicker
|
|
@bind-Date="_to"
|
|
Label="@L["Export.DateRange.To"]"
|
|
DateFormat="yyyy-MM-dd"
|
|
Variant="Variant.Outlined" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="m-field-row">
|
|
<div>
|
|
<label style="font-weight: 500;">@L["Export.Kind.Label"]</label>
|
|
</div>
|
|
<div>
|
|
<MudRadioGroup T="ExportKind" @bind-Value="_kind">
|
|
<MudRadio Value="@(ExportKind.PreMatch)" Color="Color.Primary">@L["Export.Kind.PreMatch"]</MudRadio>
|
|
<MudRadio Value="@(ExportKind.Live)" Color="Color.Primary">@L["Export.Kind.Live"]</MudRadio>
|
|
<MudRadio Value="@(ExportKind.Combined)" Color="Color.Primary">@L["Export.Kind.Combined"]</MudRadio>
|
|
</MudRadioGroup>
|
|
</div>
|
|
</div>
|
|
|
|
@if (_error is not null)
|
|
{
|
|
<div role="alert" class="m-export-dialog__error">
|
|
@_error
|
|
</div>
|
|
}
|
|
</div>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<MudButton OnClick="Cancel" Variant="Variant.Text">@L["Export.Cancel"]</MudButton>
|
|
<MudButton OnClick="Submit"
|
|
Color="Color.Primary"
|
|
Variant="Variant.Filled"
|
|
Disabled="@_busy">
|
|
@if (_busy)
|
|
{
|
|
<MudProgressCircular Indeterminate="true" Size="Size.Small" Color="Color.Inherit" />
|
|
<span style="margin-left: 8px;">@L["Common.Loading"]</span>
|
|
}
|
|
else
|
|
{
|
|
@L["Export.Submit"]
|
|
}
|
|
</MudButton>
|
|
</DialogActions>
|
|
</MudDialog>
|
|
|
|
<style>
|
|
.m-export-dialog__error {
|
|
background: rgba(220, 38, 38, 0.08);
|
|
border-left: 3px solid var(--m-c-anomaly);
|
|
padding: var(--m-space-3);
|
|
font-family: var(--m-font-mono);
|
|
font-size: 0.8125rem;
|
|
color: var(--m-c-anomaly);
|
|
}
|
|
</style>
|
|
|
|
@code {
|
|
[CascadingParameter] private MudDialogInstance Dialog { get; set; } = default!;
|
|
|
|
[Parameter] public DateTime? InitialFrom { get; set; }
|
|
[Parameter] public DateTime? InitialTo { get; set; }
|
|
[Parameter] public ExportKind InitialKind { get; set; } = ExportKind.Combined;
|
|
|
|
private DateTime? _from;
|
|
private DateTime? _to;
|
|
private ExportKind _kind;
|
|
private bool _busy;
|
|
private string? _error;
|
|
|
|
protected override void OnInitialized()
|
|
{
|
|
var moscow = MoscowTime.Now;
|
|
_from = InitialFrom ?? moscow.AddDays(-1).Date;
|
|
_to = InitialTo ?? moscow.AddDays(1).Date;
|
|
_kind = InitialKind;
|
|
}
|
|
|
|
private void Cancel() => Dialog.Cancel();
|
|
|
|
private async Task Submit()
|
|
{
|
|
if (_from is null || _to is null)
|
|
{
|
|
_error = L["Export.Error.MissingDates"];
|
|
return;
|
|
}
|
|
if (_from > _to)
|
|
{
|
|
_error = L["Export.Error.InvalidRange"];
|
|
return;
|
|
}
|
|
|
|
_error = null;
|
|
_busy = true;
|
|
StateHasChanged();
|
|
try
|
|
{
|
|
// Use Moscow offset to match domain ScheduledAt invariant.
|
|
var range = new AppDateRange(
|
|
new DateTimeOffset(_from.Value.Date, MoscowTime.Offset),
|
|
MoscowTime.EndOfMoscowDay(DateOnly.FromDateTime(_to.Value.Date)));
|
|
|
|
var path = await ExportUseCase.ExecuteAsync(range, _kind, CancellationToken.None);
|
|
Dialog.Close(DialogResult.Ok(path));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Logger.LogError(ex, "Export failed");
|
|
_error = L["Export.Error.Failed"].Value + " — " + ex.Message;
|
|
}
|
|
finally
|
|
{
|
|
_busy = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
private async Task HandleKeyDown(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs e)
|
|
{
|
|
if (e.Key == "Enter" && !_busy)
|
|
{
|
|
await Submit();
|
|
}
|
|
else if (e.Key == "Escape")
|
|
{
|
|
Cancel();
|
|
}
|
|
}
|
|
}
|