Files
vtuber-awards/Backend/Endpoints/AdminSeasonCategoryEndpoints.cs
AzuTear b53c7fb736 Add viewer-range categories, nomination tracking, dynamic showact form, session timeout, share URLs, and workflow-per-season
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>
2026-06-28 23:32:21 +02:00

151 lines
5.3 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> CreateCategory(
HttpContext context,
int seasonId,
UpsertCategoryRequest request,
AwardsDbContext db,
IAdminAuditService adminAuditService)
{
var session = AdminEndpointConventions.CurrentSession(context);
var validationError = ValidateCategoryRequest(request);
if (validationError is not null)
{
return validationError;
}
var season = await db.Seasons.FirstOrDefaultAsync(item => item.Id == seasonId);
if (season is null)
{
return Results.NotFound();
}
var normalizedSlug = request.Slug.Trim();
if (await db.Categories.AnyAsync(item =>
item.SeasonId == seasonId
&& item.Slug.ToLower() == normalizedSlug.ToLower()))
{
return Results.BadRequest(new { message = "A category with this slug already exists in the selected season." });
}
var category = new Category
{
SeasonId = seasonId,
GroupName = request.GroupName.Trim(),
Name = request.Name.Trim(),
Slug = normalizedSlug,
Description = request.Description.Trim(),
SortOrder = request.SortOrder,
MaxNomineesPerUser = request.MaxNomineesPerUser,
ViewerRangeMin = request.ViewerRangeMin,
ViewerRangeMax = request.ViewerRangeMax,
};
db.Categories.Add(category);
adminAuditService.AddEntry(
session.TwitchUserId,
"category.create",
"category",
request.Slug.Trim(),
$"Kategorie {request.Name.Trim()} wurde angelegt.",
new { seasonId, request.GroupName, request.SortOrder },
RequestMetadataReader.Read(context));
await db.SaveChangesAsync(context.RequestAborted);
return Results.Ok(new { saved = true, categoryId = category.Id });
}
private static async Task<IResult> UpdateCategory(
HttpContext context,
int categoryId,
UpsertCategoryRequest request,
AwardsDbContext db,
IAdminAuditService adminAuditService)
{
var session = AdminEndpointConventions.CurrentSession(context);
var validationError = ValidateCategoryRequest(request);
if (validationError is not null)
{
return validationError;
}
var category = await db.Categories.FirstOrDefaultAsync(item => item.Id == categoryId);
if (category is null)
{
return Results.NotFound();
}
var normalizedSlug = request.Slug.Trim();
if (await db.Categories.AnyAsync(item =>
item.SeasonId == category.SeasonId
&& item.Id != categoryId
&& item.Slug.ToLower() == normalizedSlug.ToLower()))
{
return Results.BadRequest(new { message = "A category with this slug already exists in the selected season." });
}
category.GroupName = request.GroupName.Trim();
category.Name = request.Name.Trim();
category.Slug = normalizedSlug;
category.Description = request.Description.Trim();
category.SortOrder = request.SortOrder;
category.MaxNomineesPerUser = request.MaxNomineesPerUser;
category.ViewerRangeMin = request.ViewerRangeMin;
category.ViewerRangeMax = request.ViewerRangeMax;
adminAuditService.AddEntry(
session.TwitchUserId,
"category.update",
"category",
category.Id.ToString(),
$"Kategorie {request.Name.Trim()} wurde aktualisiert.",
new { request.GroupName, request.SortOrder, request.MaxNomineesPerUser },
RequestMetadataReader.Read(context));
await db.SaveChangesAsync(context.RequestAborted);
return Results.Ok(new { saved = true, categoryId = category.Id });
}
private static async Task<IResult> DeleteCategory(
HttpContext context,
int categoryId,
AwardsDbContext db,
IAdminAuditService adminAuditService)
{
var session = AdminEndpointConventions.CurrentSession(context);
var category = await db.Categories.FirstOrDefaultAsync(item => item.Id == categoryId);
if (category is null)
{
return Results.NotFound();
}
var candidates = await db.Candidates.Where(item => item.CategoryId == categoryId).ToArrayAsync();
if (candidates.Length > 0)
{
db.Candidates.RemoveRange(candidates);
}
db.Categories.Remove(category);
adminAuditService.AddEntry(
session.TwitchUserId,
"category.delete",
"category",
category.Id.ToString(),
$"Kategorie {category.Name} wurde gelöscht.",
new { removedCandidates = candidates.Length },
RequestMetadataReader.Read(context));
await db.SaveChangesAsync(context.RequestAborted);
return Results.Ok(new { deleted = true, categoryId });
}
}