@page "/group/{GroupId:guid}" @using GmRelay.Web.Services @using GmRelay.Shared.Domain @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.Authorization @attribute [Authorize] @inject AuthorizedSessionService SessionService @inject AuthenticationStateProvider AuthStateProvider @inject NavigationManager Navigation Сессии группы — GM-Relay
@if (!string.IsNullOrEmpty(errorMessage)) {
⚠️ @errorMessage
} @if (!string.IsNullOrEmpty(successMessage)) {
✅ @successMessage
} @if (sessions == null) {
} else if (sessions.Count == 0) {
🎯
Нет предстоящих сессий

Для этой группы пока не запланировано игровых сессий.

} else {
@foreach (var batch in batchModels) {

@batch.Title

@FormatBatchSummary(batch)

Batch
}
@* Desktop table *@
@foreach (var session in sessions) { }
Название Время (МСК) Места Статус Ссылка Действие
@session.Title @session.ScheduledAt.FormatMoscow() @FormatSeats(session) @TranslateStatus(session.Status) Подключиться ↗
✏️ Изменить @if (CanPromote(session)) { }
@* Mobile cards *@
@foreach (var session in sessions) {
@session.Title @TranslateStatus(session.Status)
🕐 Время @session.ScheduledAt.FormatMoscow()
👥 Места @FormatSeats(session)
🔗 Ссылка Подключиться ↗
✏️ Изменить @if (CanPromote(session)) { }
}
}
@code { [Parameter] public Guid GroupId { get; set; } private List? sessions; private List batchModels = []; private Guid? promotingSessionId; private Guid? processingBatchId; private long telegramId; private string? errorMessage; private string? successMessage; protected override async Task OnInitializedAsync() { var authState = await AuthStateProvider.GetAuthenticationStateAsync(); if (!authState.User.TryGetTelegramId(out telegramId)) { Navigation.NavigateTo("/access-denied"); return; } await LoadSessions(); } private async Task LoadSessions() { sessions = await SessionService.GetUpcomingSessionsForGmAsync(GroupId, telegramId); if (sessions is null) { Navigation.NavigateTo("/access-denied"); return; } RebuildBatchModels(); } private async Task PromoteWaitlisted(Guid sessionId) { errorMessage = null; successMessage = null; promotingSessionId = sessionId; try { await SessionService.PromoteWaitlistedPlayerForGmAsync(sessionId, telegramId); await LoadSessions(); } catch (SessionAccessDeniedException) { Navigation.NavigateTo("/access-denied"); } catch (Exception ex) { errorMessage = ex.Message; } finally { promotingSessionId = null; } } private async Task UpdateBatchDetails(BatchBulkEditModel batch) { errorMessage = null; successMessage = null; if (!ValidateBatchDetails(batch)) { errorMessage = "Название и ссылка для batch не должны быть пустыми."; return; } processingBatchId = batch.BatchId; try { await SessionService.UpdateBatchDetailsForGmAsync(batch.BatchId, telegramId, batch.Title, batch.JoinLink); await SessionService.UpdateBatchNotificationModeForGmAsync( batch.BatchId, telegramId, SessionNotificationModeExtensions.FromDatabaseValue(batch.NotificationMode)); successMessage = "Настройки batch обновлены."; await LoadSessions(); } catch (SessionAccessDeniedException) { Navigation.NavigateTo("/access-denied"); } catch (Exception ex) { errorMessage = "Не удалось обновить пачку: " + ex.Message; } finally { processingBatchId = null; } } private async Task RescheduleBatch(BatchBulkEditModel batch) { errorMessage = null; successMessage = null; if (batch.IntervalDays <= 0) { errorMessage = "Шаг между играми должен быть больше 0 дней."; return; } processingBatchId = batch.BatchId; try { var utcTime = new DateTimeOffset(batch.FirstScheduledAtLocal, TimeSpan.FromHours(3)).ToUniversalTime().UtcDateTime; await SessionService.RescheduleBatchForGmAsync(batch.BatchId, telegramId, utcTime, batch.IntervalDays); successMessage = "Расписание пачки обновлено."; await LoadSessions(); } catch (SessionAccessDeniedException) { Navigation.NavigateTo("/access-denied"); } catch (Exception ex) { errorMessage = "Не удалось перенести пачку: " + ex.Message; } finally { processingBatchId = null; } } private async Task CloneBatch(BatchBulkEditModel batch) { errorMessage = null; successMessage = null; processingBatchId = batch.BatchId; try { var interval = batch.CloneInterval == "month" ? BatchCloneInterval.NextMonth : BatchCloneInterval.NextWeek; var clonedBatch = await SessionService.CloneBatchForGmAsync(batch.BatchId, telegramId, interval); successMessage = $"Пачка склонирована: {clonedBatch.SessionCount} игр."; await LoadSessions(); } catch (SessionAccessDeniedException) { Navigation.NavigateTo("/access-denied"); } catch (Exception ex) { errorMessage = "Не удалось клонировать пачку: " + ex.Message; } finally { processingBatchId = null; } } private void RebuildBatchModels() { batchModels = sessions? .GroupBy(session => session.BatchId) .Select(group => { var orderedSessions = group.OrderBy(session => session.ScheduledAt).ToList(); var firstSession = orderedSessions[0]; var lastSession = orderedSessions[^1]; return new BatchBulkEditModel { BatchId = group.Key, Title = firstSession.Title, JoinLink = firstSession.JoinLink, NotificationMode = firstSession.NotificationMode, FirstScheduledAtLocal = firstSession.ScheduledAt.ToMoscow(), LastScheduledAtLocal = lastSession.ScheduledAt.ToMoscow(), IntervalDays = InferIntervalDays(orderedSessions), SessionCount = orderedSessions.Count }; }) .OrderBy(batch => batch.FirstScheduledAtLocal) .ToList() ?? []; } private static bool ValidateBatchDetails(BatchBulkEditModel batch) { batch.Title = batch.Title.Trim(); batch.JoinLink = batch.JoinLink.Trim(); return batch.Title.Length > 0 && batch.JoinLink.Length > 0; } private bool IsBatchBusy(BatchBulkEditModel batch) => processingBatchId == batch.BatchId; private static int InferIntervalDays(IReadOnlyList orderedSessions) { if (orderedSessions.Count < 2) { return 7; } var days = (orderedSessions[1].ScheduledAt - orderedSessions[0].ScheduledAt).TotalDays; return Math.Max(1, (int)Math.Round(days, MidpointRounding.AwayFromZero)); } private static bool CanPromote(WebSession session) => session.WaitlistedPlayerCount > 0 && (!session.MaxPlayers.HasValue || session.ActivePlayerCount < session.MaxPlayers.Value); private static string FormatSeats(WebSession session) { var seats = session.MaxPlayers.HasValue ? $"{session.ActivePlayerCount}/{session.MaxPlayers.Value}" : session.ActivePlayerCount.ToString(System.Globalization.CultureInfo.InvariantCulture); return session.WaitlistedPlayerCount > 0 ? $"{seats} · ожидание {session.WaitlistedPlayerCount}" : seats; } private static string FormatBatchSummary(BatchBulkEditModel batch) => $"{batch.SessionCount} игр · {FormatLocalMoscow(batch.FirstScheduledAtLocal)} — {FormatLocalMoscow(batch.LastScheduledAtLocal)}"; private static string FormatLocalMoscow(DateTime localMoscow) => localMoscow.ToString("d MMMM yyyy, HH:mm", System.Globalization.CultureInfo.GetCultureInfo("ru-RU")); private string GetStatusClass(string status) => status switch { SessionStatus.Confirmed => "status-success", SessionStatus.Cancelled => "status-danger", SessionStatus.ConfirmationSent => "status-warning", SessionStatus.Planned => "status-info", _ => "status-neutral" }; private string TranslateStatus(string status) => status switch { SessionStatus.Planned => "Запланировано", SessionStatus.ConfirmationSent => "Ждём подтверждения", SessionStatus.Confirmed => "Подтверждено", SessionStatus.Cancelled => "Отменено", _ => status }; private sealed class BatchBulkEditModel { public Guid BatchId { get; init; } public string Title { get; set; } = ""; public string JoinLink { get; set; } = ""; public string NotificationMode { get; set; } = SessionNotificationModeExtensions.GroupAndDirectValue; public DateTime FirstScheduledAtLocal { get; set; } = DateTime.Now; public DateTime LastScheduledAtLocal { get; init; } = DateTime.Now; public int IntervalDays { get; set; } = 7; public int SessionCount { get; init; } public string CloneInterval { get; set; } = "week"; } }