Add publication settings for clubs and sessions, read-only public club/session pages, dashboard controls, privacy-focused public queries, docs, and tests. Bump version to 3.3.0
This commit is contained in:
@@ -72,6 +72,58 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (publicSettings is not null)
|
||||
{
|
||||
<div class="glass-card animate-slide-up public-settings-panel" style="margin-bottom: 1rem;">
|
||||
<div class="batch-bulk-header">
|
||||
<div>
|
||||
<h3>Публичная страница клуба</h3>
|
||||
<p>@publicSettings.PublicSessionCount опубликованных игр без состава игроков и приватных ссылок</p>
|
||||
</div>
|
||||
<span class="status-badge @(publicSettings.PublicScheduleEnabled ? "status-success" : "status-neutral")">
|
||||
@(publicSettings.PublicScheduleEnabled ? "Включена" : "Выключена")
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<EditForm Model="@publicSettingsModel" OnValidSubmit="SavePublicSettings">
|
||||
<div class="batch-bulk-fields">
|
||||
<div class="gm-form-group public-toggle-field">
|
||||
<label class="gm-checkbox-label">
|
||||
<InputCheckbox @bind-Value="publicSettingsModel.PublicScheduleEnabled" />
|
||||
<span>Включить публичное расписание</span>
|
||||
</label>
|
||||
<div class="gm-form-hint">Если выключено, публичная страница и ссылки на сессии недоступны.</div>
|
||||
</div>
|
||||
<div class="gm-form-group">
|
||||
<label class="gm-form-label">Короткий адрес</label>
|
||||
<InputText @bind-Value="publicSettingsModel.PublicSlug" class="gm-form-control" />
|
||||
<div class="gm-form-hint">Латиница, цифры и дефисы, например `night-city-club`.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="public-settings-actions">
|
||||
<button type="submit" class="btn-gm btn-gm-primary" disabled="@savingPublicSettings">
|
||||
@(savingPublicSettings ? "Сохраняем..." : "Сохранить публикацию")
|
||||
</button>
|
||||
@if (PublicClubUrl is not null && publicSettings.PublicScheduleEnabled)
|
||||
{
|
||||
<a href="@PublicClubUrl" target="_blank" rel="noopener noreferrer" class="btn-gm btn-gm-outline">
|
||||
Открыть публичную страницу
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</EditForm>
|
||||
|
||||
@if (PublicClubUrl is not null && publicSettings.PublicScheduleEnabled)
|
||||
{
|
||||
<div class="public-link-row">
|
||||
<span>Ссылка клуба</span>
|
||||
<a href="@PublicClubUrl" target="_blank" rel="noopener noreferrer">@PublicClubUrl</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(errorMessage))
|
||||
{
|
||||
<div class="gm-alert gm-alert-danger" style="margin-bottom: 1rem;">
|
||||
@@ -201,6 +253,17 @@
|
||||
</button>
|
||||
</EditForm>
|
||||
|
||||
<div class="batch-publish-row">
|
||||
<span class="status-badge @(batch.AllSessionsPublic ? "status-success" : batch.PublicSessionCount > 0 ? "status-warning" : "status-neutral")">
|
||||
@FormatBatchPublication(batch)
|
||||
</span>
|
||||
<button type="button" class="btn-gm btn-gm-outline" disabled="@IsBatchPublishBusy(batch)" @onclick="() => SetBatchPublic(batch, !batch.AllSessionsPublic)">
|
||||
@(IsBatchPublishBusy(batch)
|
||||
? "Обновляем..."
|
||||
: batch.AllSessionsPublic ? "Скрыть batch" : "Опубликовать batch")
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="batch-clone-row">
|
||||
<select @bind="batch.CloneInterval" class="gm-form-control">
|
||||
<option value="week">Следующая неделя</option>
|
||||
@@ -249,6 +312,16 @@
|
||||
</td>
|
||||
<td>
|
||||
<div class="session-table-actions">
|
||||
<span class="status-badge @GetPublicationStatusClass(session)">@FormatPublicationStatus(session)</span>
|
||||
<button type="button" class="btn-gm btn-gm-outline" disabled="@(publishingSessionId == session.Id)" @onclick="() => SetSessionPublic(session.Id, !session.IsPublic)">
|
||||
@(publishingSessionId == session.Id
|
||||
? "Обновляем..."
|
||||
: session.IsPublic ? "Скрыть" : "Опубликовать")
|
||||
</button>
|
||||
@if (session.IsPublic && publicSettings?.PublicScheduleEnabled == true)
|
||||
{
|
||||
<a href="@PublicSessionUrl(session.Id)" target="_blank" rel="noopener noreferrer" class="btn-gm btn-gm-outline">Публичная ссылка</a>
|
||||
}
|
||||
<a href="/session/edit/@session.Id" class="btn-gm btn-gm-outline">
|
||||
✏️ Изменить
|
||||
</a>
|
||||
@@ -337,6 +410,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="session-card-actions">
|
||||
<button type="button" class="btn-gm btn-gm-outline" style="flex: 1; justify-content: center; font-size: 0.8125rem; padding: 0.5rem;" disabled="@(publishingSessionId == session.Id)" @onclick="() => SetSessionPublic(session.Id, !session.IsPublic)">
|
||||
@(publishingSessionId == session.Id
|
||||
? "Обновляем..."
|
||||
: session.IsPublic ? "Скрыть" : "Опубликовать")
|
||||
</button>
|
||||
@if (session.IsPublic && publicSettings?.PublicScheduleEnabled == true)
|
||||
{
|
||||
<a href="@PublicSessionUrl(session.Id)" target="_blank" rel="noopener noreferrer" class="btn-gm btn-gm-outline" style="flex: 1; justify-content: center; font-size: 0.8125rem; padding: 0.5rem;">Публичная ссылка</a>
|
||||
}
|
||||
<a href="/session/edit/@session.Id" class="btn-gm btn-gm-outline" style="flex: 1; justify-content: center; font-size: 0.8125rem; padding: 0.5rem;">
|
||||
✏️ Изменить
|
||||
</a>
|
||||
@@ -398,18 +480,23 @@
|
||||
private List<WebSession>? sessions;
|
||||
private List<WebCampaignTemplate>? campaignTemplates;
|
||||
private WebGroupManagement? groupManagement;
|
||||
private WebPublicGroupSettings? publicSettings;
|
||||
private List<BatchBulkEditModel> batchModels = [];
|
||||
private List<CampaignTemplateUsageModel> campaignTemplateModels = [];
|
||||
private Guid? promotingSessionId;
|
||||
private Guid? processingBatchId;
|
||||
private Guid? processingTemplateId;
|
||||
private Guid? publishingBatchId;
|
||||
private Guid? publishingSessionId;
|
||||
private string? removingCoGmId;
|
||||
private bool isAddingCoGm;
|
||||
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<Guid, List<WebParticipant>> participantsCache = new();
|
||||
private HashSet<Guid> expandedSessions = new();
|
||||
private Guid? kickingParticipantId;
|
||||
@@ -444,6 +531,13 @@
|
||||
return;
|
||||
}
|
||||
|
||||
publicSettings = await SessionService.GetPublicGroupSettingsForCurrentUserAsync(GroupId);
|
||||
if (publicSettings is null)
|
||||
{
|
||||
Navigation.NavigateTo("/access-denied");
|
||||
return;
|
||||
}
|
||||
|
||||
campaignTemplates = await SessionService.GetCampaignTemplatesForCurrentUserAsync(GroupId);
|
||||
if (campaignTemplates is null)
|
||||
{
|
||||
@@ -453,6 +547,92 @@
|
||||
|
||||
RebuildBatchModels();
|
||||
RebuildCampaignTemplateModels();
|
||||
RebuildPublicSettingsModel();
|
||||
}
|
||||
|
||||
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 SetBatchPublic(BatchBulkEditModel batch, bool isPublic)
|
||||
{
|
||||
errorMessage = null;
|
||||
successMessage = null;
|
||||
publishingBatchId = batch.BatchId;
|
||||
|
||||
try
|
||||
{
|
||||
await SessionService.SetBatchPublicForCurrentUserAsync(batch.BatchId, isPublic);
|
||||
successMessage = isPublic
|
||||
? "Batch опубликован в публичном расписании."
|
||||
: "Batch скрыт из публичного расписания.";
|
||||
await LoadSessions();
|
||||
}
|
||||
catch (SessionAccessDeniedException)
|
||||
{
|
||||
Navigation.NavigateTo("/access-denied");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = "Не удалось обновить публичность batch: " + ex.Message;
|
||||
}
|
||||
finally
|
||||
{
|
||||
publishingBatchId = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SetSessionPublic(Guid sessionId, bool isPublic)
|
||||
{
|
||||
errorMessage = null;
|
||||
successMessage = null;
|
||||
publishingSessionId = sessionId;
|
||||
|
||||
try
|
||||
{
|
||||
await SessionService.SetSessionPublicForCurrentUserAsync(sessionId, isPublic);
|
||||
successMessage = isPublic
|
||||
? "Сессия опубликована в публичном расписании."
|
||||
: "Сессия скрыта из публичного расписания.";
|
||||
await LoadSessions();
|
||||
}
|
||||
catch (SessionAccessDeniedException)
|
||||
{
|
||||
Navigation.NavigateTo("/access-denied");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = "Не удалось обновить публичность сессии: " + ex.Message;
|
||||
}
|
||||
finally
|
||||
{
|
||||
publishingSessionId = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AddCoGm()
|
||||
@@ -806,7 +986,9 @@
|
||||
FirstScheduledAtLocal = firstSession.ScheduledAt.ToMoscow(),
|
||||
LastScheduledAtLocal = lastSession.ScheduledAt.ToMoscow(),
|
||||
IntervalDays = InferIntervalDays(orderedSessions),
|
||||
SessionCount = orderedSessions.Count
|
||||
SessionCount = orderedSessions.Count,
|
||||
PublicSessionCount = orderedSessions.Count(session => session.IsPublic),
|
||||
AllSessionsPublic = orderedSessions.All(session => session.IsPublic)
|
||||
};
|
||||
})
|
||||
.OrderBy(batch => batch.FirstScheduledAtLocal)
|
||||
@@ -834,6 +1016,20 @@
|
||||
.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();
|
||||
@@ -843,8 +1039,31 @@
|
||||
|
||||
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"
|
||||
@@ -966,6 +1185,8 @@
|
||||
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 string CloneInterval { get; set; } = "week";
|
||||
}
|
||||
|
||||
@@ -988,4 +1209,10 @@
|
||||
public string DisplayName { get; set; } = "";
|
||||
public string? ExternalUsername { get; set; }
|
||||
}
|
||||
|
||||
private sealed class PublicSettingsEditModel
|
||||
{
|
||||
public bool PublicScheduleEnabled { get; set; }
|
||||
public string? PublicSlug { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user