966 lines
32 KiB
Markdown
966 lines
32 KiB
Markdown
# Special Awards System
|
|
|
|
## Overview
|
|
|
|
Special Awards are standalone award tracks that run parallel to the main competition flow. They enable the MOPC platform to recognize excellence in specific areas (e.g., "Innovation Award", "Impact Award", "Youth Leadership Award") with dedicated juries and evaluation processes while referencing the same pool of projects.
|
|
|
|
### Purpose
|
|
|
|
Special Awards serve three key purposes:
|
|
|
|
1. **Parallel Recognition** — Recognize excellence in specific domains beyond the main competition prizes
|
|
2. **Specialized Evaluation** — Enable dedicated jury groups with domain expertise to evaluate specific criteria
|
|
3. **Flexible Integration** — Awards can piggyback on main rounds or run independently with their own timelines
|
|
|
|
### Design Philosophy
|
|
|
|
- **Standalone Entities** — Awards are not tracks; they're first-class entities linked to competitions
|
|
- **Two Modes** — STAY_IN_MAIN (piggyback evaluation) or SEPARATE_POOL (independent flow)
|
|
- **Dedicated Juries** — Each award can have its own jury group with unique members or shared members
|
|
- **Flexible Eligibility** — AI-suggested, manual, round-based, or all-eligible modes
|
|
- **Integration with Results** — Award results feed into the confirmation round alongside main competition winners
|
|
|
|
---
|
|
|
|
## Current System Analysis
|
|
|
|
### Current Architecture (Pipeline-Based)
|
|
|
|
**Current State:**
|
|
```
|
|
Program
|
|
└── Pipeline
|
|
├── Track: "Main Competition" (MAIN)
|
|
└── Track: "Innovation Award" (AWARD)
|
|
├── Stage: "Evaluation" (EVALUATION)
|
|
└── Stage: "Results" (RESULTS)
|
|
|
|
SpecialAward {
|
|
id, programId, name, description
|
|
trackId → Track (AWARD track)
|
|
criteriaText (for AI)
|
|
scoringMode: PICK_WINNER | RANKED | SCORED
|
|
votingStartAt, votingEndAt
|
|
winnerProjectId
|
|
useAiEligibility: boolean
|
|
}
|
|
|
|
AwardEligibility { awardId, projectId, eligible, method, aiReasoningJson }
|
|
AwardJuror { awardId, userId }
|
|
AwardVote { awardId, userId, projectId, rank? }
|
|
```
|
|
|
|
**Current Flow:**
|
|
1. Admin creates AWARD track within pipeline
|
|
2. Admin configures SpecialAward linked to track
|
|
3. Projects routed to award track via ProjectStageState
|
|
4. AI or manual eligibility determination
|
|
5. Award jurors evaluate/vote
|
|
6. Winner selected (admin/award master decision)
|
|
|
|
**Current Limitations:**
|
|
- Awards tied to track concept (being eliminated)
|
|
- No distinction between "piggyback" awards and independent awards
|
|
- No round-based eligibility
|
|
- No jury group integration
|
|
- No evaluation form linkage
|
|
- No audience voting support
|
|
- No integration with confirmation round
|
|
|
|
---
|
|
|
|
## Redesigned System: Two Award Modes
|
|
|
|
### Mode 1: STAY_IN_MAIN
|
|
|
|
**Concept:** Projects remain in the main competition flow. A dedicated award jury evaluates them using the same submissions, during the same evaluation windows.
|
|
|
|
**Use Case:** "Innovation Award" — Members of Jury 2 who also serve on the Innovation Award jury score projects specifically for innovation criteria during the Jury 2 evaluation round.
|
|
|
|
**Characteristics:**
|
|
- Projects never leave main track
|
|
- Award jury evaluates during specific main evaluation rounds
|
|
- Award jury sees the same docs/submissions as main jury
|
|
- Award uses its own evaluation form with award-specific criteria
|
|
- No separate stages/timeline needed
|
|
- Results announced alongside main results
|
|
|
|
**Data Flow:**
|
|
```
|
|
Competition → Round 5 (Jury 2 Evaluation)
|
|
├─ Main Jury (Jury 2) evaluates with standard criteria
|
|
└─ Innovation Award Jury evaluates same projects with innovation criteria
|
|
|
|
SpecialAward {
|
|
evaluationMode: "STAY_IN_MAIN"
|
|
evaluationRoundId: "round-5" ← Which main round this award evaluates during
|
|
juryGroupId: "innovation-jury" ← Dedicated jury
|
|
evaluationFormId: "innovation-form" ← Award-specific criteria
|
|
}
|
|
```
|
|
|
|
### Mode 2: SEPARATE_POOL
|
|
|
|
**Concept:** Dedicated evaluation with separate criteria, submission requirements, and timeline. Projects may be pulled out for award-specific evaluation.
|
|
|
|
**Use Case:** "Community Impact Award" — Separate jury evaluates finalists specifically for community impact using a unique rubric and potentially additional documentation.
|
|
|
|
**Characteristics:**
|
|
- Own jury group with unique members
|
|
- Own evaluation criteria/form
|
|
- Can have own submission requirements
|
|
- Runs on its own timeline
|
|
- Can pull projects from specific rounds
|
|
- Independent results timeline
|
|
|
|
**Data Flow:**
|
|
```
|
|
Competition
|
|
└── SpecialAward {
|
|
evaluationMode: "SEPARATE_POOL"
|
|
eligibilityMode: "ROUND_BASED" ← Projects from Round 5 (finalists)
|
|
juryGroupId: "impact-jury"
|
|
evaluationFormId: "impact-form"
|
|
votingStartAt: [own window]
|
|
votingEndAt: [own window]
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Enhanced SpecialAward Model
|
|
|
|
### Complete Schema
|
|
|
|
```prisma
|
|
model SpecialAward {
|
|
id String @id @default(cuid())
|
|
competitionId String // CHANGED: Links to Competition, not Track
|
|
name String
|
|
description String? @db.Text
|
|
|
|
// Eligibility configuration
|
|
eligibilityMode AwardEligibilityMode @default(AI_SUGGESTED)
|
|
eligibilityCriteria Json? @db.JsonB // Mode-specific config
|
|
|
|
// Evaluation configuration
|
|
evaluationMode AwardEvaluationMode @default(STAY_IN_MAIN)
|
|
evaluationRoundId String? // Which main round (for STAY_IN_MAIN)
|
|
evaluationFormId String? // Custom criteria
|
|
juryGroupId String? // Dedicated or shared jury
|
|
|
|
// Voting configuration
|
|
votingMode AwardVotingMode @default(JURY_ONLY)
|
|
scoringMode AwardScoringMode @default(PICK_WINNER)
|
|
maxRankedPicks Int? // For RANKED mode
|
|
maxWinners Int @default(1) // Number of winners
|
|
audienceVotingWeight Float? // 0.0-1.0 for COMBINED mode
|
|
|
|
// Timing
|
|
votingStartAt DateTime?
|
|
votingEndAt DateTime?
|
|
|
|
// Results
|
|
status AwardStatus @default(DRAFT)
|
|
winnerProjectId String? // Single winner (for backward compat)
|
|
|
|
// AI eligibility
|
|
useAiEligibility Boolean @default(false)
|
|
criteriaText String? @db.Text // Plain-language for AI
|
|
|
|
// Job tracking (for AI eligibility)
|
|
eligibilityJobStatus String?
|
|
eligibilityJobTotal Int?
|
|
eligibilityJobDone Int?
|
|
eligibilityJobError String? @db.Text
|
|
eligibilityJobStarted DateTime?
|
|
|
|
sortOrder Int @default(0)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
competition Competition @relation(fields: [competitionId], references: [id], onDelete: Cascade)
|
|
evaluationRound Round? @relation("AwardEvaluationRound", fields: [evaluationRoundId], references: [id], onDelete: SetNull)
|
|
evaluationForm EvaluationForm? @relation(fields: [evaluationFormId], references: [id], onDelete: SetNull)
|
|
juryGroup JuryGroup? @relation("AwardJuryGroup", fields: [juryGroupId], references: [id], onDelete: SetNull)
|
|
winnerProject Project? @relation("AwardWinner", fields: [winnerProjectId], references: [id], onDelete: SetNull)
|
|
|
|
eligibilities AwardEligibility[]
|
|
votes AwardVote[]
|
|
winners AwardWinner[] // NEW: Multi-winner support
|
|
|
|
@@index([competitionId])
|
|
@@index([status])
|
|
@@index([evaluationRoundId])
|
|
@@index([juryGroupId])
|
|
}
|
|
|
|
enum AwardEligibilityMode {
|
|
AI_SUGGESTED // AI analyzes and suggests eligible projects
|
|
MANUAL // Admin manually selects eligible projects
|
|
ALL_ELIGIBLE // All projects in competition are eligible
|
|
ROUND_BASED // All projects that reach a specific round
|
|
}
|
|
|
|
enum AwardEvaluationMode {
|
|
STAY_IN_MAIN // Evaluate during main competition round
|
|
SEPARATE_POOL // Independent evaluation flow
|
|
}
|
|
|
|
enum AwardVotingMode {
|
|
JURY_ONLY // Only jury votes
|
|
AUDIENCE_ONLY // Only audience votes
|
|
COMBINED // Jury + audience with weighted scoring
|
|
}
|
|
|
|
enum AwardScoringMode {
|
|
PICK_WINNER // Simple winner selection (1 or N winners)
|
|
RANKED // Ranked-choice voting
|
|
SCORED // Criteria-based scoring
|
|
}
|
|
|
|
enum AwardStatus {
|
|
DRAFT
|
|
NOMINATIONS_OPEN
|
|
EVALUATION // NEW: Award jury evaluation in progress
|
|
DECIDED // NEW: Winner(s) selected, pending announcement
|
|
ANNOUNCED // NEW: Winner(s) publicly announced
|
|
ARCHIVED
|
|
}
|
|
```
|
|
|
|
### New Model: AwardWinner (Multi-Winner Support)
|
|
|
|
```prisma
|
|
model AwardWinner {
|
|
id String @id @default(cuid())
|
|
awardId String
|
|
projectId String
|
|
rank Int // 1st place, 2nd place, etc.
|
|
|
|
// Selection metadata
|
|
selectedAt DateTime @default(now())
|
|
selectedById String
|
|
selectionMethod String // "JURY_VOTE" | "AUDIENCE_VOTE" | "COMBINED" | "ADMIN_DECISION"
|
|
|
|
// Score breakdown (for transparency)
|
|
juryScore Float?
|
|
audienceScore Float?
|
|
finalScore Float?
|
|
|
|
createdAt DateTime @default(now())
|
|
|
|
// Relations
|
|
award SpecialAward @relation(fields: [awardId], references: [id], onDelete: Cascade)
|
|
project Project @relation("AwardWinners", fields: [projectId], references: [id], onDelete: Cascade)
|
|
selectedBy User @relation("AwardWinnerSelector", fields: [selectedById], references: [id])
|
|
|
|
@@unique([awardId, projectId])
|
|
@@unique([awardId, rank])
|
|
@@index([awardId])
|
|
@@index([projectId])
|
|
}
|
|
```
|
|
|
|
### Enhanced AwardVote Model
|
|
|
|
```prisma
|
|
model AwardVote {
|
|
id String @id @default(cuid())
|
|
awardId String
|
|
userId String? // Nullable for audience votes
|
|
projectId String
|
|
|
|
// Voting type
|
|
isAudienceVote Boolean @default(false)
|
|
|
|
// Scoring (mode-dependent)
|
|
rank Int? // For RANKED mode (1 = first choice)
|
|
score Float? // For SCORED mode
|
|
|
|
// Criteria scores (for SCORED mode)
|
|
criterionScoresJson Json? @db.JsonB
|
|
|
|
votedAt DateTime @default(now())
|
|
|
|
// Relations
|
|
award SpecialAward @relation(fields: [awardId], references: [id], onDelete: Cascade)
|
|
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([awardId, userId, projectId])
|
|
@@index([awardId])
|
|
@@index([userId])
|
|
@@index([projectId])
|
|
@@index([awardId, isAudienceVote])
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Eligibility System Deep Dive
|
|
|
|
### Eligibility Modes
|
|
|
|
#### 1. AI_SUGGESTED
|
|
|
|
AI analyzes all projects and suggests eligible ones based on plain-language criteria.
|
|
|
|
**Config JSON:**
|
|
```typescript
|
|
type AISuggestedConfig = {
|
|
criteriaText: string // "Projects using innovative ocean tech"
|
|
confidenceThreshold: number // 0.0-1.0 (default: 0.7)
|
|
autoAcceptAbove: number // Auto-accept above this (default: 0.9)
|
|
requireManualReview: boolean // All need admin review (default: false)
|
|
sourceRoundId?: string // Only projects from this round
|
|
}
|
|
```
|
|
|
|
**Flow:**
|
|
1. Admin triggers AI eligibility analysis
|
|
2. AI processes projects in batches (anonymized)
|
|
3. AI returns: `{ projectId, eligible, confidence, reasoning }`
|
|
4. High-confidence results auto-applied
|
|
5. Medium-confidence results flagged for review
|
|
6. Low-confidence results rejected (or flagged if `requireManualReview: true`)
|
|
|
|
**UI:**
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Innovation Award — AI Eligibility Analysis │
|
|
├─────────────────────────────────────────────────────────────┤
|
|
│ Status: Running... (47/120 projects analyzed) │
|
|
│ [████████████████░░░░░░░░] 68% │
|
|
│ │
|
|
│ Results So Far: │
|
|
│ ✓ Auto-Accepted (confidence > 0.9): 12 projects │
|
|
│ ⚠ Flagged for Review (0.6-0.9): 23 projects │
|
|
│ ✗ Rejected (< 0.6): 12 projects │
|
|
│ │
|
|
│ [View Flagged Projects] [Stop Analysis] │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
#### 2. MANUAL
|
|
|
|
Admin manually selects eligible projects.
|
|
|
|
**Config JSON:**
|
|
```typescript
|
|
type ManualConfig = {
|
|
sourceRoundId?: string // Limit to projects from specific round
|
|
categoryFilter?: "STARTUP" | "BUSINESS_CONCEPT"
|
|
tagFilters?: string[] // Only projects with these tags
|
|
}
|
|
```
|
|
|
|
#### 3. ALL_ELIGIBLE
|
|
|
|
All projects in the competition are automatically eligible.
|
|
|
|
**Config JSON:**
|
|
```typescript
|
|
type AllEligibleConfig = {
|
|
minimumStatus?: ProjectStatus // e.g., "SEMIFINALIST" or above
|
|
excludeWithdrawn: boolean // Exclude WITHDRAWN (default: true)
|
|
}
|
|
```
|
|
|
|
#### 4. ROUND_BASED
|
|
|
|
All projects that reach a specific round are automatically eligible.
|
|
|
|
**Config JSON:**
|
|
```typescript
|
|
type RoundBasedConfig = {
|
|
sourceRoundId: string // Required: which round
|
|
requiredState: ProjectRoundStateValue // PASSED, COMPLETED, etc.
|
|
autoUpdate: boolean // Auto-update when projects advance (default: true)
|
|
}
|
|
```
|
|
|
|
**Example:**
|
|
```json
|
|
{
|
|
"sourceRoundId": "round-5-jury-2",
|
|
"requiredState": "PASSED",
|
|
"autoUpdate": true
|
|
}
|
|
```
|
|
|
|
### Admin Override System
|
|
|
|
**All eligibility modes support admin override:**
|
|
|
|
```prisma
|
|
model AwardEligibility {
|
|
id String @id @default(cuid())
|
|
awardId String
|
|
projectId String
|
|
|
|
// Original determination
|
|
method EligibilityMethod @default(AUTO) // AUTO, AI, MANUAL
|
|
eligible Boolean @default(false)
|
|
aiReasoningJson Json? @db.JsonB
|
|
|
|
// Override
|
|
overriddenBy String?
|
|
overriddenAt DateTime?
|
|
overrideReason String? @db.Text
|
|
|
|
// Final decision
|
|
finalEligible Boolean // Computed: overridden ? override : original
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
award SpecialAward @relation(fields: [awardId], references: [id], onDelete: Cascade)
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
overriddenByUser User? @relation("AwardEligibilityOverriddenBy", fields: [overriddenBy], references: [id])
|
|
|
|
@@unique([awardId, projectId])
|
|
@@index([awardId, eligible])
|
|
@@index([awardId, finalEligible])
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Award Jury Groups
|
|
|
|
### Integration with JuryGroup Model
|
|
|
|
Awards can have:
|
|
1. **Dedicated Jury** — Own `JuryGroup` with unique members
|
|
2. **Shared Jury** — Reuse existing competition jury group (e.g., Jury 2)
|
|
3. **Mixed Jury** — Some overlap with main jury, some unique members
|
|
|
|
**Example:**
|
|
```typescript
|
|
// Dedicated jury for Innovation Award
|
|
const innovationJury = await prisma.juryGroup.create({
|
|
data: {
|
|
competitionId: "comp-2026",
|
|
name: "Innovation Award Jury",
|
|
slug: "innovation-jury",
|
|
description: "Technology and innovation experts",
|
|
defaultMaxAssignments: 15,
|
|
defaultCapMode: "SOFT",
|
|
categoryQuotasEnabled: false,
|
|
}
|
|
})
|
|
|
|
// Add members (can overlap with main jury)
|
|
await prisma.juryGroupMember.createMany({
|
|
data: [
|
|
{ juryGroupId: innovationJury.id, userId: "user-tech-1", isLead: true },
|
|
{ juryGroupId: innovationJury.id, userId: "user-tech-2" },
|
|
{ juryGroupId: innovationJury.id, userId: "jury-2-member-overlap" }, // Also on Jury 2
|
|
]
|
|
})
|
|
|
|
// Link to award
|
|
await prisma.specialAward.update({
|
|
where: { id: awardId },
|
|
data: { juryGroupId: innovationJury.id }
|
|
})
|
|
```
|
|
|
|
### Award Jury Assignment
|
|
|
|
#### For STAY_IN_MAIN Mode
|
|
|
|
Award jury members evaluate the same projects as the main jury, but with award-specific criteria.
|
|
|
|
**Assignment Creation:**
|
|
```typescript
|
|
// Main jury assignments (created by round)
|
|
Assignment { userId: "jury-2-member-1", projectId: "proj-A", roundId: "round-5", juryGroupId: "jury-2" }
|
|
|
|
// Award jury assignments (created separately, same round)
|
|
Assignment { userId: "innovation-jury-1", projectId: "proj-A", roundId: "round-5", juryGroupId: "innovation-jury" }
|
|
```
|
|
|
|
**Evaluation:**
|
|
- Award jury uses `evaluationFormId` linked to award
|
|
- Evaluations stored separately (different `assignmentId`)
|
|
- Both juries can evaluate same project in same round
|
|
|
|
#### For SEPARATE_POOL Mode
|
|
|
|
Award has its own assignment workflow, potentially for a subset of projects.
|
|
|
|
---
|
|
|
|
## Award Evaluation Flow
|
|
|
|
### STAY_IN_MAIN Evaluation
|
|
|
|
**Timeline:**
|
|
```
|
|
Round 5: Jury 2 Evaluation (Main)
|
|
├─ Opens: 2026-03-01
|
|
├─ Main Jury evaluates with standard form
|
|
├─ Innovation Award Jury evaluates with innovation form
|
|
└─ Closes: 2026-03-15
|
|
|
|
Award results calculated separately but announced together
|
|
```
|
|
|
|
**Step-by-Step:**
|
|
|
|
1. **Setup Phase**
|
|
- Admin creates `SpecialAward { evaluationMode: "STAY_IN_MAIN", evaluationRoundId: "round-5" }`
|
|
- Admin creates award-specific `EvaluationForm` with innovation criteria
|
|
- Admin creates `JuryGroup` for Innovation Award
|
|
- Admin adds members to jury group
|
|
|
|
2. **Eligibility Phase**
|
|
- Eligibility determined (AI/manual/round-based)
|
|
- Only eligible projects evaluated by award jury
|
|
|
|
3. **Assignment Phase**
|
|
- When Round 5 opens, assignments created for award jury
|
|
- Each award juror assigned eligible projects
|
|
- Award assignments reference same `roundId` as main evaluation
|
|
|
|
4. **Evaluation Phase**
|
|
- Award jurors see projects in their dashboard
|
|
- Form shows award-specific criteria
|
|
- Evaluations stored with `formId` = innovation form
|
|
|
|
5. **Results Phase**
|
|
- Scores aggregated separately from main jury
|
|
- Winner selection (jury vote, admin decision, etc.)
|
|
- Results feed into confirmation round
|
|
|
|
### SEPARATE_POOL Evaluation
|
|
|
|
**Timeline:**
|
|
```
|
|
Round 5: Jury 2 Evaluation (Main) — March 1-15
|
|
↓
|
|
Round 6: Finalist Selection
|
|
↓
|
|
Impact Award Evaluation (Separate) — March 20 - April 5
|
|
├─ Own voting window
|
|
├─ Own evaluation form
|
|
├─ Impact Award Jury evaluates finalists
|
|
└─ Results: April 10
|
|
```
|
|
|
|
---
|
|
|
|
## Audience Voting for Awards
|
|
|
|
### Voting Modes
|
|
|
|
#### JURY_ONLY
|
|
|
|
Only jury members vote. Standard model.
|
|
|
|
#### AUDIENCE_ONLY
|
|
|
|
Only audience (public) votes. No jury involvement.
|
|
|
|
**Config:**
|
|
```typescript
|
|
type AudienceOnlyConfig = {
|
|
requireIdentification: boolean // Require email/phone (default: false)
|
|
votesPerPerson: number // Max votes per person (default: 1)
|
|
allowRanking: boolean // Ranked-choice (default: false)
|
|
maxChoices?: number // For ranked mode
|
|
}
|
|
```
|
|
|
|
#### COMBINED
|
|
|
|
Jury + audience votes combined with weighted scoring.
|
|
|
|
**Config:**
|
|
```typescript
|
|
type CombinedConfig = {
|
|
audienceWeight: number // 0.0-1.0 (e.g., 0.3 = 30% audience, 70% jury)
|
|
juryWeight: number // 0.0-1.0 (should sum to 1.0)
|
|
requireMinimumAudienceVotes: number // Min votes for validity (default: 50)
|
|
showAudienceResultsToJury: boolean // Jury sees audience results (default: false)
|
|
}
|
|
```
|
|
|
|
**Scoring Calculation:**
|
|
```typescript
|
|
function calculateCombinedScore(
|
|
juryScores: number[],
|
|
audienceVoteCount: number,
|
|
totalAudienceVotes: number,
|
|
config: CombinedConfig
|
|
): number {
|
|
const juryAvg = juryScores.reduce((a, b) => a + b, 0) / juryScores.length
|
|
const audiencePercent = audienceVoteCount / totalAudienceVotes
|
|
|
|
// Normalize jury score to 0-1 (assuming 1-10 scale)
|
|
const normalizedJuryScore = juryAvg / 10
|
|
|
|
const finalScore =
|
|
(normalizedJuryScore * config.juryWeight) +
|
|
(audiencePercent * config.audienceWeight)
|
|
|
|
return finalScore
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Admin Experience
|
|
|
|
### Award Management Dashboard
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
│ MOPC 2026 — Special Awards [+ New Award] │
|
|
├─────────────────────────────────────────────────────────────────────────────┤
|
|
│ │
|
|
│ ┌───────────────────────────────────────────────────────────────────────┐ │
|
|
│ │ Innovation Award [Edit ▼] │ │
|
|
│ │ Mode: Stay in Main (Jury 2 Evaluation) • Status: EVALUATION │ │
|
|
│ ├───────────────────────────────────────────────────────────────────────┤ │
|
|
│ │ Eligible Projects: 18 / 20 finalists │ │
|
|
│ │ Jury: Innovation Jury (5 members) │ │
|
|
│ │ Evaluations: 72 / 90 (80% complete) │ │
|
|
│ │ Voting Closes: March 15, 2026 │ │
|
|
│ │ │ │
|
|
│ │ [View Eligibility] [View Evaluations] [Select Winner] │ │
|
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
│ ┌───────────────────────────────────────────────────────────────────────┐ │
|
|
│ │ Community Impact Award [Edit ▼] │ │
|
|
│ │ Mode: Separate Pool • Status: DRAFT │ │
|
|
│ ├───────────────────────────────────────────────────────────────────────┤ │
|
|
│ │ Eligible Projects: Not yet determined (AI pending) │ │
|
|
│ │ Jury: Not assigned │ │
|
|
│ │ Voting Window: Not set │ │
|
|
│ │ │ │
|
|
│ │ [Configure Eligibility] [Set Up Jury] [Set Timeline] │ │
|
|
│ └───────────────────────────────────────────────────────────────────────┘ │
|
|
│ │
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## Integration with Main Flow
|
|
|
|
### Awards Reference Main Competition Projects
|
|
|
|
Awards don't create their own project pool — they reference existing competition projects.
|
|
|
|
**Data Relationship:**
|
|
```
|
|
Competition
|
|
├── Projects (shared pool)
|
|
│ ├── Project A
|
|
│ ├── Project B
|
|
│ └── Project C
|
|
│
|
|
├── Main Rounds (linear flow)
|
|
│ ├── Round 1: Intake
|
|
│ ├── Round 5: Jury 2 Evaluation
|
|
│ └── Round 7: Live Finals
|
|
│
|
|
└── Special Awards (parallel evaluation)
|
|
├── Innovation Award
|
|
│ └── AwardEligibility { projectId: "A", eligible: true }
|
|
│ └── AwardEligibility { projectId: "B", eligible: false }
|
|
└── Impact Award
|
|
└── AwardEligibility { projectId: "A", eligible: true }
|
|
└── AwardEligibility { projectId: "C", eligible: true }
|
|
```
|
|
|
|
### Award Results Feed into Confirmation Round
|
|
|
|
**Confirmation Round Integration:**
|
|
|
|
The confirmation round (Round 8) includes:
|
|
1. Main competition winners (1st, 2nd, 3rd per category)
|
|
2. Special award winners
|
|
|
|
**WinnerProposal Extension:**
|
|
```prisma
|
|
model WinnerProposal {
|
|
id String @id @default(cuid())
|
|
competitionId String
|
|
category CompetitionCategory? // Null for award winners
|
|
|
|
// Main competition or award
|
|
proposalType WinnerProposalType @default(MAIN_COMPETITION)
|
|
awardId String? // If proposalType = SPECIAL_AWARD
|
|
|
|
status WinnerProposalStatus @default(PENDING)
|
|
rankedProjectIds String[]
|
|
|
|
// ... rest of fields ...
|
|
}
|
|
|
|
enum WinnerProposalType {
|
|
MAIN_COMPETITION // Main 1st/2nd/3rd place
|
|
SPECIAL_AWARD // Award winner
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## API Changes
|
|
|
|
### New tRPC Procedures
|
|
|
|
```typescript
|
|
// src/server/routers/award-redesign.ts
|
|
|
|
export const awardRedesignRouter = router({
|
|
/**
|
|
* Create a new special award
|
|
*/
|
|
create: adminProcedure
|
|
.input(z.object({
|
|
competitionId: z.string(),
|
|
name: z.string().min(1).max(255),
|
|
description: z.string().optional(),
|
|
eligibilityMode: z.enum(['AI_SUGGESTED', 'MANUAL', 'ALL_ELIGIBLE', 'ROUND_BASED']),
|
|
evaluationMode: z.enum(['STAY_IN_MAIN', 'SEPARATE_POOL']),
|
|
votingMode: z.enum(['JURY_ONLY', 'AUDIENCE_ONLY', 'COMBINED']),
|
|
scoringMode: z.enum(['PICK_WINNER', 'RANKED', 'SCORED']),
|
|
maxWinners: z.number().int().min(1).default(1),
|
|
}))
|
|
.mutation(async ({ ctx, input }) => { /* ... */ }),
|
|
|
|
/**
|
|
* Run eligibility determination
|
|
*/
|
|
runEligibility: adminProcedure
|
|
.input(z.object({ awardId: z.string() }))
|
|
.mutation(async ({ ctx, input }) => { /* ... */ }),
|
|
|
|
/**
|
|
* Cast vote (jury or audience)
|
|
*/
|
|
vote: protectedProcedure
|
|
.input(z.object({
|
|
awardId: z.string(),
|
|
projectId: z.string(),
|
|
rank: z.number().int().min(1).optional(),
|
|
score: z.number().min(0).max(10).optional(),
|
|
}))
|
|
.mutation(async ({ ctx, input }) => { /* ... */ }),
|
|
|
|
/**
|
|
* Select winner(s)
|
|
*/
|
|
selectWinners: adminProcedure
|
|
.input(z.object({
|
|
awardId: z.string(),
|
|
winnerProjectIds: z.array(z.string()).min(1),
|
|
selectionMethod: z.enum(['JURY_VOTE', 'AUDIENCE_VOTE', 'COMBINED', 'ADMIN_DECISION']),
|
|
}))
|
|
.mutation(async ({ ctx, input }) => { /* ... */ }),
|
|
})
|
|
```
|
|
|
|
---
|
|
|
|
## Service Functions
|
|
|
|
### Award Service Enhancements
|
|
|
|
```typescript
|
|
// src/server/services/award-service.ts
|
|
|
|
/**
|
|
* Run round-based eligibility
|
|
*/
|
|
export async function runRoundBasedEligibility(
|
|
award: SpecialAward,
|
|
prisma = getPrisma()
|
|
) {
|
|
const config = award.eligibilityCriteria as RoundBasedConfig
|
|
|
|
if (!config.sourceRoundId) {
|
|
throw new Error('Round-based eligibility requires sourceRoundId')
|
|
}
|
|
|
|
// Get all projects in the specified round with the required state
|
|
const projectRoundStates = await prisma.projectRoundState.findMany({
|
|
where: {
|
|
roundId: config.sourceRoundId,
|
|
state: config.requiredState ?? 'PASSED',
|
|
},
|
|
select: { projectId: true }
|
|
})
|
|
|
|
// Create/update eligibility records
|
|
let created = 0
|
|
let updated = 0
|
|
|
|
for (const prs of projectRoundStates) {
|
|
const existing = await prisma.awardEligibility.findUnique({
|
|
where: {
|
|
awardId_projectId: {
|
|
awardId: award.id,
|
|
projectId: prs.projectId
|
|
}
|
|
}
|
|
})
|
|
|
|
if (existing) {
|
|
await prisma.awardEligibility.update({
|
|
where: { id: existing.id },
|
|
data: { eligible: true, method: 'AUTO' }
|
|
})
|
|
updated++
|
|
} else {
|
|
await prisma.awardEligibility.create({
|
|
data: {
|
|
awardId: award.id,
|
|
projectId: prs.projectId,
|
|
eligible: true,
|
|
method: 'AUTO',
|
|
}
|
|
})
|
|
created++
|
|
}
|
|
}
|
|
|
|
return { created, updated, total: projectRoundStates.length }
|
|
}
|
|
|
|
/**
|
|
* Calculate combined jury + audience score
|
|
*/
|
|
export function calculateCombinedScore(
|
|
juryScores: number[],
|
|
audienceVoteCount: number,
|
|
totalAudienceVotes: number,
|
|
juryWeight: number,
|
|
audienceWeight: number
|
|
): number {
|
|
if (juryScores.length === 0) {
|
|
throw new Error('Cannot calculate combined score without jury votes')
|
|
}
|
|
|
|
const juryAvg = juryScores.reduce((a, b) => a + b, 0) / juryScores.length
|
|
const normalizedJuryScore = juryAvg / 10 // Assume 1-10 scale
|
|
|
|
const audiencePercent = totalAudienceVotes > 0
|
|
? audienceVoteCount / totalAudienceVotes
|
|
: 0
|
|
|
|
const finalScore =
|
|
(normalizedJuryScore * juryWeight) +
|
|
(audiencePercent * audienceWeight)
|
|
|
|
return finalScore
|
|
}
|
|
|
|
/**
|
|
* Create award jury assignments
|
|
*/
|
|
export async function createAwardAssignments(
|
|
awardId: string,
|
|
prisma = getPrisma()
|
|
) {
|
|
const award = await prisma.specialAward.findUniqueOrThrow({
|
|
where: { id: awardId },
|
|
include: {
|
|
juryGroup: {
|
|
include: { members: true }
|
|
}
|
|
}
|
|
})
|
|
|
|
if (!award.juryGroupId || !award.juryGroup) {
|
|
throw new Error('Award must have a jury group to create assignments')
|
|
}
|
|
|
|
const eligibleProjects = await getEligibleProjects(awardId, prisma)
|
|
|
|
const assignments = []
|
|
|
|
for (const project of eligibleProjects) {
|
|
for (const member of award.juryGroup.members) {
|
|
assignments.push({
|
|
userId: member.userId,
|
|
projectId: project.id,
|
|
roundId: award.evaluationRoundId ?? null,
|
|
juryGroupId: award.juryGroupId,
|
|
method: 'MANUAL' as const,
|
|
})
|
|
}
|
|
}
|
|
|
|
await prisma.assignment.createMany({
|
|
data: assignments,
|
|
skipDuplicates: true,
|
|
})
|
|
|
|
return { created: assignments.length }
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Edge Cases
|
|
|
|
| Scenario | Handling |
|
|
|----------|----------|
|
|
| **Project eligible for multiple awards** | Allowed — project can win multiple awards |
|
|
| **Jury member on both main and award juries** | Allowed — separate assignments, separate evaluations |
|
|
| **Award voting ends before main results** | Award winner held until main results finalized, announced together |
|
|
| **Award eligibility changes mid-voting** | Admin override can remove eligibility; active votes invalidated |
|
|
| **Audience vote spam/fraud** | IP rate limiting, device fingerprinting, email verification, admin review |
|
|
| **Tie in award voting** | Admin decision or re-vote (configurable) |
|
|
| **Award jury not complete evaluations** | Admin can close voting with partial data or extend deadline |
|
|
| **Project withdrawn after eligible** | Eligibility auto-removed; votes invalidated |
|
|
| **Award criteria change after eligibility** | Re-run eligibility or grandfather existing eligible projects |
|
|
| **No eligible projects for award** | Award status set to DRAFT/ARCHIVED; no voting |
|
|
|
|
---
|
|
|
|
## Integration Points
|
|
|
|
### With Evaluation System
|
|
- Awards use `EvaluationForm` for criteria
|
|
- Award evaluations stored in `Evaluation` table with `formId` linkage
|
|
- Assignment system handles both main and award assignments
|
|
|
|
### With Jury Groups
|
|
- Awards can link to existing `JuryGroup` or have dedicated groups
|
|
- Jury members can overlap between main and award juries
|
|
- Caps and quotas honored for award assignments
|
|
|
|
### With Confirmation Round
|
|
- Award winners included in `WinnerProposal` system
|
|
- Confirmation flow handles both main and award winners
|
|
- Approval workflow requires sign-off on all winners
|
|
|
|
### With Notification System
|
|
- Eligibility notifications sent to eligible teams
|
|
- Voting reminders sent to award jurors
|
|
- Winner announcements coordinated with main results
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
The redesigned Special Awards system provides:
|
|
|
|
1. **Flexibility**: Two modes (STAY_IN_MAIN, SEPARATE_POOL) cover all use cases
|
|
2. **Integration**: Deep integration with competition rounds, juries, and results
|
|
3. **Autonomy**: Awards can run independently or piggyback on main flow
|
|
4. **Transparency**: AI eligibility with admin override, full audit trail
|
|
5. **Engagement**: Audience voting support with anti-fraud measures
|
|
6. **Scalability**: Support for multiple awards, multiple winners, complex scoring
|
|
|
|
This architecture eliminates the Track dependency, integrates awards as standalone entities, and provides a robust, flexible system for recognizing excellence across multiple dimensions while maintaining the integrity of the main competition flow.
|