Fix evaluation criteria, jury preferences, assignment config, and dashboard stats
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m5s
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m5s
- Fix criteria not showing for jurors: fetch active form independently via
getStageForm query instead of relying on existing evaluation record
- Fix scoringMode default from 'global' to 'criteria' (matching schema)
- Parse scale string format ("1-10") into minScore/maxScore for criteria display
- Fix COI dialog dismissal: prevent outside click on evaluate page Dialog
- Fix requiredReviews hardcoded to 3: read from round configJson in 4 locations
- Add jury preferences banner for unconfirmed caps on jury dashboard
- Add updateJuryPreferences tRPC procedure for self-service cap/ratio
- Simplify onboarding: always show jury step, allow cap up to 50
- Add role/ratio/availability fields to jury member invite dialog
- Simplify jury group settings (keep only defaultMaxAssignments)
- Enforce deliberation showCollectiveRankings flag for non-admin users
- Redesign dashboard stat cards: editorial data strip on mobile,
clean grid layout on desktop (no more generic card pattern)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -186,7 +186,8 @@ export const deliberationRouter = router({
|
||||
}),
|
||||
|
||||
/**
|
||||
* Get session with votes, results, and participants
|
||||
* Get session with votes, results, and participants.
|
||||
* Redacts juror identities for non-admin users when session flags are off.
|
||||
*/
|
||||
getSession: protectedProcedure
|
||||
.input(z.object({ sessionId: z.string() }))
|
||||
@@ -195,6 +196,37 @@ export const deliberationRouter = router({
|
||||
if (!session) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Session not found' })
|
||||
}
|
||||
|
||||
const isAdmin = ctx.user.role === 'SUPER_ADMIN' || ctx.user.role === 'PROGRAM_ADMIN'
|
||||
if (isAdmin) return session
|
||||
|
||||
// Non-admin: enforce visibility flags
|
||||
if (!session.showCollectiveRankings) {
|
||||
// Anonymize juror identity on votes — only show own votes with identity
|
||||
session.votes = session.votes.map((v: any, i: number) => {
|
||||
const isOwn = v.juryMember?.user?.id === ctx.user.id
|
||||
if (isOwn) return v
|
||||
return {
|
||||
...v,
|
||||
juryMember: {
|
||||
...v.juryMember,
|
||||
user: { id: `anon-${i}`, name: `Juror ${i + 1}`, email: '' },
|
||||
},
|
||||
}
|
||||
})
|
||||
// Anonymize participants
|
||||
session.participants = session.participants.map((p: any, i: number) => {
|
||||
const isOwn = p.user?.user?.id === ctx.user.id
|
||||
if (isOwn) return p
|
||||
return {
|
||||
...p,
|
||||
user: p.user
|
||||
? { ...p.user, user: { id: `anon-${i}`, name: `Juror ${i + 1}`, email: '' } }
|
||||
: p.user,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return session
|
||||
}),
|
||||
|
||||
|
||||
@@ -1112,30 +1112,16 @@ export const userRouter = router({
|
||||
// Security: verify this member belongs to the current user
|
||||
const member = await tx.juryGroupMember.findUnique({
|
||||
where: { id: pref.juryGroupMemberId },
|
||||
include: { juryGroup: { select: { allowJurorCapAdjustment: true, allowJurorRatioAdjustment: true, defaultMaxAssignments: true } } },
|
||||
})
|
||||
if (!member || member.userId !== ctx.user.id) continue
|
||||
|
||||
const updateData: Record<string, unknown> = {}
|
||||
|
||||
// Only set selfServiceCap if group allows it
|
||||
if (pref.selfServiceCap != null && member.juryGroup.allowJurorCapAdjustment) {
|
||||
// Bound by admin max (override or group default)
|
||||
const adminMax = member.maxAssignmentsOverride ?? member.juryGroup.defaultMaxAssignments
|
||||
updateData.selfServiceCap = Math.min(pref.selfServiceCap, adminMax)
|
||||
}
|
||||
|
||||
// Only set selfServiceRatio if group allows it
|
||||
if (pref.selfServiceRatio != null && member.juryGroup.allowJurorRatioAdjustment) {
|
||||
updateData.selfServiceRatio = pref.selfServiceRatio
|
||||
}
|
||||
|
||||
if (Object.keys(updateData).length > 0) {
|
||||
await tx.juryGroupMember.update({
|
||||
where: { id: pref.juryGroupMemberId },
|
||||
data: updateData,
|
||||
})
|
||||
}
|
||||
await tx.juryGroupMember.update({
|
||||
where: { id: pref.juryGroupMemberId },
|
||||
data: {
|
||||
selfServiceCap: pref.selfServiceCap != null ? Math.min(pref.selfServiceCap, 50) : undefined,
|
||||
selfServiceRatio: pref.selfServiceRatio,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1157,9 +1143,42 @@ export const userRouter = router({
|
||||
return user
|
||||
}),
|
||||
|
||||
/**
|
||||
* Update jury preferences outside of onboarding (e.g., when a new round opens).
|
||||
*/
|
||||
updateJuryPreferences: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
preferences: z.array(
|
||||
z.object({
|
||||
juryGroupMemberId: z.string(),
|
||||
selfServiceCap: z.number().int().min(1).max(50),
|
||||
selfServiceRatio: z.number().min(0).max(1),
|
||||
})
|
||||
),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
for (const pref of input.preferences) {
|
||||
const member = await ctx.prisma.juryGroupMember.findUnique({
|
||||
where: { id: pref.juryGroupMemberId },
|
||||
})
|
||||
if (!member || member.userId !== ctx.user.id) continue
|
||||
|
||||
await ctx.prisma.juryGroupMember.update({
|
||||
where: { id: pref.juryGroupMemberId },
|
||||
data: {
|
||||
selfServiceCap: pref.selfServiceCap,
|
||||
selfServiceRatio: pref.selfServiceRatio,
|
||||
},
|
||||
})
|
||||
}
|
||||
return { success: true }
|
||||
}),
|
||||
|
||||
/**
|
||||
* Get onboarding context for the current user.
|
||||
* Returns jury group memberships that allow self-service preferences.
|
||||
* Returns jury group memberships for self-service preferences.
|
||||
*/
|
||||
getOnboardingContext: protectedProcedure.query(async ({ ctx }) => {
|
||||
const memberships = await ctx.prisma.juryGroupMember.findMany({
|
||||
@@ -1170,29 +1189,20 @@ export const userRouter = router({
|
||||
id: true,
|
||||
name: true,
|
||||
defaultMaxAssignments: true,
|
||||
allowJurorCapAdjustment: true,
|
||||
allowJurorRatioAdjustment: true,
|
||||
categoryQuotasEnabled: true,
|
||||
defaultCategoryQuotas: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const selfServiceGroups = memberships.filter(
|
||||
(m) => m.juryGroup.allowJurorCapAdjustment || m.juryGroup.allowJurorRatioAdjustment,
|
||||
)
|
||||
|
||||
return {
|
||||
hasSelfServiceOptions: selfServiceGroups.length > 0,
|
||||
memberships: selfServiceGroups.map((m) => ({
|
||||
hasSelfServiceOptions: memberships.length > 0,
|
||||
memberships: memberships.map((m) => ({
|
||||
juryGroupMemberId: m.id,
|
||||
juryGroupName: m.juryGroup.name,
|
||||
currentCap: m.maxAssignmentsOverride ?? m.juryGroup.defaultMaxAssignments,
|
||||
allowCapAdjustment: m.juryGroup.allowJurorCapAdjustment,
|
||||
allowRatioAdjustment: m.juryGroup.allowJurorRatioAdjustment,
|
||||
selfServiceCap: m.selfServiceCap,
|
||||
selfServiceRatio: m.selfServiceRatio,
|
||||
preferredStartupRatio: m.preferredStartupRatio,
|
||||
})),
|
||||
}
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user