2053 lines
86 KiB
Markdown
2053 lines
86 KiB
Markdown
# 07: SUBMISSION Round — Multi-Window Document Collection System
|
||
|
||
**Document Version**: 2.0
|
||
**Last Updated**: 2026-02-15
|
||
**Status**: Architecture Design (Redesign)
|
||
|
||
---
|
||
|
||
## Table of Contents
|
||
|
||
1. [Purpose & Position in Flow](#1-purpose--position-in-flow)
|
||
2. [Multi-Round Submission Architecture](#2-multi-round-submission-architecture)
|
||
3. [SubmissionConfig Shape](#3-submissionconfig-shape)
|
||
4. [Applicant Experience (Detailed)](#4-applicant-experience-detailed)
|
||
5. [Jury Cross-Round Visibility (Detailed)](#5-jury-cross-round-visibility-detailed)
|
||
6. [Admin Experience](#6-admin-experience)
|
||
7. [API Changes](#7-api-changes)
|
||
8. [File Promotion from Mentoring](#8-file-promotion-from-mentoring)
|
||
9. [Edge Cases](#9-edge-cases)
|
||
10. [Implementation Checklist](#10-implementation-checklist)
|
||
|
||
---
|
||
|
||
## 1. Purpose & Position in Flow
|
||
|
||
### 1.1 Core Problem Statement
|
||
|
||
**Current System Gap**: The MOPC platform currently only supports **one** submission window tied to the INTAKE round. There is no mechanism to:
|
||
|
||
1. Request **additional documents** from advancing teams mid-competition
|
||
2. **Lock previous submissions** as read-only while collecting new materials
|
||
3. Keep **Round 1 documents intact** while teams submit Round 2 materials
|
||
4. Control which **jury rounds see which documents**
|
||
|
||
**Real-World Impact**:
|
||
- Semi-finalists can't be asked for updated pitch decks without replacing Round 1 materials
|
||
- Finalists can't submit presentation materials separately from initial applications
|
||
- If teams update files, original submissions are lost (no audit trail)
|
||
- Organizers resort to email-based document collection (breaks platform integrity)
|
||
|
||
### 1.2 SUBMISSION Round Solution
|
||
|
||
The **SUBMISSION** round is a NEW round type that:
|
||
|
||
1. **Opens a second (or subsequent) submission window** for advancing projects
|
||
2. **Locks previous windows to read-only** for applicants (admin retains full control)
|
||
3. **Collects different file requirements** per competition stage
|
||
4. **Controls jury visibility** via RoundSubmissionVisibility records
|
||
5. **Maintains complete audit trail** across all submission phases
|
||
|
||
### 1.3 Position in Competition Flow
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────────┐
|
||
│ FULL COMPETITION FLOW │
|
||
└──────────────────────────────────────────────────────────────────┘
|
||
|
||
Round 1: INTAKE
|
||
├─ Creates SubmissionWindow 1: "Application Documents"
|
||
├─ Projects submit: Pitch Deck, Budget, Team CV
|
||
├─ Window State: OPEN (Feb 1 - Mar 1)
|
||
└─ All applicants participate (150 projects)
|
||
|
||
Round 2: FILTERING
|
||
├─ Admin/AI screens applications using Window 1 documents
|
||
├─ 50 projects advance with status = PASSED
|
||
└─ Window 1 remains OPEN (teams can still edit)
|
||
|
||
Round 3: EVALUATION (Jury 1)
|
||
├─ Jury evaluates 50 projects using Window 1 documents
|
||
├─ Visibility: Window 1 ONLY
|
||
├─ 20 projects advance with status = PASSED
|
||
└─ Window 1 remains OPEN
|
||
|
||
Round 4: SUBMISSION ◄──── THIS IS THE NEW ROUND TYPE
|
||
├─ Creates SubmissionWindow 2: "Semi-finalist Materials"
|
||
├─ When Round 4 starts:
|
||
│ ├─ Window 1 LOCKS automatically (read-only for applicants)
|
||
│ ├─ Window 2 OPENS (Apr 1 - Apr 15)
|
||
│ ├─ Only 20 PASSED projects from Round 3 can submit
|
||
│ └─ Email notifications sent to eligible teams
|
||
├─ New requirements:
|
||
│ ├─ Updated Pitch Deck (required)
|
||
│ ├─ Video Pitch (required, max 100MB)
|
||
│ ├─ Financial Projections (required)
|
||
│ └─ Team Photos (optional)
|
||
└─ Deadline policy: GRACE (48 hours after Apr 15)
|
||
|
||
Round 5: EVALUATION (Jury 2)
|
||
├─ Jury evaluates 20 projects using BOTH Window 1 + Window 2
|
||
├─ Visibility: Window 1 AND Window 2
|
||
├─ UI shows two tabs: "Round 1 Docs" | "Round 2 Docs"
|
||
├─ 10 projects advance with status = PASSED
|
||
└─ Window 2 LOCKS when Round 5 starts
|
||
|
||
Round 6: SUBMISSION (Optional Third Window)
|
||
├─ Creates SubmissionWindow 3: "Finalist Presentations"
|
||
├─ Only 10 PASSED projects from Round 5 can submit
|
||
├─ New requirements:
|
||
│ ├─ Final Pitch Deck (required)
|
||
│ ├─ 3-Minute Video (required)
|
||
│ └─ Impact Report (required)
|
||
└─ Window 1 + 2 remain LOCKED, Window 3 OPEN
|
||
|
||
Round 7: LIVE_FINAL
|
||
└─ Top 10 present live using Window 3 materials
|
||
```
|
||
|
||
**Key Characteristics**:
|
||
|
||
| Feature | Behavior |
|
||
|---------|----------|
|
||
| **Multiple Windows** | Each SUBMISSION round creates exactly ONE new SubmissionWindow |
|
||
| **Sequential Locking** | When Window N opens, Window N-1 locks for applicants |
|
||
| **Eligibility Filtering** | Only projects with specific statuses from previous round can submit |
|
||
| **Independent Requirements** | Each window has its own file requirements (not shared) |
|
||
| **Cumulative Visibility** | Later EVALUATION rounds can see multiple windows simultaneously |
|
||
| **Admin Override** | Admins always have full control regardless of lock state |
|
||
|
||
### 1.4 When to Use SUBMISSION Rounds
|
||
|
||
| Scenario | Use SUBMISSION? | Explanation |
|
||
|----------|----------------|-------------|
|
||
| **Single-round competition** (one evaluation) | ❌ No | One INTAKE window is sufficient |
|
||
| **Two-stage competition** (semi-final + final) | ✅ Yes | INTAKE → EVALUATION → **SUBMISSION** → EVALUATION |
|
||
| **Three-stage competition** (screening + semi + final) | ✅ Yes (2x) | INTAKE → EVAL → **SUBMISSION** → EVAL → **SUBMISSION** → EVAL |
|
||
| **Requesting updated docs from same pool** | ❌ Maybe | Consider MENTORING with file promotion instead |
|
||
| **Post-award deliverables** (winner docs) | ✅ Yes | Create SUBMISSION round after awards announced |
|
||
| **Mid-competition clarifications** | ❌ No | Use comments or mentoring workspace |
|
||
|
||
### 1.5 Relationship to Other Round Types
|
||
|
||
**SUBMISSION vs INTAKE**:
|
||
- **INTAKE** = First submission window (all applicants, competition start)
|
||
- **SUBMISSION** = Subsequent submission windows (filtered subset, mid-competition)
|
||
- **Both create SubmissionWindow records** (INTAKE creates Window 1, SUBMISSION creates Window 2+)
|
||
|
||
**SUBMISSION vs EVALUATION**:
|
||
- **SUBMISSION** = Document collection phase (no scoring, just uploads)
|
||
- **EVALUATION** = Assessment phase (jury scores projects using submitted docs)
|
||
- **EVALUATION rounds declare which windows they can see** via RoundSubmissionVisibility
|
||
|
||
**SUBMISSION vs MENTORING**:
|
||
- **MENTORING** = Collaborative workspace (mentor + team iterate on documents)
|
||
- **SUBMISSION** = Official competition documents (locked after deadline)
|
||
- **Files can be promoted** from MENTORING → SUBMISSION (see Section 8)
|
||
|
||
**SUBMISSION vs FILTERING**:
|
||
- **FILTERING** = Automated screening using existing documents
|
||
- **SUBMISSION** = New document collection opportunity
|
||
- **FILTERING typically comes BEFORE** SUBMISSION (screen first, then request more docs)
|
||
|
||
---
|
||
|
||
## 2. Multi-Round Submission Architecture
|
||
|
||
### 2.1 Data Model Overview
|
||
|
||
```prisma
|
||
// NEW: SubmissionWindow model
|
||
model SubmissionWindow {
|
||
id String @id @default(cuid())
|
||
competitionId String
|
||
roundId String @unique // Each round creates max ONE window
|
||
name String // "Application Docs", "Semi-finalist Materials"
|
||
description String? @db.Text
|
||
|
||
// Timing
|
||
openDate DateTime
|
||
closeDate DateTime
|
||
|
||
// Deadline policy
|
||
latePolicy LateSubmissionPolicy // HARD | FLAG | GRACE
|
||
gracePeriodHours Int? // If latePolicy = GRACE
|
||
|
||
// Locking
|
||
lockDate DateTime? // When window becomes read-only for applicants
|
||
isLocked Boolean @default(false)
|
||
|
||
// Relations
|
||
competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade)
|
||
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
|
||
|
||
fileRequirements SubmissionFileRequirement[]
|
||
projectFiles ProjectFile[]
|
||
visibleToRounds RoundSubmissionVisibility[]
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@unique([competitionId, roundId])
|
||
@@index([competitionId])
|
||
@@index([openDate])
|
||
@@index([closeDate])
|
||
}
|
||
|
||
enum LateSubmissionPolicy {
|
||
HARD // Reject submissions after closeDate
|
||
FLAG // Accept late submissions but mark as late
|
||
GRACE // Allow submissions during grace period, then hard reject
|
||
}
|
||
|
||
// NEW: SubmissionFileRequirement model
|
||
model SubmissionFileRequirement {
|
||
id String @id @default(cuid())
|
||
submissionWindowId String
|
||
label String // "Pitch Deck", "Budget Spreadsheet"
|
||
description String? @db.Text
|
||
isRequired Boolean @default(true)
|
||
allowedFileTypes String[] // ["pdf", "pptx", "docx"]
|
||
maxSizeMB Int @default(10)
|
||
displayOrder Int
|
||
|
||
submissionWindow SubmissionWindow @relation(fields: [submissionWindowId], references: [id], onDelete: Cascade)
|
||
projectFiles ProjectFile[]
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@index([submissionWindowId])
|
||
@@index([displayOrder])
|
||
}
|
||
|
||
// NEW: RoundSubmissionVisibility model
|
||
model RoundSubmissionVisibility {
|
||
id String @id @default(cuid())
|
||
evaluationRoundId String // Which EVALUATION round can see docs
|
||
submissionWindowId String // Which SubmissionWindow's docs
|
||
displayLabel String // "Round 1 Documents", "Semi-final Submissions"
|
||
displayOrder Int
|
||
|
||
evaluationRound Round @relation("EvaluationRoundVisibility", fields: [evaluationRoundId], references: [id], onDelete: Cascade)
|
||
submissionWindow SubmissionWindow @relation(fields: [submissionWindowId], references: [id], onDelete: Cascade)
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
@@unique([evaluationRoundId, submissionWindowId])
|
||
@@index([evaluationRoundId])
|
||
@@index([submissionWindowId])
|
||
}
|
||
|
||
// UPDATED: ProjectFile model (add submissionWindowId)
|
||
model ProjectFile {
|
||
id String @id @default(cuid())
|
||
projectId String
|
||
submissionWindowId String // NEW: Links to SubmissionWindow
|
||
requirementId String? // NEW: Links to SubmissionFileRequirement
|
||
|
||
filename String
|
||
mimeType String
|
||
sizeBytes BigInt
|
||
storagePath String
|
||
uploadedBy String
|
||
uploadedAt DateTime @default(now())
|
||
|
||
isLate Boolean @default(false)
|
||
|
||
// Versioning
|
||
supersededBy String? // Points to newer version of this file
|
||
supersededAt DateTime?
|
||
|
||
// Promotion from mentoring
|
||
promotedFrom String? // Points to MentorFile.id
|
||
promotedAt DateTime?
|
||
|
||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||
submissionWindow SubmissionWindow @relation(fields: [submissionWindowId], references: [id], onDelete: Cascade)
|
||
requirement SubmissionFileRequirement? @relation(fields: [requirementId], references: [id], onDelete: SetNull)
|
||
uploadedByUser User @relation(fields: [uploadedBy], references: [id])
|
||
|
||
@@index([projectId])
|
||
@@index([submissionWindowId])
|
||
@@index([requirementId])
|
||
@@index([uploadedBy])
|
||
}
|
||
|
||
// UPDATED: Round model (add submissionWindowId)
|
||
model Round {
|
||
id String @id @default(cuid())
|
||
competitionId String
|
||
type RoundType
|
||
name String
|
||
description String? @db.Text
|
||
displayOrder Int
|
||
|
||
// For SUBMISSION rounds
|
||
submissionWindowId String? @unique // Links to SubmissionWindow
|
||
|
||
configJson Json? @db.JsonB // Stores SubmissionConfig for SUBMISSION rounds
|
||
|
||
// ... other fields ...
|
||
|
||
submissionWindow SubmissionWindow?
|
||
visibleWindows RoundSubmissionVisibility[] @relation("EvaluationRoundVisibility")
|
||
}
|
||
```
|
||
|
||
### 2.2 Window Lifecycle States
|
||
|
||
Each SubmissionWindow progresses through these states:
|
||
|
||
```
|
||
┌─────────────┐
|
||
│ PENDING │ Created but openDate not reached
|
||
│ │ - Not visible to applicants
|
||
│ │ - Admin can edit requirements freely
|
||
│ │ - isLocked = false, lockDate = null
|
||
└──────┬──────┘
|
||
│ openDate arrives (Round transitions to ACTIVE)
|
||
▼
|
||
┌─────────────┐
|
||
│ OPEN │ Accepting submissions
|
||
│ │ - Applicants can upload/replace/delete files
|
||
│ │ - Admin can edit requirements (with warnings if teams already submitted)
|
||
│ │ - Previous windows may be locked
|
||
│ │ - isLocked = false
|
||
└──────┬──────┘
|
||
│ closeDate arrives
|
||
▼
|
||
┌─────────────┐
|
||
│ CLOSED │ Past deadline (policy-dependent)
|
||
│ │ - latePolicy = HARD: No uploads allowed
|
||
│ │ - latePolicy = FLAG: Uploads allowed, marked as late
|
||
│ │ - latePolicy = GRACE: Uploads allowed until lockDate
|
||
│ │ - isLocked = depends on policy
|
||
└──────┬──────┘
|
||
│ lockDate arrives (if GRACE) OR admin manually locks
|
||
▼
|
||
┌─────────────┐
|
||
│ LOCKED │ Read-only for applicants, full control for admins
|
||
│ │ - Applicants: view/download only, no uploads/deletes
|
||
│ │ - Admins: can still upload/replace/delete on behalf
|
||
│ │ - Juries: view-only (if visibility configured)
|
||
│ │ - isLocked = true
|
||
└─────────────┘
|
||
```
|
||
|
||
**State Transitions**:
|
||
|
||
| From | To | Trigger | Side Effects |
|
||
|------|----|---------| -------------|
|
||
| PENDING | OPEN | `openDate` reached | Lock previous windows (if config), notify eligible teams |
|
||
| OPEN | CLOSED | `closeDate` reached | Start grace period (if GRACE policy) |
|
||
| CLOSED | LOCKED | `lockDate` reached OR admin action | Final state, no more applicant changes |
|
||
|
||
### 2.3 Multi-Window Coordination Example
|
||
|
||
**Scenario**: MOPC 2026 with 3 evaluation rounds
|
||
|
||
```
|
||
Competition: MOPC 2026
|
||
├─ Round 1: INTAKE
|
||
│ └─ SubmissionWindow 1: "Application Documents"
|
||
│ ├─ Open: Jan 1 - Feb 1
|
||
│ ├─ Close: Feb 1
|
||
│ ├─ Late Policy: GRACE (72 hours → lock Feb 4)
|
||
│ ├─ Requirements:
|
||
│ │ ├─ Pitch Deck (PDF, required, max 10MB)
|
||
│ │ ├─ Budget (XLSX/PDF, required, max 5MB)
|
||
│ │ └─ Team CV (PDF, optional, max 5MB)
|
||
│ └─ Status: OPEN (150 projects submit)
|
||
│
|
||
├─ Round 2: FILTERING
|
||
│ └─ Screens 150 applications → 50 advance (status = PASSED)
|
||
│
|
||
├─ Round 3: EVALUATION (Jury 1)
|
||
│ ├─ Visibility: Window 1 ONLY
|
||
│ ├─ Jury sees:
|
||
│ │ └─ Tab: "Application Documents" (Pitch, Budget, CV)
|
||
│ └─ 20 projects advance (status = PASSED)
|
||
│
|
||
├─ Round 4: SUBMISSION ◄──── NEW ROUND TYPE
|
||
│ └─ SubmissionWindow 2: "Semi-finalist Materials"
|
||
│ ├─ Opens: Mar 1 (Window 1 auto-locks at this moment)
|
||
│ ├─ Close: Mar 15
|
||
│ ├─ Late Policy: HARD (no late submissions)
|
||
│ ├─ Eligible: 20 projects with status=PASSED from Round 3
|
||
│ ├─ Requirements:
|
||
│ │ ├─ Updated Pitch Deck (PDF, required, max 15MB)
|
||
│ │ ├─ Video Pitch (MP4/MOV, required, max 100MB)
|
||
│ │ ├─ Financial Projections (XLSX/PDF, required, max 10MB)
|
||
│ │ └─ Team Photos (JPG/PNG, optional, max 5MB each)
|
||
│ └─ Status: OPEN (20 eligible projects can submit)
|
||
│
|
||
├─ Round 5: EVALUATION (Jury 2)
|
||
│ ├─ Visibility: Window 1 AND Window 2
|
||
│ ├─ Jury sees:
|
||
│ │ ├─ Tab 1: "Round 1 Application" (Pitch, Budget, CV from Jan)
|
||
│ │ └─ Tab 2: "Semi-final Materials" (Updated Pitch, Video, Financials from Mar)
|
||
│ └─ 10 projects advance (status = PASSED)
|
||
│
|
||
├─ Round 6: SUBMISSION ◄──── SECOND SUBMISSION ROUND
|
||
│ └─ SubmissionWindow 3: "Finalist Presentations"
|
||
│ ├─ Opens: Apr 1 (Window 2 auto-locks)
|
||
│ ├─ Close: Apr 10
|
||
│ ├─ Late Policy: FLAG (late submissions marked)
|
||
│ ├─ Eligible: 10 projects with status=PASSED from Round 5
|
||
│ ├─ Requirements:
|
||
│ │ ├─ Final Pitch Deck (PDF/PPTX, required, max 20MB)
|
||
│ │ ├─ 3-Minute Video (MP4, required, max 150MB)
|
||
│ │ └─ Impact Report (PDF/DOCX, required, max 10MB)
|
||
│ └─ Status: OPEN (10 eligible projects can submit)
|
||
│
|
||
└─ Round 7: LIVE_FINAL
|
||
├─ Visibility: Window 1, Window 2, AND Window 3
|
||
├─ Jury sees:
|
||
│ ├─ Tab 1: "Original Application" (Jan docs)
|
||
│ ├─ Tab 2: "Semi-final Submission" (Mar docs)
|
||
│ └─ Tab 3: "Finalist Presentation" (Apr docs)
|
||
└─ 3 winners selected
|
||
```
|
||
|
||
**Key Rules**:
|
||
|
||
1. **One Window Per Round**: Each SUBMISSION/INTAKE round creates exactly ONE SubmissionWindow
|
||
2. **Never Delete Windows**: Windows persist forever (audit trail), only lock/unlock
|
||
3. **Applicant Access: One-Way Lock**: Can only go from READ-WRITE → READ-ONLY (never reverses)
|
||
4. **Admin Access: Always Full**: Admins can upload/delete in any window regardless of lock state
|
||
5. **Jury Visibility: Explicit Declaration**: Must create RoundSubmissionVisibility records
|
||
|
||
### 2.4 Locking Mechanism Implementation
|
||
|
||
**When a new SubmissionWindow opens**:
|
||
|
||
```typescript
|
||
async function openSubmissionWindow(windowId: string, ctx: Context) {
|
||
const window = await ctx.prisma.submissionWindow.findUnique({
|
||
where: { id: windowId },
|
||
include: {
|
||
round: {
|
||
select: {
|
||
configJson: true,
|
||
competitionId: true
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
if (!window) throw new Error("Window not found");
|
||
|
||
const config = window.round.configJson as SubmissionConfig;
|
||
|
||
// Step 1: Set this window's openDate to now
|
||
await ctx.prisma.submissionWindow.update({
|
||
where: { id: windowId },
|
||
data: { openDate: new Date() }
|
||
});
|
||
|
||
// Step 2: Lock previous windows if config says so
|
||
if (config.lockPreviousWindows) {
|
||
const allWindows = await ctx.prisma.submissionWindow.findMany({
|
||
where: {
|
||
competitionId: window.competitionId,
|
||
openDate: { lt: new Date() } // Opened before now
|
||
},
|
||
select: { id: true, isLocked: true }
|
||
});
|
||
|
||
for (const prevWindow of allWindows) {
|
||
if (!prevWindow.isLocked) {
|
||
await ctx.prisma.submissionWindow.update({
|
||
where: { id: prevWindow.id },
|
||
data: {
|
||
isLocked: true,
|
||
lockDate: new Date()
|
||
}
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// Step 3: Notify eligible teams
|
||
if (config.notifyEligibleTeams) {
|
||
await notifyEligibleTeams(windowId, ctx);
|
||
}
|
||
|
||
// Step 4: Audit log
|
||
await ctx.prisma.decisionAuditLog.create({
|
||
data: {
|
||
action: "SUBMISSION_WINDOW_OPENED",
|
||
userId: ctx.session.user.id,
|
||
entityType: "SubmissionWindow",
|
||
entityId: windowId,
|
||
metadata: {
|
||
windowName: window.name,
|
||
openDate: new Date(),
|
||
previousWindowsLocked: config.lockPreviousWindows
|
||
}
|
||
}
|
||
});
|
||
}
|
||
```
|
||
|
||
**Upload Permission Check**:
|
||
|
||
```typescript
|
||
async function checkUploadPermission(
|
||
projectId: string,
|
||
windowId: string,
|
||
userId: string,
|
||
ctx: Context
|
||
): Promise<{ allowed: boolean; reason?: string }> {
|
||
const user = await ctx.prisma.user.findUnique({
|
||
where: { id: userId },
|
||
select: { role: true }
|
||
});
|
||
|
||
// Admins bypass all checks
|
||
if (user?.role === "SUPER_ADMIN" || user?.role === "PROGRAM_ADMIN") {
|
||
return { allowed: true };
|
||
}
|
||
|
||
// Check 1: User is applicant for this project?
|
||
const project = await ctx.prisma.project.findFirst({
|
||
where: {
|
||
id: projectId,
|
||
applicantId: userId
|
||
}
|
||
});
|
||
|
||
if (!project) {
|
||
return { allowed: false, reason: "Unauthorized: You do not own this project" };
|
||
}
|
||
|
||
// Check 2: Fetch window
|
||
const window = await ctx.prisma.submissionWindow.findUnique({
|
||
where: { id: windowId }
|
||
});
|
||
|
||
if (!window) {
|
||
return { allowed: false, reason: "Submission window not found" };
|
||
}
|
||
|
||
// Check 3: Window locked?
|
||
if (window.isLocked) {
|
||
return { allowed: false, reason: "This submission window is now closed." };
|
||
}
|
||
|
||
const now = new Date();
|
||
|
||
// Check 4: Window not yet open?
|
||
if (now < window.openDate) {
|
||
return { allowed: false, reason: "Submission window has not opened yet." };
|
||
}
|
||
|
||
// Check 5: Deadline enforcement
|
||
if (now > window.closeDate) {
|
||
switch (window.latePolicy) {
|
||
case "HARD":
|
||
return { allowed: false, reason: "Submission deadline has passed. No late submissions allowed." };
|
||
|
||
case "FLAG":
|
||
// Allow upload, will be marked as late
|
||
return { allowed: true };
|
||
|
||
case "GRACE":
|
||
if (window.lockDate && now > window.lockDate) {
|
||
return { allowed: false, reason: "Grace period has ended. Submissions are now closed." };
|
||
}
|
||
// Within grace period
|
||
return { allowed: true };
|
||
}
|
||
}
|
||
|
||
// All checks passed
|
||
return { allowed: true };
|
||
}
|
||
```
|
||
|
||
### 2.5 File Organization by Window
|
||
|
||
**Database Structure**:
|
||
|
||
```typescript
|
||
// Project: proj_abc123
|
||
// Competition with 3 submission windows
|
||
|
||
// Window 1: Application Documents (Jan)
|
||
ProjectFile {
|
||
id: "file_001"
|
||
projectId: "proj_abc123"
|
||
submissionWindowId: "window_1"
|
||
requirementId: "req_pitch_deck_w1"
|
||
filename: "OceanCleanup_Pitch.pdf"
|
||
uploadedAt: "2026-01-25T14:30:00Z"
|
||
uploadedBy: "applicant_user_id"
|
||
isLate: false
|
||
}
|
||
|
||
ProjectFile {
|
||
id: "file_002"
|
||
projectId: "proj_abc123"
|
||
submissionWindowId: "window_1"
|
||
requirementId: "req_budget_w1"
|
||
filename: "Budget_2026.xlsx"
|
||
uploadedAt: "2026-02-01T09:15:00Z"
|
||
uploadedBy: "applicant_user_id"
|
||
isLate: false
|
||
}
|
||
|
||
// Window 2: Semi-finalist Materials (Mar)
|
||
ProjectFile {
|
||
id: "file_003"
|
||
projectId: "proj_abc123"
|
||
submissionWindowId: "window_2"
|
||
requirementId: "req_video_w2"
|
||
filename: "Pitch_Video.mp4"
|
||
uploadedAt: "2026-03-10T16:45:00Z"
|
||
uploadedBy: "applicant_user_id"
|
||
isLate: false
|
||
}
|
||
|
||
ProjectFile {
|
||
id: "file_004"
|
||
projectId: "proj_abc123"
|
||
submissionWindowId: "window_2"
|
||
requirementId: "req_updated_deck_w2"
|
||
filename: "OceanCleanup_Pitch_v2.pdf"
|
||
uploadedAt: "2026-03-15T23:59:00Z"
|
||
uploadedBy: "admin_user_id" // Admin uploaded on behalf
|
||
isLate: false
|
||
}
|
||
|
||
// Window 3: Finalist Presentation (Apr)
|
||
ProjectFile {
|
||
id: "file_005"
|
||
projectId: "proj_abc123"
|
||
submissionWindowId: "window_3"
|
||
requirementId: "req_final_deck_w3"
|
||
filename: "OceanCleanup_Final.pdf"
|
||
uploadedAt: "2026-04-09T18:20:00Z"
|
||
uploadedBy: "applicant_user_id"
|
||
isLate: false
|
||
}
|
||
|
||
ProjectFile {
|
||
id: "file_006"
|
||
projectId: "proj_abc123"
|
||
submissionWindowId: "window_3"
|
||
requirementId: "req_impact_report_w3"
|
||
filename: "Impact_Report.pdf"
|
||
uploadedAt: "2026-04-11T10:30:00Z" // After deadline (Apr 10)
|
||
uploadedBy: "applicant_user_id"
|
||
isLate: true // Marked late (FLAG policy)
|
||
}
|
||
```
|
||
|
||
**Query Pattern for Jury View** (EVALUATION Round 2):
|
||
|
||
```typescript
|
||
// Fetch all visible files for a project in an evaluation round
|
||
const visibleWindows = await ctx.prisma.roundSubmissionVisibility.findMany({
|
||
where: { evaluationRoundId: "eval_round_2" },
|
||
include: {
|
||
submissionWindow: {
|
||
include: {
|
||
fileRequirements: {
|
||
orderBy: { displayOrder: "asc" }
|
||
}
|
||
}
|
||
}
|
||
},
|
||
orderBy: { displayOrder: "asc" }
|
||
});
|
||
|
||
// Returns:
|
||
// [
|
||
// {
|
||
// id: "vis_1",
|
||
// submissionWindow: {
|
||
// id: "window_1",
|
||
// name: "Application Documents",
|
||
// fileRequirements: [...]
|
||
// },
|
||
// displayLabel: "Round 1 Application",
|
||
// displayOrder: 1
|
||
// },
|
||
// {
|
||
// id: "vis_2",
|
||
// submissionWindow: {
|
||
// id: "window_2",
|
||
// name: "Semi-finalist Materials",
|
||
// fileRequirements: [...]
|
||
// },
|
||
// displayLabel: "Semi-final Submissions",
|
||
// displayOrder: 2
|
||
// }
|
||
// ]
|
||
|
||
const windowIds = visibleWindows.map(v => v.submissionWindowId);
|
||
|
||
const files = await ctx.prisma.projectFile.findMany({
|
||
where: {
|
||
projectId: "proj_abc123",
|
||
submissionWindowId: { in: windowIds },
|
||
supersededBy: null // Only current versions
|
||
},
|
||
include: {
|
||
requirement: true,
|
||
submissionWindow: true,
|
||
uploadedByUser: {
|
||
select: { name: true, role: true }
|
||
}
|
||
}
|
||
});
|
||
|
||
// Group by window
|
||
const filesByWindow = visibleWindows.map(vis => ({
|
||
windowId: vis.submissionWindowId,
|
||
windowName: vis.displayLabel,
|
||
displayOrder: vis.displayOrder,
|
||
files: files.filter(f => f.submissionWindowId === vis.submissionWindowId)
|
||
}));
|
||
```
|
||
|
||
---
|
||
|
||
## 3. SubmissionConfig Shape
|
||
|
||
### 3.1 Type Definition
|
||
|
||
```typescript
|
||
/**
|
||
* Configuration for SUBMISSION round type
|
||
* Stored in Round.configJson for rounds with type = "SUBMISSION"
|
||
*/
|
||
type SubmissionConfig = {
|
||
/**
|
||
* Which project statuses from the PREVIOUS round are eligible to submit
|
||
*
|
||
* Common values:
|
||
* - ["PASSED"] → Only projects that passed previous evaluation
|
||
* - ["PASSED", "CONDITIONAL"] → Passed + conditionally advanced
|
||
* - ["WINNER", "FINALIST"] → For post-award deliverables
|
||
*
|
||
* Default: ["PASSED"]
|
||
*/
|
||
eligibleStatuses: ProjectRoundStateValue[];
|
||
|
||
/**
|
||
* Send email + in-app notifications when submission window opens
|
||
*
|
||
* If true:
|
||
* - Sends to all eligible project applicants
|
||
* - Includes deadline, requirements list, upload link
|
||
* - Uses template from notificationTemplate if provided
|
||
*
|
||
* Default: true
|
||
*/
|
||
notifyEligibleTeams: boolean;
|
||
|
||
/**
|
||
* Lock all previous submission windows when this round starts
|
||
*
|
||
* If true:
|
||
* - All windows with openDate < current window's openDate
|
||
* - Set isLocked = true, lockDate = now
|
||
* - Applicants can no longer upload/delete (read-only)
|
||
* - Admins retain full control
|
||
*
|
||
* Default: true
|
||
*/
|
||
lockPreviousWindows: boolean;
|
||
|
||
/**
|
||
* Optional: Custom email template for notifications
|
||
*
|
||
* Variables available:
|
||
* - {projectName}, {teamName}, {deadline}, {windowName}
|
||
* - {requirementsList}, {uploadUrl}
|
||
*/
|
||
notificationTemplate?: {
|
||
subject: string;
|
||
bodyHtml: string;
|
||
variables: Record<string, string>;
|
||
};
|
||
|
||
/**
|
||
* Optional: Submission window configuration
|
||
* (Can also be set separately via SubmissionWindow model)
|
||
*/
|
||
windowConfig?: {
|
||
name: string;
|
||
description: string;
|
||
openDate: string; // ISO 8601
|
||
closeDate: string; // ISO 8601
|
||
latePolicy: "HARD" | "FLAG" | "GRACE";
|
||
gracePeriodHours?: number;
|
||
};
|
||
|
||
/**
|
||
* Optional: File requirements
|
||
* (Can also be set separately via SubmissionFileRequirement model)
|
||
*/
|
||
fileRequirements?: Array<{
|
||
label: string;
|
||
description?: string;
|
||
isRequired: boolean;
|
||
allowedFileTypes: string[]; // ["pdf", "docx", "mp4"]
|
||
maxSizeMB: number;
|
||
displayOrder: number;
|
||
}>;
|
||
};
|
||
```
|
||
|
||
### 3.2 Example Configurations
|
||
|
||
**Example 1: Simple Semi-Finalist Submission**
|
||
|
||
```json
|
||
{
|
||
"eligibleStatuses": ["PASSED"],
|
||
"notifyEligibleTeams": true,
|
||
"lockPreviousWindows": true,
|
||
"windowConfig": {
|
||
"name": "Semi-Finalist Documents",
|
||
"description": "Congratulations on advancing! Please submit the following materials.",
|
||
"openDate": "2026-03-01T00:00:00Z",
|
||
"closeDate": "2026-03-15T23:59:59Z",
|
||
"latePolicy": "HARD"
|
||
},
|
||
"fileRequirements": [
|
||
{
|
||
"label": "3-Minute Video Pitch",
|
||
"description": "MP4 format, max 100MB. Introduce your team and demonstrate your solution.",
|
||
"isRequired": true,
|
||
"allowedFileTypes": ["mp4", "mov"],
|
||
"maxSizeMB": 100,
|
||
"displayOrder": 1
|
||
},
|
||
{
|
||
"label": "Updated Pitch Deck",
|
||
"description": "PDF format. Include any updates since your initial application.",
|
||
"isRequired": true,
|
||
"allowedFileTypes": ["pdf"],
|
||
"maxSizeMB": 10,
|
||
"displayOrder": 2
|
||
},
|
||
{
|
||
"label": "Financial Projections",
|
||
"description": "Excel or PDF. 3-year revenue and cost forecast with assumptions.",
|
||
"isRequired": true,
|
||
"allowedFileTypes": ["xlsx", "pdf"],
|
||
"maxSizeMB": 5,
|
||
"displayOrder": 3
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
**Example 2: Flexible Finalist Submission with Grace Period**
|
||
|
||
```json
|
||
{
|
||
"eligibleStatuses": ["PASSED", "CONDITIONAL"],
|
||
"notifyEligibleTeams": true,
|
||
"lockPreviousWindows": true,
|
||
"windowConfig": {
|
||
"name": "Finalist Presentation Materials",
|
||
"description": "Final round submissions for live presentations.",
|
||
"openDate": "2026-04-01T00:00:00Z",
|
||
"closeDate": "2026-04-10T23:59:59Z",
|
||
"latePolicy": "GRACE",
|
||
"gracePeriodHours": 48
|
||
},
|
||
"fileRequirements": [
|
||
{
|
||
"label": "Final Pitch Deck",
|
||
"description": "PDF or PowerPoint, max 20 slides",
|
||
"isRequired": true,
|
||
"allowedFileTypes": ["pdf", "pptx"],
|
||
"maxSizeMB": 15,
|
||
"displayOrder": 1
|
||
},
|
||
{
|
||
"label": "Impact Report",
|
||
"description": "Summary of expected environmental impact (PDF or Word)",
|
||
"isRequired": true,
|
||
"allowedFileTypes": ["pdf", "docx"],
|
||
"maxSizeMB": 10,
|
||
"displayOrder": 2
|
||
},
|
||
{
|
||
"label": "Team Photo",
|
||
"description": "High-resolution team photo for program materials (optional)",
|
||
"isRequired": false,
|
||
"allowedFileTypes": ["jpg", "png"],
|
||
"maxSizeMB": 5,
|
||
"displayOrder": 3
|
||
}
|
||
],
|
||
"notificationTemplate": {
|
||
"subject": "🎉 You're a Finalist! Submit Your Final Materials",
|
||
"bodyHtml": "<p>Dear {teamName},</p><p>Congratulations on reaching the finals!</p><p>Please submit your materials by <strong>{deadline}</strong>.</p>",
|
||
"variables": {
|
||
"teamName": "project.name",
|
||
"deadline": "window.closeDate"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**Example 3: Post-Award Deliverables (Don't Lock Previous Windows)**
|
||
|
||
```json
|
||
{
|
||
"eligibleStatuses": ["WINNER", "FINALIST"],
|
||
"notifyEligibleTeams": true,
|
||
"lockPreviousWindows": false,
|
||
"windowConfig": {
|
||
"name": "Winner Deliverables",
|
||
"description": "Required documentation for prize disbursement and program promotion.",
|
||
"openDate": "2026-06-01T00:00:00Z",
|
||
"closeDate": "2026-06-30T23:59:59Z",
|
||
"latePolicy": "FLAG"
|
||
},
|
||
"fileRequirements": [
|
||
{
|
||
"label": "W-9 Tax Form",
|
||
"description": "Required for prize payment (US teams only)",
|
||
"isRequired": true,
|
||
"allowedFileTypes": ["pdf"],
|
||
"maxSizeMB": 2,
|
||
"displayOrder": 1
|
||
},
|
||
{
|
||
"label": "High-Res Logo",
|
||
"description": "Vector format (SVG preferred) or high-res PNG (min 2000px width)",
|
||
"isRequired": true,
|
||
"allowedFileTypes": ["svg", "png", "ai"],
|
||
"maxSizeMB": 10,
|
||
"displayOrder": 2
|
||
},
|
||
{
|
||
"label": "Success Story",
|
||
"description": "500-word story about your project for our website and press releases",
|
||
"isRequired": true,
|
||
"allowedFileTypes": ["pdf", "docx"],
|
||
"maxSizeMB": 2,
|
||
"displayOrder": 3
|
||
},
|
||
{
|
||
"label": "Promotional Video",
|
||
"description": "Optional: 1-2 minute video for social media",
|
||
"isRequired": false,
|
||
"allowedFileTypes": ["mp4", "mov"],
|
||
"maxSizeMB": 200,
|
||
"displayOrder": 4
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### 3.3 Validation Schema
|
||
|
||
```typescript
|
||
import { z } from "zod";
|
||
|
||
const submissionConfigSchema = z.object({
|
||
eligibleStatuses: z.array(
|
||
z.enum([
|
||
"DRAFT",
|
||
"SUBMITTED",
|
||
"UNDER_REVIEW",
|
||
"PASSED",
|
||
"FAILED",
|
||
"CONDITIONAL",
|
||
"WITHDRAWN",
|
||
"WINNER",
|
||
"FINALIST"
|
||
])
|
||
).min(1, "Must specify at least one eligible status"),
|
||
|
||
notifyEligibleTeams: z.boolean(),
|
||
|
||
lockPreviousWindows: z.boolean(),
|
||
|
||
notificationTemplate: z.object({
|
||
subject: z.string().min(1).max(200),
|
||
bodyHtml: z.string().min(1),
|
||
variables: z.record(z.string())
|
||
}).optional(),
|
||
|
||
windowConfig: z.object({
|
||
name: z.string().min(1).max(100),
|
||
description: z.string().max(1000).optional(),
|
||
openDate: z.string().datetime(),
|
||
closeDate: z.string().datetime(),
|
||
latePolicy: z.enum(["HARD", "FLAG", "GRACE"]),
|
||
gracePeriodHours: z.number().int().min(1).max(168).optional()
|
||
}).refine(
|
||
(data) => new Date(data.closeDate) > new Date(data.openDate),
|
||
{ message: "closeDate must be after openDate" }
|
||
).refine(
|
||
(data) => data.latePolicy !== "GRACE" || data.gracePeriodHours !== undefined,
|
||
{ message: "gracePeriodHours required when latePolicy is GRACE" }
|
||
).optional(),
|
||
|
||
fileRequirements: z.array(
|
||
z.object({
|
||
label: z.string().min(1).max(100),
|
||
description: z.string().max(500).optional(),
|
||
isRequired: z.boolean(),
|
||
allowedFileTypes: z.array(z.string()).min(1),
|
||
maxSizeMB: z.number().int().min(1).max(500),
|
||
displayOrder: z.number().int().min(0)
|
||
})
|
||
).optional()
|
||
});
|
||
|
||
type SubmissionConfig = z.infer<typeof submissionConfigSchema>;
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Applicant Experience (Detailed)
|
||
|
||
### 4.1 Dashboard Before SUBMISSION Round Opens
|
||
|
||
**Applicant sees (after advancing from Round 1)**:
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Monaco Ocean Protection Challenge 2026 │
|
||
│ │
|
||
│ Project: OceanCleanup AI │
|
||
│ Status: 🎉 Semi-Finalist (Advancing to Round 2) │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 📄 Round 1: Application Documents (Submitted) │
|
||
│ │
|
||
│ ✅ Pitch Deck │
|
||
│ Filename: OceanCleanup_Pitch.pdf │
|
||
│ Uploaded: Jan 25, 2026 at 2:30 PM │
|
||
│ [View] [Download] [Replace] ← Can still edit │
|
||
│ │
|
||
│ ✅ Budget │
|
||
│ Filename: Budget_2026.xlsx │
|
||
│ Uploaded: Feb 1, 2026 at 9:15 AM │
|
||
│ [View] [Download] [Replace] │
|
||
│ │
|
||
│ ✅ Team CV │
|
||
│ Filename: Team_Bios.pdf │
|
||
│ Uploaded: Jan 25, 2026 at 2:30 PM │
|
||
│ [View] [Download] [Replace] │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ ⏳ Next Steps │
|
||
│ │
|
||
│ Congratulations on advancing to the semi-finals! │
|
||
│ │
|
||
│ Additional submission requirements will be announced soon. │
|
||
│ You will receive an email notification when the next │
|
||
│ submission window opens. │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**Notes**:
|
||
- All Round 1 documents are **still editable** (window not locked yet)
|
||
- No indication of upcoming requirements
|
||
- "Semi-Finalist" badge visible in project status
|
||
- Replace buttons are active
|
||
|
||
### 4.2 Dashboard After SUBMISSION Round Opens
|
||
|
||
**When Window 2 opens (March 1, 2026), applicant sees**:
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Monaco Ocean Protection Challenge 2026 │
|
||
│ │
|
||
│ Project: OceanCleanup AI │
|
||
│ Status: 🎉 Semi-Finalist (Round 2 Submission Open) │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 🔒 Round 1: Application Documents (Locked) │
|
||
│ │
|
||
│ These documents were submitted for Round 1 and are now │
|
||
│ locked. You can view and download them, but cannot make │
|
||
│ changes. Admins retain the ability to manage these files │
|
||
│ on your behalf if needed. │
|
||
│ │
|
||
│ ✅ Pitch Deck │
|
||
│ Filename: OceanCleanup_Pitch.pdf │
|
||
│ Uploaded: Jan 25, 2026 at 2:30 PM │
|
||
│ [View] [Download] ← No Replace button │
|
||
│ │
|
||
│ ✅ Budget │
|
||
│ Filename: Budget_2026.xlsx │
|
||
│ Uploaded: Feb 1, 2026 at 9:15 AM │
|
||
│ [View] [Download] │
|
||
│ │
|
||
│ ✅ Team CV │
|
||
│ Filename: Team_Bios.pdf │
|
||
│ Uploaded: Jan 25, 2026 at 2:30 PM │
|
||
│ [View] [Download] │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 📤 Round 2: Semi-Finalist Materials │
|
||
│ Deadline: March 15, 2026 at 11:59 PM CET │
|
||
│ ⏰ 14 days, 15 hours remaining │
|
||
│ │
|
||
│ Progress: 0 of 3 required files submitted │
|
||
│ [░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0% │
|
||
│ │
|
||
│ Congratulations on advancing! Please submit the following │
|
||
│ materials by March 15. │
|
||
│ │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ❌ 3-Minute Video Pitch (Required) │
|
||
│ MP4 format, max 100MB. Introduce your team and │
|
||
│ demonstrate your solution. │
|
||
│ │
|
||
│ Not uploaded │
|
||
│ [Choose File] or Drag & Drop Here │
|
||
│ │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ❌ Updated Pitch Deck (Required) │
|
||
│ PDF format. Include any updates since your initial │
|
||
│ application. │
|
||
│ │
|
||
│ Not uploaded │
|
||
│ [Choose File] or Drag & Drop Here │
|
||
│ │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ❌ Financial Projections (Required) │
|
||
│ Excel or PDF. 3-year revenue and cost forecast with │
|
||
│ assumptions. │
|
||
│ │
|
||
│ Not uploaded │
|
||
│ [Choose File] or Drag & Drop Here │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**UI Changes**:
|
||
1. Round 1 section shows **🔒 lock icon** in header
|
||
2. All **Replace/Delete buttons removed** for Window 1 files
|
||
3. **New Round 2 section appears** with upload slots
|
||
4. **Countdown timer** shows time remaining
|
||
5. **Progress bar** shows completion status
|
||
6. Red **❌ indicators** for missing required files
|
||
|
||
### 4.3 File Upload Flow (Detailed)
|
||
|
||
**Step 1: User clicks "Choose File" for a requirement**
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Upload: 3-Minute Video Pitch │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ Requirements: │
|
||
│ • Accepted file types: MP4, MOV │
|
||
│ • Maximum file size: 100 MB │
|
||
│ • This file is REQUIRED │
|
||
│ │
|
||
│ ┌───────────────────────────────────────────────────────┐ │
|
||
│ │ │ │
|
||
│ │ Drag and drop your file here │ │
|
||
│ │ or │ │
|
||
│ │ Click to Browse │ │
|
||
│ │ │ │
|
||
│ └───────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Description: │
|
||
│ Introduce your team and demonstrate your solution. Show │
|
||
│ how your technology works and its potential impact. │
|
||
│ │
|
||
│ [Cancel] [Upload] │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**Step 2: File selected, validation in progress**
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Upload: 3-Minute Video Pitch │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ✅ File selected: Pitch_Video.mp4 │
|
||
│ Size: 85.2 MB (within 100 MB limit) │
|
||
│ Type: video/mp4 ✓ │
|
||
│ │
|
||
│ Validating... │
|
||
│ │
|
||
│ [████████████████████░░░░] 88% Uploading │
|
||
│ │
|
||
│ [Cancel Upload] │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**Step 3: Upload complete**
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 📤 Round 2: Semi-Finalist Materials │
|
||
│ Deadline: March 15, 2026 at 11:59 PM CET │
|
||
│ ⏰ 14 days, 14 hours remaining │
|
||
│ │
|
||
│ Progress: 1 of 3 required files submitted │
|
||
│ [██████████░░░░░░░░░░░░░░░░░░░░░░░] 33% │
|
||
│ │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ✅ 3-Minute Video Pitch (Required) │
|
||
│ MP4 format, max 100MB │
|
||
│ │
|
||
│ Filename: Pitch_Video.mp4 │
|
||
│ Size: 85.2 MB │
|
||
│ Uploaded: Mar 1, 2026 at 10:30 AM │
|
||
│ [View] [Download] [Replace] [Delete] │
|
||
│ │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ❌ Updated Pitch Deck (Required) │
|
||
│ PDF format │
|
||
│ [Choose File] or Drag & Drop Here │
|
||
│ │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ❌ Financial Projections (Required) │
|
||
│ Excel or PDF │
|
||
│ [Choose File] or Drag & Drop Here │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**Validation Error Example** (file too large):
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ ❌ Upload Failed │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ File: Pitch_Video_HD.mp4 │
|
||
│ Size: 152.8 MB │
|
||
│ │
|
||
│ Error: File size exceeds the maximum limit of 100 MB. │
|
||
│ │
|
||
│ Please compress your video or upload a smaller version. │
|
||
│ │
|
||
│ Tip: You can use free tools like HandBrake to compress │
|
||
│ video files while maintaining quality. │
|
||
│ │
|
||
│ [OK] │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**Validation Error Example** (wrong file type):
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ ❌ Upload Failed │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ File: Budget_Report.docx │
|
||
│ Type: application/vnd.openxmlformats-officedocument... │
|
||
│ │
|
||
│ Error: File type not accepted for this requirement. │
|
||
│ │
|
||
│ Accepted types: PDF, Excel (XLSX) │
|
||
│ │
|
||
│ Please convert your document to PDF or Excel format and │
|
||
│ try again. │
|
||
│ │
|
||
│ [OK] │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 4.4 Deadline Countdown UI States
|
||
|
||
**14 days before deadline (Normal)**:
|
||
```
|
||
⏰ 14 days, 15 hours remaining
|
||
```
|
||
|
||
**7 days before deadline (Normal)**:
|
||
```
|
||
⏰ 7 days remaining
|
||
```
|
||
|
||
**3 days before deadline (Warning)**:
|
||
```
|
||
⚠️ 3 days remaining — Please submit soon!
|
||
[Highlighted in yellow]
|
||
```
|
||
|
||
**24 hours before deadline (Urgent)**:
|
||
```
|
||
🚨 URGENT: Less than 1 day remaining
|
||
Deadline: Tomorrow at 11:59 PM
|
||
[Highlighted in orange]
|
||
```
|
||
|
||
**6 hours before deadline (Critical)**:
|
||
```
|
||
🚨 CRITICAL: 6 hours remaining
|
||
Deadline: Today at 11:59 PM
|
||
[Highlighted in red, pulsing animation]
|
||
```
|
||
|
||
**After deadline (HARD policy)**:
|
||
```
|
||
❌ Deadline passed: March 15, 2026 at 11:59 PM
|
||
Submissions are no longer accepted.
|
||
|
||
If you believe this is an error, please contact the
|
||
program administrator.
|
||
|
||
[Contact Admin]
|
||
```
|
||
|
||
**After deadline (FLAG policy)**:
|
||
```
|
||
⚠️ Deadline passed: March 15, 2026 at 11:59 PM
|
||
Late submissions will be flagged and may impact your
|
||
evaluation.
|
||
|
||
You can still submit, but your files will be marked as
|
||
late and the jury will be notified.
|
||
|
||
[Upload File] (Late submission)
|
||
```
|
||
|
||
**After deadline, within grace period (GRACE policy)**:
|
||
```
|
||
⏳ Grace Period Active
|
||
Deadline was: March 15, 2026 at 11:59 PM
|
||
Grace period ends: March 17, 2026 at 11:59 PM
|
||
|
||
⏰ 1 day, 23 hours remaining in grace period
|
||
|
||
You can still submit without penalty during this time.
|
||
|
||
[Upload File]
|
||
```
|
||
|
||
**After grace period ends (GRACE policy)**:
|
||
```
|
||
❌ Grace period ended: March 17, 2026 at 11:59 PM
|
||
Submissions are now closed.
|
||
|
||
If you have extenuating circumstances, please contact
|
||
the program administrator to request an extension.
|
||
|
||
[Contact Admin]
|
||
```
|
||
|
||
### 4.5 Completion Status Indicators
|
||
|
||
**Incomplete (missing required files)**:
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 📤 Round 2: Semi-Finalist Materials │
|
||
│ Deadline: March 15, 2026 at 11:59 PM CET │
|
||
│ │
|
||
│ Status: ⚠️ INCOMPLETE │
|
||
│ 1 of 3 required files submitted │
|
||
│ │
|
||
│ [██████████░░░░░░░░░░░░░░░░░░░░░░░] 33% │
|
||
│ │
|
||
│ You still need to submit: │
|
||
│ • Updated Pitch Deck (required) │
|
||
│ • Financial Projections (required) │
|
||
│ │
|
||
│ ⏰ 12 days remaining │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**Complete (all required files submitted)**:
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 📤 Round 2: Semi-Finalist Materials │
|
||
│ Deadline: March 15, 2026 at 11:59 PM CET │
|
||
│ │
|
||
│ Status: ✅ COMPLETE │
|
||
│ 3 of 3 required files submitted │
|
||
│ │
|
||
│ [██████████████████████████████████████] 100% │
|
||
│ │
|
||
│ All required files have been submitted. You can still │
|
||
│ update or replace files until the deadline. │
|
||
│ │
|
||
│ ⏰ 12 days remaining │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**Complete with late submissions**:
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 📤 Round 2: Semi-Finalist Materials │
|
||
│ Deadline: March 15, 2026 at 11:59 PM CET (PASSED) │
|
||
│ │
|
||
│ Status: ⚠️ COMPLETE (1 file submitted late) │
|
||
│ 3 of 3 required files submitted │
|
||
│ │
|
||
│ [██████████████████████████████████████] 100% │
|
||
│ │
|
||
│ All files submitted, but the Financial Projections file │
|
||
│ was submitted after the deadline. This has been flagged │
|
||
│ and the jury will be notified during evaluation. │
|
||
│ │
|
||
│ Submitted late: │
|
||
│ • Financial Projections (submitted Mar 16 at 2:15 PM) │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**Complete with optional files missing**:
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 📤 Round 2: Semi-Finalist Materials │
|
||
│ Deadline: March 15, 2026 at 11:59 PM CET │
|
||
│ │
|
||
│ Status: ✅ COMPLETE │
|
||
│ 3 of 3 required files submitted │
|
||
│ 0 of 1 optional files submitted │
|
||
│ │
|
||
│ All required files submitted. Optional: │
|
||
│ ○ Team Photos (optional, not submitted) │
|
||
│ │
|
||
│ ⏰ 12 days remaining │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 4.6 Email Notifications
|
||
|
||
**Email 1: Window Opening Notification**
|
||
|
||
```
|
||
From: Monaco Ocean Protection Challenge <noreply@monaco-opc.com>
|
||
To: team@oceancleanup.ai
|
||
Subject: 🎉 You're a Semi-Finalist! Submit Your Round 2 Materials
|
||
Date: March 1, 2026 at 9:00 AM
|
||
|
||
───────────────────────────────────────────────────────────
|
||
|
||
Dear OceanCleanup AI Team,
|
||
|
||
Congratulations on advancing to the semi-finals of the Monaco
|
||
Ocean Protection Challenge 2026!
|
||
|
||
You have been selected from 150 applicants to advance to the
|
||
next round. We were impressed by your innovative approach to
|
||
ocean conservation.
|
||
|
||
NEXT STEPS: SUBMIT ROUND 2 MATERIALS
|
||
|
||
To continue in the competition, please submit the following
|
||
documents by:
|
||
|
||
📅 DEADLINE: March 15, 2026 at 11:59 PM CET
|
||
|
||
Required Materials:
|
||
✓ 3-Minute Video Pitch (MP4, max 100MB)
|
||
Introduce your team and demonstrate your solution
|
||
|
||
✓ Updated Pitch Deck (PDF, max 10MB)
|
||
Include any updates since your initial application
|
||
|
||
✓ Financial Projections (Excel or PDF, max 5MB)
|
||
3-year revenue and cost forecast with assumptions
|
||
|
||
──────────────────────────────────────────────────────────
|
||
|
||
[Upload Documents →]
|
||
|
||
──────────────────────────────────────────────────────────
|
||
|
||
IMPORTANT NOTES:
|
||
|
||
• Your Round 1 application documents are now locked and
|
||
cannot be edited. They will remain available for viewing.
|
||
|
||
• You have 14 days to submit your materials.
|
||
|
||
• All required files must be submitted by the deadline.
|
||
|
||
• You can update or replace files at any time before the
|
||
deadline.
|
||
|
||
If you have any questions or encounter technical difficulties,
|
||
please contact us at support@monaco-opc.com.
|
||
|
||
Best regards,
|
||
Monaco Ocean Protection Challenge Team
|
||
|
||
───────────────────────────────────────────────────────────
|
||
```
|
||
|
||
**Email 2: Reminder (7 days before)**
|
||
|
||
```
|
||
From: Monaco Ocean Protection Challenge <noreply@monaco-opc.com>
|
||
To: team@oceancleanup.ai
|
||
Subject: ⏰ Reminder: Semi-Finalist Materials Due in 7 Days
|
||
Date: March 8, 2026 at 9:00 AM
|
||
|
||
───────────────────────────────────────────────────────────
|
||
|
||
Dear OceanCleanup AI Team,
|
||
|
||
This is a friendly reminder that your semi-finalist materials
|
||
are due in 7 days.
|
||
|
||
DEADLINE: March 15, 2026 at 11:59 PM CET
|
||
|
||
Current Status: 1 of 3 required files submitted ⚠️
|
||
|
||
Missing Files:
|
||
❌ Updated Pitch Deck (PDF, required)
|
||
❌ Financial Projections (Excel/PDF, required)
|
||
|
||
──────────────────────────────────────────────────────────
|
||
|
||
[Complete Your Submission →]
|
||
|
||
──────────────────────────────────────────────────────────
|
||
|
||
Don't wait until the last minute! Upload your remaining
|
||
documents now to ensure you don't miss the deadline.
|
||
|
||
Best regards,
|
||
Monaco Ocean Protection Challenge Team
|
||
```
|
||
|
||
**Email 3: Urgent Reminder (24 hours before)**
|
||
|
||
```
|
||
From: Monaco Ocean Protection Challenge <noreply@monaco-opc.com>
|
||
To: team@oceancleanup.ai
|
||
Subject: 🚨 URGENT: Semi-Finalist Materials Due Tomorrow
|
||
Date: March 14, 2026 at 9:00 AM
|
||
|
||
───────────────────────────────────────────────────────────
|
||
|
||
Dear OceanCleanup AI Team,
|
||
|
||
URGENT: Your semi-finalist materials are due TOMORROW
|
||
(March 15) at 11:59 PM CET.
|
||
|
||
Current Status: 2 of 3 required files submitted ⚠️
|
||
|
||
Missing Files:
|
||
❌ Financial Projections (Excel/PDF, REQUIRED)
|
||
|
||
──────────────────────────────────────────────────────────
|
||
|
||
[Upload Missing File Now →]
|
||
|
||
──────────────────────────────────────────────────────────
|
||
|
||
PLEASE ACT IMMEDIATELY. Incomplete submissions may result
|
||
in disqualification from the competition.
|
||
|
||
If you are experiencing technical difficulties, contact us
|
||
immediately at support@monaco-opc.com or call +377 XXX XXX.
|
||
|
||
Best regards,
|
||
Monaco Ocean Protection Challenge Team
|
||
```
|
||
|
||
**Email 4: Late Submission Warning (FLAG policy)**
|
||
|
||
```
|
||
From: Monaco Ocean Protection Challenge <noreply@monaco-opc.com>
|
||
To: team@oceancleanup.ai
|
||
Subject: ⚠️ Late Submission Notice
|
||
Date: March 16, 2026 at 10:15 AM
|
||
|
||
───────────────────────────────────────────────────────────
|
||
|
||
Dear OceanCleanup AI Team,
|
||
|
||
We received your Financial Projections file on March 16 at
|
||
2:15 PM, which was after the deadline of March 15 at 11:59 PM.
|
||
|
||
Your submission has been marked as LATE and flagged in our
|
||
system. The jury will be notified of the late submission
|
||
during evaluation, which may impact your score.
|
||
|
||
While we understand that unexpected circumstances can arise,
|
||
please be mindful of deadlines in future rounds.
|
||
|
||
Your submission is now complete. Good luck in the next round!
|
||
|
||
Best regards,
|
||
Monaco Ocean Protection Challenge Team
|
||
```
|
||
|
||
---
|
||
|
||
## 5. Jury Cross-Round Visibility (Detailed)
|
||
|
||
### 5.1 RoundSubmissionVisibility Configuration
|
||
|
||
**Purpose**: Control which EVALUATION rounds can see which SubmissionWindows.
|
||
|
||
**Configuration Example**:
|
||
|
||
```typescript
|
||
// Evaluation Round 1: Jury Screening (sees only Window 1)
|
||
await prisma.roundSubmissionVisibility.create({
|
||
data: {
|
||
evaluationRoundId: "eval_round_1",
|
||
submissionWindowId: "window_1",
|
||
displayLabel: "Application Documents",
|
||
displayOrder: 1
|
||
}
|
||
});
|
||
|
||
// Evaluation Round 2: Jury Semi-Finals (sees Window 1 + Window 2)
|
||
await prisma.roundSubmissionVisibility.createMany({
|
||
data: [
|
||
{
|
||
evaluationRoundId: "eval_round_2",
|
||
submissionWindowId: "window_1",
|
||
displayLabel: "Round 1 Application",
|
||
displayOrder: 1
|
||
},
|
||
{
|
||
evaluationRoundId: "eval_round_2",
|
||
submissionWindowId: "window_2",
|
||
displayLabel: "Semi-Final Materials",
|
||
displayOrder: 2
|
||
}
|
||
]
|
||
});
|
||
|
||
// Evaluation Round 3: Jury Finals (sees all three windows)
|
||
await prisma.roundSubmissionVisibility.createMany({
|
||
data: [
|
||
{
|
||
evaluationRoundId: "eval_round_3",
|
||
submissionWindowId: "window_1",
|
||
displayLabel: "Original Application (Jan)",
|
||
displayOrder: 1
|
||
},
|
||
{
|
||
evaluationRoundId: "eval_round_3",
|
||
submissionWindowId: "window_2",
|
||
displayLabel: "Semi-Final Submission (Mar)",
|
||
displayOrder: 2
|
||
},
|
||
{
|
||
evaluationRoundId: "eval_round_3",
|
||
submissionWindowId: "window_3",
|
||
displayLabel: "Finalist Presentation (Apr)",
|
||
displayOrder: 3
|
||
}
|
||
]
|
||
});
|
||
```
|
||
|
||
### 5.2 Jury Evaluation View (Round 1 - Single Window)
|
||
|
||
**Jury sees ONLY Window 1**:
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Evaluating: OceanCleanup AI │
|
||
│ Round: Jury Screening (Round 1) │
|
||
│ Assigned: Feb 10, 2026 │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 📄 Application Documents │
|
||
│ │
|
||
│ Pitch Deck │
|
||
│ Filename: OceanCleanup_Pitch.pdf │
|
||
│ Uploaded: Jan 25, 2026 at 2:30 PM │
|
||
│ Size: 2.4 MB │
|
||
│ [View] [Download] │
|
||
│ │
|
||
│ Budget │
|
||
│ Filename: Budget_2026.xlsx │
|
||
│ Uploaded: Feb 1, 2026 at 9:15 AM │
|
||
│ Size: 850 KB │
|
||
│ [View] [Download] │
|
||
│ │
|
||
│ Team CV │
|
||
│ Filename: Team_Bios.pdf │
|
||
│ Uploaded: Jan 25, 2026 at 2:30 PM │
|
||
│ Size: 1.2 MB │
|
||
│ [View] [Download] │
|
||
│ │
|
||
│ [Download All Files (ZIP)] │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Your Evaluation │
|
||
│ │
|
||
│ Impact Score (1-10): [_____] │
|
||
│ Feasibility Score (1-10): [_____] │
|
||
│ Innovation Score (1-10): [_____] │
|
||
│ │
|
||
│ Comments: │
|
||
│ ┌───────────────────────────────────────────────────────┐ │
|
||
│ │ │ │
|
||
│ │ │ │
|
||
│ └───────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Recommendation: │
|
||
│ ( ) Pass ( ) Fail ( ) Conditional │
|
||
│ │
|
||
│ [Save Draft] [Submit Evaluation] │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**Notes**:
|
||
- Jury does NOT see that Window 2 exists
|
||
- No indication of future submission rounds
|
||
- Clean, focused evaluation on Round 1 materials only
|
||
- UI is simple and uncluttered
|
||
|
||
### 5.3 Jury Evaluation View (Round 2 - Multi-Window)
|
||
|
||
**Jury sees BOTH Window 1 and Window 2**:
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Evaluating: OceanCleanup AI │
|
||
│ Round: Semi-Final Evaluation (Round 2) │
|
||
│ Assigned: Mar 20, 2026 │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 📂 Project Documents │
|
||
│ │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ [ Round 1 Application ] [ Semi-Final Materials ] │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
│ ▲ Active tab │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Tab 1 Active: "Round 1 Application" │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ 📄 Round 1 Application │
|
||
│ │
|
||
│ These documents were submitted during the initial │
|
||
│ application phase (January 2026). │
|
||
│ │
|
||
│ Pitch Deck │
|
||
│ Filename: OceanCleanup_Pitch.pdf │
|
||
│ Uploaded: Jan 25, 2026 at 2:30 PM │
|
||
│ Size: 2.4 MB │
|
||
│ [View] [Download] │
|
||
│ │
|
||
│ Budget │
|
||
│ Filename: Budget_2026.xlsx │
|
||
│ Uploaded: Feb 1, 2026 at 9:15 AM │
|
||
│ Size: 850 KB │
|
||
│ [View] [Download] │
|
||
│ │
|
||
│ Team CV │
|
||
│ Filename: Team_Bios.pdf │
|
||
│ Uploaded: Jan 25, 2026 at 2:30 PM │
|
||
│ Size: 1.2 MB │
|
||
│ [View] [Download] │
|
||
│ │
|
||
│ [Download All Round 1 Files (ZIP)] │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**Click "Semi-Final Materials" tab**:
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ 📂 Project Documents │
|
||
│ │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ [ Round 1 Application ] [ Semi-Final Materials ] │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
│ ▲ Active tab │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Tab 2 Active: "Semi-Final Materials" │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ 📄 Semi-Final Materials │
|
||
│ │
|
||
│ These documents were submitted for the semi-final │
|
||
│ evaluation round (March 2026). │
|
||
│ │
|
||
│ 3-Minute Video Pitch │
|
||
│ Filename: Pitch_Video.mp4 │
|
||
│ Uploaded: Mar 1, 2026 at 10:30 AM │
|
||
│ Size: 85.2 MB │
|
||
│ [View] [Download] │
|
||
│ │
|
||
│ Updated Pitch Deck │
|
||
│ Filename: OceanCleanup_Pitch_v2.pdf │
|
||
│ Uploaded: Mar 10, 2026 at 4:20 PM │
|
||
│ Size: 3.1 MB │
|
||
│ [View] [Download] │
|
||
│ │
|
||
│ Financial Projections │
|
||
│ Filename: Financials_2026.xlsx │
|
||
│ Uploaded: Mar 16, 2026 at 2:15 PM ⚠️ LATE SUBMISSION │
|
||
│ Size: 1.5 MB │
|
||
│ [View] [Download] │
|
||
│ ⚠️ This file was submitted after the deadline of Mar 15 │
|
||
│ │
|
||
│ [Download All Round 2 Files (ZIP)] │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**Evaluation Form (Below Documents)**:
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Your Evaluation │
|
||
│ │
|
||
│ ℹ️ Please evaluate based on ALL submitted materials │
|
||
│ (both Round 1 and Round 2 documents). │
|
||
│ │
|
||
│ Business Model Score (1-10): [_____] │
|
||
│ Team Strength Score (1-10): [_____] │
|
||
│ Presentation Quality (1-10): [_____] │
|
||
│ Financial Viability (1-10): [_____] │
|
||
│ │
|
||
│ Comments: │
|
||
│ ┌───────────────────────────────────────────────────────┐ │
|
||
│ │ │ │
|
||
│ │ │ │
|
||
│ └───────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Recommendation: │
|
||
│ ( ) Pass ( ) Fail ( ) Conditional │
|
||
│ │
|
||
│ ⚠️ Note: Financial Projections was submitted late. │
|
||
│ │
|
||
│ [Save Draft] [Submit Evaluation] │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
**UI Enhancements**:
|
||
1. **Tab Navigation**: Clear separation between submission windows
|
||
2. **Context Labels**: Each tab shows description and submission date range
|
||
3. **Late Indicators**: Flagged submissions clearly visible
|
||
4. **Download Options**: Bulk download per window or all files
|
||
5. **Submission Metadata**: Upload dates, file sizes, uploaders visible
|
||
6. **Badge Indicators**: Late submissions have ⚠️ warning badge
|
||
|
||
### 5.4 Query Implementation for Jury View
|
||
|
||
```typescript
|
||
// src/server/routers/evaluation.ts
|
||
|
||
export const evaluationRouter = router({
|
||
/**
|
||
* Get all visible files for a project in an evaluation round
|
||
*/
|
||
getVisibleFilesForProject: juryProcedure
|
||
.input(z.object({
|
||
projectId: z.string().cuid(),
|
||
evaluationRoundId: z.string().cuid()
|
||
}))
|
||
.query(async ({ input, ctx }) => {
|
||
// Step 1: Get visible windows for this evaluation round
|
||
const visibleWindows = await ctx.prisma.roundSubmissionVisibility.findMany({
|
||
where: { evaluationRoundId: input.evaluationRoundId },
|
||
include: {
|
||
submissionWindow: {
|
||
include: {
|
||
fileRequirements: {
|
||
orderBy: { displayOrder: "asc" }
|
||
}
|
||
}
|
||
}
|
||
},
|
||
orderBy: { displayOrder: "asc" }
|
||
});
|
||
|
||
if (visibleWindows.length === 0) {
|
||
throw new TRPCError({
|
||
code: "NOT_FOUND",
|
||
message: "No submission windows configured for this evaluation round"
|
||
});
|
||
}
|
||
|
||
// Step 2: Fetch files for each visible window
|
||
const filesByWindow = await Promise.all(
|
||
visibleWindows.map(async (visibility) => {
|
||
const files = await ctx.prisma.projectFile.findMany({
|
||
where: {
|
||
projectId: input.projectId,
|
||
submissionWindowId: visibility.submissionWindowId,
|
||
supersededBy: null // Only current versions
|
||
},
|
||
include: {
|
||
requirement: true,
|
||
uploadedByUser: {
|
||
select: {
|
||
name: true,
|
||
email: true,
|
||
role: true
|
||
}
|
||
}
|
||
},
|
||
orderBy: { requirement: { displayOrder: "asc" } }
|
||
});
|
||
|
||
// Generate download URLs
|
||
const filesWithUrls = await Promise.all(
|
||
files.map(async (file) => ({
|
||
id: file.id,
|
||
filename: file.filename,
|
||
requirementLabel: file.requirement?.label || "Untitled",
|
||
uploadedAt: file.uploadedAt,
|
||
uploadedBy: file.uploadedByUser.name,
|
||
uploadedByRole: file.uploadedByUser.role,
|
||
sizeBytes: file.sizeBytes,
|
||
isLate: file.isLate,
|
||
downloadUrl: await getPresignedDownloadUrl(file.id)
|
||
}))
|
||
);
|
||
|
||
return {
|
||
windowId: visibility.submissionWindowId,
|
||
windowName: visibility.displayLabel,
|
||
windowDescription: visibility.submissionWindow.description,
|
||
displayOrder: visibility.displayOrder,
|
||
files: filesWithUrls
|
||
};
|
||
})
|
||
);
|
||
|
||
return filesByWindow;
|
||
})
|
||
});
|
||
```
|
||
|
||
**Frontend Usage**:
|
||
|
||
```tsx
|
||
// components/jury/ProjectFilesView.tsx
|
||
|
||
export function ProjectFilesView({ projectId, evaluationRoundId }: Props) {
|
||
const { data: filesByWindow, isLoading } = trpc.evaluation.getVisibleFilesForProject.useQuery({
|
||
projectId,
|
||
evaluationRoundId
|
||
});
|
||
|
||
if (isLoading) return <LoadingSpinner />;
|
||
|
||
return (
|
||
<div>
|
||
<Tabs defaultValue={filesByWindow?.[0]?.windowId}>
|
||
<TabsList>
|
||
{filesByWindow?.map((window) => (
|
||
<TabsTrigger key={window.windowId} value={window.windowId}>
|
||
{window.windowName}
|
||
</TabsTrigger>
|
||
))}
|
||
</TabsList>
|
||
|
||
{filesByWindow?.map((window) => (
|
||
<TabsContent key={window.windowId} value={window.windowId}>
|
||
<div className="space-y-4">
|
||
<p className="text-muted-foreground">{window.windowDescription}</p>
|
||
|
||
{window.files.map((file) => (
|
||
<FileCard
|
||
key={file.id}
|
||
file={file}
|
||
showLateIndicator={file.isLate}
|
||
/>
|
||
))}
|
||
|
||
<Button variant="outline">
|
||
Download All {window.windowName} Files (ZIP)
|
||
</Button>
|
||
</div>
|
||
</TabsContent>
|
||
))}
|
||
</Tabs>
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
### 5.5 Admin Visibility Configuration UI
|
||
|
||
**Admin panel for configuring which windows Jury 2 can see**:
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Configure Document Visibility: Semi-Final Evaluation │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
|
||
Which submission windows should jury members see during this
|
||
evaluation round?
|
||
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Available Submission Windows: │
|
||
│ │
|
||
│ ☑ Window 1: Application Documents │
|
||
│ Created: Jan 1, 2026 │
|
||
│ Closed: Feb 1, 2026 │
|
||
│ │
|
||
│ Display as: [Round 1 Application______________] │
|
||
│ Order: [1] │
|
||
│ │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ☑ Window 2: Semi-Finalist Materials │
|
||
│ Created: Mar 1, 2026 │
|
||
│ Closed: Mar 15, 2026 │
|
||
│ │
|
||
│ Display as: [Semi-Final Submissions___________] │
|
||
│ Order: [2] │
|
||
│ │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ☐ Window 3: Finalist Presentations │
|
||
│ Status: Not yet created │
|
||
│ (This window will be created in a future round) │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
|
||
Preview: Jury will see 2 tabs in this order:
|
||
1. Round 1 Application
|
||
2. Semi-Final Submissions
|
||
|
||
[Cancel] [Save Configuration]
|
||
```
|
||
|
||
**Validation Rules**:
|
||
- At least one window must be selected
|
||
- Display labels must be unique within an evaluation round
|
||
- Display order must be positive integers
|
||
- Cannot select a window that hasn't opened yet (optional warning)
|
||
- Cannot select a window from a different competition
|
||
|
||
---
|
||
|
||
## 6. Admin Experience
|
||
|
||
(Content continues in next part due to length...)
|
||
|
||
Would you like me to continue with the rest of the document (sections 6-10)? |