2026-02-14 15:26:42 +01:00
|
|
|
import { z } from 'zod'
|
|
|
|
|
import { router, observerProcedure } from '../trpc'
|
|
|
|
|
import { normalizeCountryToCode } from '@/lib/countries'
|
2026-02-20 18:39:53 +01:00
|
|
|
import { getUserAvatarUrl } from '../utils/avatar-url'
|
2026-02-14 15:26:42 +01:00
|
|
|
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
const editionOrRoundInput = z.object({
|
|
|
|
|
roundId: z.string().optional(),
|
2026-02-14 15:26:42 +01:00
|
|
|
programId: z.string().optional(),
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
}).refine(data => data.roundId || data.programId, {
|
|
|
|
|
message: 'Either roundId or programId is required',
|
2026-02-14 15:26:42 +01:00
|
|
|
})
|
|
|
|
|
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
function projectWhere(input: { roundId?: string; programId?: string }) {
|
2026-02-19 09:56:09 +01:00
|
|
|
if (input.roundId) return { projectRoundStates: { some: { roundId: input.roundId } } }
|
2026-02-14 15:26:42 +01:00
|
|
|
return { programId: input.programId! }
|
|
|
|
|
}
|
|
|
|
|
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
function assignmentWhere(input: { roundId?: string; programId?: string }) {
|
|
|
|
|
if (input.roundId) return { roundId: input.roundId }
|
|
|
|
|
return { round: { competition: { programId: input.programId! } } }
|
2026-02-14 15:26:42 +01:00
|
|
|
}
|
|
|
|
|
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
function evalWhere(input: { roundId?: string; programId?: string }, extra: Record<string, unknown> = {}) {
|
|
|
|
|
const base = input.roundId
|
|
|
|
|
? { assignment: { roundId: input.roundId } }
|
|
|
|
|
: { assignment: { round: { competition: { programId: input.programId! } } } }
|
2026-02-14 15:26:42 +01:00
|
|
|
return { ...base, ...extra }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const analyticsRouter = router({
|
|
|
|
|
/**
|
|
|
|
|
* Get score distribution (histogram data)
|
|
|
|
|
*/
|
|
|
|
|
getScoreDistribution: observerProcedure
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
.input(editionOrRoundInput)
|
2026-02-14 15:26:42 +01:00
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
const evaluations = await ctx.prisma.evaluation.findMany({
|
|
|
|
|
where: evalWhere(input, { status: 'SUBMITTED' }),
|
|
|
|
|
select: {
|
|
|
|
|
criterionScoresJson: true,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Extract all scores and calculate distribution
|
|
|
|
|
const allScores: number[] = []
|
|
|
|
|
evaluations.forEach((evaluation) => {
|
|
|
|
|
const scores = evaluation.criterionScoresJson as Record<string, number> | null
|
|
|
|
|
if (scores) {
|
|
|
|
|
Object.values(scores).forEach((score) => {
|
|
|
|
|
if (typeof score === 'number') {
|
|
|
|
|
allScores.push(score)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Count scores by bucket (1-10)
|
|
|
|
|
const distribution = Array.from({ length: 10 }, (_, i) => ({
|
|
|
|
|
score: i + 1,
|
|
|
|
|
count: allScores.filter((s) => Math.round(s) === i + 1).length,
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
distribution,
|
|
|
|
|
totalScores: allScores.length,
|
|
|
|
|
averageScore:
|
|
|
|
|
allScores.length > 0
|
|
|
|
|
? allScores.reduce((a, b) => a + b, 0) / allScores.length
|
|
|
|
|
: 0,
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get evaluation completion over time (timeline data)
|
|
|
|
|
*/
|
|
|
|
|
getEvaluationTimeline: observerProcedure
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
.input(editionOrRoundInput)
|
2026-02-14 15:26:42 +01:00
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
const evaluations = await ctx.prisma.evaluation.findMany({
|
|
|
|
|
where: evalWhere(input, { status: 'SUBMITTED' }),
|
|
|
|
|
select: {
|
|
|
|
|
submittedAt: true,
|
|
|
|
|
},
|
|
|
|
|
orderBy: { submittedAt: 'asc' },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Group by date
|
|
|
|
|
const byDate: Record<string, number> = {}
|
|
|
|
|
let cumulative = 0
|
|
|
|
|
|
|
|
|
|
evaluations.forEach((evaluation) => {
|
|
|
|
|
if (evaluation.submittedAt) {
|
|
|
|
|
const date = evaluation.submittedAt.toISOString().split('T')[0]
|
|
|
|
|
if (!byDate[date]) {
|
|
|
|
|
byDate[date] = 0
|
|
|
|
|
}
|
|
|
|
|
byDate[date]++
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Convert to cumulative timeline
|
|
|
|
|
const timeline = Object.entries(byDate)
|
|
|
|
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
|
|
|
.map(([date, count]) => {
|
|
|
|
|
cumulative += count
|
|
|
|
|
return {
|
|
|
|
|
date,
|
|
|
|
|
daily: count,
|
|
|
|
|
cumulative,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return timeline
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get juror workload distribution
|
|
|
|
|
*/
|
|
|
|
|
getJurorWorkload: observerProcedure
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
.input(editionOrRoundInput)
|
2026-02-14 15:26:42 +01:00
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
const assignments = await ctx.prisma.assignment.findMany({
|
|
|
|
|
where: assignmentWhere(input),
|
|
|
|
|
include: {
|
Observer platform overhaul: Nivo charts, round-type stats, UX improvements
Phase 1: Fix 6 backend data bugs in analytics.ts (roundName filtering,
unscored projects, criteria scores, activeRoundCount scoping, email
privacy leaks in juror consistency + workload)
Phase 2-3: Migrate all 9 chart components from Recharts to Nivo
(@nivo/bar, @nivo/line, @nivo/pie, @nivo/scatterplot) with shared brand
theme, scoreGradient colors, and STATUS_COLORS map. Fixes scatter plot
outlier coloring and pie chart label visibility bugs.
Phase 4: Add round-type-aware stats (getRoundTypeStats backend +
RoundTypeStatsCards component) showing appropriate metrics per round
type (intake/filtering/evaluation/submission/mentoring/live/deliberation).
Phase 5: UX improvements — Stage→Round terminology, clickable dashboard
round links, URL-based round selection (?round=), round type indicators
in selectors, accessible Toggle-based cross-round comparison, sortable
project table columns (title/score/evaluations), brand score colors on
dashboard bar chart with aria labels.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 21:44:38 +01:00
|
|
|
user: { select: { name: true } },
|
2026-02-14 15:26:42 +01:00
|
|
|
evaluation: {
|
|
|
|
|
select: { id: true, status: true },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Group by user
|
|
|
|
|
const byUser: Record<
|
|
|
|
|
string,
|
|
|
|
|
{ name: string; assigned: number; completed: number }
|
|
|
|
|
> = {}
|
|
|
|
|
|
|
|
|
|
assignments.forEach((assignment) => {
|
|
|
|
|
const userId = assignment.userId
|
|
|
|
|
if (!byUser[userId]) {
|
|
|
|
|
byUser[userId] = {
|
Observer platform overhaul: Nivo charts, round-type stats, UX improvements
Phase 1: Fix 6 backend data bugs in analytics.ts (roundName filtering,
unscored projects, criteria scores, activeRoundCount scoping, email
privacy leaks in juror consistency + workload)
Phase 2-3: Migrate all 9 chart components from Recharts to Nivo
(@nivo/bar, @nivo/line, @nivo/pie, @nivo/scatterplot) with shared brand
theme, scoreGradient colors, and STATUS_COLORS map. Fixes scatter plot
outlier coloring and pie chart label visibility bugs.
Phase 4: Add round-type-aware stats (getRoundTypeStats backend +
RoundTypeStatsCards component) showing appropriate metrics per round
type (intake/filtering/evaluation/submission/mentoring/live/deliberation).
Phase 5: UX improvements — Stage→Round terminology, clickable dashboard
round links, URL-based round selection (?round=), round type indicators
in selectors, accessible Toggle-based cross-round comparison, sortable
project table columns (title/score/evaluations), brand score colors on
dashboard bar chart with aria labels.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 21:44:38 +01:00
|
|
|
name: assignment.user.name || 'Unknown',
|
2026-02-14 15:26:42 +01:00
|
|
|
assigned: 0,
|
|
|
|
|
completed: 0,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
byUser[userId].assigned++
|
|
|
|
|
if (assignment.evaluation?.status === 'SUBMITTED') {
|
|
|
|
|
byUser[userId].completed++
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return Object.entries(byUser)
|
|
|
|
|
.map(([id, data]) => ({
|
|
|
|
|
id,
|
|
|
|
|
...data,
|
|
|
|
|
completionRate:
|
|
|
|
|
data.assigned > 0
|
|
|
|
|
? Math.round((data.completed / data.assigned) * 100)
|
|
|
|
|
: 0,
|
|
|
|
|
}))
|
|
|
|
|
.sort((a, b) => b.assigned - a.assigned)
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get project rankings with average scores
|
|
|
|
|
*/
|
|
|
|
|
getProjectRankings: observerProcedure
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
.input(editionOrRoundInput.and(z.object({ limit: z.number().optional() })))
|
2026-02-14 15:26:42 +01:00
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
const projects = await ctx.prisma.project.findMany({
|
|
|
|
|
where: projectWhere(input),
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
title: true,
|
|
|
|
|
teamName: true,
|
|
|
|
|
status: true,
|
|
|
|
|
assignments: {
|
|
|
|
|
select: {
|
|
|
|
|
evaluation: {
|
|
|
|
|
select: { criterionScoresJson: true, status: true },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Calculate average scores
|
|
|
|
|
const rankings = projects
|
|
|
|
|
.map((project) => {
|
|
|
|
|
const allScores: number[] = []
|
|
|
|
|
|
|
|
|
|
project.assignments.forEach((assignment) => {
|
|
|
|
|
const evaluation = assignment.evaluation
|
|
|
|
|
if (evaluation?.status === 'SUBMITTED') {
|
|
|
|
|
const scores = evaluation.criterionScoresJson as Record<
|
|
|
|
|
string,
|
|
|
|
|
number
|
|
|
|
|
> | null
|
|
|
|
|
if (scores) {
|
|
|
|
|
const scoreValues = Object.values(scores).filter(
|
|
|
|
|
(s): s is number => typeof s === 'number'
|
|
|
|
|
)
|
|
|
|
|
if (scoreValues.length > 0) {
|
|
|
|
|
const average =
|
|
|
|
|
scoreValues.reduce((a, b) => a + b, 0) / scoreValues.length
|
|
|
|
|
allScores.push(average)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const averageScore =
|
|
|
|
|
allScores.length > 0
|
|
|
|
|
? allScores.reduce((a, b) => a + b, 0) / allScores.length
|
|
|
|
|
: null
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: project.id,
|
|
|
|
|
title: project.title,
|
|
|
|
|
teamName: project.teamName,
|
|
|
|
|
status: project.status,
|
|
|
|
|
averageScore,
|
|
|
|
|
evaluationCount: allScores.length,
|
|
|
|
|
}
|
|
|
|
|
})
|
2026-02-19 09:56:09 +01:00
|
|
|
.sort((a, b) => {
|
|
|
|
|
// Evaluated projects first (sorted by score desc), unevaluated at bottom
|
|
|
|
|
if (a.averageScore !== null && b.averageScore !== null) return b.averageScore - a.averageScore
|
|
|
|
|
if (a.averageScore !== null) return -1
|
|
|
|
|
if (b.averageScore !== null) return 1
|
|
|
|
|
return 0
|
|
|
|
|
})
|
2026-02-14 15:26:42 +01:00
|
|
|
|
|
|
|
|
return input.limit ? rankings.slice(0, input.limit) : rankings
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get status breakdown (pie chart data)
|
|
|
|
|
*/
|
|
|
|
|
getStatusBreakdown: observerProcedure
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
.input(editionOrRoundInput)
|
2026-02-14 15:26:42 +01:00
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
const projects = await ctx.prisma.project.groupBy({
|
|
|
|
|
by: ['status'],
|
|
|
|
|
where: projectWhere(input),
|
|
|
|
|
_count: true,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return projects.map((p) => ({
|
|
|
|
|
status: p.status,
|
|
|
|
|
count: p._count,
|
|
|
|
|
}))
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get overview stats for dashboard
|
|
|
|
|
*/
|
|
|
|
|
getOverviewStats: observerProcedure
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
.input(editionOrRoundInput)
|
2026-02-14 15:26:42 +01:00
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
const [
|
|
|
|
|
projectCount,
|
|
|
|
|
assignmentCount,
|
|
|
|
|
evaluationCount,
|
|
|
|
|
jurorCount,
|
|
|
|
|
statusCounts,
|
|
|
|
|
] = await Promise.all([
|
|
|
|
|
ctx.prisma.project.count({ where: projectWhere(input) }),
|
|
|
|
|
ctx.prisma.assignment.count({ where: assignmentWhere(input) }),
|
|
|
|
|
ctx.prisma.evaluation.count({
|
|
|
|
|
where: evalWhere(input, { status: 'SUBMITTED' }),
|
|
|
|
|
}),
|
|
|
|
|
ctx.prisma.assignment.groupBy({
|
|
|
|
|
by: ['userId'],
|
|
|
|
|
where: assignmentWhere(input),
|
|
|
|
|
}),
|
|
|
|
|
ctx.prisma.project.groupBy({
|
|
|
|
|
by: ['status'],
|
|
|
|
|
where: projectWhere(input),
|
|
|
|
|
_count: true,
|
|
|
|
|
}),
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
const completionRate =
|
|
|
|
|
assignmentCount > 0
|
|
|
|
|
? Math.round((evaluationCount / assignmentCount) * 100)
|
|
|
|
|
: 0
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
projectCount,
|
|
|
|
|
assignmentCount,
|
|
|
|
|
evaluationCount,
|
|
|
|
|
jurorCount: jurorCount.length,
|
|
|
|
|
completionRate,
|
|
|
|
|
statusBreakdown: statusCounts.map((s) => ({
|
|
|
|
|
status: s.status,
|
|
|
|
|
count: s._count,
|
|
|
|
|
})),
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get criteria-level score distribution
|
|
|
|
|
*/
|
|
|
|
|
getCriteriaScores: observerProcedure
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
.input(editionOrRoundInput)
|
2026-02-14 15:26:42 +01:00
|
|
|
.query(async ({ ctx, input }) => {
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
const formWhere = input.roundId
|
|
|
|
|
? { roundId: input.roundId, isActive: true }
|
|
|
|
|
: { round: { competition: { programId: input.programId! } }, isActive: true }
|
2026-02-14 15:26:42 +01:00
|
|
|
|
|
|
|
|
const evaluationForms = await ctx.prisma.evaluationForm.findMany({
|
|
|
|
|
where: formWhere,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (!evaluationForms.length) {
|
|
|
|
|
return []
|
|
|
|
|
}
|
|
|
|
|
|
Observer platform overhaul: Nivo charts, round-type stats, UX improvements
Phase 1: Fix 6 backend data bugs in analytics.ts (roundName filtering,
unscored projects, criteria scores, activeRoundCount scoping, email
privacy leaks in juror consistency + workload)
Phase 2-3: Migrate all 9 chart components from Recharts to Nivo
(@nivo/bar, @nivo/line, @nivo/pie, @nivo/scatterplot) with shared brand
theme, scoreGradient colors, and STATUS_COLORS map. Fixes scatter plot
outlier coloring and pie chart label visibility bugs.
Phase 4: Add round-type-aware stats (getRoundTypeStats backend +
RoundTypeStatsCards component) showing appropriate metrics per round
type (intake/filtering/evaluation/submission/mentoring/live/deliberation).
Phase 5: UX improvements — Stage→Round terminology, clickable dashboard
round links, URL-based round selection (?round=), round type indicators
in selectors, accessible Toggle-based cross-round comparison, sortable
project table columns (title/score/evaluations), brand score colors on
dashboard bar chart with aria labels.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 21:44:38 +01:00
|
|
|
// Build label → Set<id> map so program-level queries match all IDs for the same criterion label
|
|
|
|
|
const labelToIds = new Map<string, Set<string>>()
|
|
|
|
|
const labelToFirst = new Map<string, { id: string; label: string }>()
|
2026-02-14 15:26:42 +01:00
|
|
|
evaluationForms.forEach((form) => {
|
|
|
|
|
const criteria = form.criteriaJson as Array<{ id: string; label: string }> | null
|
|
|
|
|
if (criteria) {
|
|
|
|
|
criteria.forEach((c) => {
|
Observer platform overhaul: Nivo charts, round-type stats, UX improvements
Phase 1: Fix 6 backend data bugs in analytics.ts (roundName filtering,
unscored projects, criteria scores, activeRoundCount scoping, email
privacy leaks in juror consistency + workload)
Phase 2-3: Migrate all 9 chart components from Recharts to Nivo
(@nivo/bar, @nivo/line, @nivo/pie, @nivo/scatterplot) with shared brand
theme, scoreGradient colors, and STATUS_COLORS map. Fixes scatter plot
outlier coloring and pie chart label visibility bugs.
Phase 4: Add round-type-aware stats (getRoundTypeStats backend +
RoundTypeStatsCards component) showing appropriate metrics per round
type (intake/filtering/evaluation/submission/mentoring/live/deliberation).
Phase 5: UX improvements — Stage→Round terminology, clickable dashboard
round links, URL-based round selection (?round=), round type indicators
in selectors, accessible Toggle-based cross-round comparison, sortable
project table columns (title/score/evaluations), brand score colors on
dashboard bar chart with aria labels.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 21:44:38 +01:00
|
|
|
if (!labelToIds.has(c.label)) {
|
|
|
|
|
labelToIds.set(c.label, new Set())
|
|
|
|
|
labelToFirst.set(c.label, c)
|
2026-02-14 15:26:42 +01:00
|
|
|
}
|
Observer platform overhaul: Nivo charts, round-type stats, UX improvements
Phase 1: Fix 6 backend data bugs in analytics.ts (roundName filtering,
unscored projects, criteria scores, activeRoundCount scoping, email
privacy leaks in juror consistency + workload)
Phase 2-3: Migrate all 9 chart components from Recharts to Nivo
(@nivo/bar, @nivo/line, @nivo/pie, @nivo/scatterplot) with shared brand
theme, scoreGradient colors, and STATUS_COLORS map. Fixes scatter plot
outlier coloring and pie chart label visibility bugs.
Phase 4: Add round-type-aware stats (getRoundTypeStats backend +
RoundTypeStatsCards component) showing appropriate metrics per round
type (intake/filtering/evaluation/submission/mentoring/live/deliberation).
Phase 5: UX improvements — Stage→Round terminology, clickable dashboard
round links, URL-based round selection (?round=), round type indicators
in selectors, accessible Toggle-based cross-round comparison, sortable
project table columns (title/score/evaluations), brand score colors on
dashboard bar chart with aria labels.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 21:44:38 +01:00
|
|
|
labelToIds.get(c.label)!.add(c.id)
|
2026-02-14 15:26:42 +01:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
Observer platform overhaul: Nivo charts, round-type stats, UX improvements
Phase 1: Fix 6 backend data bugs in analytics.ts (roundName filtering,
unscored projects, criteria scores, activeRoundCount scoping, email
privacy leaks in juror consistency + workload)
Phase 2-3: Migrate all 9 chart components from Recharts to Nivo
(@nivo/bar, @nivo/line, @nivo/pie, @nivo/scatterplot) with shared brand
theme, scoreGradient colors, and STATUS_COLORS map. Fixes scatter plot
outlier coloring and pie chart label visibility bugs.
Phase 4: Add round-type-aware stats (getRoundTypeStats backend +
RoundTypeStatsCards component) showing appropriate metrics per round
type (intake/filtering/evaluation/submission/mentoring/live/deliberation).
Phase 5: UX improvements — Stage→Round terminology, clickable dashboard
round links, URL-based round selection (?round=), round type indicators
in selectors, accessible Toggle-based cross-round comparison, sortable
project table columns (title/score/evaluations), brand score colors on
dashboard bar chart with aria labels.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 21:44:38 +01:00
|
|
|
const criteriaLabels = Array.from(labelToFirst.values())
|
|
|
|
|
if (criteriaLabels.length === 0) {
|
2026-02-14 15:26:42 +01:00
|
|
|
return []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get all evaluations
|
|
|
|
|
const evaluations = await ctx.prisma.evaluation.findMany({
|
|
|
|
|
where: evalWhere(input, { status: 'SUBMITTED' }),
|
|
|
|
|
select: { criterionScoresJson: true },
|
|
|
|
|
})
|
|
|
|
|
|
Observer platform overhaul: Nivo charts, round-type stats, UX improvements
Phase 1: Fix 6 backend data bugs in analytics.ts (roundName filtering,
unscored projects, criteria scores, activeRoundCount scoping, email
privacy leaks in juror consistency + workload)
Phase 2-3: Migrate all 9 chart components from Recharts to Nivo
(@nivo/bar, @nivo/line, @nivo/pie, @nivo/scatterplot) with shared brand
theme, scoreGradient colors, and STATUS_COLORS map. Fixes scatter plot
outlier coloring and pie chart label visibility bugs.
Phase 4: Add round-type-aware stats (getRoundTypeStats backend +
RoundTypeStatsCards component) showing appropriate metrics per round
type (intake/filtering/evaluation/submission/mentoring/live/deliberation).
Phase 5: UX improvements — Stage→Round terminology, clickable dashboard
round links, URL-based round selection (?round=), round type indicators
in selectors, accessible Toggle-based cross-round comparison, sortable
project table columns (title/score/evaluations), brand score colors on
dashboard bar chart with aria labels.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 21:44:38 +01:00
|
|
|
// Calculate average score per criterion, checking ALL IDs that share the same label
|
|
|
|
|
const criteriaScores = criteriaLabels.map((criterion) => {
|
2026-02-14 15:26:42 +01:00
|
|
|
const scores: number[] = []
|
Observer platform overhaul: Nivo charts, round-type stats, UX improvements
Phase 1: Fix 6 backend data bugs in analytics.ts (roundName filtering,
unscored projects, criteria scores, activeRoundCount scoping, email
privacy leaks in juror consistency + workload)
Phase 2-3: Migrate all 9 chart components from Recharts to Nivo
(@nivo/bar, @nivo/line, @nivo/pie, @nivo/scatterplot) with shared brand
theme, scoreGradient colors, and STATUS_COLORS map. Fixes scatter plot
outlier coloring and pie chart label visibility bugs.
Phase 4: Add round-type-aware stats (getRoundTypeStats backend +
RoundTypeStatsCards component) showing appropriate metrics per round
type (intake/filtering/evaluation/submission/mentoring/live/deliberation).
Phase 5: UX improvements — Stage→Round terminology, clickable dashboard
round links, URL-based round selection (?round=), round type indicators
in selectors, accessible Toggle-based cross-round comparison, sortable
project table columns (title/score/evaluations), brand score colors on
dashboard bar chart with aria labels.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 21:44:38 +01:00
|
|
|
const ids = labelToIds.get(criterion.label) ?? new Set([criterion.id])
|
2026-02-14 15:26:42 +01:00
|
|
|
|
|
|
|
|
evaluations.forEach((evaluation) => {
|
|
|
|
|
const criterionScoresJson = evaluation.criterionScoresJson as Record<
|
|
|
|
|
string,
|
|
|
|
|
number
|
|
|
|
|
> | null
|
Observer platform overhaul: Nivo charts, round-type stats, UX improvements
Phase 1: Fix 6 backend data bugs in analytics.ts (roundName filtering,
unscored projects, criteria scores, activeRoundCount scoping, email
privacy leaks in juror consistency + workload)
Phase 2-3: Migrate all 9 chart components from Recharts to Nivo
(@nivo/bar, @nivo/line, @nivo/pie, @nivo/scatterplot) with shared brand
theme, scoreGradient colors, and STATUS_COLORS map. Fixes scatter plot
outlier coloring and pie chart label visibility bugs.
Phase 4: Add round-type-aware stats (getRoundTypeStats backend +
RoundTypeStatsCards component) showing appropriate metrics per round
type (intake/filtering/evaluation/submission/mentoring/live/deliberation).
Phase 5: UX improvements — Stage→Round terminology, clickable dashboard
round links, URL-based round selection (?round=), round type indicators
in selectors, accessible Toggle-based cross-round comparison, sortable
project table columns (title/score/evaluations), brand score colors on
dashboard bar chart with aria labels.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 21:44:38 +01:00
|
|
|
if (criterionScoresJson) {
|
|
|
|
|
for (const cid of ids) {
|
|
|
|
|
if (typeof criterionScoresJson[cid] === 'number') {
|
|
|
|
|
scores.push(criterionScoresJson[cid])
|
|
|
|
|
break // Only count one score per evaluation per criterion
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-14 15:26:42 +01:00
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: criterion.id,
|
|
|
|
|
name: criterion.label,
|
|
|
|
|
averageScore:
|
|
|
|
|
scores.length > 0
|
|
|
|
|
? scores.reduce((a, b) => a + b, 0) / scores.length
|
|
|
|
|
: 0,
|
|
|
|
|
count: scores.length,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return criteriaScores
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get geographic distribution of projects by country
|
|
|
|
|
*/
|
|
|
|
|
getGeographicDistribution: observerProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
programId: z.string(),
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
roundId: z.string().optional(),
|
2026-02-14 15:26:42 +01:00
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.query(async ({ ctx, input }) => {
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
const where = input.roundId
|
|
|
|
|
? { assignments: { some: { roundId: input.roundId } } }
|
2026-02-14 15:26:42 +01:00
|
|
|
: { programId: input.programId }
|
|
|
|
|
|
|
|
|
|
const distribution = await ctx.prisma.project.groupBy({
|
|
|
|
|
by: ['country'],
|
|
|
|
|
where: { ...where, country: { not: null } },
|
|
|
|
|
_count: { id: true },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Resolve country names to ISO codes (DB may store "France" instead of "FR")
|
|
|
|
|
const codeMap = new Map<string, number>()
|
|
|
|
|
for (const d of distribution) {
|
|
|
|
|
const resolved = normalizeCountryToCode(d.country) ?? d.country ?? 'UNKNOWN'
|
|
|
|
|
codeMap.set(resolved, (codeMap.get(resolved) ?? 0) + d._count.id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Array.from(codeMap.entries()).map(([countryCode, count]) => ({
|
|
|
|
|
countryCode,
|
|
|
|
|
count,
|
|
|
|
|
}))
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
// Advanced Analytics (F10)
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
* Compare metrics across multiple rounds
|
2026-02-14 15:26:42 +01:00
|
|
|
*/
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
getCrossRoundComparison: observerProcedure
|
|
|
|
|
.input(z.object({ roundIds: z.array(z.string()).min(2) }))
|
2026-02-14 15:26:42 +01:00
|
|
|
.query(async ({ ctx, input }) => {
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
const { roundIds } = input
|
2026-02-14 15:26:42 +01:00
|
|
|
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
// Batch: fetch all rounds, assignments, and evaluations in 3 queries
|
|
|
|
|
const [rounds, assignments, evaluations] = await Promise.all([
|
|
|
|
|
ctx.prisma.round.findMany({
|
|
|
|
|
where: { id: { in: roundIds } },
|
|
|
|
|
select: { id: true, name: true },
|
|
|
|
|
}),
|
|
|
|
|
ctx.prisma.assignment.groupBy({
|
|
|
|
|
by: ['roundId'],
|
|
|
|
|
where: { roundId: { in: roundIds } },
|
|
|
|
|
_count: true,
|
|
|
|
|
}),
|
|
|
|
|
ctx.prisma.evaluation.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
assignment: { roundId: { in: roundIds } },
|
|
|
|
|
status: 'SUBMITTED',
|
|
|
|
|
},
|
|
|
|
|
select: { globalScore: true, assignment: { select: { roundId: true } } },
|
|
|
|
|
}),
|
|
|
|
|
])
|
2026-02-14 15:26:42 +01:00
|
|
|
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
const roundMap = new Map(rounds.map((r) => [r.id, r.name]))
|
|
|
|
|
const assignmentCountMap = new Map(assignments.map((a) => [a.roundId, a._count]))
|
2026-02-14 15:26:42 +01:00
|
|
|
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
// Group evaluations by round
|
|
|
|
|
const evalsByRound = new Map<string, number[]>()
|
|
|
|
|
const projectsByRound = new Map<string, Set<string>>()
|
|
|
|
|
for (const e of evaluations) {
|
|
|
|
|
const rid = e.assignment.roundId
|
|
|
|
|
if (!evalsByRound.has(rid)) evalsByRound.set(rid, [])
|
|
|
|
|
if (e.globalScore !== null) evalsByRound.get(rid)!.push(e.globalScore)
|
|
|
|
|
}
|
2026-02-14 15:26:42 +01:00
|
|
|
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
// Count distinct projects per round via assignments
|
|
|
|
|
const projectAssignments = await ctx.prisma.assignment.findMany({
|
|
|
|
|
where: { roundId: { in: roundIds } },
|
|
|
|
|
select: { roundId: true, projectId: true },
|
|
|
|
|
distinct: ['roundId', 'projectId'],
|
|
|
|
|
})
|
|
|
|
|
for (const pa of projectAssignments) {
|
|
|
|
|
if (!projectsByRound.has(pa.roundId)) projectsByRound.set(pa.roundId, new Set())
|
|
|
|
|
projectsByRound.get(pa.roundId)!.add(pa.projectId)
|
|
|
|
|
}
|
2026-02-14 15:26:42 +01:00
|
|
|
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
return roundIds.map((roundId) => {
|
|
|
|
|
const globalScores = evalsByRound.get(roundId) ?? []
|
|
|
|
|
const assignmentCount = assignmentCountMap.get(roundId) ?? 0
|
|
|
|
|
const evaluationCount = globalScores.length
|
|
|
|
|
const completionRate = assignmentCount > 0
|
|
|
|
|
? Math.round((evaluationCount / assignmentCount) * 100)
|
|
|
|
|
: 0
|
|
|
|
|
const averageScore = globalScores.length > 0
|
|
|
|
|
? globalScores.reduce((a, b) => a + b, 0) / globalScores.length
|
|
|
|
|
: null
|
|
|
|
|
const distribution = Array.from({ length: 10 }, (_, i) => ({
|
|
|
|
|
score: i + 1,
|
|
|
|
|
count: globalScores.filter((s) => Math.round(s) === i + 1).length,
|
|
|
|
|
}))
|
2026-02-14 15:26:42 +01:00
|
|
|
|
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code
Phase 1 — Critical bugs:
- Fix deliberation participant selection (wire jury group query)
- Fix reports "By Round" tab (inline content instead of 404 route)
- Fix messages "Sent History" (add message.sent procedure, wire tab)
- Add missing fields to competition award form (criteriaText, maxRankedPicks)
- Wire LiveControlPanel buttons (cursor, voting, scores)
- Fix ResultLockControls empty snapshot (fetch actual data before lock)
- Fix SubmissionWindowManager losing fields on edit
Phase 2 — Backend fixes:
- Remove write-in-query from specialAward.get
- Fix award eligibility job overwriting manual shortlist overrides
- Fix filtering startJob deleting all prior results (defer cleanup to post-success)
- Tighten access control: protectedProcedure → adminProcedure on 8 procedures
- Add audit logging to deliberation mutations
- Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete
Phase 3 — Auto-refresh:
- Add refetchInterval to 15+ admin pages/components (10s–30s)
- Fix AI job polling: derive speed from job status for all viewers
Phase 4 — Dead code cleanup:
- Delete unused command-palette, pdf-report, admin-page-transition
- Remove dead subItems sidebar code, unused GripVertical import
- Replace redundant isGenerating state with mutation.isPending
- Add Role column to jury members table
- Remove misleading manual mentor assignment stub
Phase 5 — UX improvements:
- Fix rounds page single-competition assumption (add selector)
- Remove raw UUID fallback in deliberation config
- Fix programs page "Stage" → "Round" terminology
Phase 6 — Backend hardening:
- Complete logAudit calls (add prisma, ipAddress, userAgent)
- Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear)
- Batch user.bulkCreate writes (assignments, jury memberships, intents)
- Remove any casts from deliberation service (typed PrismaClient + TransactionClient)
- Fix stale DeliberationStatus enum values blocking build
40 files changed, 1010 insertions(+), 612 deletions(-)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
|
|
|
return {
|
|
|
|
|
roundId,
|
|
|
|
|
roundName: roundMap.get(roundId) ?? roundId,
|
|
|
|
|
projectCount: projectsByRound.get(roundId)?.size ?? 0,
|
|
|
|
|
evaluationCount,
|
|
|
|
|
completionRate,
|
|
|
|
|
averageScore,
|
|
|
|
|
scoreDistribution: distribution,
|
|
|
|
|
}
|
|
|
|
|
})
|
2026-02-14 15:26:42 +01:00
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
* Get juror consistency metrics for a round
|
2026-02-14 15:26:42 +01:00
|
|
|
*/
|
|
|
|
|
getJurorConsistency: observerProcedure
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
.input(editionOrRoundInput)
|
2026-02-14 15:26:42 +01:00
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
const evaluations = await ctx.prisma.evaluation.findMany({
|
|
|
|
|
where: evalWhere(input, { status: 'SUBMITTED' }),
|
|
|
|
|
include: {
|
|
|
|
|
assignment: {
|
|
|
|
|
include: {
|
Observer platform overhaul: Nivo charts, round-type stats, UX improvements
Phase 1: Fix 6 backend data bugs in analytics.ts (roundName filtering,
unscored projects, criteria scores, activeRoundCount scoping, email
privacy leaks in juror consistency + workload)
Phase 2-3: Migrate all 9 chart components from Recharts to Nivo
(@nivo/bar, @nivo/line, @nivo/pie, @nivo/scatterplot) with shared brand
theme, scoreGradient colors, and STATUS_COLORS map. Fixes scatter plot
outlier coloring and pie chart label visibility bugs.
Phase 4: Add round-type-aware stats (getRoundTypeStats backend +
RoundTypeStatsCards component) showing appropriate metrics per round
type (intake/filtering/evaluation/submission/mentoring/live/deliberation).
Phase 5: UX improvements — Stage→Round terminology, clickable dashboard
round links, URL-based round selection (?round=), round type indicators
in selectors, accessible Toggle-based cross-round comparison, sortable
project table columns (title/score/evaluations), brand score colors on
dashboard bar chart with aria labels.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 21:44:38 +01:00
|
|
|
user: { select: { id: true, name: true } },
|
2026-02-14 15:26:42 +01:00
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Group scores by juror
|
Observer platform overhaul: Nivo charts, round-type stats, UX improvements
Phase 1: Fix 6 backend data bugs in analytics.ts (roundName filtering,
unscored projects, criteria scores, activeRoundCount scoping, email
privacy leaks in juror consistency + workload)
Phase 2-3: Migrate all 9 chart components from Recharts to Nivo
(@nivo/bar, @nivo/line, @nivo/pie, @nivo/scatterplot) with shared brand
theme, scoreGradient colors, and STATUS_COLORS map. Fixes scatter plot
outlier coloring and pie chart label visibility bugs.
Phase 4: Add round-type-aware stats (getRoundTypeStats backend +
RoundTypeStatsCards component) showing appropriate metrics per round
type (intake/filtering/evaluation/submission/mentoring/live/deliberation).
Phase 5: UX improvements — Stage→Round terminology, clickable dashboard
round links, URL-based round selection (?round=), round type indicators
in selectors, accessible Toggle-based cross-round comparison, sortable
project table columns (title/score/evaluations), brand score colors on
dashboard bar chart with aria labels.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 21:44:38 +01:00
|
|
|
const jurorScores: Record<string, { name: string; scores: number[] }> = {}
|
2026-02-14 15:26:42 +01:00
|
|
|
|
|
|
|
|
evaluations.forEach((e) => {
|
|
|
|
|
const userId = e.assignment.userId
|
|
|
|
|
if (!jurorScores[userId]) {
|
|
|
|
|
jurorScores[userId] = {
|
Observer platform overhaul: Nivo charts, round-type stats, UX improvements
Phase 1: Fix 6 backend data bugs in analytics.ts (roundName filtering,
unscored projects, criteria scores, activeRoundCount scoping, email
privacy leaks in juror consistency + workload)
Phase 2-3: Migrate all 9 chart components from Recharts to Nivo
(@nivo/bar, @nivo/line, @nivo/pie, @nivo/scatterplot) with shared brand
theme, scoreGradient colors, and STATUS_COLORS map. Fixes scatter plot
outlier coloring and pie chart label visibility bugs.
Phase 4: Add round-type-aware stats (getRoundTypeStats backend +
RoundTypeStatsCards component) showing appropriate metrics per round
type (intake/filtering/evaluation/submission/mentoring/live/deliberation).
Phase 5: UX improvements — Stage→Round terminology, clickable dashboard
round links, URL-based round selection (?round=), round type indicators
in selectors, accessible Toggle-based cross-round comparison, sortable
project table columns (title/score/evaluations), brand score colors on
dashboard bar chart with aria labels.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 21:44:38 +01:00
|
|
|
name: e.assignment.user.name || 'Unknown',
|
2026-02-14 15:26:42 +01:00
|
|
|
scores: [],
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (e.globalScore !== null) {
|
|
|
|
|
jurorScores[userId].scores.push(e.globalScore)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Calculate overall average
|
|
|
|
|
const allScores = Object.values(jurorScores).flatMap((j) => j.scores)
|
|
|
|
|
const overallAverage = allScores.length > 0
|
|
|
|
|
? allScores.reduce((a, b) => a + b, 0) / allScores.length
|
|
|
|
|
: 0
|
|
|
|
|
|
|
|
|
|
// Calculate per-juror metrics
|
|
|
|
|
const metrics = Object.entries(jurorScores).map(([userId, data]) => {
|
|
|
|
|
const avg = data.scores.length > 0
|
|
|
|
|
? data.scores.reduce((a, b) => a + b, 0) / data.scores.length
|
|
|
|
|
: 0
|
|
|
|
|
const variance = data.scores.length > 1
|
|
|
|
|
? data.scores.reduce((sum, s) => sum + Math.pow(s - avg, 2), 0) / data.scores.length
|
|
|
|
|
: 0
|
|
|
|
|
const stddev = Math.sqrt(variance)
|
|
|
|
|
const deviationFromOverall = Math.abs(avg - overallAverage)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
userId,
|
|
|
|
|
name: data.name,
|
|
|
|
|
evaluationCount: data.scores.length,
|
|
|
|
|
averageScore: avg,
|
|
|
|
|
stddev,
|
|
|
|
|
deviationFromOverall,
|
|
|
|
|
isOutlier: deviationFromOverall > 2, // Flag if 2+ points from mean
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
overallAverage,
|
|
|
|
|
jurors: metrics.sort((a, b) => b.deviationFromOverall - a.deviationFromOverall),
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
* Get diversity metrics for projects in a round
|
2026-02-14 15:26:42 +01:00
|
|
|
*/
|
|
|
|
|
getDiversityMetrics: observerProcedure
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
.input(editionOrRoundInput)
|
2026-02-14 15:26:42 +01:00
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
const projects = await ctx.prisma.project.findMany({
|
|
|
|
|
where: projectWhere(input),
|
|
|
|
|
select: {
|
|
|
|
|
country: true,
|
|
|
|
|
competitionCategory: true,
|
|
|
|
|
oceanIssue: true,
|
|
|
|
|
tags: true,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const total = projects.length
|
|
|
|
|
if (total === 0) {
|
|
|
|
|
return { total: 0, byCountry: [], byCategory: [], byOceanIssue: [], byTag: [] }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// By country
|
|
|
|
|
const countryCounts: Record<string, number> = {}
|
|
|
|
|
projects.forEach((p) => {
|
|
|
|
|
const key = p.country || 'Unknown'
|
|
|
|
|
countryCounts[key] = (countryCounts[key] || 0) + 1
|
|
|
|
|
})
|
|
|
|
|
const byCountry = Object.entries(countryCounts)
|
|
|
|
|
.map(([country, count]) => ({ country, count, percentage: (count / total) * 100 }))
|
|
|
|
|
.sort((a, b) => b.count - a.count)
|
|
|
|
|
|
|
|
|
|
// By competition category
|
|
|
|
|
const categoryCounts: Record<string, number> = {}
|
|
|
|
|
projects.forEach((p) => {
|
|
|
|
|
const key = p.competitionCategory || 'Uncategorized'
|
|
|
|
|
categoryCounts[key] = (categoryCounts[key] || 0) + 1
|
|
|
|
|
})
|
|
|
|
|
const byCategory = Object.entries(categoryCounts)
|
|
|
|
|
.map(([category, count]) => ({ category, count, percentage: (count / total) * 100 }))
|
|
|
|
|
.sort((a, b) => b.count - a.count)
|
|
|
|
|
|
|
|
|
|
// By ocean issue
|
|
|
|
|
const issueCounts: Record<string, number> = {}
|
|
|
|
|
projects.forEach((p) => {
|
|
|
|
|
const key = p.oceanIssue || 'Unspecified'
|
|
|
|
|
issueCounts[key] = (issueCounts[key] || 0) + 1
|
|
|
|
|
})
|
|
|
|
|
const byOceanIssue = Object.entries(issueCounts)
|
|
|
|
|
.map(([issue, count]) => ({ issue, count, percentage: (count / total) * 100 }))
|
|
|
|
|
.sort((a, b) => b.count - a.count)
|
|
|
|
|
|
|
|
|
|
// By tag
|
|
|
|
|
const tagCounts: Record<string, number> = {}
|
|
|
|
|
projects.forEach((p) => {
|
2026-02-20 13:42:31 +01:00
|
|
|
(p.tags || []).forEach((tag) => {
|
2026-02-14 15:26:42 +01:00
|
|
|
tagCounts[tag] = (tagCounts[tag] || 0) + 1
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
const byTag = Object.entries(tagCounts)
|
|
|
|
|
.map(([tag, count]) => ({ tag, count, percentage: (count / total) * 100 }))
|
|
|
|
|
.sort((a, b) => b.count - a.count)
|
|
|
|
|
|
|
|
|
|
return { total, byCountry, byCategory, byOceanIssue, byTag }
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
* Get dashboard stats (optionally scoped to a round)
|
2026-02-14 15:26:42 +01:00
|
|
|
*/
|
|
|
|
|
getDashboardStats: observerProcedure
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
.input(z.object({ roundId: z.string().optional() }).optional())
|
2026-02-14 15:26:42 +01:00
|
|
|
.query(async ({ ctx, input }) => {
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
const roundId = input?.roundId
|
2026-02-14 15:26:42 +01:00
|
|
|
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
const projectFilter = roundId
|
2026-02-19 09:56:09 +01:00
|
|
|
? { projectRoundStates: { some: { roundId } } }
|
2026-02-14 15:26:42 +01:00
|
|
|
: {}
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
const assignmentFilter = roundId ? { roundId } : {}
|
|
|
|
|
const evalFilter = roundId
|
|
|
|
|
? { assignment: { roundId }, status: 'SUBMITTED' as const }
|
2026-02-14 15:26:42 +01:00
|
|
|
: { status: 'SUBMITTED' as const }
|
|
|
|
|
|
|
|
|
|
const [
|
|
|
|
|
programCount,
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
activeRoundCount,
|
2026-02-14 15:26:42 +01:00
|
|
|
projectCount,
|
|
|
|
|
jurorCount,
|
|
|
|
|
submittedEvaluations,
|
|
|
|
|
totalAssignments,
|
|
|
|
|
evaluationScores,
|
|
|
|
|
] = await Promise.all([
|
|
|
|
|
ctx.prisma.program.count(),
|
Observer platform overhaul: Nivo charts, round-type stats, UX improvements
Phase 1: Fix 6 backend data bugs in analytics.ts (roundName filtering,
unscored projects, criteria scores, activeRoundCount scoping, email
privacy leaks in juror consistency + workload)
Phase 2-3: Migrate all 9 chart components from Recharts to Nivo
(@nivo/bar, @nivo/line, @nivo/pie, @nivo/scatterplot) with shared brand
theme, scoreGradient colors, and STATUS_COLORS map. Fixes scatter plot
outlier coloring and pie chart label visibility bugs.
Phase 4: Add round-type-aware stats (getRoundTypeStats backend +
RoundTypeStatsCards component) showing appropriate metrics per round
type (intake/filtering/evaluation/submission/mentoring/live/deliberation).
Phase 5: UX improvements — Stage→Round terminology, clickable dashboard
round links, URL-based round selection (?round=), round type indicators
in selectors, accessible Toggle-based cross-round comparison, sortable
project table columns (title/score/evaluations), brand score colors on
dashboard bar chart with aria labels.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 21:44:38 +01:00
|
|
|
roundId
|
|
|
|
|
? ctx.prisma.round.findUnique({ where: { id: roundId }, select: { competitionId: true } })
|
|
|
|
|
.then((r) => r?.competitionId
|
|
|
|
|
? ctx.prisma.round.count({ where: { competitionId: r.competitionId, status: 'ROUND_ACTIVE' } })
|
|
|
|
|
: ctx.prisma.round.count({ where: { status: 'ROUND_ACTIVE' } }))
|
|
|
|
|
: ctx.prisma.round.count({ where: { status: 'ROUND_ACTIVE' } }),
|
2026-02-14 15:26:42 +01:00
|
|
|
ctx.prisma.project.count({ where: projectFilter }),
|
2026-02-19 09:56:09 +01:00
|
|
|
roundId
|
|
|
|
|
? ctx.prisma.assignment.findMany({
|
|
|
|
|
where: { roundId },
|
|
|
|
|
select: { userId: true },
|
|
|
|
|
distinct: ['userId'],
|
|
|
|
|
}).then((rows) => rows.length)
|
|
|
|
|
: ctx.prisma.user.count({ where: { role: 'JURY_MEMBER', status: 'ACTIVE' } }),
|
2026-02-14 15:26:42 +01:00
|
|
|
ctx.prisma.evaluation.count({ where: evalFilter }),
|
|
|
|
|
ctx.prisma.assignment.count({ where: assignmentFilter }),
|
|
|
|
|
ctx.prisma.evaluation.findMany({
|
|
|
|
|
where: { ...evalFilter, globalScore: { not: null } },
|
|
|
|
|
select: { globalScore: true },
|
|
|
|
|
}),
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
const completionRate = totalAssignments > 0
|
|
|
|
|
? Math.round((submittedEvaluations / totalAssignments) * 100)
|
|
|
|
|
: 0
|
|
|
|
|
|
|
|
|
|
const scores = evaluationScores.map((e) => e.globalScore!).filter((s) => s != null)
|
|
|
|
|
const scoreDistribution = [
|
2026-02-19 11:11:00 +01:00
|
|
|
{ label: '9-10', min: 9, max: Infinity },
|
|
|
|
|
{ label: '7-8', min: 7, max: 9 },
|
|
|
|
|
{ label: '5-6', min: 5, max: 7 },
|
|
|
|
|
{ label: '3-4', min: 3, max: 5 },
|
|
|
|
|
{ label: '1-2', min: 1, max: 3 },
|
2026-02-14 15:26:42 +01:00
|
|
|
].map((b) => ({
|
|
|
|
|
label: b.label,
|
2026-02-19 11:11:00 +01:00
|
|
|
count: scores.filter((s) => s >= b.min && s < b.max).length,
|
2026-02-14 15:26:42 +01:00
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
programCount,
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
activeRoundCount,
|
2026-02-14 15:26:42 +01:00
|
|
|
projectCount,
|
|
|
|
|
jurorCount,
|
|
|
|
|
submittedEvaluations,
|
2026-02-19 11:11:00 +01:00
|
|
|
totalAssignments,
|
2026-02-14 15:26:42 +01:00
|
|
|
completionRate,
|
|
|
|
|
scoreDistribution,
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
// Stage-Scoped Analytics (Phase 4)
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
* Get score distribution histogram for round evaluations
|
2026-02-14 15:26:42 +01:00
|
|
|
*/
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
getRoundScoreDistribution: observerProcedure
|
|
|
|
|
.input(z.object({ roundId: z.string() }))
|
2026-02-14 15:26:42 +01:00
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
const evaluations = await ctx.prisma.evaluation.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
status: 'SUBMITTED',
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
assignment: { roundId: input.roundId },
|
2026-02-14 15:26:42 +01:00
|
|
|
},
|
|
|
|
|
select: {
|
|
|
|
|
globalScore: true,
|
|
|
|
|
criterionScoresJson: true,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Global score distribution (1-10 buckets)
|
|
|
|
|
const globalScores = evaluations
|
|
|
|
|
.map((e) => e.globalScore)
|
|
|
|
|
.filter((s): s is number => s !== null)
|
|
|
|
|
|
|
|
|
|
const globalDistribution = Array.from({ length: 10 }, (_, i) => ({
|
|
|
|
|
score: i + 1,
|
|
|
|
|
count: globalScores.filter((s) => Math.round(s) === i + 1).length,
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
// Per-criterion score distribution
|
|
|
|
|
const criterionScores: Record<string, number[]> = {}
|
|
|
|
|
evaluations.forEach((e) => {
|
|
|
|
|
const scores = e.criterionScoresJson as Record<string, number> | null
|
|
|
|
|
if (scores) {
|
|
|
|
|
Object.entries(scores).forEach(([key, value]) => {
|
|
|
|
|
if (typeof value === 'number') {
|
|
|
|
|
if (!criterionScores[key]) criterionScores[key] = []
|
|
|
|
|
criterionScores[key].push(value)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const criterionDistributions = Object.entries(criterionScores).map(([criterionId, scores]) => ({
|
|
|
|
|
criterionId,
|
|
|
|
|
average: scores.reduce((a, b) => a + b, 0) / scores.length,
|
|
|
|
|
count: scores.length,
|
|
|
|
|
distribution: Array.from({ length: 10 }, (_, i) => ({
|
|
|
|
|
score: i + 1,
|
|
|
|
|
count: scores.filter((s) => Math.round(s) === i + 1).length,
|
|
|
|
|
})),
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
globalDistribution,
|
|
|
|
|
totalEvaluations: evaluations.length,
|
|
|
|
|
averageGlobalScore:
|
|
|
|
|
globalScores.length > 0
|
|
|
|
|
? globalScores.reduce((a, b) => a + b, 0) / globalScores.length
|
|
|
|
|
: 0,
|
|
|
|
|
criterionDistributions,
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
* Get per-round completion summary for a competition
|
|
|
|
|
* NOTE: This replaces the old pipeline-based getStageCompletionOverview
|
2026-02-14 15:26:42 +01:00
|
|
|
*/
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
getRoundCompletionOverview: observerProcedure
|
|
|
|
|
.input(z.object({ competitionId: z.string() }))
|
2026-02-14 15:26:42 +01:00
|
|
|
.query(async ({ ctx, input }) => {
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
// Get all rounds in the competition
|
|
|
|
|
const rounds = await ctx.prisma.round.findMany({
|
|
|
|
|
where: { competitionId: input.competitionId },
|
2026-02-14 15:26:42 +01:00
|
|
|
orderBy: { sortOrder: 'asc' },
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
name: true,
|
|
|
|
|
roundType: true,
|
|
|
|
|
status: true,
|
|
|
|
|
sortOrder: true,
|
|
|
|
|
},
|
2026-02-14 15:26:42 +01:00
|
|
|
})
|
|
|
|
|
|
Observer platform redesign Phase 4: migrate charts to Tremor, redesign all pages
- Migrate 9 chart components from Nivo to @tremor/react (BarChart, AreaChart, DonutChart, ScatterChart)
- Remove @nivo/*, @react-spring/web dependencies (45 packages removed)
- Redesign dashboard: 6 stat tiles, competition pipeline, score distribution, juror workload, activity feed
- Add new /observer/projects page with search, filters, sorting, pagination, CSV export
- Restructure reports page from 5 tabs to 3 (Progress, Jurors, Scores & Analytics) with per-tab CSV export
- Redesign project detail: breadcrumb nav, score card header, 3-tab layout (Overview/Evaluations/Files)
- Update loading skeletons to match new layouts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 21:45:01 +01:00
|
|
|
// Batch all queries by roundIds to avoid N+1
|
|
|
|
|
const roundIds = rounds.map((r) => r.id)
|
2026-02-14 15:26:42 +01:00
|
|
|
|
Observer platform redesign Phase 4: migrate charts to Tremor, redesign all pages
- Migrate 9 chart components from Nivo to @tremor/react (BarChart, AreaChart, DonutChart, ScatterChart)
- Remove @nivo/*, @react-spring/web dependencies (45 packages removed)
- Redesign dashboard: 6 stat tiles, competition pipeline, score distribution, juror workload, activity feed
- Add new /observer/projects page with search, filters, sorting, pagination, CSV export
- Restructure reports page from 5 tabs to 3 (Progress, Jurors, Scores & Analytics) with per-tab CSV export
- Redesign project detail: breadcrumb nav, score card header, 3-tab layout (Overview/Evaluations/Files)
- Update loading skeletons to match new layouts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 21:45:01 +01:00
|
|
|
const [
|
|
|
|
|
allProjectRoundStates,
|
|
|
|
|
allAssignmentCounts,
|
|
|
|
|
allCompletedEvals,
|
|
|
|
|
allDistinctJurors,
|
|
|
|
|
] = await Promise.all([
|
|
|
|
|
ctx.prisma.projectRoundState.groupBy({
|
|
|
|
|
by: ['roundId', 'state'],
|
|
|
|
|
where: { roundId: { in: roundIds } },
|
|
|
|
|
_count: true,
|
|
|
|
|
}),
|
|
|
|
|
ctx.prisma.assignment.groupBy({
|
|
|
|
|
by: ['roundId'],
|
|
|
|
|
where: { roundId: { in: roundIds } },
|
|
|
|
|
_count: true,
|
|
|
|
|
}),
|
|
|
|
|
// groupBy on relation field not supported, use raw count per round
|
|
|
|
|
ctx.prisma.$queryRaw<{ roundId: string; count: bigint }[]>`
|
|
|
|
|
SELECT a."roundId", COUNT(e.id)::bigint as count
|
|
|
|
|
FROM "Evaluation" e
|
|
|
|
|
JOIN "Assignment" a ON e."assignmentId" = a.id
|
|
|
|
|
WHERE a."roundId" = ANY(${roundIds}) AND e.status = 'SUBMITTED'
|
|
|
|
|
GROUP BY a."roundId"
|
|
|
|
|
`,
|
|
|
|
|
ctx.prisma.assignment.groupBy({
|
|
|
|
|
by: ['roundId', 'userId'],
|
|
|
|
|
where: { roundId: { in: roundIds } },
|
|
|
|
|
}),
|
|
|
|
|
])
|
2026-02-14 15:26:42 +01:00
|
|
|
|
Observer platform redesign Phase 4: migrate charts to Tremor, redesign all pages
- Migrate 9 chart components from Nivo to @tremor/react (BarChart, AreaChart, DonutChart, ScatterChart)
- Remove @nivo/*, @react-spring/web dependencies (45 packages removed)
- Redesign dashboard: 6 stat tiles, competition pipeline, score distribution, juror workload, activity feed
- Add new /observer/projects page with search, filters, sorting, pagination, CSV export
- Restructure reports page from 5 tabs to 3 (Progress, Jurors, Scores & Analytics) with per-tab CSV export
- Redesign project detail: breadcrumb nav, score card header, 3-tab layout (Overview/Evaluations/Files)
- Update loading skeletons to match new layouts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 21:45:01 +01:00
|
|
|
// Build lookup maps
|
|
|
|
|
const statesByRound = new Map<string, { state: string; count: number }[]>()
|
|
|
|
|
for (const ps of allProjectRoundStates) {
|
|
|
|
|
const list = statesByRound.get(ps.roundId) || []
|
|
|
|
|
list.push({ state: ps.state, count: ps._count })
|
|
|
|
|
statesByRound.set(ps.roundId, list)
|
|
|
|
|
}
|
2026-02-14 15:26:42 +01:00
|
|
|
|
Observer platform redesign Phase 4: migrate charts to Tremor, redesign all pages
- Migrate 9 chart components from Nivo to @tremor/react (BarChart, AreaChart, DonutChart, ScatterChart)
- Remove @nivo/*, @react-spring/web dependencies (45 packages removed)
- Redesign dashboard: 6 stat tiles, competition pipeline, score distribution, juror workload, activity feed
- Add new /observer/projects page with search, filters, sorting, pagination, CSV export
- Restructure reports page from 5 tabs to 3 (Progress, Jurors, Scores & Analytics) with per-tab CSV export
- Redesign project detail: breadcrumb nav, score card header, 3-tab layout (Overview/Evaluations/Files)
- Update loading skeletons to match new layouts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 21:45:01 +01:00
|
|
|
const assignmentCountByRound = new Map<string, number>()
|
|
|
|
|
for (const ac of allAssignmentCounts) {
|
|
|
|
|
assignmentCountByRound.set(ac.roundId, ac._count)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const completedEvalsByRound = new Map<string, number>()
|
|
|
|
|
for (const ce of allCompletedEvals) {
|
|
|
|
|
completedEvalsByRound.set(ce.roundId, Number(ce.count))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const jurorCountByRound = new Map<string, number>()
|
|
|
|
|
for (const j of allDistinctJurors) {
|
|
|
|
|
jurorCountByRound.set(j.roundId, (jurorCountByRound.get(j.roundId) || 0) + 1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const roundOverviews = rounds.map((round) => {
|
|
|
|
|
const stateBreakdown = statesByRound.get(round.id) || []
|
|
|
|
|
const totalProjects = stateBreakdown.reduce((sum, ps) => sum + ps.count, 0)
|
|
|
|
|
const totalAssignments = assignmentCountByRound.get(round.id) || 0
|
|
|
|
|
const completedEvaluations = completedEvalsByRound.get(round.id) || 0
|
|
|
|
|
const completionRate = totalAssignments > 0
|
|
|
|
|
? Math.round((completedEvaluations / totalAssignments) * 100)
|
|
|
|
|
: 0
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
roundId: round.id,
|
|
|
|
|
roundName: round.name,
|
|
|
|
|
roundType: round.roundType,
|
|
|
|
|
roundStatus: round.status,
|
|
|
|
|
sortOrder: round.sortOrder,
|
|
|
|
|
totalProjects,
|
|
|
|
|
stateBreakdown,
|
|
|
|
|
totalAssignments,
|
|
|
|
|
completedEvaluations,
|
|
|
|
|
pendingEvaluations: totalAssignments - completedEvaluations,
|
|
|
|
|
completionRate,
|
|
|
|
|
jurorCount: jurorCountByRound.get(round.id) || 0,
|
|
|
|
|
}
|
|
|
|
|
})
|
2026-02-14 15:26:42 +01:00
|
|
|
|
|
|
|
|
return {
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
competitionId: input.competitionId,
|
|
|
|
|
rounds: roundOverviews,
|
2026-02-14 15:26:42 +01:00
|
|
|
summary: {
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
totalRounds: rounds.length,
|
|
|
|
|
totalProjects: roundOverviews.reduce((sum, s) => sum + s.totalProjects, 0),
|
|
|
|
|
totalAssignments: roundOverviews.reduce((sum, s) => sum + s.totalAssignments, 0),
|
|
|
|
|
totalCompleted: roundOverviews.reduce((sum, s) => sum + s.completedEvaluations, 0),
|
2026-02-14 15:26:42 +01:00
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
// Award Analytics (Phase 5)
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
// NOTE: getAwardSummary procedure removed - depends on deleted Pipeline/Track/Stage/SpecialAward models
|
|
|
|
|
// Will need to be reimplemented with new Competition/Round/Award architecture
|
2026-02-14 15:26:42 +01:00
|
|
|
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
// NOTE: getAwardVoteDistribution procedure removed - depends on deleted Stage/Track/SpecialAward/AwardVote models
|
|
|
|
|
// Will need to be reimplemented with new Competition/Round/Award architecture
|
2026-02-14 15:26:42 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get all projects with pagination, filtering, and search (for observer dashboard)
|
|
|
|
|
*/
|
|
|
|
|
getAllProjects: observerProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
roundId: z.string().optional(),
|
2026-02-14 15:26:42 +01:00
|
|
|
search: z.string().optional(),
|
|
|
|
|
status: z.string().optional(),
|
Observer platform overhaul: Nivo charts, round-type stats, UX improvements
Phase 1: Fix 6 backend data bugs in analytics.ts (roundName filtering,
unscored projects, criteria scores, activeRoundCount scoping, email
privacy leaks in juror consistency + workload)
Phase 2-3: Migrate all 9 chart components from Recharts to Nivo
(@nivo/bar, @nivo/line, @nivo/pie, @nivo/scatterplot) with shared brand
theme, scoreGradient colors, and STATUS_COLORS map. Fixes scatter plot
outlier coloring and pie chart label visibility bugs.
Phase 4: Add round-type-aware stats (getRoundTypeStats backend +
RoundTypeStatsCards component) showing appropriate metrics per round
type (intake/filtering/evaluation/submission/mentoring/live/deliberation).
Phase 5: UX improvements — Stage→Round terminology, clickable dashboard
round links, URL-based round selection (?round=), round type indicators
in selectors, accessible Toggle-based cross-round comparison, sortable
project table columns (title/score/evaluations), brand score colors on
dashboard bar chart with aria labels.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 21:44:38 +01:00
|
|
|
sortBy: z.enum(['title', 'score', 'evaluations']).default('title'),
|
|
|
|
|
sortDir: z.enum(['asc', 'desc']).default('asc'),
|
2026-02-14 15:26:42 +01:00
|
|
|
page: z.number().min(1).default(1),
|
|
|
|
|
perPage: z.number().min(1).max(100).default(20),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
const where: Record<string, unknown> = {}
|
|
|
|
|
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
if (input.roundId) {
|
Observer platform redesign Phase 4: migrate charts to Tremor, redesign all pages
- Migrate 9 chart components from Nivo to @tremor/react (BarChart, AreaChart, DonutChart, ScatterChart)
- Remove @nivo/*, @react-spring/web dependencies (45 packages removed)
- Redesign dashboard: 6 stat tiles, competition pipeline, score distribution, juror workload, activity feed
- Add new /observer/projects page with search, filters, sorting, pagination, CSV export
- Restructure reports page from 5 tabs to 3 (Progress, Jurors, Scores & Analytics) with per-tab CSV export
- Redesign project detail: breadcrumb nav, score card header, 3-tab layout (Overview/Evaluations/Files)
- Update loading skeletons to match new layouts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 21:45:01 +01:00
|
|
|
where.projectRoundStates = { some: { roundId: input.roundId } }
|
2026-02-14 15:26:42 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (input.status) {
|
|
|
|
|
where.status = input.status
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (input.search) {
|
|
|
|
|
where.OR = [
|
|
|
|
|
{ title: { contains: input.search, mode: 'insensitive' } },
|
|
|
|
|
{ teamName: { contains: input.search, mode: 'insensitive' } },
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
Observer platform overhaul: Nivo charts, round-type stats, UX improvements
Phase 1: Fix 6 backend data bugs in analytics.ts (roundName filtering,
unscored projects, criteria scores, activeRoundCount scoping, email
privacy leaks in juror consistency + workload)
Phase 2-3: Migrate all 9 chart components from Recharts to Nivo
(@nivo/bar, @nivo/line, @nivo/pie, @nivo/scatterplot) with shared brand
theme, scoreGradient colors, and STATUS_COLORS map. Fixes scatter plot
outlier coloring and pie chart label visibility bugs.
Phase 4: Add round-type-aware stats (getRoundTypeStats backend +
RoundTypeStatsCards component) showing appropriate metrics per round
type (intake/filtering/evaluation/submission/mentoring/live/deliberation).
Phase 5: UX improvements — Stage→Round terminology, clickable dashboard
round links, URL-based round selection (?round=), round type indicators
in selectors, accessible Toggle-based cross-round comparison, sortable
project table columns (title/score/evaluations), brand score colors on
dashboard bar chart with aria labels.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 21:44:38 +01:00
|
|
|
// Prisma-level sort for title; score/evaluations sorted post-query
|
|
|
|
|
const prismaOrderBy = input.sortBy === 'title'
|
|
|
|
|
? { title: input.sortDir as 'asc' | 'desc' }
|
|
|
|
|
: { title: 'asc' as const }
|
|
|
|
|
|
2026-02-14 15:26:42 +01:00
|
|
|
const [projects, total] = await Promise.all([
|
|
|
|
|
ctx.prisma.project.findMany({
|
|
|
|
|
where,
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
title: true,
|
|
|
|
|
teamName: true,
|
|
|
|
|
status: true,
|
|
|
|
|
country: true,
|
|
|
|
|
assignments: {
|
|
|
|
|
select: {
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
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>
2026-02-15 23:04:15 +01:00
|
|
|
roundId: true,
|
|
|
|
|
round: { select: { id: true, name: true } },
|
2026-02-14 15:26:42 +01:00
|
|
|
evaluation: {
|
|
|
|
|
select: { globalScore: true, status: true },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
Observer platform overhaul: Nivo charts, round-type stats, UX improvements
Phase 1: Fix 6 backend data bugs in analytics.ts (roundName filtering,
unscored projects, criteria scores, activeRoundCount scoping, email
privacy leaks in juror consistency + workload)
Phase 2-3: Migrate all 9 chart components from Recharts to Nivo
(@nivo/bar, @nivo/line, @nivo/pie, @nivo/scatterplot) with shared brand
theme, scoreGradient colors, and STATUS_COLORS map. Fixes scatter plot
outlier coloring and pie chart label visibility bugs.
Phase 4: Add round-type-aware stats (getRoundTypeStats backend +
RoundTypeStatsCards component) showing appropriate metrics per round
type (intake/filtering/evaluation/submission/mentoring/live/deliberation).
Phase 5: UX improvements — Stage→Round terminology, clickable dashboard
round links, URL-based round selection (?round=), round type indicators
in selectors, accessible Toggle-based cross-round comparison, sortable
project table columns (title/score/evaluations), brand score colors on
dashboard bar chart with aria labels.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 21:44:38 +01:00
|
|
|
orderBy: prismaOrderBy,
|
|
|
|
|
// When sorting by computed fields, fetch all then slice in JS
|
|
|
|
|
...(input.sortBy === 'title'
|
|
|
|
|
? { skip: (input.page - 1) * input.perPage, take: input.perPage }
|
|
|
|
|
: {}),
|
2026-02-14 15:26:42 +01:00
|
|
|
}),
|
|
|
|
|
ctx.prisma.project.count({ where }),
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
const mapped = projects.map((p) => {
|
|
|
|
|
const submitted = p.assignments
|
|
|
|
|
.map((a) => a.evaluation)
|
|
|
|
|
.filter((e) => e?.status === 'SUBMITTED')
|
|
|
|
|
const scores = submitted
|
|
|
|
|
.map((e) => e?.globalScore)
|
|
|
|
|
.filter((s): s is number => s !== null)
|
|
|
|
|
const averageScore =
|
|
|
|
|
scores.length > 0
|
|
|
|
|
? scores.reduce((a, b) => a + b, 0) / scores.length
|
|
|
|
|
: null
|
|
|
|
|
|
Observer platform overhaul: Nivo charts, round-type stats, UX improvements
Phase 1: Fix 6 backend data bugs in analytics.ts (roundName filtering,
unscored projects, criteria scores, activeRoundCount scoping, email
privacy leaks in juror consistency + workload)
Phase 2-3: Migrate all 9 chart components from Recharts to Nivo
(@nivo/bar, @nivo/line, @nivo/pie, @nivo/scatterplot) with shared brand
theme, scoreGradient colors, and STATUS_COLORS map. Fixes scatter plot
outlier coloring and pie chart label visibility bugs.
Phase 4: Add round-type-aware stats (getRoundTypeStats backend +
RoundTypeStatsCards component) showing appropriate metrics per round
type (intake/filtering/evaluation/submission/mentoring/live/deliberation).
Phase 5: UX improvements — Stage→Round terminology, clickable dashboard
round links, URL-based round selection (?round=), round type indicators
in selectors, accessible Toggle-based cross-round comparison, sortable
project table columns (title/score/evaluations), brand score colors on
dashboard bar chart with aria labels.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 21:44:38 +01:00
|
|
|
// Filter assignments to the queried round so we show the correct round name
|
|
|
|
|
const roundAssignment = input.roundId
|
|
|
|
|
? p.assignments.find((a) => a.roundId === input.roundId)
|
|
|
|
|
: p.assignments[0]
|
2026-02-14 15:26:42 +01:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: p.id,
|
|
|
|
|
title: p.title,
|
|
|
|
|
teamName: p.teamName,
|
|
|
|
|
status: p.status,
|
|
|
|
|
country: p.country,
|
Observer platform overhaul: Nivo charts, round-type stats, UX improvements
Phase 1: Fix 6 backend data bugs in analytics.ts (roundName filtering,
unscored projects, criteria scores, activeRoundCount scoping, email
privacy leaks in juror consistency + workload)
Phase 2-3: Migrate all 9 chart components from Recharts to Nivo
(@nivo/bar, @nivo/line, @nivo/pie, @nivo/scatterplot) with shared brand
theme, scoreGradient colors, and STATUS_COLORS map. Fixes scatter plot
outlier coloring and pie chart label visibility bugs.
Phase 4: Add round-type-aware stats (getRoundTypeStats backend +
RoundTypeStatsCards component) showing appropriate metrics per round
type (intake/filtering/evaluation/submission/mentoring/live/deliberation).
Phase 5: UX improvements — Stage→Round terminology, clickable dashboard
round links, URL-based round selection (?round=), round type indicators
in selectors, accessible Toggle-based cross-round comparison, sortable
project table columns (title/score/evaluations), brand score colors on
dashboard bar chart with aria labels.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 21:44:38 +01:00
|
|
|
roundId: roundAssignment?.round?.id ?? '',
|
|
|
|
|
roundName: roundAssignment?.round?.name ?? '',
|
2026-02-14 15:26:42 +01:00
|
|
|
averageScore,
|
|
|
|
|
evaluationCount: submitted.length,
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
Observer platform overhaul: Nivo charts, round-type stats, UX improvements
Phase 1: Fix 6 backend data bugs in analytics.ts (roundName filtering,
unscored projects, criteria scores, activeRoundCount scoping, email
privacy leaks in juror consistency + workload)
Phase 2-3: Migrate all 9 chart components from Recharts to Nivo
(@nivo/bar, @nivo/line, @nivo/pie, @nivo/scatterplot) with shared brand
theme, scoreGradient colors, and STATUS_COLORS map. Fixes scatter plot
outlier coloring and pie chart label visibility bugs.
Phase 4: Add round-type-aware stats (getRoundTypeStats backend +
RoundTypeStatsCards component) showing appropriate metrics per round
type (intake/filtering/evaluation/submission/mentoring/live/deliberation).
Phase 5: UX improvements — Stage→Round terminology, clickable dashboard
round links, URL-based round selection (?round=), round type indicators
in selectors, accessible Toggle-based cross-round comparison, sortable
project table columns (title/score/evaluations), brand score colors on
dashboard bar chart with aria labels.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 21:44:38 +01:00
|
|
|
// Sort by computed fields (score, evaluations) in JS
|
|
|
|
|
let sorted = mapped
|
|
|
|
|
if (input.sortBy === 'score') {
|
|
|
|
|
sorted = mapped.sort((a, b) => {
|
|
|
|
|
const sa = a.averageScore ?? -1
|
|
|
|
|
const sb = b.averageScore ?? -1
|
|
|
|
|
return input.sortDir === 'asc' ? sa - sb : sb - sa
|
|
|
|
|
})
|
|
|
|
|
} else if (input.sortBy === 'evaluations') {
|
|
|
|
|
sorted = mapped.sort((a, b) =>
|
|
|
|
|
input.sortDir === 'asc'
|
|
|
|
|
? a.evaluationCount - b.evaluationCount
|
|
|
|
|
: b.evaluationCount - a.evaluationCount
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Paginate in JS for computed-field sorts
|
|
|
|
|
const paginated = input.sortBy !== 'title'
|
|
|
|
|
? sorted.slice((input.page - 1) * input.perPage, input.page * input.perPage)
|
|
|
|
|
: sorted
|
|
|
|
|
|
2026-02-14 15:26:42 +01:00
|
|
|
return {
|
Observer platform overhaul: Nivo charts, round-type stats, UX improvements
Phase 1: Fix 6 backend data bugs in analytics.ts (roundName filtering,
unscored projects, criteria scores, activeRoundCount scoping, email
privacy leaks in juror consistency + workload)
Phase 2-3: Migrate all 9 chart components from Recharts to Nivo
(@nivo/bar, @nivo/line, @nivo/pie, @nivo/scatterplot) with shared brand
theme, scoreGradient colors, and STATUS_COLORS map. Fixes scatter plot
outlier coloring and pie chart label visibility bugs.
Phase 4: Add round-type-aware stats (getRoundTypeStats backend +
RoundTypeStatsCards component) showing appropriate metrics per round
type (intake/filtering/evaluation/submission/mentoring/live/deliberation).
Phase 5: UX improvements — Stage→Round terminology, clickable dashboard
round links, URL-based round selection (?round=), round type indicators
in selectors, accessible Toggle-based cross-round comparison, sortable
project table columns (title/score/evaluations), brand score colors on
dashboard bar chart with aria labels.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 21:44:38 +01:00
|
|
|
projects: paginated,
|
2026-02-14 15:26:42 +01:00
|
|
|
total,
|
|
|
|
|
page: input.page,
|
|
|
|
|
perPage: input.perPage,
|
|
|
|
|
totalPages: Math.ceil(total / input.perPage),
|
|
|
|
|
}
|
|
|
|
|
}),
|
Observer platform overhaul: Nivo charts, round-type stats, UX improvements
Phase 1: Fix 6 backend data bugs in analytics.ts (roundName filtering,
unscored projects, criteria scores, activeRoundCount scoping, email
privacy leaks in juror consistency + workload)
Phase 2-3: Migrate all 9 chart components from Recharts to Nivo
(@nivo/bar, @nivo/line, @nivo/pie, @nivo/scatterplot) with shared brand
theme, scoreGradient colors, and STATUS_COLORS map. Fixes scatter plot
outlier coloring and pie chart label visibility bugs.
Phase 4: Add round-type-aware stats (getRoundTypeStats backend +
RoundTypeStatsCards component) showing appropriate metrics per round
type (intake/filtering/evaluation/submission/mentoring/live/deliberation).
Phase 5: UX improvements — Stage→Round terminology, clickable dashboard
round links, URL-based round selection (?round=), round type indicators
in selectors, accessible Toggle-based cross-round comparison, sortable
project table columns (title/score/evaluations), brand score colors on
dashboard bar chart with aria labels.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 21:44:38 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get round-type-aware stats for a specific round.
|
|
|
|
|
* Returns different metrics depending on the round type.
|
|
|
|
|
*/
|
|
|
|
|
getRoundTypeStats: observerProcedure
|
|
|
|
|
.input(z.object({ roundId: z.string() }))
|
|
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
const round = await ctx.prisma.round.findUniqueOrThrow({
|
|
|
|
|
where: { id: input.roundId },
|
|
|
|
|
select: { id: true, roundType: true, competitionId: true },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const roundType = round.roundType
|
|
|
|
|
|
|
|
|
|
switch (roundType) {
|
|
|
|
|
case 'INTAKE': {
|
|
|
|
|
const [total, byState, byCategory] = await Promise.all([
|
|
|
|
|
ctx.prisma.projectRoundState.count({ where: { roundId: input.roundId } }),
|
|
|
|
|
ctx.prisma.projectRoundState.groupBy({
|
|
|
|
|
by: ['state'],
|
|
|
|
|
where: { roundId: input.roundId },
|
|
|
|
|
_count: true,
|
|
|
|
|
}),
|
|
|
|
|
ctx.prisma.project.groupBy({
|
|
|
|
|
by: ['competitionCategory'],
|
|
|
|
|
where: { projectRoundStates: { some: { roundId: input.roundId } } },
|
|
|
|
|
_count: true,
|
|
|
|
|
}),
|
|
|
|
|
])
|
|
|
|
|
return {
|
|
|
|
|
roundType,
|
|
|
|
|
stats: {
|
|
|
|
|
totalProjects: total,
|
|
|
|
|
byState: byState.map((s) => ({ state: s.state, count: s._count })),
|
|
|
|
|
byCategory: byCategory.map((c) => ({
|
|
|
|
|
category: c.competitionCategory ?? 'Uncategorized',
|
|
|
|
|
count: c._count,
|
|
|
|
|
})),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'FILTERING': {
|
|
|
|
|
const [total, byOutcome] = await Promise.all([
|
|
|
|
|
ctx.prisma.filteringResult.count({ where: { roundId: input.roundId } }),
|
|
|
|
|
ctx.prisma.filteringResult.groupBy({
|
|
|
|
|
by: ['outcome'],
|
|
|
|
|
where: { roundId: input.roundId },
|
|
|
|
|
_count: true,
|
|
|
|
|
}),
|
|
|
|
|
])
|
|
|
|
|
const passed = byOutcome.find((o) => o.outcome === 'PASSED')?._count ?? 0
|
|
|
|
|
return {
|
|
|
|
|
roundType,
|
|
|
|
|
stats: {
|
|
|
|
|
totalScreened: total,
|
|
|
|
|
passed,
|
|
|
|
|
filteredOut: byOutcome.find((o) => o.outcome === 'FILTERED_OUT')?._count ?? 0,
|
|
|
|
|
flagged: byOutcome.find((o) => o.outcome === 'FLAGGED')?._count ?? 0,
|
|
|
|
|
passRate: total > 0 ? Math.round((passed / total) * 100) : 0,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'EVALUATION': {
|
|
|
|
|
const [assignmentCount, submittedCount, jurorCount] = await Promise.all([
|
|
|
|
|
ctx.prisma.assignment.count({ where: { roundId: input.roundId } }),
|
|
|
|
|
ctx.prisma.evaluation.count({
|
|
|
|
|
where: { assignment: { roundId: input.roundId }, status: 'SUBMITTED' },
|
|
|
|
|
}),
|
|
|
|
|
ctx.prisma.assignment.findMany({
|
|
|
|
|
where: { roundId: input.roundId },
|
|
|
|
|
select: { userId: true },
|
|
|
|
|
distinct: ['userId'],
|
|
|
|
|
}).then((rows) => rows.length),
|
|
|
|
|
])
|
|
|
|
|
return {
|
|
|
|
|
roundType,
|
|
|
|
|
stats: {
|
|
|
|
|
totalAssignments: assignmentCount,
|
|
|
|
|
completedEvaluations: submittedCount,
|
|
|
|
|
completionRate: assignmentCount > 0 ? Math.round((submittedCount / assignmentCount) * 100) : 0,
|
|
|
|
|
activeJurors: jurorCount,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'SUBMISSION': {
|
|
|
|
|
const [fileCount, teamsWithFiles] = await Promise.all([
|
|
|
|
|
ctx.prisma.projectFile.count({ where: { roundId: input.roundId } }),
|
|
|
|
|
ctx.prisma.projectFile.findMany({
|
|
|
|
|
where: { roundId: input.roundId },
|
|
|
|
|
select: { projectId: true },
|
|
|
|
|
distinct: ['projectId'],
|
|
|
|
|
}).then((rows) => rows.length),
|
|
|
|
|
])
|
|
|
|
|
return {
|
|
|
|
|
roundType,
|
|
|
|
|
stats: {
|
|
|
|
|
totalFiles: fileCount,
|
|
|
|
|
teamsSubmitted: teamsWithFiles,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'MENTORING': {
|
|
|
|
|
const [assignmentCount, messageCount] = await Promise.all([
|
|
|
|
|
ctx.prisma.mentorAssignment.count({
|
|
|
|
|
where: { project: { projectRoundStates: { some: { roundId: input.roundId } } } },
|
|
|
|
|
}),
|
|
|
|
|
ctx.prisma.mentorMessage.count({
|
|
|
|
|
where: { project: { projectRoundStates: { some: { roundId: input.roundId } } } },
|
|
|
|
|
}),
|
|
|
|
|
])
|
|
|
|
|
return {
|
|
|
|
|
roundType,
|
|
|
|
|
stats: {
|
|
|
|
|
mentorAssignments: assignmentCount,
|
|
|
|
|
totalMessages: messageCount,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'LIVE_FINAL': {
|
|
|
|
|
const session = await ctx.prisma.liveVotingSession.findUnique({
|
|
|
|
|
where: { roundId: input.roundId },
|
|
|
|
|
select: { id: true, status: true, _count: { select: { votes: true } } },
|
|
|
|
|
})
|
|
|
|
|
return {
|
|
|
|
|
roundType,
|
|
|
|
|
stats: {
|
|
|
|
|
sessionStatus: session?.status ?? 'NOT_STARTED',
|
|
|
|
|
voteCount: session?._count.votes ?? 0,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case 'DELIBERATION': {
|
|
|
|
|
const [sessions, votes, locks] = await Promise.all([
|
|
|
|
|
ctx.prisma.deliberationSession.count({ where: { roundId: input.roundId } }),
|
|
|
|
|
ctx.prisma.deliberationVote.count({
|
|
|
|
|
where: { session: { roundId: input.roundId } },
|
|
|
|
|
}),
|
|
|
|
|
ctx.prisma.resultLock.count({ where: { roundId: input.roundId } }),
|
|
|
|
|
])
|
|
|
|
|
return {
|
|
|
|
|
roundType,
|
|
|
|
|
stats: {
|
|
|
|
|
totalSessions: sessions,
|
|
|
|
|
totalVotes: votes,
|
|
|
|
|
resultsLocked: locks,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
return { roundType, stats: {} }
|
|
|
|
|
}
|
|
|
|
|
}),
|
2026-02-20 18:39:53 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Observer-accessible project detail: project info + assignments with evaluations + competition rounds + files.
|
|
|
|
|
* Read-only combined endpoint to avoid multiple round-trips.
|
|
|
|
|
*/
|
|
|
|
|
getProjectDetail: observerProcedure
|
|
|
|
|
.input(z.object({ id: z.string() }))
|
|
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
const [projectRaw, projectTags, assignments, submittedEvaluations] = await Promise.all([
|
|
|
|
|
ctx.prisma.project.findUniqueOrThrow({
|
|
|
|
|
where: { id: input.id },
|
|
|
|
|
include: {
|
|
|
|
|
files: {
|
|
|
|
|
select: {
|
|
|
|
|
id: true, fileName: true, fileType: true, mimeType: true, size: true,
|
|
|
|
|
bucket: true, objectKey: true, pageCount: true, textPreview: true,
|
|
|
|
|
detectedLang: true, langConfidence: true, analyzedAt: true,
|
|
|
|
|
requirementId: true,
|
|
|
|
|
requirement: { select: { id: true, name: true, description: true, isRequired: true } },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
teamMembers: {
|
|
|
|
|
include: {
|
|
|
|
|
user: {
|
|
|
|
|
select: { id: true, name: true, email: true, profileImageKey: true, profileImageProvider: true },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
orderBy: { joinedAt: 'asc' },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
ctx.prisma.projectTag.findMany({
|
|
|
|
|
where: { projectId: input.id },
|
|
|
|
|
include: { tag: { select: { id: true, name: true, category: true, color: true } } },
|
|
|
|
|
orderBy: { confidence: 'desc' },
|
|
|
|
|
}).catch(() => [] as { id: string; projectId: string; tagId: string; confidence: number; tag: { id: string; name: string; category: string | null; color: string | null } }[]),
|
|
|
|
|
ctx.prisma.assignment.findMany({
|
|
|
|
|
where: { projectId: input.id },
|
|
|
|
|
include: {
|
|
|
|
|
user: { select: { id: true, name: true, email: true, profileImageKey: true, profileImageProvider: true } },
|
|
|
|
|
round: { select: { id: true, name: true } },
|
|
|
|
|
evaluation: {
|
|
|
|
|
select: {
|
|
|
|
|
id: true, status: true, submittedAt: true, globalScore: true,
|
|
|
|
|
binaryDecision: true, criterionScoresJson: true, feedbackText: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
orderBy: { createdAt: 'desc' },
|
|
|
|
|
}),
|
|
|
|
|
ctx.prisma.evaluation.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
status: 'SUBMITTED',
|
|
|
|
|
assignment: { projectId: input.id },
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
// Compute evaluation stats
|
|
|
|
|
let stats = null
|
|
|
|
|
if (submittedEvaluations.length > 0) {
|
|
|
|
|
const globalScores = submittedEvaluations
|
|
|
|
|
.map((e) => e.globalScore)
|
|
|
|
|
.filter((s): s is number => s !== null)
|
|
|
|
|
const yesVotes = submittedEvaluations.filter((e) => e.binaryDecision === true).length
|
|
|
|
|
stats = {
|
|
|
|
|
totalEvaluations: submittedEvaluations.length,
|
|
|
|
|
averageGlobalScore: globalScores.length > 0
|
|
|
|
|
? globalScores.reduce((a, b) => a + b, 0) / globalScores.length
|
|
|
|
|
: null,
|
|
|
|
|
minScore: globalScores.length > 0 ? Math.min(...globalScores) : null,
|
|
|
|
|
maxScore: globalScores.length > 0 ? Math.max(...globalScores) : null,
|
|
|
|
|
yesVotes,
|
|
|
|
|
noVotes: submittedEvaluations.length - yesVotes,
|
|
|
|
|
yesPercentage: (yesVotes / submittedEvaluations.length) * 100,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get competition rounds for file grouping
|
|
|
|
|
let competitionRounds: { id: string; name: string }[] = []
|
|
|
|
|
const competition = await ctx.prisma.competition.findFirst({
|
|
|
|
|
where: { programId: projectRaw.programId },
|
|
|
|
|
include: { rounds: { select: { id: true, name: true }, orderBy: { sortOrder: 'asc' } } },
|
|
|
|
|
})
|
|
|
|
|
if (competition) {
|
|
|
|
|
competitionRounds = competition.rounds
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get file requirements for all rounds
|
|
|
|
|
let allRequirements: { id: string; roundId: string; name: string; description: string | null; isRequired: boolean; acceptedMimeTypes: string[]; maxSizeMB: number | null }[] = []
|
|
|
|
|
if (competitionRounds.length > 0) {
|
|
|
|
|
allRequirements = await ctx.prisma.fileRequirement.findMany({
|
|
|
|
|
where: { roundId: { in: competitionRounds.map((r) => r.id) } },
|
|
|
|
|
select: { id: true, roundId: true, name: true, description: true, isRequired: true, acceptedMimeTypes: true, maxSizeMB: true },
|
|
|
|
|
orderBy: { sortOrder: 'asc' },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Attach avatar URLs
|
|
|
|
|
const [teamMembersWithAvatars, assignmentsWithAvatars] = await Promise.all([
|
|
|
|
|
Promise.all(
|
|
|
|
|
projectRaw.teamMembers.map(async (member) => ({
|
|
|
|
|
...member,
|
|
|
|
|
user: {
|
|
|
|
|
...member.user,
|
|
|
|
|
avatarUrl: await getUserAvatarUrl(member.user.profileImageKey, member.user.profileImageProvider),
|
|
|
|
|
},
|
|
|
|
|
}))
|
|
|
|
|
),
|
|
|
|
|
Promise.all(
|
|
|
|
|
assignments.map(async (a) => ({
|
|
|
|
|
...a,
|
|
|
|
|
user: {
|
|
|
|
|
...a.user,
|
|
|
|
|
avatarUrl: await getUserAvatarUrl(a.user.profileImageKey, a.user.profileImageProvider),
|
|
|
|
|
},
|
|
|
|
|
}))
|
|
|
|
|
),
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
project: {
|
|
|
|
|
...projectRaw,
|
|
|
|
|
projectTags,
|
|
|
|
|
teamMembers: teamMembersWithAvatars,
|
|
|
|
|
},
|
|
|
|
|
assignments: assignmentsWithAvatars,
|
|
|
|
|
stats,
|
|
|
|
|
competitionRounds,
|
|
|
|
|
allRequirements,
|
|
|
|
|
}
|
|
|
|
|
}),
|
Observer platform redesign Phase 4: migrate charts to Tremor, redesign all pages
- Migrate 9 chart components from Nivo to @tremor/react (BarChart, AreaChart, DonutChart, ScatterChart)
- Remove @nivo/*, @react-spring/web dependencies (45 packages removed)
- Redesign dashboard: 6 stat tiles, competition pipeline, score distribution, juror workload, activity feed
- Add new /observer/projects page with search, filters, sorting, pagination, CSV export
- Restructure reports page from 5 tabs to 3 (Progress, Jurors, Scores & Analytics) with per-tab CSV export
- Redesign project detail: breadcrumb nav, score card header, 3-tab layout (Overview/Evaluations/Files)
- Update loading skeletons to match new layouts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 21:45:01 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Activity feed — recent audit log entries for observer dashboard
|
|
|
|
|
*/
|
|
|
|
|
getActivityFeed: observerProcedure
|
|
|
|
|
.input(z.object({ limit: z.number().min(1).max(50).default(10) }).optional())
|
|
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
const limit = input?.limit ?? 10
|
|
|
|
|
|
|
|
|
|
const entries = await ctx.prisma.decisionAuditLog.findMany({
|
|
|
|
|
orderBy: { createdAt: 'desc' },
|
|
|
|
|
take: limit,
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
eventType: true,
|
|
|
|
|
entityType: true,
|
|
|
|
|
entityId: true,
|
|
|
|
|
actorId: true,
|
|
|
|
|
detailsJson: true,
|
|
|
|
|
createdAt: true,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Batch-fetch actor names
|
|
|
|
|
const actorIds = [...new Set(entries.map((e) => e.actorId).filter(Boolean))] as string[]
|
|
|
|
|
const actors = actorIds.length > 0
|
|
|
|
|
? await ctx.prisma.user.findMany({
|
|
|
|
|
where: { id: { in: actorIds } },
|
|
|
|
|
select: { id: true, name: true },
|
|
|
|
|
})
|
|
|
|
|
: []
|
|
|
|
|
const actorMap = new Map(actors.map((a) => [a.id, a.name]))
|
|
|
|
|
|
|
|
|
|
return entries.map((entry) => ({
|
|
|
|
|
id: entry.id,
|
|
|
|
|
eventType: entry.eventType,
|
|
|
|
|
entityType: entry.entityType,
|
|
|
|
|
entityId: entry.entityId,
|
|
|
|
|
actorName: entry.actorId ? actorMap.get(entry.actorId) ?? null : null,
|
|
|
|
|
details: entry.detailsJson as Record<string, unknown> | null,
|
|
|
|
|
createdAt: entry.createdAt,
|
|
|
|
|
}))
|
|
|
|
|
}),
|
2026-02-14 15:26:42 +01:00
|
|
|
})
|