b53c7fb736
Features: - Category viewer ranges + subcategory templates (admin group modal, tree workspace) - Nomination enrichment via TwitchTracker API (NominationEnrichmentService, TwitchTrackerViewerStatsProvider) with admin tracking rules editor - Nomination group tracker: CategoryGroupName as primary identifier, CategoryId stays as nullable legacy field; StreamerIdentity table - Dynamic showact application form builder (AdminShowactFormBuilder, ShowactApplicationSchedule) - Session idle timeout setting (AdminSessionTimeoutCard) - Share URLs for X and Discord (SiteSettings, public extras) - Workflow rules now stored per season (falls back to global SiteSettings) - New admin routes: settings/access, settings/workflows, tracking-rules - New admin review workspace with subcategory tabs - AdminCategoriesView rebuilt with group/subcategory modals Migrations (all additive): - AddShareUrls, AddShowactDynamicForm, AddCategoryViewerRanges, AddSessionIdleTimeoutSettings, AddSeasonSubcategoryTemplates, AddNominationGroupTrackerIdentity, AddShowactApplicationSchedule, AddSeasonWorkflowRulesJson Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
96 lines
4.3 KiB
C#
96 lines
4.3 KiB
C#
using Backend.Contracts;
|
|
using Backend.Data;
|
|
using Backend.Common;
|
|
using Backend.Services;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace Backend.Endpoints;
|
|
|
|
public static partial class PublicEndpoints
|
|
{
|
|
private static async Task<IResult> GetSeasonCategories(int year, AwardsDbContext db)
|
|
{
|
|
var season = await db.Seasons
|
|
.AsNoTracking()
|
|
.Include(item => item.Categories.OrderBy(category => category.SortOrder))
|
|
.ThenInclude(category => category.Candidates.OrderBy(candidate => candidate.DisplayName))
|
|
.FirstOrDefaultAsync(item => item.Year == year);
|
|
|
|
if (season is null)
|
|
{
|
|
return Results.NotFound();
|
|
}
|
|
|
|
var phaseKey = SeasonMappings.NormalizePhaseKey(season.CurrentPhase);
|
|
var subcategoryTemplates = SeasonSubcategoryTemplateSettings.Read(season, season.Categories);
|
|
var publicCategories = season.Categories
|
|
.Where(category => SeasonSubcategoryTemplateSettings.MatchesTemplate(category, subcategoryTemplates))
|
|
.Where(category => ShouldExposePublicCategory(phaseKey, category.Candidates.Count))
|
|
.ToArray();
|
|
var publicCategoryIds = publicCategories.Select(category => category.Id).ToArray();
|
|
var approvedClips = await db.ClipSubmissions
|
|
.AsNoTracking()
|
|
.Where(item =>
|
|
item.SeasonId == season.Id
|
|
&& item.Status == "approved"
|
|
&& item.CategoryId != null
|
|
&& publicCategoryIds.Contains(item.CategoryId.Value))
|
|
.Select(item => new PublicCandidateClip(
|
|
item.CategoryId,
|
|
item.CandidateId,
|
|
item.Creator,
|
|
item.ClipUrl,
|
|
item.Title,
|
|
item.Platform,
|
|
item.CreatedAt,
|
|
item.ReviewedAt))
|
|
.ToArrayAsync();
|
|
var clipsByCandidateId = BuildCandidateClipLookup(approvedClips);
|
|
var clipsByCreatorKey = BuildCreatorClipLookup(approvedClips);
|
|
|
|
return Results.Ok(new SeasonCategoriesResponse(
|
|
season.Id,
|
|
season.Year,
|
|
publicCategories.Select(category => new PublicCategoryDetailDto(
|
|
category.Id,
|
|
category.Name,
|
|
category.GroupName,
|
|
category.Description,
|
|
category.MaxNomineesPerUser,
|
|
category.ViewerRangeMin,
|
|
category.ViewerRangeMax,
|
|
category.Candidates
|
|
.Where(candidate => !string.Equals(candidate.AcceptanceStatus, "declined", StringComparison.OrdinalIgnoreCase))
|
|
.Select(candidate =>
|
|
{
|
|
var clip = ResolveCandidateClip(candidate, clipsByCandidateId, clipsByCreatorKey);
|
|
var candidateClipUrl = string.IsNullOrWhiteSpace(candidate.ClipCompilationUrl)
|
|
|| string.Equals(candidate.ClipEmbedStatus, "blocked", StringComparison.OrdinalIgnoreCase)
|
|
? null
|
|
: candidate.ClipCompilationUrl.Trim();
|
|
var clipUrl = candidateClipUrl ?? clip?.ClipUrl;
|
|
var clipTitle = candidateClipUrl is not null
|
|
? string.IsNullOrWhiteSpace(candidate.ClipCompilationTitle) ? "Highlight-Clip ansehen" : candidate.ClipCompilationTitle.Trim()
|
|
: clip?.Title;
|
|
var clipPlatform = candidateClipUrl is not null
|
|
? string.IsNullOrWhiteSpace(candidate.ClipCompilationPlatform) ? candidate.Platform : candidate.ClipCompilationPlatform.Trim()
|
|
: clip?.Platform;
|
|
var clipEmbedStatus = candidateClipUrl is not null
|
|
? candidate.ClipEmbedStatus
|
|
: null;
|
|
|
|
return new CandidateSummaryDto(
|
|
candidate.Id,
|
|
candidate.DisplayName,
|
|
candidate.ChannelSlug,
|
|
SeasonMappings.BuildProfileUrl(candidate.Platform, candidate.ChannelSlug),
|
|
candidate.Platform,
|
|
clipUrl,
|
|
clipTitle,
|
|
clipPlatform,
|
|
clipEmbedStatus);
|
|
}).ToArray()))
|
|
.ToArray()));
|
|
}
|
|
}
|