refactor(awards): remove AWARD_MASTER role, fold features into jury chair flow
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m5s
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m5s
The AWARD_MASTER role split sponsor jurors into a parallel UI that hid project files (only showed when the award was anchored to an evaluation round) and duplicated the jury voting path with no real difference in authority — tie-break and finalize were already governed by AwardJuror.isChair regardless of the user's global role. Inviting a juror via the award page defaulted to AWARD_MASTER, randomly fragmenting jury panels. This collapses the role into JURY_MEMBER + isChair: - specialAward.getMyAwardDetail now returns evaluation scores, chair visibility into other jurors' votes, and juror roster - specialAward.submitVote accepts an optional justification per vote - specialAward.confirmWinner moves from awardMasterProcedure to protectedProcedure (juror+chair check inside) - bulkInviteJurors creates JURY_MEMBER accounts and, when the award has a juryGroupId, also adds them to that JuryGroup so they appear on the round-page jury panel - jury award page renders justification, eval-score badges, and a chair tools panel with vote tally + finalize-winner CTA - juryGroup.list includes attached SpecialAwards; the jury-list UI shows a trophy pill alongside round pills - (award-master) route group, awardMasterProcedure, AWARD_MASTER role enum value, and AWARD_MASTER_DECISION decisionMode are deleted - migration demotes any residual AWARD_MASTER users to JURY_MEMBER and recreates the UserRole enum without the value Coup de Coeur on prod: Didier (the sponsor juror added today as AWARD_MASTER by the buggy invite form) was migrated to JURY_MEMBER and attached to the existing "Coup de Coeur" JuryGroup; the SpecialAward itself was linked to that group (juryGroupId was NULL). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,7 +22,7 @@ export const fileRouter = router({
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const isAdminOrObserver = ['SUPER_ADMIN', 'PROGRAM_ADMIN', 'OBSERVER', 'AWARD_MASTER'].includes(ctx.user.role)
|
||||
const isAdminOrObserver = ['SUPER_ADMIN', 'PROGRAM_ADMIN', 'OBSERVER'].includes(ctx.user.role)
|
||||
|
||||
if (!isAdminOrObserver) {
|
||||
const file = await ctx.prisma.projectFile.findFirst({
|
||||
@@ -321,7 +321,7 @@ export const fileRouter = router({
|
||||
roundId: z.string().optional(),
|
||||
}))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const isAdminOrObserver = ['SUPER_ADMIN', 'PROGRAM_ADMIN', 'OBSERVER', 'AWARD_MASTER'].includes(ctx.user.role)
|
||||
const isAdminOrObserver = ['SUPER_ADMIN', 'PROGRAM_ADMIN', 'OBSERVER'].includes(ctx.user.role)
|
||||
|
||||
if (!isAdminOrObserver) {
|
||||
const [juryAssignment, mentorAssignment, teamMembership] = await Promise.all([
|
||||
@@ -393,7 +393,7 @@ export const fileRouter = router({
|
||||
roundId: z.string(),
|
||||
}))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const isAdminOrObserver = ['SUPER_ADMIN', 'PROGRAM_ADMIN', 'OBSERVER', 'AWARD_MASTER'].includes(ctx.user.role)
|
||||
const isAdminOrObserver = ['SUPER_ADMIN', 'PROGRAM_ADMIN', 'OBSERVER'].includes(ctx.user.role)
|
||||
|
||||
if (!isAdminOrObserver) {
|
||||
const [juryAssignment, mentorAssignment, teamMembership] = await Promise.all([
|
||||
@@ -716,7 +716,7 @@ export const fileRouter = router({
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const isAdminOrObserver = ['SUPER_ADMIN', 'PROGRAM_ADMIN', 'OBSERVER', 'AWARD_MASTER'].includes(ctx.user.role)
|
||||
const isAdminOrObserver = ['SUPER_ADMIN', 'PROGRAM_ADMIN', 'OBSERVER'].includes(ctx.user.role)
|
||||
|
||||
// For non-admin/observer callers, mirror the per-round scope used by
|
||||
// file.getDownloadUrl: a juror assigned to round N may only pull URLs
|
||||
|
||||
@@ -102,6 +102,10 @@ export const juryGroupRouter = router({
|
||||
select: { id: true, name: true, roundType: true, status: true },
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
},
|
||||
awards: {
|
||||
select: { id: true, name: true, status: true },
|
||||
orderBy: { sortOrder: 'asc' },
|
||||
},
|
||||
members: {
|
||||
take: 5,
|
||||
orderBy: { joinedAt: 'asc' },
|
||||
|
||||
@@ -177,11 +177,11 @@ export const projectRouter = router({
|
||||
]
|
||||
}
|
||||
|
||||
// Per-role visibility filters. Admin / Observer / Award master see all
|
||||
// (these roles are designed for cross-program oversight). Other roles
|
||||
// are scoped to projects they have a relationship with.
|
||||
// Per-role visibility filters. Admin / Observer see all (these roles
|
||||
// are designed for cross-program oversight). Other roles are scoped to
|
||||
// projects they have a relationship with.
|
||||
const isAdmin = userHasRole(ctx.user, 'SUPER_ADMIN', 'PROGRAM_ADMIN')
|
||||
const isObserverLevel = userHasRole(ctx.user, 'OBSERVER', 'AWARD_MASTER')
|
||||
const isObserverLevel = userHasRole(ctx.user, 'OBSERVER')
|
||||
if (!isAdmin && !isObserverLevel) {
|
||||
const orClauses: Array<Record<string, unknown>> = []
|
||||
if (userHasRole(ctx.user, 'JURY_MEMBER')) {
|
||||
@@ -534,10 +534,10 @@ export const projectRouter = router({
|
||||
// ProjectTag table may not exist yet
|
||||
}
|
||||
|
||||
// Per-role access check. Admin / Observer / Award master can read any
|
||||
// project. Jury / Mentor / Applicant must have a relationship to it.
|
||||
// Per-role access check. Admin / Observer can read any project.
|
||||
// Jury / Mentor / Applicant must have a relationship to it.
|
||||
const isAdmin = userHasRole(ctx.user, 'SUPER_ADMIN', 'PROGRAM_ADMIN')
|
||||
const isObserverLevel = userHasRole(ctx.user, 'OBSERVER', 'AWARD_MASTER')
|
||||
const isObserverLevel = userHasRole(ctx.user, 'OBSERVER')
|
||||
if (!isAdmin && !isObserverLevel) {
|
||||
const checks: Array<Promise<unknown>> = []
|
||||
if (userHasRole(ctx.user, 'JURY_MEMBER')) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from 'zod'
|
||||
import { TRPCError } from '@trpc/server'
|
||||
import { Prisma } from '@prisma/client'
|
||||
import { router, protectedProcedure, adminProcedure, awardMasterProcedure } from '../trpc'
|
||||
import { router, protectedProcedure, adminProcedure } from '../trpc'
|
||||
import { getUserAvatarUrl } from '../utils/avatar-url'
|
||||
import { logAudit } from '../utils/audit'
|
||||
import { processEligibilityJob } from '../services/award-eligibility-job'
|
||||
@@ -267,7 +267,7 @@ export const specialAwardRouter = router({
|
||||
evaluationRoundId: z.string().nullable().optional(),
|
||||
juryGroupId: z.string().nullable().optional(),
|
||||
eligibilityMode: z.enum(['STAY_IN_MAIN', 'SEPARATE_POOL']).optional(),
|
||||
decisionMode: z.enum(['JURY_VOTE', 'AWARD_MASTER_DECISION', 'ADMIN_DECISION']).nullable().optional(),
|
||||
decisionMode: z.enum(['JURY_VOTE', 'ADMIN_DECISION']).nullable().optional(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
@@ -656,13 +656,15 @@ export const specialAwardRouter = router({
|
||||
}),
|
||||
|
||||
/**
|
||||
* Bulk invite new users as award jurors — creates accounts, assigns role, sends invite emails
|
||||
* Bulk invite new users as award jurors. Creates JURY_MEMBER accounts,
|
||||
* attaches them as AwardJuror, and (if the award has an assigned jury
|
||||
* group) also adds them as JuryGroupMember so they appear on the
|
||||
* round-page jury panel alongside existing members.
|
||||
*/
|
||||
bulkInviteJurors: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
awardId: z.string(),
|
||||
role: z.enum(['JURY_MEMBER', 'AWARD_MASTER']).default('AWARD_MASTER'),
|
||||
invitees: z.array(
|
||||
z.object({
|
||||
name: z.string().optional(),
|
||||
@@ -674,7 +676,7 @@ export const specialAwardRouter = router({
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const award = await ctx.prisma.specialAward.findUniqueOrThrow({
|
||||
where: { id: input.awardId },
|
||||
select: { id: true, name: true },
|
||||
select: { id: true, name: true, juryGroupId: true },
|
||||
})
|
||||
|
||||
const results: Array<{ email: string; status: 'created' | 'existing' | 'error'; error?: string }> = []
|
||||
@@ -694,7 +696,7 @@ export const specialAwardRouter = router({
|
||||
data: {
|
||||
email: invitee.email,
|
||||
name: invitee.name || null,
|
||||
role: input.role,
|
||||
role: 'JURY_MEMBER',
|
||||
status: 'INVITED',
|
||||
inviteToken,
|
||||
inviteTokenExpiresAt: new Date(Date.now() + expiryMs),
|
||||
@@ -734,6 +736,23 @@ export const specialAwardRouter = router({
|
||||
create: { awardId: input.awardId, userId: user.id },
|
||||
})
|
||||
|
||||
if (award.juryGroupId) {
|
||||
await ctx.prisma.juryGroupMember.upsert({
|
||||
where: {
|
||||
juryGroupId_userId: {
|
||||
juryGroupId: award.juryGroupId,
|
||||
userId: user.id,
|
||||
},
|
||||
},
|
||||
update: {},
|
||||
create: {
|
||||
juryGroupId: award.juryGroupId,
|
||||
userId: user.id,
|
||||
role: 'MEMBER',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// For existing-user invitees the new-account invite email above
|
||||
// never fired (no `created` branch). Send the juror-assignment
|
||||
// notification so they know they were added — but only if this
|
||||
@@ -760,7 +779,7 @@ export const specialAwardRouter = router({
|
||||
detailsJson: {
|
||||
action: 'BULK_INVITE',
|
||||
awardName: award.name,
|
||||
role: input.role,
|
||||
juryGroupId: award.juryGroupId,
|
||||
count: input.invitees.length,
|
||||
results,
|
||||
},
|
||||
@@ -842,12 +861,14 @@ export const specialAwardRouter = router({
|
||||
}),
|
||||
|
||||
/**
|
||||
* Get award detail for voting (jury view)
|
||||
* Get award detail for voting (jury view). Includes upstream evaluation
|
||||
* scores per project (when the award is anchored to an evaluation round)
|
||||
* and — for the chair — the votes cast by other jurors so they can
|
||||
* tie-break and finalize the award.
|
||||
*/
|
||||
getMyAwardDetail: protectedProcedure
|
||||
.input(z.object({ awardId: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
// Verify user is a juror
|
||||
const juror = await ctx.prisma.awardJuror.findUnique({
|
||||
where: {
|
||||
awardId_userId: {
|
||||
@@ -864,8 +885,7 @@ export const specialAwardRouter = router({
|
||||
})
|
||||
}
|
||||
|
||||
// Fetch award, eligible projects, and votes in parallel
|
||||
const [award, eligibleProjects, myVotes] = await Promise.all([
|
||||
const [award, eligibleProjects, myVotes, allJurors] = await Promise.all([
|
||||
ctx.prisma.specialAward.findUniqueOrThrow({
|
||||
where: { id: input.awardId },
|
||||
}),
|
||||
@@ -917,62 +937,12 @@ export const specialAwardRouter = router({
|
||||
ctx.prisma.awardVote.findMany({
|
||||
where: { awardId: input.awardId, userId: ctx.user.id },
|
||||
}),
|
||||
])
|
||||
|
||||
const projectsRaw = eligibleProjects.map((e) => e.project)
|
||||
const projectsWithLogos = await attachProjectLogoUrls(projectsRaw)
|
||||
|
||||
return {
|
||||
award,
|
||||
projects: projectsWithLogos,
|
||||
myVotes,
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Enhanced award detail for Award Master — includes project scores and chair vote visibility
|
||||
*/
|
||||
getMyAwardDetailEnhanced: awardMasterProcedure
|
||||
.input(z.object({ awardId: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const juror = await ctx.prisma.awardJuror.findUnique({
|
||||
where: {
|
||||
awardId_userId: { awardId: input.awardId, userId: ctx.user.id },
|
||||
},
|
||||
})
|
||||
if (!juror) {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: 'You are not assigned to this award' })
|
||||
}
|
||||
|
||||
const [award, eligibleProjects, myVotes, allJurors] = await Promise.all([
|
||||
ctx.prisma.specialAward.findUniqueOrThrow({
|
||||
where: { id: input.awardId },
|
||||
include: {
|
||||
competition: { select: { id: true, name: true } },
|
||||
},
|
||||
}),
|
||||
ctx.prisma.awardEligibility.findMany({
|
||||
where: { awardId: input.awardId, eligible: true },
|
||||
include: {
|
||||
project: {
|
||||
select: {
|
||||
id: true, title: true, teamName: true, description: true,
|
||||
competitionCategory: true, country: true, tags: true,
|
||||
logoKey: true, logoProvider: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
ctx.prisma.awardVote.findMany({
|
||||
where: { awardId: input.awardId, userId: ctx.user.id },
|
||||
}),
|
||||
ctx.prisma.awardJuror.findMany({
|
||||
where: { awardId: input.awardId },
|
||||
select: { userId: true, isChair: true, user: { select: { name: true } } },
|
||||
}),
|
||||
])
|
||||
|
||||
// Fetch evaluation scores for eligible projects
|
||||
const projectIds = eligibleProjects.map((e) => e.project.id)
|
||||
const projectScores: Record<string, { avg: number; count: number }> = {}
|
||||
|
||||
@@ -1007,7 +977,6 @@ export const specialAwardRouter = router({
|
||||
}
|
||||
}
|
||||
|
||||
// Chair sees other votes
|
||||
const isSolo = allJurors.length === 1
|
||||
const isChair = juror.isChair || isSolo
|
||||
let otherVotes: Array<{ userId: string; userName: string | null; projectId: string; justification: string | null }> = []
|
||||
@@ -1047,7 +1016,8 @@ export const specialAwardRouter = router({
|
||||
// ─── Voting ─────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Submit vote (PICK_WINNER or RANKED)
|
||||
* Submit vote (PICK_WINNER or RANKED). For PICK_WINNER, jurors may attach
|
||||
* an optional justification — visible to the chair when they review votes.
|
||||
*/
|
||||
submitVote: protectedProcedure
|
||||
.input(
|
||||
@@ -1057,12 +1027,12 @@ export const specialAwardRouter = router({
|
||||
z.object({
|
||||
projectId: z.string(),
|
||||
rank: z.number().int().min(1).optional(),
|
||||
justification: z.string().max(2000).optional(),
|
||||
})
|
||||
),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
// Verify juror
|
||||
const juror = await ctx.prisma.awardJuror.findUnique({
|
||||
where: {
|
||||
awardId_userId: {
|
||||
@@ -1079,7 +1049,6 @@ export const specialAwardRouter = router({
|
||||
})
|
||||
}
|
||||
|
||||
// Verify award is open for voting
|
||||
const award = await ctx.prisma.specialAward.findUniqueOrThrow({
|
||||
where: { id: input.awardId },
|
||||
})
|
||||
@@ -1091,7 +1060,6 @@ export const specialAwardRouter = router({
|
||||
})
|
||||
}
|
||||
|
||||
// Delete existing votes and create new ones
|
||||
await ctx.prisma.$transaction([
|
||||
ctx.prisma.awardVote.deleteMany({
|
||||
where: { awardId: input.awardId, userId: ctx.user.id },
|
||||
@@ -1103,6 +1071,7 @@ export const specialAwardRouter = router({
|
||||
userId: ctx.user.id,
|
||||
projectId: vote.projectId,
|
||||
rank: vote.rank,
|
||||
justification: vote.justification || null,
|
||||
},
|
||||
})
|
||||
),
|
||||
@@ -1123,55 +1092,6 @@ export const specialAwardRouter = router({
|
||||
return { submitted: input.votes.length }
|
||||
}),
|
||||
|
||||
/**
|
||||
* Submit award master vote with optional justification (PICK_WINNER only)
|
||||
*/
|
||||
submitAwardMasterVote: awardMasterProcedure
|
||||
.input(z.object({
|
||||
awardId: z.string(),
|
||||
projectId: z.string(),
|
||||
justification: z.string().max(2000).optional(),
|
||||
}))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const juror = await ctx.prisma.awardJuror.findUnique({
|
||||
where: { awardId_userId: { awardId: input.awardId, userId: ctx.user.id } },
|
||||
})
|
||||
if (!juror) {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: 'Not assigned to this award' })
|
||||
}
|
||||
|
||||
const award = await ctx.prisma.specialAward.findUniqueOrThrow({
|
||||
where: { id: input.awardId },
|
||||
})
|
||||
if (award.status !== 'VOTING_OPEN') {
|
||||
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Voting is not open' })
|
||||
}
|
||||
|
||||
await ctx.prisma.$transaction([
|
||||
ctx.prisma.awardVote.deleteMany({
|
||||
where: { awardId: input.awardId, userId: ctx.user.id },
|
||||
}),
|
||||
ctx.prisma.awardVote.create({
|
||||
data: {
|
||||
awardId: input.awardId,
|
||||
userId: ctx.user.id,
|
||||
projectId: input.projectId,
|
||||
justification: input.justification || null,
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
await logAudit({
|
||||
userId: ctx.user.id,
|
||||
action: 'CREATE',
|
||||
entityType: 'AwardVote',
|
||||
entityId: input.awardId,
|
||||
detailsJson: { awardId: input.awardId, projectId: input.projectId, mode: 'AWARD_MASTER_PICK' },
|
||||
})
|
||||
|
||||
return { submitted: true }
|
||||
}),
|
||||
|
||||
// ─── Results ────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -1286,7 +1206,7 @@ export const specialAwardRouter = router({
|
||||
/**
|
||||
* Chair confirms the winner — resolves tiebreaks, sets winner, closes the award
|
||||
*/
|
||||
confirmWinner: awardMasterProcedure
|
||||
confirmWinner: protectedProcedure
|
||||
.input(z.object({ awardId: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const allJurors = await ctx.prisma.awardJuror.findMany({
|
||||
|
||||
@@ -217,8 +217,8 @@ export const userRouter = router({
|
||||
list: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
role: z.enum(['SUPER_ADMIN', 'PROGRAM_ADMIN', 'AWARD_MASTER', 'JURY_MEMBER', 'MENTOR', 'OBSERVER']).optional(),
|
||||
roles: z.array(z.enum(['SUPER_ADMIN', 'PROGRAM_ADMIN', 'AWARD_MASTER', 'JURY_MEMBER', 'MENTOR', 'OBSERVER'])).optional(),
|
||||
role: z.enum(['SUPER_ADMIN', 'PROGRAM_ADMIN', 'JURY_MEMBER', 'MENTOR', 'OBSERVER']).optional(),
|
||||
roles: z.array(z.enum(['SUPER_ADMIN', 'PROGRAM_ADMIN', 'JURY_MEMBER', 'MENTOR', 'OBSERVER'])).optional(),
|
||||
status: z.enum(['NONE', 'INVITED', 'ACTIVE', 'SUSPENDED']).optional(),
|
||||
search: z.string().optional(),
|
||||
page: z.number().int().min(1).default(1),
|
||||
@@ -364,8 +364,8 @@ export const userRouter = router({
|
||||
listInvitableIds: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
role: z.enum(['SUPER_ADMIN', 'PROGRAM_ADMIN', 'AWARD_MASTER', 'JURY_MEMBER', 'MENTOR', 'OBSERVER']).optional(),
|
||||
roles: z.array(z.enum(['SUPER_ADMIN', 'PROGRAM_ADMIN', 'AWARD_MASTER', 'JURY_MEMBER', 'MENTOR', 'OBSERVER'])).optional(),
|
||||
role: z.enum(['SUPER_ADMIN', 'PROGRAM_ADMIN', 'JURY_MEMBER', 'MENTOR', 'OBSERVER']).optional(),
|
||||
roles: z.array(z.enum(['SUPER_ADMIN', 'PROGRAM_ADMIN', 'JURY_MEMBER', 'MENTOR', 'OBSERVER'])).optional(),
|
||||
search: z.string().optional(),
|
||||
})
|
||||
)
|
||||
@@ -457,7 +457,7 @@ export const userRouter = router({
|
||||
z.object({
|
||||
email: z.string().email(),
|
||||
name: z.string().optional(),
|
||||
role: z.enum(['SUPER_ADMIN', 'PROGRAM_ADMIN', 'AWARD_MASTER', 'JURY_MEMBER', 'MENTOR', 'OBSERVER']).default('JURY_MEMBER'),
|
||||
role: z.enum(['SUPER_ADMIN', 'PROGRAM_ADMIN', 'JURY_MEMBER', 'MENTOR', 'OBSERVER']).default('JURY_MEMBER'),
|
||||
expertiseTags: z.array(z.string()).optional(),
|
||||
maxAssignments: z.number().int().min(1).max(100).optional(),
|
||||
})
|
||||
@@ -528,8 +528,8 @@ export const userRouter = router({
|
||||
id: z.string(),
|
||||
email: z.string().email().optional(),
|
||||
name: z.string().optional().nullable(),
|
||||
role: z.enum(['SUPER_ADMIN', 'PROGRAM_ADMIN', 'AWARD_MASTER', 'JURY_MEMBER', 'MENTOR', 'OBSERVER', 'APPLICANT', 'AUDIENCE']).optional(),
|
||||
roles: z.array(z.enum(['SUPER_ADMIN', 'PROGRAM_ADMIN', 'AWARD_MASTER', 'JURY_MEMBER', 'MENTOR', 'OBSERVER', 'APPLICANT', 'AUDIENCE'])).optional(),
|
||||
role: z.enum(['SUPER_ADMIN', 'PROGRAM_ADMIN', 'JURY_MEMBER', 'MENTOR', 'OBSERVER', 'APPLICANT', 'AUDIENCE']).optional(),
|
||||
roles: z.array(z.enum(['SUPER_ADMIN', 'PROGRAM_ADMIN', 'JURY_MEMBER', 'MENTOR', 'OBSERVER', 'APPLICANT', 'AUDIENCE'])).optional(),
|
||||
status: z.enum(['NONE', 'INVITED', 'ACTIVE', 'SUSPENDED']).optional(),
|
||||
expertiseTags: z.array(z.string()).optional(),
|
||||
maxAssignments: z.number().int().min(1).max(100).optional().nullable(),
|
||||
@@ -708,7 +708,7 @@ export const userRouter = router({
|
||||
z.object({
|
||||
email: z.string().email(),
|
||||
name: z.string().optional(),
|
||||
role: z.enum(['SUPER_ADMIN', 'PROGRAM_ADMIN', 'AWARD_MASTER', 'JURY_MEMBER', 'MENTOR', 'OBSERVER']).default('JURY_MEMBER'),
|
||||
role: z.enum(['SUPER_ADMIN', 'PROGRAM_ADMIN', 'JURY_MEMBER', 'MENTOR', 'OBSERVER']).default('JURY_MEMBER'),
|
||||
expertiseTags: z.array(z.string()).optional(),
|
||||
// Optional pre-assignments for jury members
|
||||
assignments: z
|
||||
@@ -1835,7 +1835,7 @@ export const userRouter = router({
|
||||
}
|
||||
|
||||
// Set primary role to highest privilege role
|
||||
const rolePriority: UserRole[] = ['SUPER_ADMIN', 'PROGRAM_ADMIN', 'JURY_MEMBER', 'MENTOR', 'OBSERVER', 'AWARD_MASTER', 'APPLICANT', 'AUDIENCE']
|
||||
const rolePriority: UserRole[] = ['SUPER_ADMIN', 'PROGRAM_ADMIN', 'JURY_MEMBER', 'MENTOR', 'OBSERVER', 'APPLICANT', 'AUDIENCE']
|
||||
const primaryRole = rolePriority.find(r => input.roles.includes(r)) || input.roles[0]
|
||||
|
||||
return ctx.prisma.user.update({
|
||||
@@ -1936,7 +1936,6 @@ export const userRouter = router({
|
||||
'JURY_MEMBER',
|
||||
'MENTOR',
|
||||
'OBSERVER',
|
||||
'AWARD_MASTER',
|
||||
'APPLICANT',
|
||||
'AUDIENCE',
|
||||
]
|
||||
@@ -2236,7 +2235,7 @@ export const userRouter = router({
|
||||
* which the user has actionable work right now, or the highest-priority
|
||||
* role they hold (static fallback) if nothing is actionable.
|
||||
*
|
||||
* Priority order: SUPER_ADMIN > PROGRAM_ADMIN > AWARD_MASTER > JURY_MEMBER >
|
||||
* Priority order: SUPER_ADMIN > PROGRAM_ADMIN > JURY_MEMBER >
|
||||
* MENTOR > APPLICANT > OBSERVER > AUDIENCE.
|
||||
*
|
||||
* Used by src/app/page.tsx to route users at login.
|
||||
@@ -2253,14 +2252,6 @@ export const userRouter = router({
|
||||
const PRIORITY: Entry[] = [
|
||||
{ role: 'SUPER_ADMIN', path: '/admin', predicate: () => true },
|
||||
{ role: 'PROGRAM_ADMIN', path: '/admin', predicate: () => true },
|
||||
{
|
||||
role: 'AWARD_MASTER',
|
||||
path: '/award-master',
|
||||
predicate: async () => {
|
||||
const cnt = await ctx.prisma.awardJuror.count({ where: { userId: user.id } })
|
||||
return cnt > 0
|
||||
},
|
||||
},
|
||||
{
|
||||
role: 'JURY_MEMBER',
|
||||
path: '/jury',
|
||||
|
||||
Reference in New Issue
Block a user