1540 lines
75 KiB
Markdown
1540 lines
75 KiB
Markdown
# Round Type: INTAKE — Application Window
|
||
|
||
## 1. Overview
|
||
|
||
The **INTAKE round** is the first phase of any MOPC competition. It represents the application submission window where teams apply to participate by submitting their ocean conservation projects along with required documentation.
|
||
|
||
### Purpose
|
||
- Collect project applications from teams worldwide
|
||
- Capture essential project information (title, description, team, ocean issue addressed)
|
||
- Receive required documentation (executive summaries, business plans, videos)
|
||
- Support draft/save-and-continue workflow for incomplete applications
|
||
- Enforce submission deadlines with configurable late submission policies
|
||
- Enable both authenticated (user login) and public (anonymous) application flows
|
||
|
||
### Position in Competition Flow
|
||
|
||
```
|
||
Competition 2026
|
||
└─ Round 1: "Application Window" ────── [INTAKE]
|
||
└─ Round 2: "AI Screening" ───────── [FILTERING]
|
||
└─ Round 3: "Jury Evaluation 1" ── [EVALUATION]
|
||
└─ Round 4: "Semi-finalist Docs" ── [SUBMISSION]
|
||
└─ ...
|
||
```
|
||
|
||
The intake round creates the initial pool of projects that flow through subsequent rounds. All projects begin here.
|
||
|
||
---
|
||
|
||
## 2. Current System (Pipeline/Track/Stage)
|
||
|
||
### How Intake Works Today
|
||
|
||
In the current architecture, intake is implemented as a **Stage** with `StageType: INTAKE` inside the MAIN track.
|
||
|
||
```typescript
|
||
// Current structure
|
||
Pipeline: "MOPC 2026"
|
||
└─ Track: "Main Competition" (kind: MAIN)
|
||
└─ Stage: "Intake" (stageType: INTAKE)
|
||
├─ windowOpenAt: 2026-01-15T00:00:00Z
|
||
├─ windowCloseAt: 2026-03-01T23:59:59Z
|
||
├─ configJson: {
|
||
│ submissionWindowEnabled: true,
|
||
│ lateSubmissionPolicy: "flag",
|
||
│ lateGraceHours: 24,
|
||
│ fileRequirements: [...]
|
||
│ }
|
||
└─ FileRequirement records (linked to stageId)
|
||
```
|
||
|
||
### Current Config Fields (src/lib/pipeline-defaults.ts)
|
||
|
||
```typescript
|
||
type IntakeConfig = {
|
||
submissionWindowEnabled: boolean // Whether submission is open
|
||
lateSubmissionPolicy: 'reject' | 'flag' | 'accept'
|
||
lateGraceHours: number // Hours after close for late submissions
|
||
fileRequirements: FileRequirementConfig[]
|
||
}
|
||
|
||
type FileRequirementConfig = {
|
||
name: string // "Executive Summary"
|
||
description?: string // Help text
|
||
acceptedMimeTypes: string[] // ["application/pdf"]
|
||
maxSizeMB?: number // 50
|
||
isRequired: boolean // true
|
||
}
|
||
```
|
||
|
||
### Current Applicant Flow
|
||
|
||
1. **Access**: Applicant visits `/applicant/pipeline/{slug}` or receives invite link
|
||
2. **Auth**: User logs in (email magic link or password) or proceeds as guest
|
||
3. **Form**: Fills out project form (title, description, category, ocean issue, team info)
|
||
4. **Files**: Uploads required files (exec summary, business plan, video)
|
||
5. **Draft**: Can save as draft and return later
|
||
6. **Submit**: Final submission creates Project record with status "SUBMITTED"
|
||
7. **Email**: Confirmation email sent if configured
|
||
|
||
### Current Admin Experience
|
||
|
||
Admins configure the intake stage via the pipeline wizard:
|
||
- Set open/close dates
|
||
- Define file requirements (name, mime types, size limits, required/optional)
|
||
- Choose late submission policy (reject, flag, accept)
|
||
- Set grace period for late submissions
|
||
|
||
### Current Database Schema
|
||
|
||
```prisma
|
||
model Stage {
|
||
id String @id
|
||
trackId String
|
||
stageType StageType // INTAKE
|
||
name String
|
||
slug String
|
||
status StageStatus
|
||
configJson Json? // IntakeConfig stored here
|
||
windowOpenAt DateTime?
|
||
windowCloseAt DateTime?
|
||
...
|
||
}
|
||
|
||
model FileRequirement {
|
||
id String @id
|
||
stageId String // Links to intake stage
|
||
name String
|
||
acceptedMimeTypes String[]
|
||
maxSizeMB Int?
|
||
isRequired Boolean
|
||
sortOrder Int
|
||
...
|
||
}
|
||
|
||
model ProjectFile {
|
||
id String @id
|
||
projectId String
|
||
roundId String? // Legacy field
|
||
requirementId String? // FK to FileRequirement
|
||
fileType FileType
|
||
fileName String
|
||
mimeType String
|
||
size Int
|
||
bucket String
|
||
objectKey String
|
||
isLate Boolean @default(false)
|
||
version Int @default(1)
|
||
...
|
||
}
|
||
|
||
model Project {
|
||
id String @id
|
||
programId String
|
||
roundId String? // Legacy — which round project was submitted for
|
||
status ProjectStatus @default(SUBMITTED)
|
||
title String
|
||
teamName String?
|
||
description String?
|
||
competitionCategory CompetitionCategory? // STARTUP | BUSINESS_CONCEPT
|
||
oceanIssue OceanIssue?
|
||
country String?
|
||
wantsMentorship Boolean @default(false)
|
||
isDraft Boolean @default(false)
|
||
draftDataJson Json? // Form data for drafts
|
||
draftExpiresAt DateTime?
|
||
submissionSource SubmissionSource @default(MANUAL)
|
||
submittedByEmail String?
|
||
submittedAt DateTime?
|
||
submittedByUserId String?
|
||
...
|
||
}
|
||
```
|
||
|
||
### Current Limitations
|
||
|
||
| Issue | Impact |
|
||
|-------|--------|
|
||
| **Single submission window** | Can't require new docs from semi-finalists |
|
||
| **No form builder** | All fields hardcoded in application code |
|
||
| **No category quotas at intake** | Can't limit "first 50 startups, first 50 concepts" |
|
||
| **Generic configJson** | Unclear what fields exist for intake stages |
|
||
| **File requirements per stage** | Awkward: "intake stage" is the only stage with file requirements |
|
||
| **No dynamic forms** | Can't add custom questions per competition year |
|
||
| **No public form branding** | External applicants see generic MOPC form |
|
||
|
||
---
|
||
|
||
## 3. Redesigned Intake Round
|
||
|
||
### New Round Structure
|
||
|
||
```typescript
|
||
// Redesigned
|
||
Competition: "MOPC 2026"
|
||
└─ Round 1: "Application Window" (roundType: INTAKE)
|
||
├─ competitionId: competition-2026
|
||
├─ name: "Application Window"
|
||
├─ slug: "application-window"
|
||
├─ roundType: INTAKE
|
||
├─ status: ROUND_ACTIVE
|
||
├─ sortOrder: 0
|
||
├─ windowOpenAt: 2026-01-15T00:00:00Z
|
||
├─ windowCloseAt: 2026-03-01T23:59:59Z
|
||
├─ submissionWindowId: "sw-1" // NEW: Links to SubmissionWindow
|
||
├─ configJson: IntakeConfig { // NEW: Typed, validated config
|
||
│ applicationFormId: "form-2026",
|
||
│ deadlinePolicy: "GRACE",
|
||
│ gracePeriodMinutes: 180,
|
||
│ allowDraftSubmissions: true,
|
||
│ requireTeamProfile: true,
|
||
│ maxTeamSize: 5,
|
||
│ minTeamSize: 1,
|
||
│ autoConfirmReceipt: true,
|
||
│ publicFormEnabled: true,
|
||
│ categoryQuotas: { STARTUP: 100, BUSINESS_CONCEPT: 100 }
|
||
│ }
|
||
└─ SubmissionWindow: "Round 1 Docs"
|
||
├─ id: "sw-1"
|
||
├─ competitionId: competition-2026
|
||
├─ name: "Round 1 Application Docs"
|
||
├─ slug: "round-1-docs"
|
||
├─ roundNumber: 1
|
||
├─ windowOpenAt: 2026-01-15T00:00:00Z
|
||
├─ windowCloseAt: 2026-03-01T23:59:59Z
|
||
├─ deadlinePolicy: GRACE
|
||
├─ graceHours: 3
|
||
└─ FileRequirements: [
|
||
├─ "Executive Summary" (PDF, required)
|
||
├─ "Business Plan" (PDF, required)
|
||
└─ "Video Pitch" (video/*, optional)
|
||
]
|
||
```
|
||
|
||
### IntakeConfig Type (Zod-validated)
|
||
|
||
```typescript
|
||
type IntakeConfig = {
|
||
// Application Form
|
||
applicationFormId: string // Links to ApplicationForm template (future)
|
||
|
||
// Submission Window (linked via Round.submissionWindowId)
|
||
submissionWindowId: string // Which SubmissionWindow to use
|
||
|
||
// Deadline Behavior
|
||
deadlinePolicy: DeadlinePolicy // HARD | FLAG | GRACE
|
||
gracePeriodMinutes: number // For GRACE policy (e.g., 180 = 3 hours)
|
||
|
||
// Draft System
|
||
allowDraftSubmissions: boolean // Save-and-continue enabled
|
||
draftExpiryDays: number // Auto-delete abandoned drafts after N days
|
||
|
||
// Team Profile
|
||
requireTeamProfile: boolean // Require team member info
|
||
maxTeamSize: number // Max team members (including lead)
|
||
minTeamSize: number // Min team members (default: 1)
|
||
|
||
// Notifications
|
||
autoConfirmReceipt: boolean // Email confirmation on submission
|
||
reminderEmailSchedule: number[] // Days before deadline: [7, 3, 1]
|
||
|
||
// Public Access
|
||
publicFormEnabled: boolean // Allow external application link
|
||
publicFormSlug?: string // Custom slug for public URL
|
||
|
||
// Category Quotas (STARTUP vs BUSINESS_CONCEPT)
|
||
categoryQuotasEnabled: boolean
|
||
categoryQuotas?: {
|
||
STARTUP: number // Max startups accepted
|
||
BUSINESS_CONCEPT: number // Max concepts accepted
|
||
}
|
||
quotaOverflowPolicy?: 'reject' | 'waitlist' // What happens when quota full
|
||
|
||
// Custom Fields (future: dynamic form builder)
|
||
customFields?: CustomFieldDef[] // Additional form fields
|
||
}
|
||
|
||
enum DeadlinePolicy {
|
||
HARD // Submissions blocked after deadline, no exceptions
|
||
FLAG // Submissions accepted but flagged as late
|
||
GRACE // Accept for N minutes after deadline with warning
|
||
}
|
||
|
||
type CustomFieldDef = {
|
||
id: string
|
||
label: string
|
||
type: 'text' | 'textarea' | 'select' | 'multiselect' | 'date' | 'number'
|
||
required: boolean
|
||
options?: string[] // For select/multiselect
|
||
validation?: {
|
||
min?: number
|
||
max?: number
|
||
regex?: string
|
||
}
|
||
}
|
||
```
|
||
|
||
### Zod Schema for Validation
|
||
|
||
```typescript
|
||
import { z } from 'zod'
|
||
|
||
export const intakeConfigSchema = z.object({
|
||
applicationFormId: z.string().cuid(),
|
||
submissionWindowId: z.string().cuid(),
|
||
|
||
deadlinePolicy: z.enum(['HARD', 'FLAG', 'GRACE']),
|
||
gracePeriodMinutes: z.number().int().min(0).max(1440), // Max 24 hours
|
||
|
||
allowDraftSubmissions: z.boolean().default(true),
|
||
draftExpiryDays: z.number().int().min(1).default(30),
|
||
|
||
requireTeamProfile: z.boolean().default(true),
|
||
maxTeamSize: z.number().int().min(1).max(20).default(5),
|
||
minTeamSize: z.number().int().min(1).default(1),
|
||
|
||
autoConfirmReceipt: z.boolean().default(true),
|
||
reminderEmailSchedule: z.array(z.number().int()).default([7, 3, 1]),
|
||
|
||
publicFormEnabled: z.boolean().default(false),
|
||
publicFormSlug: z.string().optional(),
|
||
|
||
categoryQuotasEnabled: z.boolean().default(false),
|
||
categoryQuotas: z.object({
|
||
STARTUP: z.number().int().min(0),
|
||
BUSINESS_CONCEPT: z.number().int().min(0),
|
||
}).optional(),
|
||
quotaOverflowPolicy: z.enum(['reject', 'waitlist']).optional(),
|
||
|
||
customFields: z.array(z.object({
|
||
id: z.string(),
|
||
label: z.string().min(1).max(200),
|
||
type: z.enum(['text', 'textarea', 'select', 'multiselect', 'date', 'number']),
|
||
required: z.boolean(),
|
||
options: z.array(z.string()).optional(),
|
||
validation: z.object({
|
||
min: z.number().optional(),
|
||
max: z.number().optional(),
|
||
regex: z.string().optional(),
|
||
}).optional(),
|
||
})).optional(),
|
||
})
|
||
|
||
export type IntakeConfig = z.infer<typeof intakeConfigSchema>
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Application Form System
|
||
|
||
### ApplicationForm Model (Future Enhancement)
|
||
|
||
For now, the application form is hardcoded. In the future, a dynamic form builder will replace this.
|
||
|
||
```prisma
|
||
model ApplicationForm {
|
||
id String @id @default(cuid())
|
||
competitionId String
|
||
name String // "MOPC 2026 Application"
|
||
description String?
|
||
fieldsJson Json @db.JsonB // Array of field definitions
|
||
version Int @default(1)
|
||
isActive Boolean @default(true)
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade)
|
||
|
||
@@index([competitionId])
|
||
@@index([isActive])
|
||
}
|
||
```
|
||
|
||
### Standard Form Fields (Hardcoded for MVP)
|
||
|
||
```typescript
|
||
type ApplicationFormData = {
|
||
// Project Info
|
||
title: string // Required, 1-500 chars
|
||
teamName?: string // Optional
|
||
description: string // Required, max 5000 chars
|
||
competitionCategory: 'STARTUP' | 'BUSINESS_CONCEPT' // Required
|
||
oceanIssue: OceanIssue // Required enum
|
||
|
||
// Location
|
||
country: string // Required
|
||
geographicZone?: string // "Europe, France"
|
||
institution?: string // Required for BUSINESS_CONCEPT
|
||
|
||
// Founding
|
||
foundedAt?: Date // When project/company started
|
||
|
||
// Mentorship
|
||
wantsMentorship: boolean // Default: false
|
||
|
||
// Referral
|
||
referralSource?: string // "LinkedIn", "Email", etc.
|
||
|
||
// Team Members (if requireTeamProfile: true)
|
||
teamMembers?: TeamMemberInput[]
|
||
|
||
// Custom Fields (future)
|
||
customFieldValues?: Record<string, unknown>
|
||
}
|
||
|
||
type TeamMemberInput = {
|
||
name: string
|
||
email: string
|
||
role: 'LEAD' | 'MEMBER' | 'ADVISOR'
|
||
title?: string // "CEO", "CTO"
|
||
}
|
||
```
|
||
|
||
### Field Validation Rules
|
||
|
||
| Field | Validation | Error Message |
|
||
|-------|-----------|---------------|
|
||
| `title` | Required, 1-500 chars | "Project title is required" |
|
||
| `description` | Required, max 5000 chars | "Description must be under 5000 characters" |
|
||
| `competitionCategory` | Required enum | "Please select Startup or Business Concept" |
|
||
| `oceanIssue` | Required enum | "Please select an ocean issue" |
|
||
| `country` | Required string | "Country is required" |
|
||
| `institution` | Required if category = BUSINESS_CONCEPT | "Institution is required for student projects" |
|
||
| `teamMembers[].email` | Valid email format | "Invalid email address" |
|
||
| `teamMembers.length` | >= minTeamSize, <= maxTeamSize | "Team must have 1-5 members" |
|
||
|
||
### Conditional Logic
|
||
|
||
- **Institution field**: Only shown/required when `competitionCategory = BUSINESS_CONCEPT`
|
||
- **Team members section**: Only shown if `requireTeamProfile = true` in config
|
||
- **Mentorship checkbox**: Always shown, default unchecked
|
||
|
||
---
|
||
|
||
## 5. Submission Window Integration
|
||
|
||
### SubmissionWindow Model (from 03-data-model.md)
|
||
|
||
```prisma
|
||
model SubmissionWindow {
|
||
id String @id @default(cuid())
|
||
competitionId String
|
||
name String // "Round 1 Application Docs"
|
||
slug String // "round-1-docs"
|
||
roundNumber Int // 1 (first window), 2 (second window), etc.
|
||
sortOrder Int @default(0)
|
||
|
||
windowOpenAt DateTime?
|
||
windowCloseAt DateTime?
|
||
|
||
deadlinePolicy DeadlinePolicy @default(FLAG)
|
||
graceHours Int? // For GRACE policy
|
||
|
||
lockOnClose Boolean @default(true) // Prevent edits after close
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade)
|
||
fileRequirements SubmissionFileRequirement[]
|
||
projectFiles ProjectFile[]
|
||
rounds Round[] // INTAKE rounds using this window
|
||
visibility RoundSubmissionVisibility[]
|
||
|
||
@@unique([competitionId, slug])
|
||
@@unique([competitionId, roundNumber])
|
||
@@index([competitionId])
|
||
}
|
||
|
||
model SubmissionFileRequirement {
|
||
id String @id @default(cuid())
|
||
submissionWindowId String
|
||
name String // "Executive Summary"
|
||
description String? @db.Text
|
||
acceptedMimeTypes String[] // ["application/pdf"]
|
||
maxSizeMB Int?
|
||
isRequired Boolean @default(true)
|
||
sortOrder Int @default(0)
|
||
|
||
createdAt DateTime @default(now())
|
||
updatedAt DateTime @updatedAt
|
||
|
||
submissionWindow SubmissionWindow @relation(fields: [submissionWindowId], references: [id], onDelete: Cascade)
|
||
files ProjectFile[]
|
||
|
||
@@index([submissionWindowId])
|
||
}
|
||
```
|
||
|
||
### How Intake Round Links to SubmissionWindow
|
||
|
||
```typescript
|
||
// Round creation
|
||
const round = await prisma.round.create({
|
||
data: {
|
||
competitionId: competition.id,
|
||
name: "Application Window",
|
||
slug: "application-window",
|
||
roundType: "INTAKE",
|
||
sortOrder: 0,
|
||
windowOpenAt: new Date("2026-01-15T00:00:00Z"),
|
||
windowCloseAt: new Date("2026-03-01T23:59:59Z"),
|
||
submissionWindowId: submissionWindow.id, // Link to window
|
||
configJson: {
|
||
applicationFormId: "form-2026",
|
||
submissionWindowId: submissionWindow.id, // Redundant but explicit
|
||
deadlinePolicy: "GRACE",
|
||
gracePeriodMinutes: 180,
|
||
// ... rest of IntakeConfig
|
||
}
|
||
}
|
||
})
|
||
```
|
||
|
||
### File Requirements for Intake
|
||
|
||
Admin configures file requirements at the **SubmissionWindow** level, not the round level.
|
||
|
||
```typescript
|
||
// Example: Create file requirements for Round 1 docs
|
||
const requirements = await prisma.submissionFileRequirement.createMany({
|
||
data: [
|
||
{
|
||
submissionWindowId: "sw-1",
|
||
name: "Executive Summary",
|
||
description: "A PDF executive summary of your project (max 2 pages)",
|
||
acceptedMimeTypes: ["application/pdf"],
|
||
maxSizeMB: 10,
|
||
isRequired: true,
|
||
sortOrder: 0,
|
||
},
|
||
{
|
||
submissionWindowId: "sw-1",
|
||
name: "Business Plan",
|
||
description: "Full business plan or project proposal (PDF)",
|
||
acceptedMimeTypes: ["application/pdf"],
|
||
maxSizeMB: 50,
|
||
isRequired: true,
|
||
sortOrder: 1,
|
||
},
|
||
{
|
||
submissionWindowId: "sw-1",
|
||
name: "Video Pitch",
|
||
description: "Optional video pitch (max 5 minutes, MP4 or MOV)",
|
||
acceptedMimeTypes: ["video/mp4", "video/quicktime"],
|
||
maxSizeMB: 500,
|
||
isRequired: false,
|
||
sortOrder: 2,
|
||
},
|
||
]
|
||
})
|
||
```
|
||
|
||
### Deadline Enforcement
|
||
|
||
When an applicant tries to upload a file or submit the form:
|
||
|
||
```typescript
|
||
async function canSubmitToWindow(submissionWindow: SubmissionWindow): Promise<{
|
||
canSubmit: boolean
|
||
reason?: string
|
||
isLate?: boolean
|
||
}> {
|
||
const now = new Date()
|
||
|
||
// Not yet open
|
||
if (submissionWindow.windowOpenAt && now < submissionWindow.windowOpenAt) {
|
||
return {
|
||
canSubmit: false,
|
||
reason: `Window opens on ${submissionWindow.windowOpenAt.toLocaleDateString()}`
|
||
}
|
||
}
|
||
|
||
// Window closed
|
||
if (submissionWindow.windowCloseAt && now > submissionWindow.windowCloseAt) {
|
||
const { deadlinePolicy, graceHours } = submissionWindow
|
||
|
||
if (deadlinePolicy === 'HARD') {
|
||
return {
|
||
canSubmit: false,
|
||
reason: "Deadline has passed. Submissions are no longer accepted."
|
||
}
|
||
}
|
||
|
||
if (deadlinePolicy === 'GRACE' && graceHours) {
|
||
const graceDeadline = new Date(submissionWindow.windowCloseAt.getTime() + graceHours * 60 * 60 * 1000)
|
||
if (now > graceDeadline) {
|
||
return {
|
||
canSubmit: false,
|
||
reason: `Grace period ended on ${graceDeadline.toLocaleString()}`
|
||
}
|
||
}
|
||
return {
|
||
canSubmit: true,
|
||
isLate: true
|
||
}
|
||
}
|
||
|
||
if (deadlinePolicy === 'FLAG') {
|
||
return {
|
||
canSubmit: true,
|
||
isLate: true
|
||
}
|
||
}
|
||
}
|
||
|
||
return { canSubmit: true }
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Draft System
|
||
|
||
### Auto-Save Behavior
|
||
|
||
When `allowDraftSubmissions: true`, the form auto-saves every 30 seconds (or on field blur).
|
||
|
||
```typescript
|
||
// Draft auto-save
|
||
const saveDraft = async (formData: Partial<ApplicationFormData>) => {
|
||
const project = await trpc.applicant.saveDraft.mutate({
|
||
programId: programId,
|
||
projectId: existingProjectId, // null for new draft
|
||
draftData: formData,
|
||
})
|
||
|
||
return project
|
||
}
|
||
|
||
// Draft expiry
|
||
const draftExpiresAt = new Date()
|
||
draftExpiresAt.setDate(draftExpiresAt.getDate() + config.draftExpiryDays)
|
||
|
||
await prisma.project.upsert({
|
||
where: { id: projectId },
|
||
update: {
|
||
isDraft: true,
|
||
draftDataJson: formData,
|
||
draftExpiresAt: draftExpiresAt,
|
||
updatedAt: new Date(),
|
||
},
|
||
create: {
|
||
programId,
|
||
isDraft: true,
|
||
draftDataJson: formData,
|
||
draftExpiresAt: draftExpiresAt,
|
||
status: 'SUBMITTED', // Will change to DRAFT status in redesign
|
||
submittedByUserId: userId,
|
||
submissionSource: 'PUBLIC_FORM',
|
||
}
|
||
})
|
||
```
|
||
|
||
### Resume Draft Flow
|
||
|
||
1. User returns to application page
|
||
2. System checks for existing draft:
|
||
```typescript
|
||
const draft = await prisma.project.findFirst({
|
||
where: {
|
||
programId,
|
||
submittedByUserId: userId,
|
||
isDraft: true,
|
||
draftExpiresAt: { gt: new Date() } // Not expired
|
||
}
|
||
})
|
||
```
|
||
3. If found, pre-populate form with `draft.draftDataJson`
|
||
4. User can continue editing or discard draft
|
||
|
||
### Validation States
|
||
|
||
```typescript
|
||
type FormValidationState = {
|
||
isValid: boolean
|
||
canSaveDraft: boolean // Always true
|
||
canSubmit: boolean // All required fields filled + files uploaded
|
||
errors: Record<string, string>
|
||
warnings: Record<string, string>
|
||
}
|
||
|
||
// Example states:
|
||
// 1. Empty form → isValid: false, canSaveDraft: true, canSubmit: false
|
||
// 2. Partial form → isValid: false, canSaveDraft: true, canSubmit: false
|
||
// 3. Complete form → isValid: true, canSaveDraft: true, canSubmit: true
|
||
```
|
||
|
||
---
|
||
|
||
## 7. Applicant Experience
|
||
|
||
### Landing Page Flow
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ MOPC 2026 Application │
|
||
│ ══════════════════════════════════════════════════════ │
|
||
│ │
|
||
│ Application Window: Jan 15 - Mar 1, 2026 │
|
||
│ ⏱️ 23 days remaining │
|
||
│ │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ 🌊 Ocean Conservation Innovation Challenge │ │
|
||
│ │ │ │
|
||
│ │ We're looking for breakthrough ocean projects │ │
|
||
│ │ from startups and student teams worldwide. │ │
|
||
│ │ │ │
|
||
│ │ 📋 Requirements: │ │
|
||
│ │ • Executive Summary (PDF) │ │
|
||
│ │ • Business Plan (PDF) │ │
|
||
│ │ • Video Pitch (optional) │ │
|
||
│ │ │ │
|
||
│ │ ⏱️ Application takes ~30 minutes │ │
|
||
│ │ │ │
|
||
│ │ [ Login to Start ] [ Apply as Guest ] │ │
|
||
│ └────────────────────────────────────────────────────┘ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Multi-Step Application Form
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Step 1 of 4: Project Information │
|
||
│ ●───────○───────○───────○ [Save Draft] [Continue →] │
|
||
│ │
|
||
│ Project Title * │
|
||
│ ┌──────────────────────────────────────────────────────┐ │
|
||
│ │ Ocean Plastic Recycling Platform │ │
|
||
│ └──────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Team Name │
|
||
│ ┌──────────────────────────────────────────────────────┐ │
|
||
│ │ PlastiClean Solutions │ │
|
||
│ └──────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Competition Category * │
|
||
│ ○ Startup (existing company) │
|
||
│ ● Business Concept (student/graduate project) │
|
||
│ │
|
||
│ Institution * (required for Business Concept) │
|
||
│ ┌──────────────────────────────────────────────────────┐ │
|
||
│ │ Stanford University │ │
|
||
│ └──────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Ocean Issue Addressed * │
|
||
│ ┌──────────────────────────────────────────────────────┐ │
|
||
│ │ 🔍 Pollution Reduction [Dropdown ▼] │ │
|
||
│ └──────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Description * (max 5000 characters) │
|
||
│ ┌──────────────────────────────────────────────────────┐ │
|
||
│ │ PlastiClean is an AI-powered sorting system... │ │
|
||
│ │ │ │
|
||
│ │ 247 / 5000 │ │
|
||
│ └──────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Country * │
|
||
│ ┌──────────────────────────────────────────────────────┐ │
|
||
│ │ 🌍 United States [Dropdown ▼] │ │
|
||
│ └──────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Founding Date │
|
||
│ ┌──────────────────────────────────────────────────────┐ │
|
||
│ │ 2024-06-01 [Date Picker] │ │
|
||
│ └──────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ☑ I'm interested in mentorship from industry experts │
|
||
│ │
|
||
│ [ ← Back ] [ Save Draft ] [ Continue → ] │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Step 2 of 4: Team Members │
|
||
│ ○───────●───────○───────○ [Save Draft] [Continue →] │
|
||
│ │
|
||
│ Team Members (1-5 members) * │
|
||
│ │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ 👤 Sarah Johnson (You) │ │
|
||
│ │ sarah.johnson@stanford.edu │ │
|
||
│ │ Role: Lead | Title: CEO │ │
|
||
│ └────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ 👤 Mark Chen [Remove ✕] │ │
|
||
│ │ Name: ┌──────────────────────┐ │ │
|
||
│ │ │ Mark Chen │ │ │
|
||
│ │ └──────────────────────┘ │ │
|
||
│ │ Email: ┌──────────────────────┐ │ │
|
||
│ │ │ mark@stanford.edu │ │ │
|
||
│ │ └──────────────────────┘ │ │
|
||
│ │ Role: ● Member ○ Advisor │ │
|
||
│ │ Title: ┌──────────────────────┐ │ │
|
||
│ │ │ CTO │ │ │
|
||
│ │ └──────────────────────┘ │ │
|
||
│ └────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ [ + Add Team Member ] │
|
||
│ │
|
||
│ ℹ️ Team members will receive an email invite to view │
|
||
│ your project status. │
|
||
│ │
|
||
│ [ ← Back ] [ Save Draft ] [ Continue → ] │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Step 3 of 4: Document Upload │
|
||
│ ○───────○───────●───────○ [Save Draft] [Continue →] │
|
||
│ │
|
||
│ Required Documents │
|
||
│ │
|
||
│ 📄 Executive Summary (PDF, max 10 MB) * │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ ✓ PlastiClean_Executive_Summary.pdf │ │
|
||
│ │ Uploaded 2 hours ago | 2.3 MB │ │
|
||
│ │ [ View ] [ Replace ] [ Delete ] │ │
|
||
│ └────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ 📄 Business Plan (PDF, max 50 MB) * │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ 📎 Drag & drop or click to upload │ │
|
||
│ │ │ │
|
||
│ │ Accepted formats: PDF │ │
|
||
│ │ Max size: 50 MB │ │
|
||
│ └────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Optional Documents │
|
||
│ │
|
||
│ 🎥 Video Pitch (MP4/MOV, max 500 MB) │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ ⚡ PlastiClean_Pitch.mp4 │ │
|
||
│ │ Uploading... 67% complete │ │
|
||
│ │ ▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░ │ │
|
||
│ │ [ Cancel ] │ │
|
||
│ └────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ⚠️ Please upload all required documents before submitting. │
|
||
│ │
|
||
│ [ ← Back ] [ Save Draft ] [ Continue → ] │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Step 4 of 4: Review & Submit │
|
||
│ ○───────○───────○───────● [Save Draft] [Submit] │
|
||
│ │
|
||
│ Review Your Application │
|
||
│ │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ Project Information [Edit Step 1] │ │
|
||
│ │ ───────────────────────────────────────────────── │ │
|
||
│ │ Title: Ocean Plastic Recycling Platform │ │
|
||
│ │ Team: PlastiClean Solutions │ │
|
||
│ │ Category: Business Concept │ │
|
||
│ │ Institution: Stanford University │ │
|
||
│ │ Ocean Issue: Pollution Reduction │ │
|
||
│ │ Country: United States │ │
|
||
│ │ Founded: June 2024 │ │
|
||
│ │ Mentorship: Yes │ │
|
||
│ └────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ Team Members [Edit Step 2] │ │
|
||
│ │ ───────────────────────────────────────────────── │ │
|
||
│ │ • Sarah Johnson (Lead) - sarah.johnson@stanford.edu│ │
|
||
│ │ • Mark Chen (Member) - mark@stanford.edu │ │
|
||
│ └────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ Documents [Edit Step 3] │ │
|
||
│ │ ───────────────────────────────────────────────── │ │
|
||
│ │ ✓ Executive Summary (2.3 MB) │ │
|
||
│ │ ✓ Business Plan (8.7 MB) │ │
|
||
│ │ ✓ Video Pitch (124 MB) │ │
|
||
│ └────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ☑ I confirm that all information is accurate and that I │
|
||
│ have the authority to submit this application. │
|
||
│ │
|
||
│ ☑ I agree to the MOPC Terms & Conditions │
|
||
│ │
|
||
│ ⏱️ Deadline: March 1, 2026 at 11:59 PM UTC │
|
||
│ │
|
||
│ [ ← Back ] [ Save Draft ] [ Submit ✓ ] │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Post-Submission Confirmation
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ ✓ Application Submitted Successfully │
|
||
│ │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ │ │
|
||
│ │ 🎉 Thank You! │ │
|
||
│ │ │ │
|
||
│ │ Your application has been submitted successfully. │ │
|
||
│ │ │ │
|
||
│ │ Confirmation #: MOPC-2026-00123 │ │
|
||
│ │ Submitted: Feb 15, 2026 at 3:42 PM UTC │ │
|
||
│ │ │ │
|
||
│ │ ✉️ A confirmation email has been sent to: │ │
|
||
│ │ sarah.johnson@stanford.edu │ │
|
||
│ │ │ │
|
||
│ │ What's Next? │ │
|
||
│ │ ─────────────────────────────────────────── │ │
|
||
│ │ 1. AI Screening (March 2-5) │ │
|
||
│ │ 2. Jury Evaluation (March 10-31) │ │
|
||
│ │ 3. Semi-finalist Notification (April 5) │ │
|
||
│ │ │ │
|
||
│ │ You'll receive email updates at each stage. │ │
|
||
│ │ │ │
|
||
│ │ [ View My Dashboard ] [ Download Receipt ] │ │
|
||
│ │ │ │
|
||
│ └────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Applicant Dashboard (After Submission)
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ My Application sarah@stanford.edu│
|
||
│ ══════════════════════════════════════════════════════ │
|
||
│ │
|
||
│ PlastiClean Solutions │
|
||
│ Ocean Plastic Recycling Platform │
|
||
│ Status: Under Review [🟡 In Progress] │
|
||
│ │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ Application Progress │ │
|
||
│ │ │ │
|
||
│ │ ✓ Submitted Feb 15, 2026 │ │
|
||
│ │ ─────────────────────────────────────────── │ │
|
||
│ │ ⏳ AI Screening Expected: Mar 2-5 │ │
|
||
│ │ ─────────────────────────────────────────── │ │
|
||
│ │ ○ Jury Review Expected: Mar 10-31 │ │
|
||
│ │ ─────────────────────────────────────────── │ │
|
||
│ │ ○ Decision Expected: Apr 5 │ │
|
||
│ │ │ │
|
||
│ └────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ Deadline Countdown │ │
|
||
│ │ 13 days remaining until March 1, 2026 │ │
|
||
│ │ ▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░ │ │
|
||
│ └────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Quick Actions │
|
||
│ [ 📄 View Application ] [ 📎 Documents ] [ 👥 Team ] │
|
||
│ │
|
||
│ Recent Activity │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ ✓ Application submitted Feb 15, 3:42 PM │ │
|
||
│ │ ✓ Video pitch uploaded Feb 15, 3:38 PM │ │
|
||
│ │ ✓ Business plan uploaded Feb 15, 2:15 PM │ │
|
||
│ │ ✓ Executive summary uploaded Feb 15, 1:47 PM │ │
|
||
│ └────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 8. Admin Experience
|
||
|
||
### Intake Round Configuration Wizard
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Configure Round: Application Window │
|
||
│ ══════════════════════════════════════════════════════ │
|
||
│ │
|
||
│ Basic Settings │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ Round Name * │ │
|
||
│ │ ┌──────────────────────────────────────────────┐ │ │
|
||
│ │ │ Application Window │ │ │
|
||
│ │ └──────────────────────────────────────────────┘ │ │
|
||
│ │ │ │
|
||
│ │ Submission Window * │ │
|
||
│ │ ┌──────────────────────────────────────────────┐ │ │
|
||
│ │ │ 🔍 Round 1 Docs [Select Window ▼] │ │ │
|
||
│ │ └──────────────────────────────────────────────┘ │ │
|
||
│ │ [ + Create New Window ] │ │
|
||
│ │ │ │
|
||
│ │ Open Date * │ │
|
||
│ │ ┌──────────────────────────────────────────────┐ │ │
|
||
│ │ │ 2026-01-15 00:00 UTC [Date Picker] │ │ │
|
||
│ │ └──────────────────────────────────────────────┘ │ │
|
||
│ │ │ │
|
||
│ │ Close Date * │ │
|
||
│ │ ┌──────────────────────────────────────────────┐ │ │
|
||
│ │ │ 2026-03-01 23:59 UTC [Date Picker] │ │ │
|
||
│ │ └──────────────────────────────────────────────┘ │ │
|
||
│ └────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Deadline Policy │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ ○ Hard Deadline │ │
|
||
│ │ Block submissions after close time │ │
|
||
│ │ │ │
|
||
│ │ ○ Flag Late Submissions │ │
|
||
│ │ Accept but mark as late │ │
|
||
│ │ │ │
|
||
│ │ ● Grace Period │ │
|
||
│ │ Accept for: ┌────┐ minutes after deadline │ │
|
||
│ │ │ 180│ │ │
|
||
│ │ └────┘ │ │
|
||
│ └────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Draft Settings │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ ☑ Allow draft submissions (save & continue) │ │
|
||
│ │ │ │
|
||
│ │ Auto-delete drafts after: ┌────┐ days │ │
|
||
│ │ │ 30 │ │ │
|
||
│ │ └────┘ │ │
|
||
│ └────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Team Profile │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ ☑ Require team member information │ │
|
||
│ │ │ │
|
||
│ │ Min team size: ┌───┐ Max team size: ┌───┐ │ │
|
||
│ │ │ 1 │ │ 5 │ │ │
|
||
│ │ └───┘ └───┘ │ │
|
||
│ └────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Notifications │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ ☑ Send confirmation email on submission │ │
|
||
│ │ │ │
|
||
│ │ Send deadline reminders: │ │
|
||
│ │ ☑ 7 days before ☑ 3 days before ☑ 1 day before│ │
|
||
│ └────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Category Quotas (Optional) │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ ☐ Enable category quotas │ │
|
||
│ │ │ │
|
||
│ │ Max Startups: ┌─────┐ │ │
|
||
│ │ │ 100 │ │ │
|
||
│ │ └─────┘ │ │
|
||
│ │ │ │
|
||
│ │ Max Business Concepts: ┌─────┐ │ │
|
||
│ │ │ 100 │ │ │
|
||
│ │ └─────┘ │ │
|
||
│ │ │ │
|
||
│ │ When quota full: ○ Reject ● Add to waitlist │ │
|
||
│ └────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ [ Cancel ] [ Save Round ] │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Submissions Dashboard
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Round: Application Window Admin Dashboard │
|
||
│ ══════════════════════════════════════════════════════ │
|
||
│ │
|
||
│ Overview │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ 123 Total Submissions │ │
|
||
│ │ ┌──────────┬──────────┬──────────┬──────────┐ │ │
|
||
│ │ │ 68 │ 55 │ 12 │ 11 │ │ │
|
||
│ │ │ Startups │ Concepts │ Drafts │ Late │ │ │
|
||
│ │ └──────────┴──────────┴──────────┴──────────┘ │ │
|
||
│ │ │ │
|
||
│ │ Deadline: March 1, 2026 (13 days remaining) │ │
|
||
│ │ ▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░ │ │
|
||
│ └────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Filters: [ All ] [ Startups ] [ Concepts ] [ Late ] [ Drafts ]│
|
||
│ Search: ┌─────────────────────────────────────┐ [Export ↓] │
|
||
│ │ 🔍 Search by project or team... │ │
|
||
│ └─────────────────────────────────────┘ │
|
||
│ │
|
||
│ Recent Submissions │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ PlastiClean Solutions │ │
|
||
│ │ Ocean Plastic Recycling Platform │ │
|
||
│ │ Business Concept | United States | Feb 15, 3:42 PM │ │
|
||
│ │ [ View ] [ Edit ] [ Override Deadline ] │ │
|
||
│ ├────────────────────────────────────────────────────┤ │
|
||
│ │ AquaTech Innovations │ │
|
||
│ │ Sustainable Aquaculture Monitoring │ │
|
||
│ │ Startup | Norway | Feb 15, 2:18 PM │ │
|
||
│ │ [ View ] [ Edit ] [ Override Deadline ] │ │
|
||
│ ├────────────────────────────────────────────────────┤ │
|
||
│ │ OceanSense Labs 🔴 LATE │ │
|
||
│ │ AI-Powered Ocean Pollution Detection │ │
|
||
│ │ Startup | Singapore | Mar 2, 1:15 AM (+3 hours) │ │
|
||
│ │ [ View ] [ Edit ] [ Override Deadline ] │ │
|
||
│ └────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ [ Previous ] Page 1 of 7 [ Next ] │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Override Deadline Modal
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Override Deadline: PlastiClean Solutions │
|
||
│ ══════════════════════════════════════════════════════ │
|
||
│ │
|
||
│ Current Status: Submitted on time │
|
||
│ Original Deadline: March 1, 2026 11:59 PM UTC │
|
||
│ │
|
||
│ Extend submission window for this applicant: │
|
||
│ │
|
||
│ New Deadline │
|
||
│ ┌──────────────────────────────────────────────┐ │
|
||
│ │ 2026-03-08 23:59 UTC [Date Picker] │ │
|
||
│ └──────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Reason for Override * │
|
||
│ ┌──────────────────────────────────────────────┐ │
|
||
│ │ Technical issue during original submission │ │
|
||
│ │ │ │
|
||
│ └──────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ⚠️ This will create a GracePeriod record and allow the │
|
||
│ applicant to edit their submission until the new deadline│
|
||
│ │
|
||
│ [ Cancel ] [ Grant Extension ] │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 9. Deadline Behavior
|
||
|
||
### Deadline Policy Comparison
|
||
|
||
| Policy | Before Deadline | After Deadline | Grace Period | Flagged |
|
||
|--------|----------------|----------------|--------------|---------|
|
||
| **HARD** | ✅ Accept | ❌ Block | N/A | N/A |
|
||
| **FLAG** | ✅ Accept | ✅ Accept | N/A | ✅ Yes |
|
||
| **GRACE** | ✅ Accept | ✅ Accept (for N min) | ✅ Yes | ✅ Yes (after grace) |
|
||
|
||
### HARD Policy Behavior
|
||
|
||
**Configuration:**
|
||
```typescript
|
||
{
|
||
deadlinePolicy: "HARD",
|
||
gracePeriodMinutes: null // Ignored
|
||
}
|
||
```
|
||
|
||
**User Experience:**
|
||
- **Before deadline**: Form is fully functional, all uploads allowed
|
||
- **At deadline**: Form locks immediately at `windowCloseAt`
|
||
- **After deadline**: Form displays:
|
||
```
|
||
❌ Deadline Passed
|
||
|
||
The application deadline was March 1, 2026 at 11:59 PM UTC.
|
||
Submissions are no longer accepted.
|
||
|
||
Contact admin@monaco-opc.com for assistance.
|
||
```
|
||
|
||
**Admin Override:**
|
||
- Admin can create a `GracePeriod` record for specific applicant
|
||
- This extends their personal deadline (doesn't affect global deadline)
|
||
|
||
### FLAG Policy Behavior
|
||
|
||
**Configuration:**
|
||
```typescript
|
||
{
|
||
deadlinePolicy: "FLAG",
|
||
gracePeriodMinutes: null // Ignored
|
||
}
|
||
```
|
||
|
||
**User Experience:**
|
||
- **Before deadline**: Normal submission
|
||
- **After deadline**: Warning banner shown:
|
||
```
|
||
⚠️ Late Submission
|
||
|
||
The deadline was March 1, 2026. Your submission will be marked as late.
|
||
You can still submit, but late submissions may be deprioritized.
|
||
```
|
||
- Submission still works, but `ProjectFile.isLate` set to `true`
|
||
|
||
**Database Effect:**
|
||
```typescript
|
||
await prisma.projectFile.create({
|
||
data: {
|
||
projectId,
|
||
submissionWindowId,
|
||
requirementId,
|
||
fileName,
|
||
mimeType,
|
||
size,
|
||
bucket,
|
||
objectKey,
|
||
isLate: true, // Flagged
|
||
// ...
|
||
}
|
||
})
|
||
```
|
||
|
||
### GRACE Policy Behavior
|
||
|
||
**Configuration:**
|
||
```typescript
|
||
{
|
||
deadlinePolicy: "GRACE",
|
||
gracePeriodMinutes: 180 // 3 hours
|
||
}
|
||
```
|
||
|
||
**User Experience:**
|
||
- **Before deadline**: Normal submission
|
||
- **0-3 hours after deadline**: Warning banner:
|
||
```
|
||
⏱️ Grace Period Active
|
||
|
||
Deadline: March 1, 2026 11:59 PM UTC (passed)
|
||
Grace period ends: March 2, 2026 2:59 AM UTC (1 hour 23 minutes remaining)
|
||
|
||
Your submission will be marked as late. Please submit as soon as possible.
|
||
```
|
||
- **After grace period**: Hard block (same as HARD policy)
|
||
|
||
**Grace Period Calculation:**
|
||
```typescript
|
||
const graceDeadline = new Date(windowCloseAt.getTime() + gracePeriodMinutes * 60 * 1000)
|
||
|
||
if (now > windowCloseAt && now <= graceDeadline) {
|
||
// In grace period
|
||
return {
|
||
canSubmit: true,
|
||
isLate: true,
|
||
graceEndsAt: graceDeadline,
|
||
remainingMinutes: Math.floor((graceDeadline - now) / 60000)
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 10. Category Quotas
|
||
|
||
### How Category Quotas Work
|
||
|
||
When `categoryQuotasEnabled: true`, the system tracks submissions per category and enforces limits.
|
||
|
||
**Configuration:**
|
||
```typescript
|
||
{
|
||
categoryQuotasEnabled: true,
|
||
categoryQuotas: {
|
||
STARTUP: 100,
|
||
BUSINESS_CONCEPT: 100
|
||
},
|
||
quotaOverflowPolicy: "reject" // or "waitlist"
|
||
}
|
||
```
|
||
|
||
### Quota Enforcement Flow
|
||
|
||
```typescript
|
||
async function canSubmitInCategory(
|
||
competitionId: string,
|
||
category: 'STARTUP' | 'BUSINESS_CONCEPT',
|
||
config: IntakeConfig
|
||
): Promise<{ canSubmit: boolean; reason?: string }> {
|
||
if (!config.categoryQuotasEnabled || !config.categoryQuotas) {
|
||
return { canSubmit: true }
|
||
}
|
||
|
||
const quota = config.categoryQuotas[category]
|
||
if (!quota) {
|
||
return { canSubmit: true }
|
||
}
|
||
|
||
const submittedCount = await prisma.project.count({
|
||
where: {
|
||
competitionId,
|
||
competitionCategory: category,
|
||
isDraft: false, // Don't count drafts
|
||
}
|
||
})
|
||
|
||
if (submittedCount >= quota) {
|
||
if (config.quotaOverflowPolicy === 'waitlist') {
|
||
return {
|
||
canSubmit: true,
|
||
reason: `Quota full (${submittedCount}/${quota}). You're on the waitlist.`
|
||
}
|
||
} else {
|
||
return {
|
||
canSubmit: false,
|
||
reason: `Quota full (${submittedCount}/${quota}). No more ${category} applications accepted.`
|
||
}
|
||
}
|
||
}
|
||
|
||
return { canSubmit: true }
|
||
}
|
||
```
|
||
|
||
### Quota Dashboard for Admins
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ Category Quota Status │
|
||
│ ══════════════════════════════════════════════════════ │
|
||
│ │
|
||
│ Startups │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ 68 / 100 submissions │ │
|
||
│ │ ▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░ 68% │ │
|
||
│ │ │ │
|
||
│ │ 32 slots remaining │ │
|
||
│ └────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Business Concepts │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ 55 / 100 submissions │ │
|
||
│ │ ▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░ 55% │ │
|
||
│ │ │ │
|
||
│ │ 45 slots remaining │ │
|
||
│ └────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ Waitlist (if quota full) │
|
||
│ ┌────────────────────────────────────────────────────┐ │
|
||
│ │ Startups: 0 on waitlist │ │
|
||
│ │ Concepts: 0 on waitlist │ │
|
||
│ └────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### Overflow Handling
|
||
|
||
**Reject Policy:**
|
||
- Form shows error: "Category quota reached. No more startups/concepts accepted."
|
||
- User cannot submit
|
||
- Draft is saved but cannot be finalized
|
||
|
||
**Waitlist Policy:**
|
||
- Submission accepted, but `Project.status = WAITLISTED` (new status)
|
||
- User sees message: "You're on the waitlist (position #12). We'll notify you if a slot opens."
|
||
- If someone withdraws, next waitlist entry promoted to SUBMITTED
|
||
|
||
---
|
||
|
||
## 11. Email Notifications
|
||
|
||
### Receipt Confirmation Email
|
||
|
||
**Trigger:** `autoConfirmReceipt: true` + project submitted
|
||
|
||
**Template:**
|
||
```
|
||
Subject: Application Received — MOPC 2026
|
||
|
||
Dear Sarah Johnson,
|
||
|
||
Thank you for submitting your application to the Monaco Ocean Protection Challenge 2026.
|
||
|
||
Application Details:
|
||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
Project: Ocean Plastic Recycling Platform
|
||
Team: PlastiClean Solutions
|
||
Category: Business Concept
|
||
Confirmation #: MOPC-2026-00123
|
||
Submitted: February 15, 2026 at 3:42 PM UTC
|
||
|
||
What's Next?
|
||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||
1. AI Screening: March 2-5, 2026
|
||
Your application will be automatically screened for eligibility.
|
||
|
||
2. Jury Evaluation: March 10-31, 2026
|
||
Expert judges will review eligible projects.
|
||
|
||
3. Semi-finalist Notification: April 5, 2026
|
||
Selected teams will be invited to the next round.
|
||
|
||
Track Your Progress:
|
||
View your application status anytime at:
|
||
https://monaco-opc.com/applicant/dashboard
|
||
|
||
Questions?
|
||
Contact us at admin@monaco-opc.com
|
||
|
||
Best regards,
|
||
MOPC Team
|
||
```
|
||
|
||
### Deadline Reminder Emails
|
||
|
||
**Trigger:** Configured days before deadline (e.g., [7, 3, 1])
|
||
|
||
**7-Day Reminder:**
|
||
```
|
||
Subject: MOPC 2026 Application Deadline — 7 Days Remaining
|
||
|
||
Dear Applicant,
|
||
|
||
This is a friendly reminder that the MOPC 2026 application deadline is approaching.
|
||
|
||
Deadline: March 1, 2026 at 11:59 PM UTC
|
||
Time Remaining: 7 days
|
||
|
||
Have you started your application?
|
||
☐ Draft saved
|
||
☐ Documents uploaded
|
||
☐ Final submission
|
||
|
||
Complete your application:
|
||
https://monaco-opc.com/apply/mopc-2026
|
||
|
||
Need help? Contact admin@monaco-opc.com
|
||
|
||
Best regards,
|
||
MOPC Team
|
||
```
|
||
|
||
**1-Day Reminder:**
|
||
```
|
||
Subject: ⏰ MOPC 2026 Application Deadline — Tomorrow!
|
||
|
||
Dear Applicant,
|
||
|
||
The MOPC 2026 application deadline is tomorrow!
|
||
|
||
Deadline: March 1, 2026 at 11:59 PM UTC
|
||
Time Remaining: 23 hours 17 minutes
|
||
|
||
Don't miss out! Complete your application now:
|
||
https://monaco-opc.com/apply/mopc-2026
|
||
|
||
Best regards,
|
||
MOPC Team
|
||
```
|
||
|
||
---
|
||
|
||
## 12. API Changes (tRPC Procedures)
|
||
|
||
### New/Modified Procedures
|
||
|
||
All procedures are in `src/server/routers/` with these key changes:
|
||
|
||
**applicant.getSubmissionBySlug** — Get intake round info by slug (for public access)
|
||
**applicant.getMySubmission** (enhanced) — Get current user's application (draft or submitted)
|
||
**applicant.saveDraft** (new) — Auto-save form data as draft
|
||
**applicant.submitApplication** (new) — Finalize draft and mark as submitted
|
||
**file.getUploadUrl** (enhanced) — Get pre-signed URL for file upload
|
||
**file.confirmUpload** (new) — Mark file upload as complete after successful S3 upload
|
||
**admin.getIntakeSubmissions** — Admin dashboard for intake round
|
||
**admin.extendDeadline** (new) — Create grace period for specific applicant
|
||
|
||
---
|
||
|
||
## 13. Service Functions
|
||
|
||
Key service functions in `src/server/services/intake-round.ts`:
|
||
|
||
- `canSubmitToIntakeRound()` — Check if submission window is accepting
|
||
- `checkCategoryQuota()` — Validate category quota
|
||
- `validateApplicationData()` — Form validation
|
||
- `validateFileUpload()` — File requirement validation
|
||
- `checkRequiredFiles()` — Verify all required files uploaded
|
||
|
||
---
|
||
|
||
## 14. Edge Cases
|
||
|
||
| Edge Case | Behavior | Solution |
|
||
|-----------|----------|----------|
|
||
| **User starts draft, deadline passes** | Draft is preserved but cannot submit | Show banner: "Deadline passed. Contact admin if you need extension." Admin can grant GracePeriod. |
|
||
| **User submits at exact deadline second** | Accept if server time <= windowCloseAt | Use database server time for consistency |
|
||
| **Category quota reached mid-submission** | Check quota again on final submit | Race condition: if quota hit between form start and submit, show error "Quota just filled" |
|
||
| **File upload fails mid-submission** | ProjectFile record exists but no S3 object | Cleanup orphaned records via cron job; allow re-upload |
|
||
| **User replaces file after deadline** | Check deadline on upload, not just submit | Each file upload checks `canSubmitToWindow()` |
|
||
| **Team member email already registered** | Invite sent, user can claim | Email contains link: "Join team or login to existing account" |
|
||
| **Applicant deletes draft** | Hard delete or soft delete? | Soft delete: set `deletedAt` field, hide from UI but keep for audit |
|
||
| **Admin extends deadline globally** | Update Round.windowCloseAt | All applicants benefit; no GracePeriod records needed |
|
||
| **Duplicate submissions (same email)** | One email = one project per competition | Upsert logic: update existing project instead of creating new |
|
||
| **File version conflict** | User uploads same requirement twice | Create new ProjectFile, link to old via `replacedById` |
|
||
| **Draft expires while user editing** | Auto-save fails with "Draft expired" | Extend expiry on each auto-save (rolling window) |
|
||
|
||
---
|
||
|
||
## 15. Integration Points
|
||
|
||
### Connects to Filtering Round (Next Round)
|
||
|
||
When intake round closes:
|
||
|
||
1. Create ProjectRoundState records
|
||
2. Trigger filtering job
|
||
|
||
### File System (MinIO)
|
||
|
||
- All uploads go to MinIO bucket: `mopc-submissions`
|
||
- Object keys: `projects/{projectId}/submissions/{submissionWindowId}/{filename}_{timestamp}.{ext}`
|
||
- Pre-signed URLs expire after 1 hour (uploads) or 24 hours (downloads)
|
||
|
||
### Notification System
|
||
|
||
**Events emitted:**
|
||
- `INTAKE_SUBMISSION_RECEIVED` — confirmation email + in-app notification
|
||
- `INTAKE_DEADLINE_APPROACHING` — reminder emails (7d, 3d, 1d before)
|
||
- `INTAKE_LATE_SUBMISSION` — flag for admin review
|
||
- `INTAKE_QUOTA_REACHED` — notify admins
|
||
|
||
### Audit Logging
|
||
|
||
All actions logged in `DecisionAuditLog`:
|
||
- `intake.draft_saved` — auto-save triggered
|
||
- `intake.submission_finalized` — final submit
|
||
- `intake.file_uploaded` — file added
|
||
- `intake.file_replaced` — file updated
|
||
- `intake.deadline_extended` — admin override
|
||
- `intake.quota_reached` — category quota hit
|
||
|
||
---
|
||
|
||
## Document Complete
|
||
|
||
This specification defines the **INTAKE round type** for the redesigned MOPC architecture. Key takeaways:
|
||
|
||
1. **Typed Config**: IntakeConfig replaces generic JSON with validated, documented fields
|
||
2. **SubmissionWindow**: Decouples file requirements from round, enables multi-round submissions
|
||
3. **Deadline Policies**: HARD, FLAG, GRACE with clear behavior differences
|
||
4. **Draft System**: Auto-save + expiry for incomplete applications
|
||
5. **Category Quotas**: Limit startups/concepts with overflow handling
|
||
6. **Email Automation**: Confirmation + reminders built-in
|
||
7. **Admin Controls**: Dashboard, deadline extensions, quota monitoring
|
||
|
||
**Next documents:**
|
||
- 05-round-filtering.md — AI screening and eligibility
|
||
- 06-round-evaluation.md — Jury review with multi-jury support
|
||
- 07-round-submission.md — Additional docs from advancing teams
|