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

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:
Matt
2026-06-09 17:19:09 +02:00
parent f8f2d77e3b
commit 8a4184d20f
7 changed files with 241 additions and 122 deletions

View File

@@ -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