Comprehensive admin UI stats audit: fix 16 display bugs
HIGH fixes: - H1: Competition detail project count no longer double-counts across rounds - H2: Rounds page header stats use unfiltered round set - H3: Rounds page "eval" label corrected to "asgn" (assignment count) - H4: Observer reports project count uses distinct analytics count - H5: Awards eligibility count filters to only eligible=true (backend) - H6: Round detail projectCount derived from projectStates for consistency - H7: Deliberation hasVoted derived from votes array (was always undefined) MEDIUM fixes: - M1: Reports page round status badges use correct ROUND_ACTIVE/ROUND_CLOSED enums - M2: Observer reports badges use ROUND_ prefix instead of stale STAGE_ prefix - M3: Deliberation list status badges use correct VOTING/TALLYING/RUNOFF enums - M4: Competition list/detail round count excludes special-award rounds (backend) - M5: Messages page shows actual recipient count instead of hardcoded "1 user" LOW fixes: - L2: Observer analytics jurorCount scoped to round when roundId provided - L3: Analytics round-scoped project count uses ProjectRoundState not assignments - L4: JuryGroup delete audit log reports member count (not assignment count) - L5: Project rankings include unevaluated projects at bottom instead of hiding Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@ const editionOrRoundInput = z.object({
|
||||
})
|
||||
|
||||
function projectWhere(input: { roundId?: string; programId?: string }) {
|
||||
if (input.roundId) return { assignments: { some: { roundId: input.roundId } } }
|
||||
if (input.roundId) return { projectRoundStates: { some: { roundId: input.roundId } } }
|
||||
return { programId: input.programId! }
|
||||
}
|
||||
|
||||
@@ -223,8 +223,13 @@ export const analyticsRouter = router({
|
||||
evaluationCount: allScores.length,
|
||||
}
|
||||
})
|
||||
.filter((p) => p.averageScore !== null)
|
||||
.sort((a, b) => (b.averageScore || 0) - (a.averageScore || 0))
|
||||
.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
|
||||
})
|
||||
|
||||
return input.limit ? rankings.slice(0, input.limit) : rankings
|
||||
}),
|
||||
@@ -709,7 +714,7 @@ export const analyticsRouter = router({
|
||||
const roundId = input?.roundId
|
||||
|
||||
const projectFilter = roundId
|
||||
? { assignments: { some: { roundId } } }
|
||||
? { projectRoundStates: { some: { roundId } } }
|
||||
: {}
|
||||
const assignmentFilter = roundId ? { roundId } : {}
|
||||
const evalFilter = roundId
|
||||
@@ -728,7 +733,13 @@ export const analyticsRouter = router({
|
||||
ctx.prisma.program.count(),
|
||||
ctx.prisma.round.count({ where: { status: 'ROUND_ACTIVE' } }),
|
||||
ctx.prisma.project.count({ where: projectFilter }),
|
||||
ctx.prisma.user.count({ where: { role: 'JURY_MEMBER', status: 'ACTIVE' } }),
|
||||
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' } }),
|
||||
ctx.prisma.evaluation.count({ where: evalFilter }),
|
||||
ctx.prisma.assignment.count({ where: assignmentFilter }),
|
||||
ctx.prisma.evaluation.findMany({
|
||||
|
||||
@@ -146,7 +146,11 @@ export const competitionRouter = router({
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
_count: {
|
||||
select: { rounds: true, juryGroups: true, submissionWindows: true },
|
||||
select: {
|
||||
rounds: { where: { specialAwardId: null } },
|
||||
juryGroups: true,
|
||||
submissionWindows: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -256,7 +260,7 @@ export const competitionRouter = router({
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
select: { id: true, name: true, roundType: true, status: true },
|
||||
},
|
||||
_count: { select: { rounds: true, juryGroups: true } },
|
||||
_count: { select: { rounds: { where: { specialAwardId: null } }, juryGroups: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
})
|
||||
|
||||
@@ -258,7 +258,7 @@ export const juryGroupRouter = router({
|
||||
const group = await ctx.prisma.juryGroup.findUniqueOrThrow({
|
||||
where: { id: input.id },
|
||||
include: {
|
||||
_count: { select: { assignments: true, rounds: true } },
|
||||
_count: { select: { members: true, assignments: true, rounds: true } },
|
||||
},
|
||||
})
|
||||
|
||||
@@ -284,7 +284,7 @@ export const juryGroupRouter = router({
|
||||
detailsJson: {
|
||||
name: group.name,
|
||||
competitionId: group.competitionId,
|
||||
memberCount: group._count.assignments,
|
||||
memberCount: group._count.members,
|
||||
},
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.userAgent,
|
||||
|
||||
@@ -43,7 +43,7 @@ export const specialAwardRouter = router({
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
eligibilities: true,
|
||||
eligibilities: { where: { eligible: true } },
|
||||
jurors: true,
|
||||
votes: true,
|
||||
},
|
||||
@@ -66,7 +66,7 @@ export const specialAwardRouter = router({
|
||||
include: {
|
||||
_count: {
|
||||
select: {
|
||||
eligibilities: true,
|
||||
eligibilities: { where: { eligible: true } },
|
||||
jurors: true,
|
||||
votes: true,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user