Fix voting gate to use round status, make eval doc uploads toggleable
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m26s

Voting check now uses round.status === ROUND_ACTIVE instead of requiring
windowOpenAt/windowCloseAt date range, fixing manual open/reopen scenarios.
Added requireDocumentUpload toggle (default off) to evaluation round config
so rounds reusing prior-round documents don't need file requirements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt
2026-02-17 14:13:25 +01:00
parent cef4709444
commit a62f511d7f
6 changed files with 53 additions and 49 deletions

View File

@@ -468,16 +468,18 @@ export default function RoundDetailPage() {
action: undefined as Route | undefined, action: undefined as Route | undefined,
actionLabel: undefined as string | undefined, actionLabel: undefined as string | undefined,
}, },
{ ...((isEvaluation && !(config.requireDocumentUpload as boolean))
label: 'File requirements set', ? []
ready: (fileRequirements?.length ?? 0) > 0, : [{
detail: label: 'File requirements set',
(fileRequirements?.length ?? 0) > 0 ready: (fileRequirements?.length ?? 0) > 0,
? `${fileRequirements?.length} requirement(s)` detail:
: 'No file requirements \u2014 configure in Config tab', (fileRequirements?.length ?? 0) > 0
action: undefined as Route | undefined, ? `${fileRequirements?.length} requirement(s)`
actionLabel: undefined as string | undefined, : 'No file requirements \u2014 configure in Config tab',
}, action: undefined as Route | undefined,
actionLabel: undefined as string | undefined,
}]),
] ]
const readyCount = readinessItems.filter((i) => i.ready).length const readyCount = readinessItems.filter((i) => i.ready).length
@@ -1738,25 +1740,27 @@ export default function RoundDetailPage() {
{/* Evaluation Criteria Editor (EVALUATION rounds only) */} {/* Evaluation Criteria Editor (EVALUATION rounds only) */}
{isEvaluation && <EvaluationCriteriaEditor roundId={roundId} />} {isEvaluation && <EvaluationCriteriaEditor roundId={roundId} />}
{/* Document Requirements */} {/* Document Requirements — hidden for EVALUATION rounds unless requireDocumentUpload is on */}
<Card> {(!isEvaluation || !!(config.requireDocumentUpload as boolean)) && (
<CardHeader> <Card>
<CardTitle className="text-base">Document Requirements</CardTitle> <CardHeader>
<CardDescription> <CardTitle className="text-base">Document Requirements</CardTitle>
Files applicants must submit for this round <CardDescription>
{round.windowCloseAt && ( Files applicants must submit for this round
<> &mdash; due by {new Date(round.windowCloseAt).toLocaleDateString()}</> {round.windowCloseAt && (
)} <> &mdash; due by {new Date(round.windowCloseAt).toLocaleDateString()}</>
</CardDescription> )}
</CardHeader> </CardDescription>
<CardContent> </CardHeader>
<FileRequirementsEditor <CardContent>
roundId={roundId} <FileRequirementsEditor
windowOpenAt={round.windowOpenAt} roundId={roundId}
windowCloseAt={round.windowCloseAt} windowOpenAt={round.windowOpenAt}
/> windowCloseAt={round.windowCloseAt}
</CardContent> />
</Card> </CardContent>
</Card>
)}
</TabsContent> </TabsContent>
{/* ═══════════ AWARDS TAB ═══════════ */} {/* ═══════════ AWARDS TAB ═══════════ */}

View File

@@ -299,14 +299,10 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
) )
} }
// Check if round is active and voting window is open // Check if round is active — round status is the primary gate for evaluations
const now = new Date()
const isRoundActive = round.status === 'ROUND_ACTIVE' const isRoundActive = round.status === 'ROUND_ACTIVE'
const isWindowOpen = isRoundActive &&
round.windowOpenAt && round.windowCloseAt &&
new Date(round.windowOpenAt) <= now && new Date(round.windowCloseAt) >= now
if (!isWindowOpen) { if (!isRoundActive) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
@@ -325,9 +321,7 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
<div> <div>
<h2 className="text-lg font-semibold">Evaluation Not Available</h2> <h2 className="text-lg font-semibold">Evaluation Not Available</h2>
<p className="text-sm text-muted-foreground mt-1"> <p className="text-sm text-muted-foreground mt-1">
{!isRoundActive This round is not currently active. Evaluations can only be submitted during an active round.
? 'This round is not currently active. Evaluations can only be submitted during an active round.'
: 'The voting window for this round is not currently open. Please check back when the window opens.'}
</p> </p>
<Button variant="outline" size="sm" className="mt-4" asChild> <Button variant="outline" size="sm" className="mt-4" asChild>
<Link href={`/jury/competitions/${roundId}/projects/${projectId}` as Route}> <Link href={`/jury/competitions/${roundId}/projects/${projectId}` as Route}>

View File

@@ -27,11 +27,8 @@ export default function JuryProjectDetailPage() {
{ enabled: !!roundId } { enabled: !!roundId }
) )
// Determine if voting is currently open // Round status is the primary gate for evaluations
const now = new Date() const isVotingOpen = round?.status === 'ROUND_ACTIVE'
const isVotingOpen = round?.status === 'ROUND_ACTIVE' &&
round?.windowOpenAt && round?.windowCloseAt &&
new Date(round.windowOpenAt) <= now && new Date(round.windowCloseAt) >= now
if (isLoading) { if (isLoading) {
return ( return (

View File

@@ -357,12 +357,7 @@ async function JuryDashboardContent() {
const evaluation = assignment.evaluation const evaluation = assignment.evaluation
const isCompleted = evaluation?.status === 'SUBMITTED' const isCompleted = evaluation?.status === 'SUBMITTED'
const isDraft = evaluation?.status === 'DRAFT' const isDraft = evaluation?.status === 'DRAFT'
const isVotingOpen = const isVotingOpen = assignment.round.status === 'ROUND_ACTIVE'
assignment.round.status === 'ROUND_ACTIVE' &&
assignment.round.windowOpenAt &&
assignment.round.windowCloseAt &&
new Date(assignment.round.windowOpenAt) <= now &&
new Date(assignment.round.windowCloseAt) >= now
return ( return (
<div <div

View File

@@ -143,6 +143,18 @@ export function EvaluationConfig({ config, onChange }: EvaluationConfigProps) {
/> />
</div> </div>
<div className="flex items-center justify-between">
<div>
<Label htmlFor="requireDocumentUpload">Require Document Upload</Label>
<p className="text-xs text-muted-foreground">Applicants must upload documents for this evaluation round (disable if documents were uploaded in a previous round)</p>
</div>
<Switch
id="requireDocumentUpload"
checked={(config.requireDocumentUpload as boolean) ?? false}
onCheckedChange={(v) => update('requireDocumentUpload', v)}
/>
</div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<Label htmlFor="peerReviewEnabled">Peer Review</Label> <Label htmlFor="peerReviewEnabled">Peer Review</Label>

View File

@@ -83,6 +83,8 @@ export const EvaluationConfigSchema = z.object({
feedbackMinLength: z.number().int().nonnegative().default(0), feedbackMinLength: z.number().int().nonnegative().default(0),
requireAllCriteriaScored: z.boolean().default(true), requireAllCriteriaScored: z.boolean().default(true),
requireDocumentUpload: z.boolean().default(false),
coiRequired: z.boolean().default(true), coiRequired: z.boolean().default(true),
peerReviewEnabled: z.boolean().default(false), peerReviewEnabled: z.boolean().default(false),