Fix observer reports: charts, filtering, project preview, dashboard stats
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m32s

- Rewrite diversity metrics: horizontal bar charts for ocean issues and
  geographic distribution (replaces unreadable vertical/donut charts)
- Rewrite juror score heatmap: expandable table with score distribution
- Rewrite juror consistency: horizontal bar visual with juror names
- Merge filtering tabs into single screening view with per-project
  AI reasoning and expandable rows
- Add project preview dialog for juror performance table
- Fix status breakdown for evaluation rounds (Fully/Partially/Not Reviewed)
- Show active round name instead of count on observer dashboard
- Move Global tab to last position, default to first round-specific tab
- Add 4-card stats layout for evaluation with reviews/project ratio
- Fix oceanIssue field (singular) and remove non-existent aiSummary

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-21 10:12:21 +01:00
parent 2e4b95f29c
commit 161cd1684a
12 changed files with 948 additions and 334 deletions

View File

@@ -126,7 +126,7 @@ export const analyticsRouter = router({
user: { select: { name: true } },
project: { select: { id: true, title: true } },
evaluation: {
select: { id: true, status: true },
select: { id: true, status: true, globalScore: true },
},
},
})
@@ -134,7 +134,7 @@ export const analyticsRouter = router({
// Group by user
const byUser: Record<
string,
{ name: string; assigned: number; completed: number; projects: { id: string; title: string; evalStatus: string }[] }
{ name: string; assigned: number; completed: number; projects: { id: string; title: string; evalStatus: string; score: number | null }[] }
> = {}
assignments.forEach((assignment) => {
@@ -156,6 +156,9 @@ export const analyticsRouter = router({
id: assignment.project.id,
title: assignment.project.title,
evalStatus: evalStatus === 'SUBMITTED' ? 'REVIEWED' : evalStatus === 'DRAFT' ? 'UNDER_REVIEW' : 'NOT_REVIEWED',
score: evalStatus === 'SUBMITTED' && assignment.evaluation?.globalScore != null
? Number(assignment.evaluation.globalScore)
: null,
})
})
@@ -251,7 +254,59 @@ export const analyticsRouter = router({
.input(editionOrRoundInput)
.query(async ({ ctx, input }) => {
if (input.roundId) {
// Round-level: use ProjectRoundState for accurate per-round breakdown
// Check if this is an evaluation round — show eval-level status breakdown
const round = await ctx.prisma.round.findUnique({
where: { id: input.roundId },
select: { roundType: true },
})
if (round?.roundType === 'EVALUATION') {
// For evaluation rounds, break down by evaluation status per project
const projects = await ctx.prisma.projectRoundState.findMany({
where: { roundId: input.roundId },
select: {
projectId: true,
project: {
select: {
assignments: {
where: { roundId: input.roundId },
select: {
evaluation: { select: { status: true } },
},
},
},
},
},
})
let fullyReviewed = 0
let partiallyReviewed = 0
let notReviewed = 0
for (const p of projects) {
const assignments = p.project.assignments
if (assignments.length === 0) {
notReviewed++
continue
}
const submitted = assignments.filter((a) => a.evaluation?.status === 'SUBMITTED').length
if (submitted === 0) {
notReviewed++
} else if (submitted === assignments.length) {
fullyReviewed++
} else {
partiallyReviewed++
}
}
const result = []
if (fullyReviewed > 0) result.push({ status: 'FULLY_REVIEWED', count: fullyReviewed })
if (partiallyReviewed > 0) result.push({ status: 'PARTIALLY_REVIEWED', count: partiallyReviewed })
if (notReviewed > 0) result.push({ status: 'NOT_REVIEWED', count: notReviewed })
return result
}
// Non-evaluation rounds: use ProjectRoundState
const states = await ctx.prisma.projectRoundState.groupBy({
by: ['state'],
where: { roundId: input.roundId },
@@ -668,7 +723,7 @@ export const analyticsRouter = router({
const [
programCount,
activeRoundCount,
activeRounds,
projectCount,
jurorCount,
submittedEvaluations,
@@ -676,12 +731,11 @@ export const analyticsRouter = router({
evaluationScores,
] = await Promise.all([
ctx.prisma.program.count(),
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' } }),
ctx.prisma.round.findMany({
where: { status: 'ROUND_ACTIVE' },
select: { id: true, name: true },
take: 5,
}),
ctx.prisma.project.count({ where: projectFilter }),
roundId
? ctx.prisma.assignment.findMany({
@@ -716,7 +770,8 @@ export const analyticsRouter = router({
return {
programCount,
activeRoundCount,
activeRoundCount: activeRounds.length,
activeRoundName: activeRounds.length === 1 ? activeRounds[0].name : null,
projectCount,
jurorCount,
submittedEvaluations,
@@ -1551,7 +1606,12 @@ export const analyticsRouter = router({
skip,
take: perPage,
orderBy: { createdAt: 'desc' },
include: {
select: {
id: true,
outcome: true,
finalOutcome: true,
aiScreeningJson: true,
overrideReason: true,
project: {
select: {
id: true,