403 lines
16 KiB
Plaintext
403 lines
16 KiB
Plaintext
@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
|
||
|
||
<PageTitle>Шаблоны кампаний — GM-Relay</PageTitle>
|
||
|
||
<div class="page-container">
|
||
<ul class="gm-breadcrumb animate-fade-in">
|
||
<li><a href="/">Главная</a></li>
|
||
<li class="active">Шаблоны кампаний</li>
|
||
</ul>
|
||
|
||
<div class="page-header animate-fade-in">
|
||
<h2>📋 Шаблоны кампаний</h2>
|
||
<p>Сохраняйте типовые кампании один раз и применяйте их на странице нужной группы.</p>
|
||
</div>
|
||
|
||
@if (!string.IsNullOrEmpty(errorMessage))
|
||
{
|
||
<div class="gm-alert gm-alert-danger" style="margin-bottom: 1rem;">
|
||
⚠️ @errorMessage
|
||
</div>
|
||
}
|
||
|
||
@if (!string.IsNullOrEmpty(successMessage))
|
||
{
|
||
<div class="gm-alert gm-alert-success" style="margin-bottom: 1rem;">
|
||
✅ @successMessage
|
||
</div>
|
||
}
|
||
|
||
@if (groups is null)
|
||
{
|
||
<div class="glass-card" style="padding: 2rem;">
|
||
<div class="skeleton skeleton-text" style="width: 80%; margin-bottom: 1rem;"></div>
|
||
<div class="skeleton skeleton-text" style="width: 60%; margin-bottom: 0.75rem;"></div>
|
||
<div class="skeleton skeleton-text" style="width: 70%;"></div>
|
||
</div>
|
||
}
|
||
else if (groups.Count == 0)
|
||
{
|
||
<div class="glass-card">
|
||
<div class="empty-state">
|
||
<div class="empty-state-icon">🤖</div>
|
||
<div class="empty-state-title">Нет доступных групп</div>
|
||
<p class="empty-state-text">Добавьте бота GM-Relay в группу Telegram, чтобы создать первый шаблон кампании.</p>
|
||
</div>
|
||
</div>
|
||
}
|
||
else
|
||
{
|
||
<div class="glass-card campaign-template-panel animate-slide-up">
|
||
<div class="batch-bulk-header">
|
||
<div>
|
||
<h3>Группа для шаблонов</h3>
|
||
<p>@(SelectedGroup?.Name ?? "Выберите группу")</p>
|
||
</div>
|
||
@if (SelectedGroup is not null)
|
||
{
|
||
<span class="status-badge @(SelectedGroup.ManagerRole == GroupManagerRoleExtensions.OwnerValue ? "status-success" : "status-info")">
|
||
@FormatRole(SelectedGroup.ManagerRole)
|
||
</span>
|
||
}
|
||
</div>
|
||
|
||
<div class="template-group-selector">
|
||
<select value="@selectedGroupId" @onchange="OnSelectedGroupChanged" class="gm-form-control">
|
||
@foreach (var group in groups)
|
||
{
|
||
<option value="@group.Id">@group.Name</option>
|
||
}
|
||
</select>
|
||
@if (SelectedGroup is not null)
|
||
{
|
||
<a href="/group/@SelectedGroup.Id" class="btn-gm btn-gm-outline">Открыть группу →</a>
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="glass-card campaign-template-panel animate-slide-up">
|
||
<div class="batch-bulk-header">
|
||
<div>
|
||
<h3>Новый шаблон</h3>
|
||
<p>Эти параметры будут использоваться при запуске batch из группы.</p>
|
||
</div>
|
||
<span class="status-badge status-info">Template</span>
|
||
</div>
|
||
|
||
<EditForm Model="@templateModel" OnValidSubmit="CreateCampaignTemplate">
|
||
<div class="campaign-template-fields">
|
||
<div class="gm-form-group">
|
||
<label class="gm-form-label">Название шаблона</label>
|
||
<InputText @bind-Value="templateModel.Name" class="gm-form-control" />
|
||
</div>
|
||
<div class="gm-form-group">
|
||
<label class="gm-form-label">Название кампании</label>
|
||
<InputText @bind-Value="templateModel.Title" class="gm-form-control" />
|
||
</div>
|
||
<div class="gm-form-group">
|
||
<label class="gm-form-label">Ссылка</label>
|
||
<InputText @bind-Value="templateModel.JoinLink" class="gm-form-control" />
|
||
</div>
|
||
<div class="gm-form-group">
|
||
<label class="gm-form-label">Игр</label>
|
||
<InputNumber @bind-Value="templateModel.SessionCount" class="gm-form-control" min="1" max="52" />
|
||
</div>
|
||
<div class="gm-form-group">
|
||
<label class="gm-form-label">Интервал, дней</label>
|
||
<InputNumber @bind-Value="templateModel.IntervalDays" class="gm-form-control" min="1" max="365" />
|
||
</div>
|
||
<div class="gm-form-group">
|
||
<label class="gm-form-label">Мест</label>
|
||
<InputNumber @bind-Value="templateModel.MaxPlayers" class="gm-form-control" min="1" />
|
||
</div>
|
||
<div class="gm-form-group">
|
||
<label class="gm-form-label">Уведомления</label>
|
||
<select @bind="templateModel.NotificationMode" class="gm-form-control">
|
||
<option value="@SessionNotificationModeExtensions.GroupAndDirectValue">В группе и в личку</option>
|
||
<option value="@SessionNotificationModeExtensions.GroupOnlyValue">Только в группе</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<button type="submit" class="btn-gm btn-gm-primary" disabled="@isCreatingTemplate">
|
||
@(isCreatingTemplate ? "⏳ Сохраняем..." : "💾 Сохранить шаблон")
|
||
</button>
|
||
</EditForm>
|
||
</div>
|
||
|
||
<div class="glass-card campaign-template-panel animate-slide-up">
|
||
<div class="batch-bulk-header">
|
||
<div>
|
||
<h3>Сохранённые шаблоны</h3>
|
||
<p>@campaignTemplateModels.Count для выбранной группы</p>
|
||
</div>
|
||
<span class="status-badge status-info">@campaignTemplateModels.Count</span>
|
||
</div>
|
||
|
||
@if (campaignTemplates is null)
|
||
{
|
||
<div class="skeleton skeleton-text" style="width: 70%; margin-bottom: 0.75rem;"></div>
|
||
<div class="skeleton skeleton-text" style="width: 55%;"></div>
|
||
}
|
||
else if (campaignTemplateModels.Count == 0)
|
||
{
|
||
<div class="empty-state empty-state-compact">
|
||
<div class="empty-state-title">Шаблонов пока нет</div>
|
||
<p class="empty-state-text">Создайте первый шаблон для выбранной группы.</p>
|
||
</div>
|
||
}
|
||
else
|
||
{
|
||
<div class="campaign-template-list">
|
||
@foreach (var template in campaignTemplateModels)
|
||
{
|
||
<div class="campaign-template-row template-management-row">
|
||
<div class="campaign-template-info">
|
||
<h3>@template.Name</h3>
|
||
<p>@FormatTemplateSummary(template)</p>
|
||
</div>
|
||
<div class="template-management-actions">
|
||
<span class="status-badge status-neutral">@FormatLocalMoscow(template.UpdatedAt.ToMoscow())</span>
|
||
<button type="button" class="btn-gm btn-gm-danger" disabled="@(deletingTemplateId == template.Id)" @onclick="() => DeleteCampaignTemplate(template)">
|
||
@(deletingTemplateId == template.Id ? "⏳ Удаляем..." : "Удалить")
|
||
</button>
|
||
</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
}
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
@code {
|
||
private List<WebGameGroup>? groups;
|
||
private List<WebCampaignTemplate>? campaignTemplates;
|
||
private List<CampaignTemplateManagementModel> 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; }
|
||
}
|
||
}
|