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>
141 lines
5.4 KiB
C#
141 lines
5.4 KiB
C#
using Backend.Contracts;
|
|
using Backend.Common;
|
|
using Backend.Data;
|
|
using Backend.Domain;
|
|
using Backend.Services;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace Backend.Endpoints;
|
|
|
|
public static partial class AdminSeasonManagementEndpoints
|
|
{
|
|
private static async Task<IResult> CreateSeason(
|
|
HttpContext context,
|
|
CreateSeasonRequest request,
|
|
AwardsDbContext db,
|
|
IAdminAuditService adminAuditService)
|
|
{
|
|
var session = AdminEndpointConventions.CurrentSession(context);
|
|
var validationError = ValidateSeasonRequest(request);
|
|
if (validationError is not null)
|
|
{
|
|
return validationError;
|
|
}
|
|
|
|
if (await db.Seasons.AnyAsync(item => item.Year == request.Year))
|
|
{
|
|
return Results.BadRequest(new { message = $"A season for {request.Year} already exists." });
|
|
}
|
|
|
|
var showStreamUrl = NormalizeSeasonStreamUrl(request.ShowStreamUrl);
|
|
var settings = await db.SiteSettings
|
|
.AsNoTracking()
|
|
.FirstOrDefaultAsync(item => item.Id == 1, context.RequestAborted);
|
|
var initialWorkflowRulesJson = WorkflowRuleSettings.Serialize(WorkflowRuleSettings.Read(settings));
|
|
|
|
var season = new Season
|
|
{
|
|
Year = request.Year,
|
|
Name = request.Name.Trim(),
|
|
ShowStreamUrl = showStreamUrl,
|
|
CurrentPhase = request.CurrentPhase.Trim(),
|
|
IsCurrent = request.IsCurrent,
|
|
IsCommunityOnly = request.IsCommunityOnly,
|
|
NominationStartsAt = request.NominationStartsAt,
|
|
NominationEndsAt = request.NominationEndsAt,
|
|
VotingStartsAt = request.VotingStartsAt,
|
|
VotingEndsAt = request.VotingEndsAt,
|
|
ReviewStartsAt = request.ReviewStartsAt,
|
|
ReviewEndsAt = request.ReviewEndsAt,
|
|
ShowDate = request.ShowDate,
|
|
ShowStartsAt = request.ShowStartsAt,
|
|
SubcategoryTemplatesJson = "[]",
|
|
WorkflowRulesJson = initialWorkflowRulesJson,
|
|
};
|
|
|
|
db.Seasons.Add(season);
|
|
var copiedCategoryCount = 0;
|
|
if (request.CopyStructureFromSeasonId is { } sourceSeasonId)
|
|
{
|
|
if (sourceSeasonId <= 0)
|
|
{
|
|
return Results.BadRequest(new { message = "A valid source season is required for copying structure." });
|
|
}
|
|
|
|
var sourceSeasonExists = await db.Seasons
|
|
.AsNoTracking()
|
|
.AnyAsync(item => item.Id == sourceSeasonId, context.RequestAborted);
|
|
if (!sourceSeasonExists)
|
|
{
|
|
return Results.BadRequest(new { message = "Source season for structure copy was not found." });
|
|
}
|
|
|
|
var sourceCategories = await db.Categories
|
|
.AsNoTracking()
|
|
.Where(item => item.SeasonId == sourceSeasonId)
|
|
.OrderBy(item => item.SortOrder)
|
|
.ThenBy(item => item.Name)
|
|
.ToArrayAsync(context.RequestAborted);
|
|
|
|
copiedCategoryCount = sourceCategories.Length;
|
|
var sourceSeason = await db.Seasons
|
|
.AsNoTracking()
|
|
.FirstAsync(item => item.Id == sourceSeasonId, context.RequestAborted);
|
|
season.SubcategoryTemplatesJson = sourceSeason.SubcategoryTemplatesJson;
|
|
season.WorkflowRulesJson = string.IsNullOrWhiteSpace(sourceSeason.WorkflowRulesJson)
|
|
? initialWorkflowRulesJson
|
|
: sourceSeason.WorkflowRulesJson;
|
|
foreach (var category in sourceCategories)
|
|
{
|
|
db.Categories.Add(new Category
|
|
{
|
|
Season = season,
|
|
GroupName = category.GroupName,
|
|
Name = category.Name,
|
|
Slug = category.Slug,
|
|
Description = category.Description,
|
|
SortOrder = category.SortOrder,
|
|
MaxNomineesPerUser = category.MaxNomineesPerUser,
|
|
ViewerRangeMin = category.ViewerRangeMin,
|
|
ViewerRangeMax = category.ViewerRangeMax,
|
|
});
|
|
}
|
|
}
|
|
|
|
var readinessIssues = BuildNewSeasonReadinessIssues(
|
|
request.CurrentPhase,
|
|
request.IsCurrent,
|
|
copiedCategoryCount);
|
|
if (readinessIssues.Length > 0)
|
|
{
|
|
return CreateReadinessError(readinessIssues);
|
|
}
|
|
|
|
await UnsetOtherCurrentSeasonsAsync(db, request.IsCurrent, null, context.RequestAborted);
|
|
|
|
adminAuditService.AddEntry(
|
|
session.TwitchUserId,
|
|
"season.create",
|
|
"season",
|
|
request.Year.ToString(),
|
|
copiedCategoryCount > 0
|
|
? $"Season {request.Year} wurde angelegt und {copiedCategoryCount} Kategorien wurden kopiert."
|
|
: $"Season {request.Year} wurde angelegt.",
|
|
new
|
|
{
|
|
request.IsCurrent,
|
|
request.IsCommunityOnly,
|
|
showStreamUrl,
|
|
request.CurrentPhase,
|
|
request.ShowDate,
|
|
request.ShowStartsAt,
|
|
request.CopyStructureFromSeasonId,
|
|
copiedCategoryCount,
|
|
},
|
|
RequestMetadataReader.Read(context));
|
|
|
|
await db.SaveChangesAsync(context.RequestAborted);
|
|
return Results.Ok(new { saved = true, seasonId = season.Id, copiedCategoryCount });
|
|
}
|
|
}
|