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>
151 lines
5.3 KiB
C#
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 });
|
|
}
|
|
}
|