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>
159 lines
5.9 KiB
C#
159 lines
5.9 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> SetResult(
|
|
HttpContext context,
|
|
int seasonId,
|
|
SetAwardResultRequest request,
|
|
AwardsDbContext db,
|
|
IAdminAuditService adminAuditService)
|
|
{
|
|
var session = AdminEndpointConventions.CurrentSession(context);
|
|
var category = await db.Categories
|
|
.AsNoTracking()
|
|
.FirstOrDefaultAsync(item => item.Id == request.CategoryId && item.SeasonId == seasonId);
|
|
if (category is null)
|
|
{
|
|
return Results.BadRequest(new { message = "The selected category does not exist in this season." });
|
|
}
|
|
|
|
var candidate = await db.Candidates
|
|
.AsNoTracking()
|
|
.FirstOrDefaultAsync(item =>
|
|
item.Id == request.CandidateId
|
|
&& item.SeasonId == seasonId
|
|
&& item.CategoryId == request.CategoryId);
|
|
if (candidate is null)
|
|
{
|
|
return Results.BadRequest(new { message = "The selected candidate does not belong to the selected category." });
|
|
}
|
|
|
|
var workflowRules = await LoadWorkflowRulesAsync(db, seasonId, context.RequestAborted);
|
|
var winnerRequiresClipRule = WorkflowRuleSettings.Find(workflowRules, WorkflowRuleSettings.WinnerRequiresClip);
|
|
if (WorkflowRuleSettings.ShouldBlock(winnerRequiresClipRule)
|
|
&& string.IsNullOrWhiteSpace(candidate.ClipCompilationUrl))
|
|
{
|
|
return CreateWorkflowRuleError(
|
|
"Dieser Kandidat hat noch keinen gepflegten Clip-Link. Bitte zuerst die Clip-Compilation am Kandidaten hinterlegen oder die Workflow-Regel umstellen.");
|
|
}
|
|
|
|
var winnerPlacementsRule = WorkflowRuleSettings.Find(workflowRules, WorkflowRuleSettings.MaxWinnerPlacements);
|
|
if (WorkflowRuleSettings.ShouldBlock(winnerPlacementsRule))
|
|
{
|
|
var candidateIdentityKey = WorkflowRuleSettings.CandidateIdentityKey(candidate);
|
|
var existingWinnerIdentities = await db.Results
|
|
.AsNoTracking()
|
|
.Include(item => item.Candidate)
|
|
.Where(item => item.SeasonId == seasonId && item.CategoryId != request.CategoryId)
|
|
.Select(item => new
|
|
{
|
|
item.CategoryId,
|
|
item.Candidate.StreamerIdentityId,
|
|
item.Candidate.DisplayName,
|
|
item.Candidate.ChannelSlug,
|
|
})
|
|
.ToArrayAsync(context.RequestAborted);
|
|
var existingWinnerCount = existingWinnerIdentities.Count(item =>
|
|
candidate.StreamerIdentityId.HasValue && item.StreamerIdentityId == candidate.StreamerIdentityId
|
|
||
|
|
string.Equals(WorkflowRuleSettings.CandidateIdentityKey(item.DisplayName, item.ChannelSlug), candidateIdentityKey, StringComparison.Ordinal));
|
|
|
|
if (existingWinnerCount >= winnerPlacementsRule.Limit)
|
|
{
|
|
return CreateWorkflowRuleError(
|
|
$"Diese Person hat bereits {existingWinnerCount} Gewinnerplatz(e). Limit: {winnerPlacementsRule.Limit}.");
|
|
}
|
|
}
|
|
|
|
var existingResult = await db.Results.FirstOrDefaultAsync(item =>
|
|
item.SeasonId == seasonId
|
|
&& item.CategoryId == request.CategoryId);
|
|
|
|
if (existingResult is null)
|
|
{
|
|
existingResult = new AwardResult
|
|
{
|
|
SeasonId = seasonId,
|
|
CategoryId = request.CategoryId,
|
|
CandidateId = request.CandidateId,
|
|
CategoryName = category.Name,
|
|
};
|
|
db.Results.Add(existingResult);
|
|
}
|
|
else
|
|
{
|
|
existingResult.CandidateId = request.CandidateId;
|
|
existingResult.CategoryName = category.Name;
|
|
}
|
|
|
|
adminAuditService.AddEntry(
|
|
session.TwitchUserId,
|
|
"result.set",
|
|
"result",
|
|
$"{seasonId}:{request.CategoryId}",
|
|
$"Gewinner für {category.Name} wurde gesetzt.",
|
|
new
|
|
{
|
|
seasonId,
|
|
categoryId = request.CategoryId,
|
|
candidateId = request.CandidateId,
|
|
candidateName = candidate.DisplayName,
|
|
},
|
|
RequestMetadataReader.Read(context));
|
|
|
|
await db.SaveChangesAsync(context.RequestAborted);
|
|
return Results.Ok(new
|
|
{
|
|
saved = true,
|
|
resultId = existingResult.Id,
|
|
seasonId,
|
|
categoryId = request.CategoryId,
|
|
candidateId = request.CandidateId,
|
|
});
|
|
}
|
|
|
|
private static async Task<IResult> DeleteResult(
|
|
HttpContext context,
|
|
int resultId,
|
|
AwardsDbContext db,
|
|
IAdminAuditService adminAuditService)
|
|
{
|
|
var session = AdminEndpointConventions.CurrentSession(context);
|
|
var result = await db.Results
|
|
.Include(item => item.Category)
|
|
.Include(item => item.Candidate)
|
|
.FirstOrDefaultAsync(item => item.Id == resultId);
|
|
if (result is null)
|
|
{
|
|
return Results.NotFound();
|
|
}
|
|
|
|
db.Results.Remove(result);
|
|
adminAuditService.AddEntry(
|
|
session.TwitchUserId,
|
|
"result.delete",
|
|
"result",
|
|
result.Id.ToString(),
|
|
$"Gewinner für {result.Category.Name} wurde entfernt.",
|
|
new
|
|
{
|
|
result.SeasonId,
|
|
result.CategoryId,
|
|
result.CandidateId,
|
|
candidateName = result.Candidate.DisplayName,
|
|
},
|
|
RequestMetadataReader.Read(context));
|
|
|
|
await db.SaveChangesAsync(context.RequestAborted);
|
|
return Results.Ok(new { deleted = true, resultId });
|
|
}
|
|
}
|