@page "/group/{GroupId:guid}" @using GmRelay.Web.Services @using GmRelay.Shared.Domain @using GmRelay.Web.Services.Portfolio @using Microsoft.AspNetCore.Authorization @using Microsoft.AspNetCore.Components.Authorization @attribute [Authorize] @inject AuthorizedSessionService SessionService @inject AuthorizedPortfolioService PortfolioService @inject AuthorizedMembershipService MembershipService @inject AuthenticationStateProvider AuthStateProvider @inject NavigationManager Navigation Сессии группы — GM-Relay
@if (groupManagement is not null) {

Управление группой

@groupManagement.Group.Name · @FormatRole(CurrentUserRole)

@FormatRole(CurrentUserRole)
📊 Статистика @foreach (var manager in groupManagement.Managers) { @FormatManager(manager) @if (groupManagement.CurrentUserIsOwner && manager.Role == GroupManagerRoleExtensions.CoGmValue) { } }
@if (groupManagement.CurrentUserIsOwner) {
}
} @if (publicSettings is not null) {

Публичная страница клуба

@publicSettings.PublicSessionCount опубликованных игр без состава игроков и приватных ссылок

@(publicSettings.PublicScheduleEnabled ? "Включена" : "Выключена")
Если выключено, публичная страница и ссылки на сессии недоступны.
Латиница, цифры и дефисы, например `night-city-club`.
@if (PublicClubUrl is not null && publicSettings.PublicScheduleEnabled) { Открыть публичную страницу }
@if (PublicClubUrl is not null && publicSettings.PublicScheduleEnabled) { }
} @if (pendingApplicationsCount > 0) { 📨 Заявки участников (@pendingApplicationsCount) Рассмотреть заявки на участие в клубе } @if (!string.IsNullOrEmpty(errorMessage)) {
⚠️ @errorMessage
} @if (!string.IsNullOrEmpty(successMessage)) {
✅ @successMessage
} @if (portfolioGames is not null) {

Проведённые приключения

Черновики и опубликованные приключения для каталога мастера.

@if (portfolioGames.Count == 0) {
Приключений пока нет

Создайте первый черновик и добавьте проведённые сессии.

} else {
@foreach (var game in portfolioGames) {
@game.Title @(game.IsPublic ? "Опубликовано" : "Черновик")
@game.SessionCount игр @game.MasterCount мастеров @if (game.PendingReviewCount > 0) { @game.PendingReviewCount на модерации }
}
}
📜 Все проведённые сессии
} @if (campaignTemplates is not null) {

Применить шаблон

@campaignTemplateModels.Count доступных для этой группы

⚙️ Управлять шаблонами
@if (campaignTemplateModels.Count == 0) {
Шаблонов пока нет

Создайте шаблоны в отдельной вкладке, а здесь запускайте из них новые batch-расписания.

} else {
@foreach (var template in campaignTemplateModels) {

@template.Name

@FormatTemplateSummary(template)

}
}
} @if (sessions == null) {
} else if (sessions.Count == 0) {
🎯
Нет предстоящих сессий

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

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

@batch.Title

@FormatBatchSummary(batch)

Batch
0 ? "status-warning" : "status-neutral")"> @FormatBatchPublication(batch)
}
@* Desktop table *@
@foreach (var session in sessions) { var isExpanded = expandedSessions.Contains(session.Id); @if (isExpanded) { } }
Название Время (МСК) Места Статус Ссылка Действие
@session.ScheduledAt.FormatMoscow() @FormatSeats(session) @TranslateStatus(session.Status) Подключиться ↗
@FormatPublicationStatus(session) @if (session.IsPublic && publicSettings?.PublicScheduleEnabled == true) { Публичная ссылка } ✏️ Изменить 📜 История @if (CanPromote(session)) { }
@if (loadingParticipantsSessionId == session.Id) {
} else if (participantsCache.TryGetValue(session.Id, out var participants)) { @if (participants.Count == 0) {
Нет участников
} else {
@foreach (var p in participants) {
@p.DisplayName @FormatParticipantUsername(p) @TranslateParticipantStatus(p)
@if (!p.IsGm) { }
}
} }
@* Mobile cards *@
@foreach (var session in sessions) { var isExpanded = expandedSessions.Contains(session.Id);
@TranslateStatus(session.Status)
🕐 Время @session.ScheduledAt.FormatMoscow()
👥 Места @FormatSeats(session)
🔗 Ссылка Подключиться ↗
@if (session.IsPublic && publicSettings?.PublicScheduleEnabled == true) { Публичная ссылка } ✏️ Изменить 📜 История @if (CanPromote(session)) { }
@if (isExpanded) {
@if (loadingParticipantsSessionId == session.Id) {
} else if (participantsCache.TryGetValue(session.Id, out var participants)) { @if (participants.Count == 0) {
Нет участников
} else {
@foreach (var p in participants) {
@p.DisplayName @FormatParticipantUsername(p) @TranslateParticipantStatus(p)
@if (!p.IsGm) { }
}
} }
}
}
}
@code { [Parameter] public Guid GroupId { get; set; } private List? sessions; private List? campaignTemplates; private WebGroupManagement? groupManagement; private WebPublicGroupSettings? publicSettings; private IReadOnlyList? portfolioGames; private List batchModels = []; private List campaignTemplateModels = []; private int pendingApplicationsCount; private Guid? promotingSessionId; private Guid? processingBatchId; private Guid? processingTemplateId; private Guid? publishingBatchId; private Guid? publishingSessionId; private string? removingCoGmId; private bool isAddingCoGm; private bool isCreatingDraft; private bool savingPublicSettings; private string? currentPlatform; private string? externalUserId; private string? errorMessage; private string? successMessage; private CoGmEditModel coGmModel = new(); private PublicSettingsEditModel publicSettingsModel = new(); private Dictionary> participantsCache = new(); private HashSet expandedSessions = new(); private Guid? kickingParticipantId; private Guid? loadingParticipantsSessionId; protected override async Task OnInitializedAsync() { var authState = await AuthStateProvider.GetAuthenticationStateAsync(); if (!authState.User.TryGetPlatformIdentity(out var platform, out externalUserId)) { Navigation.NavigateTo("/access-denied"); return; } currentPlatform = platform; await LoadSessions(); } private async Task LoadSessions() { groupManagement = await SessionService.GetGroupManagementForCurrentUserAsync(GroupId); if (groupManagement is null) { Navigation.NavigateTo("/access-denied"); return; } sessions = await SessionService.GetUpcomingSessionsForCurrentUserAsync(GroupId); if (sessions is null) { Navigation.NavigateTo("/access-denied"); return; } publicSettings = await SessionService.GetPublicGroupSettingsForCurrentUserAsync(GroupId); if (publicSettings is null) { Navigation.NavigateTo("/access-denied"); return; } campaignTemplates = await SessionService.GetCampaignTemplatesForCurrentUserAsync(GroupId); if (campaignTemplates is null) { Navigation.NavigateTo("/access-denied"); return; } portfolioGames = await PortfolioService.GetPortfolioGamesForCurrentUserAsync(GroupId); pendingApplicationsCount = await MembershipService.GetPendingApplicationsCountForCurrentGmAsync(GroupId); RebuildBatchModels(); RebuildCampaignTemplateModels(); RebuildPublicSettingsModel(); } private async Task CreateDraft() { errorMessage = null; successMessage = null; isCreatingDraft = true; try { var portfolioId = await PortfolioService.CreateDraftForCurrentUserAsync(GroupId, null); Navigation.NavigateTo($"/portfolio/manage/{portfolioId}"); } catch (SessionAccessDeniedException) { Navigation.NavigateTo("/access-denied"); } catch (Exception ex) { errorMessage = "Не удалось создать черновик: " + ex.Message; } finally { isCreatingDraft = false; } } private async Task SavePublicSettings() { errorMessage = null; successMessage = null; savingPublicSettings = true; try { await SessionService.UpdatePublicGroupSettingsForCurrentUserAsync( GroupId, publicSettingsModel.PublicSlug, publicSettingsModel.PublicScheduleEnabled); successMessage = "Настройки публичной страницы обновлены."; await LoadSessions(); } catch (SessionAccessDeniedException) { Navigation.NavigateTo("/access-denied"); } catch (Exception ex) { errorMessage = "Не удалось обновить публичную страницу: " + ex.Message; } finally { savingPublicSettings = false; } } private async Task SetBatchPublicationMode(BatchBulkEditModel batch, PublicationMode mode) { errorMessage = null; successMessage = null; publishingBatchId = batch.BatchId; try { await SessionService.SetBatchPublicationModeForCurrentUserAsync(batch.BatchId, mode); successMessage = mode switch { PublicationMode.Catalog => "Batch опубликован в общем каталоге.", PublicationMode.ClubOnly => "Batch доступен только участникам клуба.", PublicationMode.Both => "Batch опубликован в каталоге и доступен участникам клуба.", _ => "Batch скрыт из публичного расписания." }; await LoadSessions(); } catch (SessionAccessDeniedException) { Navigation.NavigateTo("/access-denied"); } catch (Exception ex) { errorMessage = "Не удалось обновить публичность batch: " + ex.Message; } finally { publishingBatchId = null; } } private async Task SetSessionPublicationMode(Guid sessionId, PublicationMode mode) { errorMessage = null; successMessage = null; publishingSessionId = sessionId; try { await SessionService.SetSessionPublicationModeForCurrentUserAsync(sessionId, mode); successMessage = mode switch { PublicationMode.Catalog => "Сессия опубликована в общем каталоге.", PublicationMode.ClubOnly => "Сессия доступна только участникам клуба.", PublicationMode.Both => "Сессия опубликована в каталоге и доступна участникам клуба.", _ => "Сессия скрыта из публичного расписания." }; await LoadSessions(); } catch (SessionAccessDeniedException) { Navigation.NavigateTo("/access-denied"); } catch (Exception ex) { errorMessage = "Не удалось обновить публичность сессии: " + ex.Message; } finally { publishingSessionId = null; } } private async Task AddCoGm() { errorMessage = null; successMessage = null; var coGmExternalUserId = coGmModel.ExternalUserId.Trim(); if (coGmExternalUserId.Length == 0) { errorMessage = $"{CoGmIdLabel} должен быть заполнен."; return; } if (!IsValidPlatformUserId(CoGmPlatform, coGmExternalUserId)) { errorMessage = $"{CoGmIdLabel} должен быть положительным числом."; return; } isAddingCoGm = true; try { await SessionService.AddCoGmForOwnerAsync( GroupId, CoGmPlatform, coGmExternalUserId, coGmModel.DisplayName, coGmModel.ExternalUsername); coGmModel = new(); successMessage = "Co-GM добавлен."; await LoadSessions(); } catch (SessionAccessDeniedException) { Navigation.NavigateTo("/access-denied"); } catch (Exception ex) { errorMessage = "Не удалось добавить co-GM: " + ex.Message; } finally { isAddingCoGm = false; } } private async Task RemoveCoGm(WebGroupManager manager) { errorMessage = null; successMessage = null; removingCoGmId = ManagerKey(manager); var platform = ManagerPlatform(manager); var coGmExternalUserId = ManagerExternalUserId(manager); try { await SessionService.RemoveCoGmForOwnerAsync(GroupId, platform, coGmExternalUserId); successMessage = "Co-GM удалён."; await LoadSessions(); } catch (SessionAccessDeniedException) { Navigation.NavigateTo("/access-denied"); } catch (Exception ex) { errorMessage = "Не удалось удалить co-GM: " + ex.Message; } finally { removingCoGmId = null; } } private async Task PromoteWaitlisted(Guid sessionId) { errorMessage = null; successMessage = null; promotingSessionId = sessionId; try { await SessionService.PromoteWaitlistedPlayerForCurrentUserAsync(sessionId); await LoadSessions(); } catch (SessionAccessDeniedException) { Navigation.NavigateTo("/access-denied"); } catch (Exception ex) { errorMessage = ex.Message; } finally { promotingSessionId = null; } } private async Task ToggleParticipants(Guid sessionId) { if (expandedSessions.Contains(sessionId)) { expandedSessions.Remove(sessionId); return; } expandedSessions.Add(sessionId); if (!participantsCache.ContainsKey(sessionId)) { loadingParticipantsSessionId = sessionId; try { var participants = await SessionService.GetSessionParticipantsForCurrentUserAsync(sessionId); participantsCache[sessionId] = participants ?? []; } catch (Exception ex) { errorMessage = "Не удалось загрузить участников: " + ex.Message; expandedSessions.Remove(sessionId); } finally { loadingParticipantsSessionId = null; } } } private async Task KickParticipant(Guid sessionId, Guid participantId) { errorMessage = null; successMessage = null; kickingParticipantId = participantId; try { await SessionService.RemovePlayerFromSessionForCurrentUserAsync(sessionId, participantId); participantsCache.Remove(sessionId); successMessage = "Игрок исключён."; await LoadSessions(); if (expandedSessions.Contains(sessionId)) { await ToggleParticipants(sessionId); } } catch (SessionAccessDeniedException) { Navigation.NavigateTo("/access-denied"); } catch (Exception ex) { errorMessage = ex.Message; } finally { kickingParticipantId = null; } } private static string FormatParticipantUsername(WebParticipant p) { var username = string.IsNullOrWhiteSpace(p.TelegramUsername) ? p.TelegramId.ToString(System.Globalization.CultureInfo.InvariantCulture) : "@" + p.TelegramUsername; return $"{username} · {FormatParticipantRsvp(p.RsvpStatus)}"; } private static string FormatParticipantRsvp(string rsvp) => rsvp switch { RsvpStatus.Pending => "⏳ не ответил", RsvpStatus.Confirmed => "✅ подтвердил", RsvpStatus.Declined => "❌ отказался", _ => rsvp }; private static string GetParticipantStatusClass(WebParticipant p) { if (p.IsGm) return "status-success"; return p.RegistrationStatus switch { "Active" => "status-info", "Waitlisted" => "status-warning", _ => "status-neutral" }; } private static string TranslateParticipantStatus(WebParticipant p) { if (p.IsGm) return "ГМ"; return p.RegistrationStatus switch { "Active" => "Основной состав", "Waitlisted" => "Ожидание", _ => p.RegistrationStatus }; } private async Task UpdateBatchDetails(BatchBulkEditModel batch) { errorMessage = null; successMessage = null; if (!ValidateBatchDetails(batch)) { errorMessage = "Название и ссылка для batch не должны быть пустыми."; return; } processingBatchId = batch.BatchId; try { await SessionService.UpdateBatchDetailsForCurrentUserAsync(batch.BatchId, batch.Title, batch.JoinLink); await SessionService.UpdateBatchNotificationModeForCurrentUserAsync( batch.BatchId, 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.RescheduleBatchForCurrentUserAsync(batch.BatchId, 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.CloneBatchForCurrentUserAsync(batch.BatchId, interval); successMessage = $"Пачка склонирована: {clonedBatch.SessionCount} игр."; await LoadSessions(); } catch (SessionAccessDeniedException) { Navigation.NavigateTo("/access-denied"); } catch (Exception ex) { errorMessage = "Не удалось клонировать пачку: " + ex.Message; } finally { processingBatchId = null; } } private async Task CreateBatchFromTemplate(CampaignTemplateUsageModel template) { errorMessage = null; successMessage = null; processingTemplateId = template.Id; try { var utcTime = new DateTimeOffset(template.FirstScheduledAtLocal, TimeSpan.FromHours(3)).ToUniversalTime().UtcDateTime; if (utcTime <= DateTime.UtcNow) { errorMessage = "Первая дата batch должна быть в будущем."; return; } var createdBatch = await SessionService.CreateBatchFromCampaignTemplateForCurrentUserAsync(template.Id, utcTime); successMessage = $"Batch создан из шаблона: {createdBatch.SessionCount} игр."; await LoadSessions(); } catch (SessionAccessDeniedException) { Navigation.NavigateTo("/access-denied"); } catch (Exception ex) { errorMessage = "Не удалось создать batch из шаблона: " + ex.Message; } finally { processingTemplateId = 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, PublicSessionCount = orderedSessions.Count(session => session.IsPublic), AllSessionsPublic = orderedSessions.All(session => session.IsPublic), PublicationMode = orderedSessions .Select(s => PublicationModeExtensions.FromDatabaseValue(s.PublicationMode)) .GroupBy(m => m) .OrderByDescending(g => g.Count()) .First() .Key }; }) .OrderBy(batch => batch.FirstScheduledAtLocal) .ToList() ?? []; } private void RebuildCampaignTemplateModels() { var defaultStart = DateTime.UtcNow.AddDays(7).ToMoscow(); campaignTemplateModels = campaignTemplates? .OrderByDescending(template => template.UpdatedAt) .ThenBy(template => template.Name) .Select(template => new CampaignTemplateUsageModel { Id = template.Id, Name = template.Name, Title = template.Title, JoinLink = template.JoinLink, SessionCount = template.SessionCount, IntervalDays = template.IntervalDays, MaxPlayers = template.MaxPlayers, NotificationMode = template.NotificationMode, FirstScheduledAtLocal = defaultStart }) .ToList() ?? []; } private void RebuildPublicSettingsModel() { if (publicSettings is null) { return; } publicSettingsModel = new PublicSettingsEditModel { PublicScheduleEnabled = publicSettings.PublicScheduleEnabled, PublicSlug = publicSettings.PublicSlug ?? "" }; } 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 bool IsBatchPublishBusy(BatchBulkEditModel batch) => publishingBatchId == batch.BatchId; private bool IsTemplateBusy(CampaignTemplateUsageModel template) => processingTemplateId == template.Id; private string? PublicClubUrl => string.IsNullOrWhiteSpace(publicSettings?.PublicSlug) ? null : Navigation.ToAbsoluteUri($"/club/{publicSettings.PublicSlug}").ToString(); private string PublicSessionUrl(Guid sessionId) => Navigation.ToAbsoluteUri($"/s/{sessionId}").ToString(); private static string FormatPublicationStatus(WebSession session) => session.IsPublic ? "Опубликована" : "Скрыта"; private static string GetPublicationStatusClass(WebSession session) => session.IsPublic ? "status-success" : "status-neutral"; private static string FormatBatchPublication(BatchBulkEditModel batch) => batch.PublicSessionCount == 0 ? "Все игры скрыты" : batch.PublicSessionCount == batch.SessionCount ? "Все игры опубликованы" : $"{batch.PublicSessionCount}/{batch.SessionCount} опубликовано"; private string CoGmPlatform => string.IsNullOrWhiteSpace(groupManagement?.Group.Platform) ? "Telegram" : groupManagement.Group.Platform; private string CoGmIdLabel => $"{CoGmPlatform} ID co-GM"; private string CurrentUserRole => groupManagement?.Managers.FirstOrDefault(manager => string.Equals(ManagerPlatform(manager), currentPlatform, StringComparison.OrdinalIgnoreCase) && ManagerExternalUserId(manager) == externalUserId)?.Role ?? GroupManagerRoleExtensions.CoGmValue; private static string FormatRole(string role) => GroupManagerRoleExtensions.FromDatabaseValue(role).ToDisplayName(); private string FormatManager(WebGroupManager manager) { var username = string.IsNullOrWhiteSpace(manager.ExternalUsername) ? manager.TelegramUsername : manager.ExternalUsername; var identity = string.IsNullOrWhiteSpace(username) ? $"{ManagerPlatform(manager)} {ManagerExternalUserId(manager)}" : "@" + username.TrimStart('@'); return $"{FormatRole(manager.Role)} · {manager.DisplayName} · {identity}"; } private string ManagerPlatform(WebGroupManager manager) => string.IsNullOrWhiteSpace(manager.Platform) ? CoGmPlatform : manager.Platform; private static string ManagerExternalUserId(WebGroupManager manager) => string.IsNullOrWhiteSpace(manager.ExternalUserId) ? manager.TelegramId.ToString(System.Globalization.CultureInfo.InvariantCulture) : manager.ExternalUserId; private string ManagerKey(WebGroupManager manager) => $"{ManagerPlatform(manager)}:{ManagerExternalUserId(manager)}"; private static bool IsValidPlatformUserId(string platform, string externalUserId) => string.Equals(platform, "Telegram", StringComparison.OrdinalIgnoreCase) ? long.TryParse(externalUserId, out var telegramId) && telegramId > 0 : !string.Equals(platform, "Discord", StringComparison.OrdinalIgnoreCase) || (ulong.TryParse(externalUserId, out var platformId) && platformId > 0); 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 PublicationMode ParseMode(object? value) => Enum.TryParse(value?.ToString(), out var mode) ? mode : PublicationMode.None; private static string FormatBatchSummary(BatchBulkEditModel batch) => $"{batch.SessionCount} игр · {FormatLocalMoscow(batch.FirstScheduledAtLocal)} — {FormatLocalMoscow(batch.LastScheduledAtLocal)}"; private static string FormatTemplateSummary(CampaignTemplateUsageModel template) { var seats = template.MaxPlayers.HasValue ? $"{template.MaxPlayers.Value.ToString(System.Globalization.CultureInfo.InvariantCulture)} мест" : "без лимита"; return $"{template.Title} · {template.SessionCount} игр · каждые {template.IntervalDays} дн. · {seats} · {FormatNotificationMode(template.NotificationMode)}"; } private static string FormatNotificationMode(string notificationMode) => SessionNotificationModeExtensions.FromDatabaseValue(notificationMode) switch { SessionNotificationMode.GroupOnly => "только группа", _ => "группа и личка" }; 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 int PublicSessionCount { get; init; } public bool AllSessionsPublic { get; init; } public PublicationMode PublicationMode { get; set; } = PublicationMode.None; public string CloneInterval { get; set; } = "week"; } private sealed class CampaignTemplateUsageModel { public Guid Id { get; init; } public string Name { get; init; } = ""; public string Title { get; init; } = ""; public string JoinLink { get; init; } = ""; public int SessionCount { get; init; } public int IntervalDays { get; init; } public int? MaxPlayers { get; init; } public string NotificationMode { get; init; } = SessionNotificationModeExtensions.GroupAndDirectValue; public DateTime FirstScheduledAtLocal { get; set; } = DateTime.Now; } private sealed class CoGmEditModel { public string ExternalUserId { get; set; } = ""; public string DisplayName { get; set; } = ""; public string? ExternalUsername { get; set; } } private sealed class PublicSettingsEditModel { public bool PublicScheduleEnabled { get; set; } public string? PublicSlug { get; set; } } }