From ae6be912e328a2de7e2e84199daed5001a4fa566 Mon Sep 17 00:00:00 2001 From: Toutsu Date: Thu, 7 May 2026 13:26:03 +0300 Subject: [PATCH] feat(#14): add GroupStats.razor attendance page --- .../Components/Pages/GroupStats.razor | 234 ++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 src/GmRelay.Web/Components/Pages/GroupStats.razor diff --git a/src/GmRelay.Web/Components/Pages/GroupStats.razor b/src/GmRelay.Web/Components/Pages/GroupStats.razor new file mode 100644 index 0000000..c8d386b --- /dev/null +++ b/src/GmRelay.Web/Components/Pages/GroupStats.razor @@ -0,0 +1,234 @@ +@page "/group/{GroupId:guid}/stats" +@using GmRelay.Web.Services +@using GmRelay.Shared.Domain +@using Microsoft.AspNetCore.Authorization +@using Microsoft.AspNetCore.Components.Authorization +@attribute [Authorize] +@inject ISessionStore SessionStore +@inject AuthenticationStateProvider AuthStateProvider +@inject NavigationManager Navigation + +Статистика — GM-Relay + +
+ + + + + @if (!string.IsNullOrEmpty(errorMessage)) + { +
+ ⚠️ @errorMessage +
+ } + + @if (stats is null) + { +
⏳ Загружаем статистику…
+ } + else if (stats.Count == 0) + { +
+
📈
+

Пока нет данных

+

После первых сессий здесь появится аналитика.

+
+ } + else + { +
+
+
+
@stats.Count
+
Игроков
+
+
+
@TotalSessions
+
Сессий
+
+
+
@AvgAttendanceRate%
+
Средняя посещаемость
+
+
+
@topPlayer?.DisplayName
+
Самый стабильный
+
+
+ +
+ + + + + + + + + + + + + + + @foreach (var s in sortedStats) + { + + + + + + + + + + + } + +
Игрок @(SortIndicator("player"))Всего @(SortIndicator("total"))✅ @(SortIndicator("confirmed"))❌ @(SortIndicator("declined"))💤 @(SortIndicator("noresponse"))⏳ @(SortIndicator("waitlist"))% @(SortIndicator("rate"))🚫 @(SortIndicator("cancelled"))
+
+ @s.DisplayName + @if (!string.IsNullOrEmpty(s.TelegramUsername)) + { + @@@s.TelegramUsername + } +
+
@s.TotalSessions@s.ConfirmedCount@s.DeclinedCount@s.NoResponseCount@s.WaitlistedCount + + @s.AttendanceRate% + + @s.CancellationAffectedCount
+
+
+ } +
+ + + +@code { + [Parameter] public Guid GroupId { get; set; } + private List? stats; + private List sortedStats = new(); + private string? errorMessage; + private string sortColumn = "confirmed"; + private bool sortDesc = true; + private int TotalSessions => stats?.Count > 0 ? (int)(stats.Max(s => s.TotalSessions)) : 0; + private int AvgAttendanceRate => stats?.Count > 0 ? (int)(stats.Average(s => s.AttendanceRate)) : 0; + private PlayerAttendanceStats? topPlayer => stats?.OrderByDescending(s => s.AttendanceRate).ThenByDescending(s => s.ConfirmedCount).FirstOrDefault(); + + protected override async Task OnInitializedAsync() + { + var authState = await AuthStateProvider.GetAuthenticationStateAsync(); + var user = authState.User; + if (!user.Identity?.IsAuthenticated ?? true) + { + Navigation.NavigateTo("/login"); + return; + } + var telegramIdClaim = user.FindFirst("telegram_id")?.Value + ?? user.FindFirst(ClaimTypes.NameIdentifier)?.Value; + if (!long.TryParse(telegramIdClaim, out var telegramId)) + { + Navigation.NavigateTo("/login"); + return; + } + try + { + if (!await SessionStore.IsGroupManagerAsync(GroupId, telegramId)) + { + Navigation.NavigateTo("/access-denied"); + return; + } + stats = await SessionStore.GetGroupAttendanceStatsAsync(GroupId) ?? new(); + UpdateSortedStats(); + } + catch (Exception ex) + { + errorMessage = $"Ошибка загрузки статистики: {ex.Message}"; + } + } + + private void SortBy(string column) + { + if (sortColumn == column) + sortDesc = !sortDesc; + else + { + sortColumn = column; + sortDesc = true; + } + UpdateSortedStats(); + } + + private void UpdateSortedStats() + { + if (stats is null) { sortedStats = new(); return; } + IOrderedEnumerable ordered = sortColumn switch + { + "player" => sortDesc ? stats.OrderByDescending(s => s.DisplayName) : stats.OrderBy(s => s.DisplayName), + "total" => sortDesc ? stats.OrderByDescending(s => s.TotalSessions) : stats.OrderBy(s => s.TotalSessions), + "confirmed" => sortDesc ? stats.OrderByDescending(s => s.ConfirmedCount) : stats.OrderBy(s => s.ConfirmedCount), + "declined" => sortDesc ? stats.OrderByDescending(s => s.DeclinedCount) : stats.OrderBy(s => s.DeclinedCount), + "noresponse" => sortDesc ? stats.OrderByDescending(s => s.NoResponseCount) : stats.OrderBy(s => s.NoResponseCount), + "waitlist" => sortDesc ? stats.OrderByDescending(s => s.WaitlistedCount) : stats.OrderBy(s => s.WaitlistedCount), + "rate" => sortDesc ? stats.OrderByDescending(s => s.AttendanceRate) : stats.OrderBy(s => s.AttendanceRate), + "cancelled" => sortDesc ? stats.OrderByDescending(s => s.CancellationAffectedCount) : stats.OrderBy(s => s.CancellationAffectedCount), + _ => stats.OrderByDescending(s => s.ConfirmedCount) + }; + sortedStats = ordered.ToList(); + } + + private string SortIndicator(string column) => sortColumn == column ? (sortDesc ? "▼" : "▲") : ""; + + private string AttendanceBadgeClass(decimal rate) => rate switch + { + >= 75m => "rate-excellent", + >= 50m => "rate-good", + _ => "rate-poor" + }; +}