feat(final-docs): judges see all teams' prior-round files; revised uploads behind admin toggle
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m47s
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m47s
The finals jury needs the teams' EXISTING submissions (pitch deck, exec summary, business plan, videos from prior rounds) — which all 9 teams already have. So: - listFinalistDocumentsForReview now returns ALL of each finalist team's files across every round (labeled by doc type + round; finale uploads flagged 'Revised for finals'), with presigned URLs. NOT gated — judges always see. - Revised re-uploads are now an admin toggle (Round.configJson.allowFinalistRevisedUploads, default OFF): gates the banner/panel (getFinalDocumentStatusForProject), the upload guard (getUploadUrl/deleteFile), the documents-page round, and the reminders (manual + cron). When off, teams aren't prompted/able to upload. - finalist.get/setRevisedUploadSetting + a Switch on the admin finale overview. - judge review component rewritten to a per-team labeled file list. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@ import { sendStyledNotificationEmail, sendTeamMemberInviteEmail } from '@/lib/em
|
||||
import { logAudit } from '@/server/utils/audit'
|
||||
import { createNotification, notifyProjectMentors, NotificationTypes } from '../services/in-app-notification'
|
||||
import { checkRequirementsAndTransition, triggerInProgressOnActivity, transitionProject, isTerminalState } from '../services/round-engine'
|
||||
import { getFinalDocumentStatusForProject } from '../services/final-documents'
|
||||
import { getFinalDocumentStatusForProject, finalistUploadsEnabled } from '../services/final-documents'
|
||||
import { EvaluationConfigSchema, MentoringConfigSchema } from '@/types/competition-configs'
|
||||
import type { PrismaClient, Prisma, RoundType } from '@prisma/client'
|
||||
|
||||
@@ -337,12 +337,15 @@ export const applicantRouter = router({
|
||||
if (input.roundId) {
|
||||
const round = await ctx.prisma.round.findUnique({
|
||||
where: { id: input.roundId },
|
||||
select: { name: true, status: true, roundType: true, finalizedAt: true },
|
||||
select: { name: true, status: true, roundType: true, finalizedAt: true, configJson: true },
|
||||
})
|
||||
if (round) {
|
||||
const uploadable =
|
||||
round.status === 'ROUND_ACTIVE' ||
|
||||
(round.roundType === 'LIVE_FINAL' && round.status === 'ROUND_DRAFT' && !round.finalizedAt)
|
||||
round.roundType === 'LIVE_FINAL'
|
||||
? !round.finalizedAt &&
|
||||
(round.status === 'ROUND_DRAFT' || round.status === 'ROUND_ACTIVE') &&
|
||||
finalistUploadsEnabled(round.configJson)
|
||||
: round.status === 'ROUND_ACTIVE'
|
||||
if (!uploadable) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
@@ -568,12 +571,15 @@ export const applicantRouter = router({
|
||||
if (file.roundId) {
|
||||
const round = await ctx.prisma.round.findUnique({
|
||||
where: { id: file.roundId },
|
||||
select: { status: true, roundType: true, finalizedAt: true },
|
||||
select: { status: true, roundType: true, finalizedAt: true, configJson: true },
|
||||
})
|
||||
if (round) {
|
||||
const modifiable =
|
||||
round.status === 'ROUND_ACTIVE' ||
|
||||
(round.roundType === 'LIVE_FINAL' && round.status === 'ROUND_DRAFT' && !round.finalizedAt)
|
||||
round.roundType === 'LIVE_FINAL'
|
||||
? !round.finalizedAt &&
|
||||
(round.status === 'ROUND_DRAFT' || round.status === 'ROUND_ACTIVE') &&
|
||||
finalistUploadsEnabled(round.configJson)
|
||||
: round.status === 'ROUND_ACTIVE'
|
||||
if (!modifiable) {
|
||||
throw new TRPCError({
|
||||
code: 'BAD_REQUEST',
|
||||
@@ -1472,6 +1478,7 @@ export const applicantRouter = router({
|
||||
slug: true,
|
||||
roundType: true,
|
||||
windowCloseAt: true,
|
||||
configJson: true,
|
||||
specialAwardId: true,
|
||||
specialAward: { select: { name: true } },
|
||||
},
|
||||
@@ -1490,8 +1497,9 @@ export const applicantRouter = router({
|
||||
|
||||
openRounds = allActiveRounds
|
||||
.filter((r) => {
|
||||
// LIVE_FINAL (grand-final documents) only shows to enrolled finalists.
|
||||
if (r.roundType === 'LIVE_FINAL' && !projectRoundIds.has(r.id)) return false
|
||||
// LIVE_FINAL (grand-final documents) only shows to enrolled finalists,
|
||||
// and only when the admin has enabled revised uploads.
|
||||
if (r.roundType === 'LIVE_FINAL' && (!projectRoundIds.has(r.id) || !finalistUploadsEnabled(r.configJson))) return false
|
||||
// Award round project isn't in → hide
|
||||
if (r.specialAwardId && !projectRoundIds.has(r.id)) return false
|
||||
// Main round when project is in award track and has no state in this round → hide
|
||||
|
||||
@@ -1690,4 +1690,37 @@ export const finalistRouter = router({
|
||||
if (!allowed) throw new TRPCError({ code: 'FORBIDDEN', message: 'You do not have access to the finalist documents review.' })
|
||||
return listFinalistDocumentsForReview(ctx.prisma, input.programId)
|
||||
}),
|
||||
|
||||
/** Read whether finalists may upload revised grand-final documents (admin toggle). */
|
||||
getRevisedUploadSetting: adminProcedure
|
||||
.input(z.object({ roundId: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const round = await ctx.prisma.round.findUnique({ where: { id: input.roundId }, select: { configJson: true } })
|
||||
const cfg = (round?.configJson ?? {}) as { allowFinalistRevisedUploads?: boolean }
|
||||
return { enabled: !!cfg.allowFinalistRevisedUploads }
|
||||
}),
|
||||
|
||||
/** Toggle whether finalists may upload revised grand-final documents (admin setting on the LIVE_FINAL round). */
|
||||
setRevisedUploadSetting: adminProcedure
|
||||
.input(z.object({ roundId: z.string(), enabled: z.boolean() }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const round = await ctx.prisma.round.findUnique({ where: { id: input.roundId }, select: { configJson: true, roundType: true } })
|
||||
if (!round || round.roundType !== 'LIVE_FINAL') {
|
||||
throw new TRPCError({ code: 'BAD_REQUEST', message: 'Not a grand-final round' })
|
||||
}
|
||||
const cfg = (round.configJson ?? {}) as Record<string, unknown>
|
||||
await ctx.prisma.round.update({
|
||||
where: { id: input.roundId },
|
||||
data: { configJson: { ...cfg, allowFinalistRevisedUploads: input.enabled } },
|
||||
})
|
||||
await logAudit({
|
||||
prisma: ctx.prisma,
|
||||
userId: ctx.user.id,
|
||||
action: 'FINALIST_REVISED_UPLOADS_TOGGLED',
|
||||
entityType: 'Round',
|
||||
entityId: input.roundId,
|
||||
detailsJson: { enabled: input.enabled },
|
||||
})
|
||||
return { ok: true, enabled: input.enabled }
|
||||
}),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user