feat(final-docs): decouple grand-final docs from LIVE_FINAL being ROUND_ACTIVE
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m44s
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m44s
The Grand Final round = the live event; document upload + judge review happen
in the lead-up BEFORE it opens. So gate them on finalist enrollment + the round
being open-for-docs (DRAFT or ACTIVE, not closed/finalized) instead of requiring
ROUND_ACTIVE. Lets the round stay DRAFT until event time.
- getOpenFinaleRound (was getActiveFinaleRound): status in {DRAFT,ACTIVE}, not finalized
- cron + userCanReviewFinals use the same open-status condition
- getUploadUrl + deleteFile allow a not-yet-closed LIVE_FINAL round
- getMyDashboard openRounds includes the enrolled DRAFT LIVE_FINAL round (finalists only)
- tests: DRAFT now works; CLOSED returns null
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import type { PrismaClient } from '@prisma/client'
|
||||
import type { PrismaClient, RoundStatus } from '@prisma/client'
|
||||
import { createNotification, NotificationTypes } from './in-app-notification'
|
||||
import { getPresignedUrl } from '@/lib/minio'
|
||||
|
||||
@@ -20,10 +20,16 @@ export type FinalDocumentStatus = {
|
||||
allRequiredUploaded: boolean
|
||||
}
|
||||
|
||||
/** Resolve the program's active LIVE_FINAL round, or null. */
|
||||
export async function getActiveFinaleRound(prisma: PrismaClient, programId: string) {
|
||||
// A LIVE_FINAL round is "open for documents" during the lead-up — while it is
|
||||
// DRAFT (not yet opened for the live event) or ACTIVE — but not once CLOSED/
|
||||
// finalized. This decouples document upload + judge review from the ceremony
|
||||
// actually starting, so the Grand Final round can stay DRAFT until event time.
|
||||
const OPEN_FINALE_STATUS: RoundStatus[] = ['ROUND_DRAFT', 'ROUND_ACTIVE']
|
||||
|
||||
/** Resolve the program's LIVE_FINAL round while it is open for documents (DRAFT or ACTIVE, not closed/finalized), or null. */
|
||||
export async function getOpenFinaleRound(prisma: PrismaClient, programId: string) {
|
||||
return prisma.round.findFirst({
|
||||
where: { competition: { programId }, roundType: 'LIVE_FINAL', status: 'ROUND_ACTIVE' },
|
||||
where: { competition: { programId }, roundType: 'LIVE_FINAL', status: { in: OPEN_FINALE_STATUS }, finalizedAt: null },
|
||||
orderBy: { sortOrder: 'desc' },
|
||||
select: { id: true, name: true, windowCloseAt: true },
|
||||
})
|
||||
@@ -43,7 +49,7 @@ export async function getFinalDocumentStatusForProject(
|
||||
})
|
||||
if (!project) return null
|
||||
|
||||
const round = await getActiveFinaleRound(prisma, project.programId)
|
||||
const round = await getOpenFinaleRound(prisma, project.programId)
|
||||
if (!round) return null
|
||||
|
||||
const enrolled = await prisma.projectRoundState.findFirst({
|
||||
@@ -118,7 +124,7 @@ export async function sendManualFinalDocReminders(
|
||||
prisma: PrismaClient,
|
||||
opts: { programId: string; projectIds?: string[]; actorId: string },
|
||||
): Promise<{ sent: number }> {
|
||||
const round = await getActiveFinaleRound(prisma, opts.programId)
|
||||
const round = await getOpenFinaleRound(prisma, opts.programId)
|
||||
if (!round) return { sent: 0 }
|
||||
|
||||
const states = await prisma.projectRoundState.findMany({
|
||||
@@ -157,7 +163,7 @@ export async function sendManualFinalDocReminders(
|
||||
export async function sendDueFinalDocReminders(prisma: PrismaClient): Promise<{ remindersSent: number }> {
|
||||
const now = new Date()
|
||||
const rounds = await prisma.round.findMany({
|
||||
where: { roundType: 'LIVE_FINAL', status: 'ROUND_ACTIVE' },
|
||||
where: { roundType: 'LIVE_FINAL', status: { in: OPEN_FINALE_STATUS }, finalizedAt: null },
|
||||
select: { id: true, windowCloseAt: true, configJson: true, competition: { select: { programId: true } } },
|
||||
})
|
||||
|
||||
@@ -212,7 +218,7 @@ export type ReviewPayload = { round: { id: string; name: string; deadline: Date
|
||||
* the documents directly in the browser.
|
||||
*/
|
||||
export async function listFinalistDocumentsForReview(prisma: PrismaClient, programId: string): Promise<ReviewPayload> {
|
||||
const round = await getActiveFinaleRound(prisma, programId)
|
||||
const round = await getOpenFinaleRound(prisma, programId)
|
||||
if (!round) return { round: { id: '', name: '', deadline: null }, totalCount: 0, submittedCount: 0, teams: [] }
|
||||
|
||||
const requirements = await prisma.fileRequirement.findMany({ where: { roundId: round.id }, orderBy: { sortOrder: 'asc' }, select: { id: true, name: true } })
|
||||
@@ -258,11 +264,11 @@ export async function listFinalistDocumentsForReview(prisma: PrismaClient, progr
|
||||
}
|
||||
}
|
||||
|
||||
/** True if user is admin or a member of the program's active LIVE_FINAL jury group. */
|
||||
/** True if user is admin or a member of the program's open LIVE_FINAL jury group (DRAFT or ACTIVE). */
|
||||
export async function userCanReviewFinals(prisma: PrismaClient, userId: string, userRole: string, programId: string): Promise<boolean> {
|
||||
if (userRole === 'SUPER_ADMIN' || userRole === 'PROGRAM_ADMIN') return true
|
||||
const round = await prisma.round.findFirst({
|
||||
where: { competition: { programId }, roundType: 'LIVE_FINAL', status: 'ROUND_ACTIVE' },
|
||||
where: { competition: { programId }, roundType: 'LIVE_FINAL', status: { in: OPEN_FINALE_STATUS }, finalizedAt: null },
|
||||
orderBy: { sortOrder: 'desc' },
|
||||
select: { juryGroupId: true },
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user