@page "/templates" @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 (groups is null) {
} else if (groups.Count == 0) {
🤖
Нет доступных групп

Добавьте бота GM-Relay в группу Telegram, чтобы создать первый шаблон кампании.

} else {

Группа для шаблонов

@(SelectedGroup?.Name ?? "Выберите группу")

@if (SelectedGroup is not null) { @FormatRole(SelectedGroup.ManagerRole) }
@if (SelectedGroup is not null) { Открыть группу → }

Новый шаблон

Эти параметры будут использоваться при запуске batch из группы.

Template

Сохранённые шаблоны

@campaignTemplateModels.Count для выбранной группы

@campaignTemplateModels.Count
@if (campaignTemplates is null) {
} else if (campaignTemplateModels.Count == 0) {
Шаблонов пока нет

Создайте первый шаблон для выбранной группы.

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

@template.Name

@FormatTemplateSummary(template)

@FormatLocalMoscow(template.UpdatedAt.ToMoscow())
}
}
}
@code { private List? groups; private List? campaignTemplates; private List campaignTemplateModels = []; private Guid selectedGroupId; private Guid? deletingTemplateId; private bool isCreatingTemplate; private long telegramId; private string? errorMessage; private string? successMessage; private CampaignTemplateEditModel templateModel = new(); private WebGameGroup? SelectedGroup => groups?.FirstOrDefault(group => group.Id == selectedGroupId); protected override async Task OnInitializedAsync() { var authState = await AuthStateProvider.GetAuthenticationStateAsync(); if (!authState.User.TryGetTelegramId(out telegramId)) { Navigation.NavigateTo("/access-denied"); return; } groups = await SessionService.GetGroupsForGmAsync(telegramId); selectedGroupId = groups.FirstOrDefault()?.Id ?? Guid.Empty; if (selectedGroupId != Guid.Empty) { await LoadTemplates(); } } private async Task OnSelectedGroupChanged(ChangeEventArgs args) { if (!Guid.TryParse(args.Value?.ToString(), out var groupId)) { return; } selectedGroupId = groupId; errorMessage = null; successMessage = null; await LoadTemplates(); } private async Task LoadTemplates() { campaignTemplates = null; campaignTemplateModels = []; var templates = await SessionService.GetCampaignTemplatesForGmAsync(selectedGroupId, telegramId); if (templates is null) { Navigation.NavigateTo("/access-denied"); return; } campaignTemplates = templates; RebuildCampaignTemplateModels(); } private async Task CreateCampaignTemplate() { errorMessage = null; successMessage = null; if (selectedGroupId == Guid.Empty) { errorMessage = "Выберите группу для шаблона."; return; } if (!ValidateCampaignTemplate(templateModel)) { errorMessage = "Шаблон должен иметь название, ссылку, 1-52 игр, шаг 1-365 дней и положительный лимит мест, если он указан."; return; } isCreatingTemplate = true; try { await SessionService.CreateCampaignTemplateForGmAsync( selectedGroupId, telegramId, new CreateCampaignTemplateRequest( templateModel.Name, templateModel.Title, templateModel.JoinLink, templateModel.SessionCount, templateModel.IntervalDays, templateModel.MaxPlayers, SessionNotificationModeExtensions.FromDatabaseValue(templateModel.NotificationMode))); templateModel = new(); successMessage = "Шаблон кампании сохранён."; await LoadTemplates(); } catch (SessionAccessDeniedException) { Navigation.NavigateTo("/access-denied"); } catch (Exception ex) { errorMessage = "Не удалось сохранить шаблон: " + ex.Message; } finally { isCreatingTemplate = false; } } private async Task DeleteCampaignTemplate(CampaignTemplateManagementModel template) { errorMessage = null; successMessage = null; deletingTemplateId = template.Id; try { await SessionService.DeleteCampaignTemplateForGmAsync(template.Id, telegramId); successMessage = "Шаблон кампании удалён."; await LoadTemplates(); } catch (SessionAccessDeniedException) { Navigation.NavigateTo("/access-denied"); } catch (Exception ex) { errorMessage = "Не удалось удалить шаблон: " + ex.Message; } finally { deletingTemplateId = null; } } private void RebuildCampaignTemplateModels() { campaignTemplateModels = campaignTemplates? .OrderByDescending(template => template.UpdatedAt) .ThenBy(template => template.Name) .Select(template => new CampaignTemplateManagementModel { Id = template.Id, Name = template.Name, Title = template.Title, JoinLink = template.JoinLink, SessionCount = template.SessionCount, IntervalDays = template.IntervalDays, MaxPlayers = template.MaxPlayers, NotificationMode = template.NotificationMode, UpdatedAt = template.UpdatedAt }) .ToList() ?? []; } private static bool ValidateCampaignTemplate(CampaignTemplateEditModel template) { template.Name = template.Name.Trim(); template.Title = template.Title.Trim(); template.JoinLink = template.JoinLink.Trim(); if (template.MaxPlayers.HasValue && template.MaxPlayers.Value <= 0) { return false; } return template.Name.Length > 0 && template.Title.Length > 0 && template.JoinLink.Length > 0 && template.SessionCount is >= 1 and <= 52 && template.IntervalDays is >= 1 and <= 365; } private static string FormatTemplateSummary(CampaignTemplateManagementModel 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 FormatRole(string role) => GroupManagerRoleExtensions.FromDatabaseValue(role).ToDisplayName(); private static string FormatLocalMoscow(DateTime localMoscow) => localMoscow.ToString("d MMMM yyyy, HH:mm", System.Globalization.CultureInfo.GetCultureInfo("ru-RU")); private sealed class CampaignTemplateEditModel { public string Name { get; set; } = ""; public string Title { get; set; } = ""; public string JoinLink { get; set; } = ""; public int SessionCount { get; set; } = 6; public int IntervalDays { get; set; } = 7; public int? MaxPlayers { get; set; } public string NotificationMode { get; set; } = SessionNotificationModeExtensions.GroupAndDirectValue; } private sealed class CampaignTemplateManagementModel { 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 UpdatedAt { get; init; } } }