feat(user): context-aware default dashboard (§D.1)
user.getDefaultDashboard returns the highest-priority role for which the user has actionable work right now — pending eval in active round, active mentoring assignment, applicant project in active round, etc. — falling back to static priority order if nothing is actionable. src/app/page.tsx now reads roles[] (multi-role array) instead of just the primary role, fixing the bug where mentor+juror users always landed on their primary role's dashboard. Uses static priority for simplicity in the server component; the context-aware procedure remains available for client surfaces. Tests cover six cases: super-admin, juror with active eval, juror+observer fallback, mentor+juror in mentoring round, both-active-priority-tiebreak, observer-only. Plan: docs/superpowers/plans/2026-04-28-pr6-multi-role-and-workspace-previews.md
This commit is contained in:
@@ -2039,4 +2039,109 @@ export const userRouter = router({
|
||||
|
||||
return { ended: true }
|
||||
}),
|
||||
|
||||
/**
|
||||
* Context-aware default dashboard. Returns the highest-priority role for
|
||||
* 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 >
|
||||
* MENTOR > APPLICANT > OBSERVER > AUDIENCE.
|
||||
*
|
||||
* Used by src/app/page.tsx to route users at login.
|
||||
*/
|
||||
getDefaultDashboard: protectedProcedure.query(async ({ ctx }) => {
|
||||
const user = await ctx.prisma.user.findUniqueOrThrow({
|
||||
where: { id: ctx.user.id },
|
||||
select: { id: true, roles: true, role: true },
|
||||
})
|
||||
|
||||
const userRoles = new Set<UserRole>([...(user.roles ?? []), user.role])
|
||||
|
||||
type Entry = { role: UserRole; path: string; predicate: () => Promise<boolean> | boolean }
|
||||
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',
|
||||
predicate: async () => {
|
||||
const cnt = await ctx.prisma.assignment.count({
|
||||
where: {
|
||||
userId: user.id,
|
||||
isCompleted: false,
|
||||
round: { status: 'ROUND_ACTIVE' },
|
||||
},
|
||||
})
|
||||
return cnt > 0
|
||||
},
|
||||
},
|
||||
{
|
||||
role: 'MENTOR',
|
||||
path: '/mentor',
|
||||
predicate: async () => {
|
||||
const cnt = await ctx.prisma.mentorAssignment.count({
|
||||
where: {
|
||||
mentorId: user.id,
|
||||
workspaceEnabled: true,
|
||||
project: {
|
||||
projectRoundStates: {
|
||||
some: { round: { status: 'ROUND_ACTIVE' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return cnt > 0
|
||||
},
|
||||
},
|
||||
{
|
||||
role: 'APPLICANT',
|
||||
path: '/applicant',
|
||||
predicate: async () => {
|
||||
const cnt = await ctx.prisma.teamMember.count({
|
||||
where: {
|
||||
userId: user.id,
|
||||
project: {
|
||||
projectRoundStates: {
|
||||
some: {
|
||||
round: { status: 'ROUND_ACTIVE' },
|
||||
state: { in: ['PENDING', 'IN_PROGRESS'] },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
return cnt > 0
|
||||
},
|
||||
},
|
||||
{ role: 'OBSERVER', path: '/observer', predicate: () => false },
|
||||
{ role: 'AUDIENCE', path: '/applicant', predicate: () => false },
|
||||
]
|
||||
|
||||
// Walk priority. Return first role the user holds whose predicate is true.
|
||||
for (const entry of PRIORITY) {
|
||||
if (!userRoles.has(entry.role)) continue
|
||||
const has = await entry.predicate()
|
||||
if (has) {
|
||||
return { role: entry.role, path: entry.path, reason: 'has-active-work' as const }
|
||||
}
|
||||
}
|
||||
|
||||
// Static fallback: highest-priority role they hold.
|
||||
for (const entry of PRIORITY) {
|
||||
if (userRoles.has(entry.role)) {
|
||||
return { role: entry.role, path: entry.path, reason: 'static-fallback' as const }
|
||||
}
|
||||
}
|
||||
|
||||
return { role: 'APPLICANT' as UserRole, path: '/applicant', reason: 'static-fallback' as const }
|
||||
}),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user