fix: filter juror preferences banner to review-round groups

The "Confirm Your Evaluation Preferences" banner was including jury
group memberships whose only rounds are LIVE_FINAL or DELIBERATION.
Those ceremonies don't use cap+category preferences, so the sliders
were meaningless. Filter getOnboardingContext to memberships in
groups with at least one INTAKE/FILTERING/EVALUATION/SUBMISSION/
MENTORING round.

Spec: docs/superpowers/specs/2026-04-28-mentor-round-readiness-design.md §E
Plan: docs/superpowers/plans/2026-04-28-pr1-jury-preferences-filter.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt
2026-04-28 13:08:21 +02:00
parent 6ef0e50081
commit 0222da79e0
2 changed files with 165 additions and 1 deletions

View File

@@ -1396,7 +1396,18 @@ export const userRouter = router({
*/
getOnboardingContext: protectedProcedure.query(async ({ ctx }) => {
const memberships = await ctx.prisma.juryGroupMember.findMany({
where: { userId: ctx.user.id },
where: {
userId: ctx.user.id,
juryGroup: {
rounds: {
some: {
roundType: {
in: ['INTAKE', 'FILTERING', 'EVALUATION', 'SUBMISSION', 'MENTORING'],
},
},
},
},
},
include: {
juryGroup: {
select: {

View File

@@ -0,0 +1,153 @@
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { prisma, createCaller } from '../setup'
import {
createTestUser, createTestProgram, createTestCompetition, createTestRound,
cleanupTestData, uid,
} from '../helpers'
import { userRouter } from '../../src/server/routers/user'
describe('user.getOnboardingContext — preferences filter excludes LIVE_FINAL/DELIBERATION-only groups', () => {
let programId: string
let competitionId: string
let juror: { id: string; email: string; role: 'JURY_MEMBER' }
let observerOnlyGroupId: string
let reviewGroupId: string
let mixedGroupId: string
const userIds: string[] = []
beforeAll(async () => {
const program = await createTestProgram({ name: `prefs-filter-${uid()}` })
programId = program.id
const competition = await createTestCompetition(programId)
competitionId = competition.id
const reviewRound = await createTestRound(competitionId, {
name: 'Review Round', slug: `review-${uid()}`, roundType: 'EVALUATION', sortOrder: 0,
})
const liveFinalRound = await createTestRound(competitionId, {
name: 'Final Round', slug: `final-${uid()}`, roundType: 'LIVE_FINAL', sortOrder: 1,
})
const deliberationRound = await createTestRound(competitionId, {
name: 'Delib Round', slug: `delib-${uid()}`, roundType: 'DELIBERATION', sortOrder: 2,
})
const reviewOnlyGroup = await prisma.juryGroup.create({
data: {
id: uid('jg-rev'), competitionId, name: 'Review Only Group',
slug: uid('rev'), defaultMaxAssignments: 30,
},
})
reviewGroupId = reviewOnlyGroup.id
const liveFinalOnlyGroup = await prisma.juryGroup.create({
data: {
id: uid('jg-fin'), competitionId, name: 'Finals Only Group',
slug: uid('fin'), defaultMaxAssignments: 10,
},
})
observerOnlyGroupId = liveFinalOnlyGroup.id
const mixedGroup = await prisma.juryGroup.create({
data: {
id: uid('jg-mix'), competitionId, name: 'Mixed Group',
slug: uid('mix'), defaultMaxAssignments: 20,
},
})
mixedGroupId = mixedGroup.id
await prisma.round.update({ where: { id: reviewRound.id }, data: { juryGroupId: reviewOnlyGroup.id } })
await prisma.round.update({ where: { id: liveFinalRound.id }, data: { juryGroupId: liveFinalOnlyGroup.id } })
const mixedReview = await createTestRound(competitionId, {
name: 'Mixed Review', slug: `mixed-rev-${uid()}`, roundType: 'EVALUATION', sortOrder: 3,
})
const mixedFinal = await createTestRound(competitionId, {
name: 'Mixed Final', slug: `mixed-fin-${uid()}`, roundType: 'LIVE_FINAL', sortOrder: 4,
})
await prisma.round.update({ where: { id: mixedReview.id }, data: { juryGroupId: mixedGroup.id } })
await prisma.round.update({ where: { id: mixedFinal.id }, data: { juryGroupId: mixedGroup.id } })
void deliberationRound // referenced for cleanup; not attached to a group in these scenarios
const u = await createTestUser('JURY_MEMBER')
userIds.push(u.id)
juror = { id: u.id, email: u.email, role: 'JURY_MEMBER' }
await prisma.juryGroupMember.createMany({
data: [
{ id: uid('jgm-rev'), juryGroupId: reviewGroupId, userId: u.id, role: 'MEMBER' },
{ id: uid('jgm-fin'), juryGroupId: observerOnlyGroupId, userId: u.id, role: 'MEMBER' },
{ id: uid('jgm-mix'), juryGroupId: mixedGroupId, userId: u.id, role: 'MEMBER' },
],
})
})
afterAll(async () => {
await cleanupTestData(programId, userIds)
})
it('returns the review-only group membership', async () => {
const caller = createCaller(userRouter, juror)
const ctx = await caller.getOnboardingContext()
const names = ctx.memberships.map((m: { juryGroupName: string }) => m.juryGroupName).sort()
expect(names).toContain('Review Only Group')
})
it('omits the LIVE_FINAL-only group membership', async () => {
const caller = createCaller(userRouter, juror)
const ctx = await caller.getOnboardingContext()
const names = ctx.memberships.map((m: { juryGroupName: string }) => m.juryGroupName)
expect(names).not.toContain('Finals Only Group')
})
it('keeps the mixed group (has at least one review round)', async () => {
const caller = createCaller(userRouter, juror)
const ctx = await caller.getOnboardingContext()
const names = ctx.memberships.map((m: { juryGroupName: string }) => m.juryGroupName)
expect(names).toContain('Mixed Group')
})
it('returns hasSelfServiceOptions=true when at least one membership remains', async () => {
const caller = createCaller(userRouter, juror)
const ctx = await caller.getOnboardingContext()
expect(ctx.hasSelfServiceOptions).toBe(true)
expect(ctx.memberships.length).toBe(2)
})
})
describe('user.getOnboardingContext — juror with only LIVE_FINAL membership', () => {
let programId: string
let juror: { id: string; email: string; role: 'JURY_MEMBER' }
const userIds: string[] = []
beforeAll(async () => {
const program = await createTestProgram({ name: `prefs-only-fin-${uid()}` })
programId = program.id
const competition = await createTestCompetition(programId)
const liveFinalRound = await createTestRound(competition.id, {
name: 'Solo Final', slug: `solo-fin-${uid()}`, roundType: 'LIVE_FINAL', sortOrder: 0,
})
const liveFinalOnlyGroup = await prisma.juryGroup.create({
data: {
id: uid('jg-only-fin'), competitionId: competition.id, name: 'Solo Finals Group',
slug: uid('solo-fin'), defaultMaxAssignments: 10,
},
})
await prisma.round.update({ where: { id: liveFinalRound.id }, data: { juryGroupId: liveFinalOnlyGroup.id } })
const u = await createTestUser('JURY_MEMBER')
userIds.push(u.id)
juror = { id: u.id, email: u.email, role: 'JURY_MEMBER' }
await prisma.juryGroupMember.create({
data: { id: uid('jgm-only-fin'), juryGroupId: liveFinalOnlyGroup.id, userId: u.id, role: 'MEMBER' },
})
})
afterAll(async () => {
await cleanupTestData(programId, userIds)
})
it('returns no memberships and hasSelfServiceOptions=false', async () => {
const caller = createCaller(userRouter, juror)
const ctx = await caller.getOnboardingContext()
expect(ctx.memberships).toEqual([])
expect(ctx.hasSelfServiceOptions).toBe(false)
})
})