Competition/Round architecture: full platform rewrite (Phases 1-9)
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m45s
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m45s
Replace Pipeline/Stage system with Competition/Round architecture. New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy, ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow. New services: round-engine, round-assignment, deliberation, result-lock, submission-manager, competition-context, ai-prompt-guard. Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with structured prompts, retry logic, and injection detection. All legacy pipeline/stage code removed. 4 new migrations + seed aligned. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -77,7 +77,7 @@ function ReportsOverview() {
|
||||
}
|
||||
|
||||
const scopeInput = parseSelection(selectedValue)
|
||||
const hasScope = !!scopeInput.stageId || !!scopeInput.programId
|
||||
const hasScope = !!scopeInput.roundId || !!scopeInput.programId
|
||||
|
||||
const { data: projectRankings, isLoading: projectsLoading } =
|
||||
trpc.analytics.getProjectRankings.useQuery(
|
||||
@@ -107,7 +107,7 @@ function ReportsOverview() {
|
||||
|
||||
const totalPrograms = dashStats?.programCount ?? programs?.length ?? 0
|
||||
const totalProjects = dashStats?.projectCount ?? 0
|
||||
const activeRounds = dashStats?.activeStageCount ?? rounds.filter((r: { status: string }) => r.status === 'STAGE_ACTIVE').length
|
||||
const activeRounds = dashStats?.activeRoundCount ?? rounds.filter((r: { status: string }) => r.status === 'ROUND_ACTIVE').length
|
||||
const jurorCount = dashStats?.jurorCount ?? 0
|
||||
const submittedEvaluations = dashStats?.submittedEvaluations ?? 0
|
||||
const totalEvaluations = dashStats?.totalEvaluations ?? 0
|
||||
@@ -400,11 +400,11 @@ function ReportsOverview() {
|
||||
)
|
||||
}
|
||||
|
||||
// Parse selection value: "all:programId" for edition-wide, or stageId
|
||||
function parseSelection(value: string | null): { stageId?: string; programId?: string } {
|
||||
// Parse selection value: "all:programId" for edition-wide, or roundId
|
||||
function parseSelection(value: string | null): { roundId?: string; programId?: string } {
|
||||
if (!value) return {}
|
||||
if (value.startsWith('all:')) return { programId: value.slice(4) }
|
||||
return { stageId: value }
|
||||
return { roundId: value }
|
||||
}
|
||||
|
||||
function StageAnalytics() {
|
||||
@@ -423,7 +423,7 @@ function StageAnalytics() {
|
||||
}
|
||||
|
||||
const queryInput = parseSelection(selectedValue)
|
||||
const hasSelection = !!queryInput.stageId || !!queryInput.programId
|
||||
const hasSelection = !!queryInput.roundId || !!queryInput.programId
|
||||
|
||||
const { data: scoreDistribution, isLoading: scoreLoading } =
|
||||
trpc.analytics.getScoreDistribution.useQuery(
|
||||
@@ -464,11 +464,11 @@ function StageAnalytics() {
|
||||
const selectedRound = rounds.find((r) => r.id === selectedValue)
|
||||
const geoInput = queryInput.programId
|
||||
? { programId: queryInput.programId }
|
||||
: { programId: selectedRound?.programId || '', stageId: queryInput.stageId }
|
||||
: { programId: selectedRound?.programId || '', roundId: queryInput.roundId }
|
||||
const { data: geoData, isLoading: geoLoading } =
|
||||
trpc.analytics.getGeographicDistribution.useQuery(
|
||||
geoInput,
|
||||
{ enabled: hasSelection && !!(geoInput.programId || geoInput.stageId) }
|
||||
{ enabled: hasSelection && !!(geoInput.programId || geoInput.roundId) }
|
||||
)
|
||||
|
||||
if (roundsLoading) {
|
||||
@@ -613,19 +613,19 @@ function CrossStageTab() {
|
||||
((p.stages ?? []) as Array<{ id: string; name: string }>).map((s: { id: string; name: string }) => ({ id: s.id, name: s.name, programName: `${p.year} Edition` }))
|
||||
) || []
|
||||
|
||||
const [selectedStageIds, setSelectedStageIds] = useState<string[]>([])
|
||||
const [selectedRoundIds, setSelectedRoundIds] = useState<string[]>([])
|
||||
|
||||
const { data: comparison, isLoading: comparisonLoading } =
|
||||
trpc.analytics.getCrossStageComparison.useQuery(
|
||||
{ stageIds: selectedStageIds },
|
||||
{ enabled: selectedStageIds.length >= 2 }
|
||||
trpc.analytics.getCrossRoundComparison.useQuery(
|
||||
{ roundIds: selectedRoundIds },
|
||||
{ enabled: selectedRoundIds.length >= 2 }
|
||||
)
|
||||
|
||||
const toggleStage = (stageId: string) => {
|
||||
setSelectedStageIds((prev) =>
|
||||
prev.includes(stageId)
|
||||
? prev.filter((id) => id !== stageId)
|
||||
: [...prev, stageId]
|
||||
const toggleRound = (roundId: string) => {
|
||||
setSelectedRoundIds((prev) =>
|
||||
prev.includes(roundId)
|
||||
? prev.filter((id) => id !== roundId)
|
||||
: [...prev, roundId]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -646,20 +646,20 @@ function CrossStageTab() {
|
||||
<CardContent>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{stages.map((stage) => {
|
||||
const isSelected = selectedStageIds.includes(stage.id)
|
||||
const isSelected = selectedRoundIds.includes(stage.id)
|
||||
return (
|
||||
<Badge
|
||||
key={stage.id}
|
||||
variant={isSelected ? 'default' : 'outline'}
|
||||
className="cursor-pointer text-sm py-1.5 px-3"
|
||||
onClick={() => toggleStage(stage.id)}
|
||||
onClick={() => toggleRound(stage.id)}
|
||||
>
|
||||
{stage.programName} - {stage.name}
|
||||
</Badge>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{selectedStageIds.length < 2 && (
|
||||
{selectedRoundIds.length < 2 && (
|
||||
<p className="text-sm text-muted-foreground mt-3">
|
||||
Select at least 2 stages to enable comparison
|
||||
</p>
|
||||
@@ -668,7 +668,7 @@ function CrossStageTab() {
|
||||
</Card>
|
||||
|
||||
{/* Comparison charts */}
|
||||
{comparisonLoading && selectedStageIds.length >= 2 && (
|
||||
{comparisonLoading && selectedRoundIds.length >= 2 && (
|
||||
<div className="space-y-6">
|
||||
<Skeleton className="h-[350px]" />
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
@@ -680,8 +680,8 @@ function CrossStageTab() {
|
||||
|
||||
{comparison && (
|
||||
<CrossStageComparisonChart data={comparison as Array<{
|
||||
stageId: string
|
||||
stageName: string
|
||||
roundId: string
|
||||
roundName: string
|
||||
projectCount: number
|
||||
evaluationCount: number
|
||||
completionRate: number
|
||||
@@ -707,7 +707,7 @@ function JurorConsistencyTab() {
|
||||
}
|
||||
|
||||
const queryInput = parseSelection(selectedValue)
|
||||
const hasSelection = !!queryInput.stageId || !!queryInput.programId
|
||||
const hasSelection = !!queryInput.roundId || !!queryInput.programId
|
||||
|
||||
const { data: consistency, isLoading: consistencyLoading } =
|
||||
trpc.analytics.getJurorConsistency.useQuery(
|
||||
@@ -779,7 +779,7 @@ function DiversityTab() {
|
||||
}
|
||||
|
||||
const queryInput = parseSelection(selectedValue)
|
||||
const hasSelection = !!queryInput.stageId || !!queryInput.programId
|
||||
const hasSelection = !!queryInput.roundId || !!queryInput.programId
|
||||
|
||||
const { data: diversity, isLoading: diversityLoading } =
|
||||
trpc.analytics.getDiversityMetrics.useQuery(
|
||||
@@ -882,7 +882,7 @@ export default function ReportsPage() {
|
||||
<TabsTrigger value="pipeline" className="gap-2" asChild>
|
||||
<Link href={"/admin/reports/stages" as Route}>
|
||||
<Layers className="h-4 w-4" />
|
||||
By Pipeline
|
||||
By Round
|
||||
</Link>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
@@ -901,7 +901,7 @@ export default function ReportsPage() {
|
||||
</Select>
|
||||
{pdfStageId && (
|
||||
<ExportPdfButton
|
||||
stageId={pdfStageId}
|
||||
roundId={pdfStageId}
|
||||
roundName={selectedPdfStage?.name}
|
||||
programName={selectedPdfStage?.programName}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user