refactor: hoist Moscow offset + sport labels into shared helpers (HIGH)
* 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.
This commit is contained in:
@@ -2,6 +2,7 @@ using Marathon.Application.Abstractions;
|
|||||||
using Marathon.Application.Configuration;
|
using Marathon.Application.Configuration;
|
||||||
using Marathon.Domain.AnomalyDetection;
|
using Marathon.Domain.AnomalyDetection;
|
||||||
using Marathon.Domain.Entities;
|
using Marathon.Domain.Entities;
|
||||||
|
using Marathon.Domain.ValueObjects;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
@@ -24,7 +25,6 @@ namespace Marathon.Application.UseCases;
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public sealed class DetectAnomaliesUseCase
|
public sealed class DetectAnomaliesUseCase
|
||||||
{
|
{
|
||||||
private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3);
|
|
||||||
private static readonly TimeSpan SnapshotLookback = TimeSpan.FromHours(24);
|
private static readonly TimeSpan SnapshotLookback = TimeSpan.FromHours(24);
|
||||||
|
|
||||||
// Dedup window: two anomalies for the same event within this window are considered duplicates.
|
// Dedup window: two anomalies for the same event within this window are considered duplicates.
|
||||||
@@ -67,7 +67,7 @@ public sealed class DetectAnomaliesUseCase
|
|||||||
var events = await _eventRepo.ListAsync(ct);
|
var events = await _eventRepo.ListAsync(ct);
|
||||||
int newAnomalyCount = 0;
|
int newAnomalyCount = 0;
|
||||||
|
|
||||||
var now = DateTimeOffset.UtcNow.ToOffset(MoscowOffset);
|
var now = MoscowTime.Now;
|
||||||
var from = now - SnapshotLookback;
|
var from = now - SnapshotLookback;
|
||||||
|
|
||||||
// Hoisted outside the per-event loop: load existing anomalies ONCE per cycle
|
// Hoisted outside the per-event loop: load existing anomalies ONCE per cycle
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ namespace Marathon.Domain.AnomalyDetection;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class AnomalyDetector
|
public sealed class AnomalyDetector
|
||||||
{
|
{
|
||||||
private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3);
|
|
||||||
|
|
||||||
private readonly int _suspensionGapSeconds;
|
private readonly int _suspensionGapSeconds;
|
||||||
private readonly decimal _oddsFlipThreshold;
|
private readonly decimal _oddsFlipThreshold;
|
||||||
private readonly int _minSnapshotCount;
|
private readonly int _minSnapshotCount;
|
||||||
@@ -156,7 +154,7 @@ public sealed class AnomalyDetector
|
|||||||
return new Anomaly(
|
return new Anomaly(
|
||||||
Id: Guid.NewGuid(),
|
Id: Guid.NewGuid(),
|
||||||
EventId: eventId,
|
EventId: eventId,
|
||||||
DetectedAt: DateTimeOffset.UtcNow.ToOffset(MoscowOffset),
|
DetectedAt: MoscowTime.Now,
|
||||||
Kind: AnomalyKind.SuspensionFlip,
|
Kind: AnomalyKind.SuspensionFlip,
|
||||||
Score: clampedScore,
|
Score: clampedScore,
|
||||||
EvidenceJson: evidenceJson);
|
EvidenceJson: evidenceJson);
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ public sealed record Event(
|
|||||||
string Side1Name,
|
string Side1Name,
|
||||||
string Side2Name)
|
string Side2Name)
|
||||||
{
|
{
|
||||||
private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3);
|
|
||||||
|
|
||||||
public EventId Id { get; } = Id ?? throw new ArgumentNullException(nameof(Id));
|
public EventId Id { get; } = Id ?? throw new ArgumentNullException(nameof(Id));
|
||||||
|
|
||||||
public SportCode Sport { get; } = Sport ?? throw new ArgumentNullException(nameof(Sport));
|
public SportCode Sport { get; } = Sport ?? throw new ArgumentNullException(nameof(Sport));
|
||||||
@@ -37,7 +35,7 @@ public sealed record Event(
|
|||||||
|
|
||||||
public string Category { get; } = Category ?? string.Empty;
|
public string Category { get; } = Category ?? string.Empty;
|
||||||
|
|
||||||
public DateTimeOffset ScheduledAt { get; } = ScheduledAt.Offset == MoscowOffset
|
public DateTimeOffset ScheduledAt { get; } = ScheduledAt.Offset == MoscowTime.Offset
|
||||||
? ScheduledAt
|
? ScheduledAt
|
||||||
: throw new ArgumentException(
|
: throw new ArgumentException(
|
||||||
$"ScheduledAt must be in Europe/Moscow time (UTC+03:00). " +
|
$"ScheduledAt must be in Europe/Moscow time (UTC+03:00). " +
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
namespace Marathon.Domain.ValueObjects;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Single source of truth for the Moscow timezone offset.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// marathonbet.by serves all timestamps in Moscow time (UTC+3) and the domain
|
||||||
|
/// invariant on <see cref="Marathon.Domain.Entities.Event.ScheduledAt"/>
|
||||||
|
/// rejects any other offset. Code that constructs <see cref="DateTimeOffset"/>
|
||||||
|
/// values for events, results, snapshots, or test fixtures MUST use this
|
||||||
|
/// constant rather than re-deriving <c>TimeSpan.FromHours(3)</c>.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public static class MoscowTime
|
||||||
|
{
|
||||||
|
/// <summary>The Moscow time offset (UTC+3).</summary>
|
||||||
|
public static readonly TimeSpan Offset = TimeSpan.FromHours(3);
|
||||||
|
|
||||||
|
/// <summary>Current Moscow time.</summary>
|
||||||
|
public static DateTimeOffset Now => DateTimeOffset.UtcNow.ToOffset(Offset);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the inclusive end-of-day for the given Moscow date — i.e.,
|
||||||
|
/// the moment one second before the next day starts. Used by date-range
|
||||||
|
/// filters where the user picks "to: 2026-05-09" meaning "through the
|
||||||
|
/// rest of that day."
|
||||||
|
/// </summary>
|
||||||
|
public static DateTimeOffset EndOfMoscowDay(DateOnly date) =>
|
||||||
|
new DateTimeOffset(date.ToDateTime(TimeOnly.MinValue), Offset)
|
||||||
|
.AddDays(1).AddSeconds(-1);
|
||||||
|
}
|
||||||
@@ -17,8 +17,6 @@ namespace Marathon.Infrastructure.Scraping.Parsers;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class EventListingParserBase
|
public abstract class EventListingParserBase
|
||||||
{
|
{
|
||||||
private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3);
|
|
||||||
|
|
||||||
protected readonly IServerTimeProvider ServerTimeProvider;
|
protected readonly IServerTimeProvider ServerTimeProvider;
|
||||||
protected readonly ILogger Logger;
|
protected readonly ILogger Logger;
|
||||||
|
|
||||||
@@ -36,7 +34,7 @@ public abstract class EventListingParserBase
|
|||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var serverTime = ServerTimeProvider.ExtractServerTime(html)
|
var serverTime = ServerTimeProvider.ExtractServerTime(html)
|
||||||
?? DateTimeOffset.UtcNow.ToOffset(MoscowOffset);
|
?? MoscowTime.Now;
|
||||||
|
|
||||||
var config = AngleSharpConfig.Default;
|
var config = AngleSharpConfig.Default;
|
||||||
using var context = BrowsingContext.New(config);
|
using var context = BrowsingContext.New(config);
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ namespace Marathon.Infrastructure.Scraping.Parsers;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed partial class EventOddsParser : IEventOddsParser
|
public sealed partial class EventOddsParser : IEventOddsParser
|
||||||
{
|
{
|
||||||
private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3);
|
|
||||||
|
|
||||||
private readonly IServerTimeProvider _serverTime;
|
private readonly IServerTimeProvider _serverTime;
|
||||||
private readonly PeriodScopeMapper _periodMapper;
|
private readonly PeriodScopeMapper _periodMapper;
|
||||||
private readonly ILogger<EventOddsParser> _logger;
|
private readonly ILogger<EventOddsParser> _logger;
|
||||||
@@ -69,7 +67,7 @@ public sealed partial class EventOddsParser : IEventOddsParser
|
|||||||
ArgumentNullException.ThrowIfNull(html);
|
ArgumentNullException.ThrowIfNull(html);
|
||||||
|
|
||||||
var capturedAt = _serverTime.ExtractServerTime(html)
|
var capturedAt = _serverTime.ExtractServerTime(html)
|
||||||
?? DateTimeOffset.UtcNow.ToOffset(MoscowOffset);
|
?? MoscowTime.Now;
|
||||||
|
|
||||||
var config = AngleSharpConfig.Default;
|
var config = AngleSharpConfig.Default;
|
||||||
using var context = BrowsingContext.New(config);
|
using var context = BrowsingContext.New(config);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using Marathon.Domain.ValueObjects;
|
||||||
|
|
||||||
namespace Marathon.Infrastructure.Scraping.Parsers;
|
namespace Marathon.Infrastructure.Scraping.Parsers;
|
||||||
|
|
||||||
@@ -13,8 +14,6 @@ namespace Marathon.Infrastructure.Scraping.Parsers;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static partial class MoscowDateParser
|
public static partial class MoscowDateParser
|
||||||
{
|
{
|
||||||
private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3);
|
|
||||||
|
|
||||||
// Matches "HH:MM"
|
// Matches "HH:MM"
|
||||||
[GeneratedRegex(@"^\s*(\d{1,2}):(\d{2})\s*$", RegexOptions.CultureInvariant)]
|
[GeneratedRegex(@"^\s*(\d{1,2}):(\d{2})\s*$", RegexOptions.CultureInvariant)]
|
||||||
private static partial Regex TimeOnlyRegex();
|
private static partial Regex TimeOnlyRegex();
|
||||||
@@ -70,7 +69,7 @@ public static partial class MoscowDateParser
|
|||||||
var scheduled = new DateTimeOffset(
|
var scheduled = new DateTimeOffset(
|
||||||
today.Year, today.Month, today.Day,
|
today.Year, today.Month, today.Day,
|
||||||
hour, minute, 0,
|
hour, minute, 0,
|
||||||
MoscowOffset);
|
MoscowTime.Offset);
|
||||||
|
|
||||||
// If the computed time is already in the past (same day but earlier),
|
// If the computed time is already in the past (same day but earlier),
|
||||||
// that's fine — the event may have already started (live) or the listing
|
// that's fine — the event may have already started (live) or the listing
|
||||||
@@ -98,7 +97,7 @@ public static partial class MoscowDateParser
|
|||||||
if (candidate < DateOnly.FromDateTime(anchorDate))
|
if (candidate < DateOnly.FromDateTime(anchorDate))
|
||||||
year++; // e.g., anchor is Dec 2026 and event is in Jan 2027
|
year++; // e.g., anchor is Dec 2026 and event is in Jan 2027
|
||||||
|
|
||||||
return new DateTimeOffset(year, month, day, hour, minute, 0, MoscowOffset);
|
return new DateTimeOffset(year, month, day, hour, minute, 0, MoscowTime.Offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ namespace Marathon.Infrastructure.Scraping.Parsers;
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public sealed partial class ResultsParser : IResultsParser
|
public sealed partial class ResultsParser : IResultsParser
|
||||||
{
|
{
|
||||||
private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3);
|
|
||||||
|
|
||||||
private readonly ILogger<ResultsParser> _logger;
|
private readonly ILogger<ResultsParser> _logger;
|
||||||
|
|
||||||
// Matches score patterns like "2:1", "2:1 (1:1)", "2:1 (0:0) (2:1)"
|
// Matches score patterns like "2:1", "2:1 (1:1)", "2:1 (0:0) (2:1)"
|
||||||
@@ -101,7 +99,7 @@ public sealed partial class ResultsParser : IResultsParser
|
|||||||
if (string.IsNullOrWhiteSpace(eventIdRaw))
|
if (string.IsNullOrWhiteSpace(eventIdRaw))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var completedAt = DateTimeOffset.UtcNow.ToOffset(MoscowOffset);
|
var completedAt = MoscowTime.Now;
|
||||||
|
|
||||||
return new EventResult(
|
return new EventResult(
|
||||||
new DomainEventId(eventIdRaw),
|
new DomainEventId(eventIdRaw),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using Marathon.Domain.ValueObjects;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Marathon.Infrastructure.Scraping.Parsers;
|
namespace Marathon.Infrastructure.Scraping.Parsers;
|
||||||
@@ -17,8 +18,6 @@ public sealed partial class ServerTimeProvider : IServerTimeProvider
|
|||||||
RegexOptions.CultureInvariant)]
|
RegexOptions.CultureInvariant)]
|
||||||
private static partial Regex ServerTimeRegex();
|
private static partial Regex ServerTimeRegex();
|
||||||
|
|
||||||
private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3);
|
|
||||||
|
|
||||||
private readonly ILogger<ServerTimeProvider> _logger;
|
private readonly ILogger<ServerTimeProvider> _logger;
|
||||||
|
|
||||||
public ServerTimeProvider(ILogger<ServerTimeProvider> logger)
|
public ServerTimeProvider(ILogger<ServerTimeProvider> logger)
|
||||||
@@ -47,6 +46,6 @@ public sealed partial class ServerTimeProvider : IServerTimeProvider
|
|||||||
var minute = int.Parse(match.Groups[5].Value);
|
var minute = int.Parse(match.Groups[5].Value);
|
||||||
var second = int.Parse(match.Groups[6].Value);
|
var second = int.Parse(match.Groups[6].Value);
|
||||||
|
|
||||||
return new DateTimeOffset(year, month, day, hour, minute, second, MoscowOffset);
|
return new DateTimeOffset(year, month, day, hour, minute, second, MoscowTime.Offset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -215,14 +215,7 @@
|
|||||||
_ => kind.ToString(),
|
_ => kind.ToString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
private string SportLabel(int code) => code switch
|
private string SportLabel(int code) => SportLabels.Resolve(L, code);
|
||||||
{
|
|
||||||
6 => L["Sport.Basketball"],
|
|
||||||
11 => L["Sport.Football"],
|
|
||||||
22723 => L["Sport.Tennis"],
|
|
||||||
43658 => L["Sport.Hockey"],
|
|
||||||
_ => string.Format(System.Globalization.CultureInfo.InvariantCulture, "Sport {0}", code),
|
|
||||||
};
|
|
||||||
|
|
||||||
private static string FormatRate(decimal? r) => r is { } v
|
private static string FormatRate(decimal? r) => r is { } v
|
||||||
? v.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture)
|
? v.ToString("0.00", System.Globalization.CultureInfo.InvariantCulture)
|
||||||
|
|||||||
@@ -113,7 +113,7 @@
|
|||||||
|
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
var moscow = DateTimeOffset.UtcNow.ToOffset(TimeSpan.FromHours(3));
|
var moscow = MoscowTime.Now;
|
||||||
_from = InitialFrom ?? moscow.AddDays(-1).Date;
|
_from = InitialFrom ?? moscow.AddDays(-1).Date;
|
||||||
_to = InitialTo ?? moscow.AddDays(1).Date;
|
_to = InitialTo ?? moscow.AddDays(1).Date;
|
||||||
_kind = InitialKind;
|
_kind = InitialKind;
|
||||||
@@ -140,10 +140,9 @@
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Use Moscow offset to match domain ScheduledAt invariant.
|
// Use Moscow offset to match domain ScheduledAt invariant.
|
||||||
var moscow = TimeSpan.FromHours(3);
|
|
||||||
var range = new AppDateRange(
|
var range = new AppDateRange(
|
||||||
new DateTimeOffset(_from.Value.Date, moscow),
|
new DateTimeOffset(_from.Value.Date, MoscowTime.Offset),
|
||||||
new DateTimeOffset(_to.Value.Date.AddDays(1).AddSeconds(-1), moscow));
|
MoscowTime.EndOfMoscowDay(DateOnly.FromDateTime(_to.Value.Date)));
|
||||||
|
|
||||||
var path = await ExportUseCase.ExecuteAsync(range, _kind, CancellationToken.None);
|
var path = await ExportUseCase.ExecuteAsync(range, _kind, CancellationToken.None);
|
||||||
Dialog.Close(DialogResult.Ok(path));
|
Dialog.Close(DialogResult.Ok(path));
|
||||||
|
|||||||
@@ -252,8 +252,7 @@
|
|||||||
{
|
{
|
||||||
if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v))
|
if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v))
|
||||||
{
|
{
|
||||||
var moscow = TimeSpan.FromHours(3);
|
await UpdateFilter(_filter with { From = new DateTimeOffset(v.Date, MoscowTime.Offset) });
|
||||||
await UpdateFilter(_filter with { From = new DateTimeOffset(v.Date, moscow) });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,8 +260,7 @@
|
|||||||
{
|
{
|
||||||
if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v))
|
if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v))
|
||||||
{
|
{
|
||||||
var moscow = TimeSpan.FromHours(3);
|
await UpdateFilter(_filter with { To = MoscowTime.EndOfMoscowDay(DateOnly.FromDateTime(v.Date)) });
|
||||||
await UpdateFilter(_filter with { To = new DateTimeOffset(v.Date, moscow).AddDays(1).AddSeconds(-1) });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,14 +281,7 @@
|
|||||||
_ => L["Anomaly.Severity.Low"],
|
_ => L["Anomaly.Severity.Low"],
|
||||||
};
|
};
|
||||||
|
|
||||||
private string SportLabel(int code) => code switch
|
private string SportLabel(int code) => SportLabels.Resolve(L, code);
|
||||||
{
|
|
||||||
6 => L["Sport.Basketball"],
|
|
||||||
11 => L["Sport.Football"],
|
|
||||||
22723 => L["Sport.Tennis"],
|
|
||||||
43658 => L["Sport.Hockey"],
|
|
||||||
_ => string.Format(System.Globalization.CultureInfo.InvariantCulture, "Sport {0}", code),
|
|
||||||
};
|
|
||||||
|
|
||||||
private static string FormatDate(DateTimeOffset? value)
|
private static string FormatDate(DateTimeOffset? value)
|
||||||
=> value?.ToString("yyyy-MM-dd") ?? string.Empty;
|
=> value?.ToString("yyyy-MM-dd") ?? string.Empty;
|
||||||
|
|||||||
@@ -306,14 +306,7 @@
|
|||||||
_ => "—",
|
_ => "—",
|
||||||
};
|
};
|
||||||
|
|
||||||
private string SportLabel(int code) => code switch
|
private string SportLabel(int code) => SportLabels.Resolve(L, code);
|
||||||
{
|
|
||||||
6 => L["Sport.Basketball"],
|
|
||||||
11 => L["Sport.Football"],
|
|
||||||
22723 => L["Sport.Tennis"],
|
|
||||||
43658 => L["Sport.Hockey"],
|
|
||||||
_ => $"Sport {code}",
|
|
||||||
};
|
|
||||||
|
|
||||||
private string BetTypeLabel(BetType t) => t switch
|
private string BetTypeLabel(BetType t) => t switch
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -164,8 +164,6 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3);
|
|
||||||
|
|
||||||
private DateTimeOffset _from;
|
private DateTimeOffset _from;
|
||||||
private DateTimeOffset _to;
|
private DateTimeOffset _to;
|
||||||
private string _searchInput = string.Empty;
|
private string _searchInput = string.Empty;
|
||||||
@@ -181,7 +179,7 @@
|
|||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
var todayMoscow = new DateTimeOffset(DateTime.UtcNow.Date, TimeSpan.Zero).ToOffset(MoscowOffset);
|
var todayMoscow = new DateTimeOffset(DateTime.UtcNow.Date, TimeSpan.Zero).ToOffset(MoscowTime.Offset);
|
||||||
_from = todayMoscow.AddDays(-30);
|
_from = todayMoscow.AddDays(-30);
|
||||||
_to = todayMoscow.AddDays(1).AddSeconds(-1);
|
_to = todayMoscow.AddDays(1).AddSeconds(-1);
|
||||||
|
|
||||||
@@ -246,7 +244,7 @@
|
|||||||
{
|
{
|
||||||
if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v))
|
if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v))
|
||||||
{
|
{
|
||||||
_from = new DateTimeOffset(v.Date, MoscowOffset);
|
_from = new DateTimeOffset(v.Date, MoscowTime.Offset);
|
||||||
await LoadAsync();
|
await LoadAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -255,7 +253,7 @@
|
|||||||
{
|
{
|
||||||
if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v))
|
if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v))
|
||||||
{
|
{
|
||||||
_to = new DateTimeOffset(v.Date, MoscowOffset).AddDays(1).AddSeconds(-1);
|
_to = new DateTimeOffset(v.Date, MoscowTime.Offset).AddDays(1).AddSeconds(-1);
|
||||||
await LoadAsync();
|
await LoadAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,14 +309,7 @@
|
|||||||
_ => "draw",
|
_ => "draw",
|
||||||
};
|
};
|
||||||
|
|
||||||
private string SportLabel(int code) => code switch
|
private string SportLabel(int code) => SportLabels.Resolve(L, code);
|
||||||
{
|
|
||||||
6 => L["Sport.Basketball"],
|
|
||||||
11 => L["Sport.Football"],
|
|
||||||
22723 => L["Sport.Tennis"],
|
|
||||||
43658 => L["Sport.Hockey"],
|
|
||||||
_ => $"Sport {code}",
|
|
||||||
};
|
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -204,8 +204,6 @@
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private static readonly TimeSpan MoscowOffset = TimeSpan.FromHours(3);
|
|
||||||
|
|
||||||
private DateTimeOffset _from;
|
private DateTimeOffset _from;
|
||||||
private DateTimeOffset _to;
|
private DateTimeOffset _to;
|
||||||
private LoaderMode _mode = LoaderMode.AllInRange;
|
private LoaderMode _mode = LoaderMode.AllInRange;
|
||||||
@@ -234,7 +232,7 @@
|
|||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
var todayMoscow = new DateTimeOffset(DateTime.UtcNow.Date, TimeSpan.Zero).ToOffset(MoscowOffset);
|
var todayMoscow = new DateTimeOffset(DateTime.UtcNow.Date, TimeSpan.Zero).ToOffset(MoscowTime.Offset);
|
||||||
_from = todayMoscow.AddDays(-7);
|
_from = todayMoscow.AddDays(-7);
|
||||||
_to = todayMoscow.AddDays(1).AddSeconds(-1);
|
_to = todayMoscow.AddDays(1).AddSeconds(-1);
|
||||||
await ReloadCandidatesAsync();
|
await ReloadCandidatesAsync();
|
||||||
@@ -347,7 +345,7 @@
|
|||||||
{
|
{
|
||||||
if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v))
|
if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v))
|
||||||
{
|
{
|
||||||
_from = new DateTimeOffset(v.Date, MoscowOffset);
|
_from = new DateTimeOffset(v.Date, MoscowTime.Offset);
|
||||||
await ReloadCandidatesAsync();
|
await ReloadCandidatesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -356,7 +354,7 @@
|
|||||||
{
|
{
|
||||||
if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v))
|
if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v))
|
||||||
{
|
{
|
||||||
_to = new DateTimeOffset(v.Date, MoscowOffset).AddDays(1).AddSeconds(-1);
|
_to = new DateTimeOffset(v.Date, MoscowTime.Offset).AddDays(1).AddSeconds(-1);
|
||||||
await ReloadCandidatesAsync();
|
await ReloadCandidatesAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -400,14 +398,7 @@
|
|||||||
_ => o.ToString(),
|
_ => o.ToString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
private string SportLabel(int code) => code switch
|
private string SportLabel(int code) => SportLabels.Resolve(L, code);
|
||||||
{
|
|
||||||
6 => L["Sport.Basketball"],
|
|
||||||
11 => L["Sport.Football"],
|
|
||||||
22723 => L["Sport.Tennis"],
|
|
||||||
43658 => L["Sport.Hockey"],
|
|
||||||
_ => $"Sport {code}",
|
|
||||||
};
|
|
||||||
|
|
||||||
private static string FormatDate(DateTimeOffset value) => value.ToString("yyyy-MM-dd");
|
private static string FormatDate(DateTimeOffset value) => value.ToString("yyyy-MM-dd");
|
||||||
|
|
||||||
|
|||||||
@@ -437,8 +437,7 @@
|
|||||||
{
|
{
|
||||||
if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v))
|
if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v))
|
||||||
{
|
{
|
||||||
var moscow = TimeSpan.FromHours(3);
|
await UpdateFilter(_filter with { From = new DateTimeOffset(v.Date, MoscowTime.Offset) });
|
||||||
await UpdateFilter(_filter with { From = new DateTimeOffset(v.Date, moscow) });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -446,8 +445,7 @@
|
|||||||
{
|
{
|
||||||
if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v))
|
if (DateTimeOffset.TryParse(e.Value?.ToString(), out var v))
|
||||||
{
|
{
|
||||||
var moscow = TimeSpan.FromHours(3);
|
await UpdateFilter(_filter with { To = MoscowTime.EndOfMoscowDay(DateOnly.FromDateTime(v.Date)) });
|
||||||
await UpdateFilter(_filter with { To = new DateTimeOffset(v.Date, moscow).AddDays(1).AddSeconds(-1) });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,14 +517,7 @@
|
|||||||
private static string FormatDate(DateTimeOffset value)
|
private static string FormatDate(DateTimeOffset value)
|
||||||
=> value.ToString("yyyy-MM-dd");
|
=> value.ToString("yyyy-MM-dd");
|
||||||
|
|
||||||
private string SportLabel(int code) => code switch
|
private string SportLabel(int code) => SportLabels.Resolve(L, code);
|
||||||
{
|
|
||||||
6 => L["Sport.Basketball"],
|
|
||||||
11 => L["Sport.Football"],
|
|
||||||
22723 => L["Sport.Tennis"],
|
|
||||||
43658 => L["Sport.Hockey"],
|
|
||||||
_ => $"Sport {code}",
|
|
||||||
};
|
|
||||||
|
|
||||||
private readonly record struct RatesSnap(decimal? Win1, decimal? Draw, decimal? Win2);
|
private readonly record struct RatesSnap(decimal? Win1, decimal? Draw, decimal? Win2);
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using Marathon.Domain.ValueObjects;
|
||||||
|
|
||||||
namespace Marathon.UI.Services;
|
namespace Marathon.UI.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -54,8 +56,7 @@ public sealed class EventBrowsingState
|
|||||||
public static PageFilter Default(DateTime nowUtc)
|
public static PageFilter Default(DateTime nowUtc)
|
||||||
{
|
{
|
||||||
// Default window: -1d .. +7d in Moscow time, the same TZ events use.
|
// Default window: -1d .. +7d in Moscow time, the same TZ events use.
|
||||||
var moscow = TimeSpan.FromHours(3);
|
var midnight = new DateTimeOffset(nowUtc.Date, TimeSpan.Zero).ToOffset(MoscowTime.Offset);
|
||||||
var midnight = new DateTimeOffset(nowUtc.Date, TimeSpan.Zero).ToOffset(moscow);
|
|
||||||
return new PageFilter(
|
return new PageFilter(
|
||||||
From: midnight.AddDays(-1),
|
From: midnight.AddDays(-1),
|
||||||
To: midnight.AddDays(7),
|
To: midnight.AddDays(7),
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System.Globalization;
|
||||||
|
using Marathon.UI.Resources;
|
||||||
|
using Microsoft.Extensions.Localization;
|
||||||
|
|
||||||
|
namespace Marathon.UI.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Single source of truth for the sport-code → display-label mapping that
|
||||||
|
/// every list/detail page in the RCL needs. Resolves the four canonical
|
||||||
|
/// data-sport-treeId values from the marathonbet.by spike (6 = basketball,
|
||||||
|
/// 11 = football, 22723 = tennis, 43658 = hockey) against shared
|
||||||
|
/// localization resources, with a stable invariant fallback for unknown
|
||||||
|
/// codes so the UI degrades gracefully if the bookmaker adds a new sport.
|
||||||
|
/// </summary>
|
||||||
|
public static class SportLabels
|
||||||
|
{
|
||||||
|
public static string Resolve(IStringLocalizer<SharedResource> localizer, int code) =>
|
||||||
|
code switch
|
||||||
|
{
|
||||||
|
6 => localizer["Sport.Basketball"],
|
||||||
|
11 => localizer["Sport.Football"],
|
||||||
|
22723 => localizer["Sport.Tennis"],
|
||||||
|
43658 => localizer["Sport.Hockey"],
|
||||||
|
_ => string.Format(CultureInfo.InvariantCulture, "Sport {0}", code),
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user