feat: applicant dashboard — team cards, editable description, feedback visibility
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m20s
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m20s
- Replace flat team names list with proper cards showing roles and badges - Hide TeamMembers from metadata display, remove Withdraw from header - Add inline-editable project description (admin-toggleable setting) - Move applicant feedback visibility from per-round config to admin settings - Support EVALUATION, LIVE_FINAL, DELIBERATION round types in feedback - Backwards-compatible: falls back to old per-round config if no settings exist - Add observer team tab toggle and 10 new SystemSettings seed entries Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1514,35 +1514,78 @@ export const applicantRouter = router({
|
||||
// Check if mentor is assigned
|
||||
const hasMentor = !!project.mentorAssignment
|
||||
|
||||
// Check if there are EVALUATION rounds (CLOSED/ARCHIVED) with applicantVisibility.enabled
|
||||
// Only consider rounds the project actually participated in (award track filtering)
|
||||
// Check if feedback is available — first check admin settings, then fall back to per-round config
|
||||
let hasEvaluationRounds = false
|
||||
if (project.programId) {
|
||||
const projectRoundIds = new Set(
|
||||
(await ctx.prisma.projectRoundState.findMany({
|
||||
where: { projectId: project.id },
|
||||
select: { roundId: true },
|
||||
})).map((prs) => prs.roundId)
|
||||
)
|
||||
|
||||
const closedEvalRounds = projectRoundIds.size > 0
|
||||
? await ctx.prisma.round.findMany({
|
||||
where: {
|
||||
competition: { programId: project.programId },
|
||||
roundType: 'EVALUATION',
|
||||
status: { in: ['ROUND_CLOSED', 'ROUND_ARCHIVED'] },
|
||||
id: { in: [...projectRoundIds] },
|
||||
},
|
||||
select: { configJson: true },
|
||||
})
|
||||
: []
|
||||
const navProjectRejected = await isProjectRejected(ctx.prisma, project.id)
|
||||
hasEvaluationRounds = closedEvalRounds.some((r) => {
|
||||
const parsed = EvaluationConfigSchema.safeParse(r.configJson)
|
||||
if (!parsed.success || !parsed.data.applicantVisibility.enabled) return false
|
||||
if (parsed.data.applicantVisibility.hideFromRejected && navProjectRejected) return false
|
||||
return true
|
||||
// Check admin settings first
|
||||
const adminFlags = await ctx.prisma.systemSettings.findMany({
|
||||
where: { key: { in: [
|
||||
'applicant_show_evaluation_feedback',
|
||||
'applicant_show_livefinal_feedback',
|
||||
'applicant_show_deliberation_feedback',
|
||||
'applicant_hide_feedback_from_rejected',
|
||||
] } },
|
||||
})
|
||||
const adminMap = new Map(adminFlags.map((s) => [s.key, s.value]))
|
||||
const adminSettingsExist = adminFlags.length > 0
|
||||
|
||||
const navProjectRejected = await isProjectRejected(ctx.prisma, project.id)
|
||||
|
||||
if (adminSettingsExist) {
|
||||
// Use admin settings
|
||||
if (adminMap.get('applicant_hide_feedback_from_rejected') === 'true' && navProjectRejected) {
|
||||
hasEvaluationRounds = false
|
||||
} else {
|
||||
const anyEnabled = adminMap.get('applicant_show_evaluation_feedback') === 'true'
|
||||
|| adminMap.get('applicant_show_livefinal_feedback') === 'true'
|
||||
|| adminMap.get('applicant_show_deliberation_feedback') === 'true'
|
||||
if (anyEnabled) {
|
||||
const enabledTypes: RoundType[] = []
|
||||
if (adminMap.get('applicant_show_evaluation_feedback') === 'true') enabledTypes.push('EVALUATION')
|
||||
if (adminMap.get('applicant_show_livefinal_feedback') === 'true') enabledTypes.push('LIVE_FINAL')
|
||||
if (adminMap.get('applicant_show_deliberation_feedback') === 'true') enabledTypes.push('DELIBERATION')
|
||||
|
||||
const projectRoundIds = (await ctx.prisma.projectRoundState.findMany({
|
||||
where: { projectId: project.id },
|
||||
select: { roundId: true },
|
||||
})).map((prs) => prs.roundId)
|
||||
|
||||
hasEvaluationRounds = projectRoundIds.length > 0 && await ctx.prisma.round.count({
|
||||
where: {
|
||||
competition: { programId: project.programId },
|
||||
roundType: { in: enabledTypes },
|
||||
status: { in: ['ROUND_CLOSED', 'ROUND_ARCHIVED'] },
|
||||
id: { in: projectRoundIds },
|
||||
},
|
||||
}) > 0
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fall back to old per-round config
|
||||
const projectRoundIds = new Set(
|
||||
(await ctx.prisma.projectRoundState.findMany({
|
||||
where: { projectId: project.id },
|
||||
select: { roundId: true },
|
||||
})).map((prs) => prs.roundId)
|
||||
)
|
||||
const closedEvalRounds = projectRoundIds.size > 0
|
||||
? await ctx.prisma.round.findMany({
|
||||
where: {
|
||||
competition: { programId: project.programId },
|
||||
roundType: 'EVALUATION',
|
||||
status: { in: ['ROUND_CLOSED', 'ROUND_ARCHIVED'] },
|
||||
id: { in: [...projectRoundIds] },
|
||||
},
|
||||
select: { configJson: true },
|
||||
})
|
||||
: []
|
||||
hasEvaluationRounds = closedEvalRounds.some((r) => {
|
||||
const parsed = EvaluationConfigSchema.safeParse(r.configJson)
|
||||
if (!parsed.success || !parsed.data.applicantVisibility.enabled) return false
|
||||
if (parsed.data.applicantVisibility.hideFromRejected && navProjectRejected) return false
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return { hasMentor, hasEvaluationRounds }
|
||||
@@ -2518,6 +2561,40 @@ export const applicantRouter = router({
|
||||
return { success: true, requesting: input.requesting }
|
||||
}),
|
||||
|
||||
updateDescription: protectedProcedure
|
||||
.input(z.object({
|
||||
projectId: z.string(),
|
||||
description: z.string().max(10000),
|
||||
}))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
if (ctx.user.role !== 'APPLICANT') {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: 'Only applicants can update descriptions' })
|
||||
}
|
||||
|
||||
// Check admin setting
|
||||
const setting = await ctx.prisma.systemSettings.findUnique({
|
||||
where: { key: 'applicant_allow_description_edit' },
|
||||
})
|
||||
if (setting?.value !== 'true') {
|
||||
throw new TRPCError({ code: 'FORBIDDEN', message: 'Description editing is currently disabled' })
|
||||
}
|
||||
|
||||
// Verify membership
|
||||
const member = await ctx.prisma.teamMember.findFirst({
|
||||
where: { projectId: input.projectId, userId: ctx.user.id },
|
||||
})
|
||||
if (!member) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: 'Project not found' })
|
||||
}
|
||||
|
||||
await ctx.prisma.project.update({
|
||||
where: { id: input.projectId },
|
||||
data: { description: input.description },
|
||||
})
|
||||
|
||||
return { success: true }
|
||||
}),
|
||||
|
||||
withdrawFromCompetition: protectedProcedure
|
||||
.input(z.object({ projectId: z.string() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
|
||||
@@ -52,6 +52,7 @@ export const settingsRouter = router({
|
||||
'applicant_show_livefinal_feedback', 'applicant_show_livefinal_scores',
|
||||
'applicant_show_deliberation_feedback',
|
||||
'applicant_hide_feedback_from_rejected',
|
||||
'applicant_allow_description_edit',
|
||||
]
|
||||
const settings = await ctx.prisma.systemSettings.findMany({
|
||||
where: { key: { in: keys } },
|
||||
@@ -77,6 +78,7 @@ export const settingsRouter = router({
|
||||
deliberationEnabled: flag('applicant_show_deliberation_feedback'),
|
||||
hideFromRejected: flag('applicant_hide_feedback_from_rejected'),
|
||||
},
|
||||
applicantAllowDescriptionEdit: flag('applicant_allow_description_edit'),
|
||||
}
|
||||
}),
|
||||
|
||||
|
||||
Reference in New Issue
Block a user