feat: applicant dashboard — team cards, editable description, feedback visibility
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:
2026-03-05 17:08:19 +01:00
parent 94814bd505
commit ffe12a9e85
5 changed files with 247 additions and 67 deletions

View File

@@ -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 }) => {

View File

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