Fix evaluation criteria, jury preferences, assignment config, and dashboard stats
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:
Matt
2026-02-17 12:33:20 +01:00
parent f9016168e7
commit ef1bf24388
16 changed files with 761 additions and 588 deletions

View File

@@ -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,
})),
}
}),