1735 lines
43 KiB
Markdown
1735 lines
43 KiB
Markdown
|
|
# API Router Reference: Architecture Redesign
|
||
|
|
|
||
|
|
## Overview
|
||
|
|
|
||
|
|
This document provides a complete reference of all tRPC router changes for the MOPC architecture redesign. Every procedure — new, modified, preserved, and removed — is documented with full Zod schemas.
|
||
|
|
|
||
|
|
**Key Changes Summary:**
|
||
|
|
- **Renamed Routers**: `pipeline` → `competition`, Stage CRUD split between `competition` and `round` routers
|
||
|
|
- **New Routers**: `jury-group`, `winner-confirmation`, `submission-window`, `mentor-workspace`
|
||
|
|
- **Enhanced Routers**: `assignment`, `evaluation`, `file`, `applicant`, `award`, `live-control`, `mentor`
|
||
|
|
- **Eliminated**: `stageTransition` procedures (replaced by linear advancement rules)
|
||
|
|
- **Procedure Count**: ~180 existing procedures, ~60 new, ~30 modified, ~20 removed
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Router Rename Map
|
||
|
|
|
||
|
|
| Current Router | Redesigned Router | Reason |
|
||
|
|
|----------------|-------------------|--------|
|
||
|
|
| `pipeline.ts` | `competition.ts` | Domain-specific naming |
|
||
|
|
| `stage.ts` | Split: `competition.ts` (CRUD) + `round.ts` (runtime) | Clearer separation of concerns |
|
||
|
|
| `assignment.ts` | `assignment.ts` (enhanced) | Add jury group awareness |
|
||
|
|
| `specialAward.ts` | `award.ts` | Enhanced for two-mode awards |
|
||
|
|
| `mentor.ts` | `mentor.ts` + `mentor-workspace.ts` | Separate workspace features |
|
||
|
|
| *(new)* | `jury-group.ts` | Explicit jury management |
|
||
|
|
| *(new)* | `winner-confirmation.ts` | Multi-party winner approval |
|
||
|
|
| *(new)* | `submission-window.ts` | Multi-round document requirements |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 1. Competition Router (replaces Pipeline)
|
||
|
|
|
||
|
|
**File:** `src/server/routers/competition.ts` (renamed from `pipeline.ts`)
|
||
|
|
|
||
|
|
### Removed Procedures
|
||
|
|
|
||
|
|
All Track-related procedures are eliminated (Track model is removed):
|
||
|
|
- ~~`pipeline.createStructure`~~ — Replaced by `competition.createStructure` (no tracks)
|
||
|
|
- ~~`pipeline.getDraft`~~ — Replaced by `competition.getDraft` (returns rounds, not tracks)
|
||
|
|
- ~~`pipeline.updateStructure`~~ — Replaced by `competition.updateStructure` (rounds only)
|
||
|
|
- ~~`pipeline.getApplicantView`~~ — Moved to `applicant.getCompetitionView`
|
||
|
|
|
||
|
|
### Modified Procedures
|
||
|
|
|
||
|
|
#### `create` → `competition.create`
|
||
|
|
**Changes:** Rename references, simplified settings (no track concept)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
programId: z.string(),
|
||
|
|
name: z.string().min(1).max(255),
|
||
|
|
slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/),
|
||
|
|
|
||
|
|
// NEW: Competition-wide settings (typed, not generic JSON)
|
||
|
|
categoryMode: z.enum(['SHARED', 'SPLIT']).default('SHARED'),
|
||
|
|
startupFinalistCount: z.number().int().min(1).default(3),
|
||
|
|
conceptFinalistCount: z.number().int().min(1).default(3),
|
||
|
|
|
||
|
|
// Notification preferences
|
||
|
|
notifyOnRoundAdvance: z.boolean().default(true),
|
||
|
|
notifyOnDeadlineApproach: z.boolean().default(true),
|
||
|
|
deadlineReminderDays: z.array(z.number().int()).default([7, 3, 1]),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Output: Competition
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `update` → `competition.update`
|
||
|
|
**Changes:** Add typed settings fields
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
id: z.string(),
|
||
|
|
name: z.string().min(1).max(255).optional(),
|
||
|
|
slug: z.string().regex(/^[a-z0-9-]+$/).optional(),
|
||
|
|
status: z.enum(['DRAFT', 'ACTIVE', 'CLOSED', 'ARCHIVED']).optional(),
|
||
|
|
|
||
|
|
// NEW: Typed settings
|
||
|
|
categoryMode: z.enum(['SHARED', 'SPLIT']).optional(),
|
||
|
|
startupFinalistCount: z.number().int().min(1).optional(),
|
||
|
|
conceptFinalistCount: z.number().int().min(1).optional(),
|
||
|
|
notifyOnRoundAdvance: z.boolean().optional(),
|
||
|
|
notifyOnDeadlineApproach: z.boolean().optional(),
|
||
|
|
deadlineReminderDays: z.array(z.number().int()).optional(),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Output: Competition
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `get` → `competition.get`
|
||
|
|
**Changes:** Returns rounds array (flat), not tracks
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({ id: z.string() })
|
||
|
|
|
||
|
|
// Output
|
||
|
|
{
|
||
|
|
...competition,
|
||
|
|
program: { id, name },
|
||
|
|
rounds: Array<{
|
||
|
|
id, name, slug, roundType, status, sortOrder,
|
||
|
|
windowOpenAt, windowCloseAt,
|
||
|
|
juryGroup: { id, name } | null,
|
||
|
|
submissionWindow: { id, name } | null,
|
||
|
|
_count: { projectRoundStates, cohorts }
|
||
|
|
}>,
|
||
|
|
juryGroups: Array<{ id, name, _count: { members } }>,
|
||
|
|
submissionWindows: Array<{ id, name, roundNumber }>,
|
||
|
|
specialAwards: Array<{ id, name, status }>,
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `list` → `competition.list`
|
||
|
|
**Changes:** Returns competition counts (no tracks)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({ programId: z.string() })
|
||
|
|
|
||
|
|
// Output
|
||
|
|
Array<{
|
||
|
|
...competition,
|
||
|
|
_count: { rounds, juryGroups, submissionWindows, specialAwards }
|
||
|
|
}>
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `publish` → `competition.publish`
|
||
|
|
**Changes:** Validates rounds instead of tracks
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({ id: z.string() })
|
||
|
|
|
||
|
|
// Validation: Must have at least one round, all rounds must have configs
|
||
|
|
// Output: Competition with status = ACTIVE
|
||
|
|
```
|
||
|
|
|
||
|
|
### New Procedures
|
||
|
|
|
||
|
|
#### `createStructure`
|
||
|
|
**Purpose:** Atomically create Competition + Rounds + Jury Groups + Submission Windows
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
programId: z.string().min(1),
|
||
|
|
name: z.string().min(1).max(255),
|
||
|
|
slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/),
|
||
|
|
|
||
|
|
// Competition-wide settings
|
||
|
|
categoryMode: z.enum(['SHARED', 'SPLIT']).default('SHARED'),
|
||
|
|
startupFinalistCount: z.number().int().default(3),
|
||
|
|
conceptFinalistCount: z.number().int().default(3),
|
||
|
|
|
||
|
|
// Rounds (linear array, no tracks)
|
||
|
|
rounds: z.array(z.object({
|
||
|
|
name: z.string().min(1).max(255),
|
||
|
|
slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/),
|
||
|
|
roundType: z.enum([
|
||
|
|
'INTAKE', 'FILTERING', 'EVALUATION',
|
||
|
|
'SUBMISSION', 'MENTORING', 'LIVE_FINAL', 'CONFIRMATION'
|
||
|
|
]),
|
||
|
|
sortOrder: z.number().int().min(0),
|
||
|
|
configJson: z.record(z.unknown()).optional(),
|
||
|
|
windowOpenAt: z.date().optional(),
|
||
|
|
windowCloseAt: z.date().optional(),
|
||
|
|
|
||
|
|
// Links to other entities
|
||
|
|
juryGroupSlug: z.string().optional(), // Will link to JuryGroup after creation
|
||
|
|
submissionWindowSlug: z.string().optional(),
|
||
|
|
})),
|
||
|
|
|
||
|
|
// Jury groups
|
||
|
|
juryGroups: z.array(z.object({
|
||
|
|
name: z.string(),
|
||
|
|
slug: z.string(),
|
||
|
|
description: z.string().optional(),
|
||
|
|
sortOrder: z.number().int(),
|
||
|
|
defaultMaxAssignments: z.number().int().default(20),
|
||
|
|
defaultCapMode: z.enum(['HARD', 'SOFT', 'NONE']).default('SOFT'),
|
||
|
|
softCapBuffer: z.number().int().default(2),
|
||
|
|
categoryQuotasEnabled: z.boolean().default(false),
|
||
|
|
defaultCategoryQuotas: z.record(z.object({
|
||
|
|
min: z.number().int(),
|
||
|
|
max: z.number().int(),
|
||
|
|
})).optional(),
|
||
|
|
})),
|
||
|
|
|
||
|
|
// Submission windows
|
||
|
|
submissionWindows: z.array(z.object({
|
||
|
|
name: z.string(),
|
||
|
|
slug: z.string(),
|
||
|
|
roundNumber: z.number().int(),
|
||
|
|
sortOrder: z.number().int(),
|
||
|
|
windowOpenAt: z.date().optional(),
|
||
|
|
windowCloseAt: z.date().optional(),
|
||
|
|
deadlinePolicy: z.enum(['HARD', 'FLAG', 'GRACE']).default('FLAG'),
|
||
|
|
graceHours: z.number().int().optional(),
|
||
|
|
lockOnClose: z.boolean().default(true),
|
||
|
|
|
||
|
|
fileRequirements: z.array(z.object({
|
||
|
|
name: z.string(),
|
||
|
|
description: z.string().optional(),
|
||
|
|
acceptedMimeTypes: z.array(z.string()),
|
||
|
|
maxSizeMB: z.number().int().optional(),
|
||
|
|
isRequired: z.boolean().default(true),
|
||
|
|
sortOrder: z.number().int(),
|
||
|
|
})),
|
||
|
|
})),
|
||
|
|
|
||
|
|
// Advancement rules (auto-generated linear flow by default)
|
||
|
|
autoAdvancement: z.boolean().default(true),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Output
|
||
|
|
{
|
||
|
|
competition: Competition,
|
||
|
|
rounds: Array<Round>,
|
||
|
|
juryGroups: Array<JuryGroup>,
|
||
|
|
submissionWindows: Array<SubmissionWindow>,
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `updateStructure`
|
||
|
|
**Purpose:** Diff-based update (create/update/delete rounds, jury groups, windows)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
id: z.string(),
|
||
|
|
name: z.string().optional(),
|
||
|
|
slug: z.string().optional(),
|
||
|
|
status: z.enum(['DRAFT', 'ACTIVE', 'CLOSED', 'ARCHIVED']).optional(),
|
||
|
|
|
||
|
|
// Same nested arrays as createStructure, but with optional `id` fields
|
||
|
|
rounds: z.array(z.object({
|
||
|
|
id: z.string().optional(), // Present = update, absent = create
|
||
|
|
name: z.string(),
|
||
|
|
// ... all other round fields
|
||
|
|
})),
|
||
|
|
|
||
|
|
juryGroups: z.array(z.object({
|
||
|
|
id: z.string().optional(),
|
||
|
|
name: z.string(),
|
||
|
|
// ... all other jury group fields
|
||
|
|
})),
|
||
|
|
|
||
|
|
submissionWindows: z.array(z.object({
|
||
|
|
id: z.string().optional(),
|
||
|
|
name: z.string(),
|
||
|
|
// ... all other submission window fields
|
||
|
|
})),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Logic:
|
||
|
|
// - Rounds/groups/windows with ID → update
|
||
|
|
// - Rounds/groups/windows without ID → create
|
||
|
|
// - Existing IDs not in input → delete (with safety checks: no active ProjectRoundStates)
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `getDraft`
|
||
|
|
**Purpose:** Get full structure for wizard editing
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({ id: z.string() })
|
||
|
|
|
||
|
|
// Output
|
||
|
|
{
|
||
|
|
...competition,
|
||
|
|
program: { id, name },
|
||
|
|
rounds: Array<{
|
||
|
|
...round,
|
||
|
|
juryGroup: JuryGroup | null,
|
||
|
|
submissionWindow: SubmissionWindow | null,
|
||
|
|
visibleSubmissionWindows: Array<RoundSubmissionVisibility>,
|
||
|
|
advancementRules: Array<AdvancementRule>,
|
||
|
|
_count: { projectRoundStates },
|
||
|
|
}>,
|
||
|
|
juryGroups: Array<{
|
||
|
|
...juryGroup,
|
||
|
|
members: Array<JuryGroupMember>,
|
||
|
|
_count: { rounds, assignments },
|
||
|
|
}>,
|
||
|
|
submissionWindows: Array<{
|
||
|
|
...submissionWindow,
|
||
|
|
fileRequirements: Array<SubmissionFileRequirement>,
|
||
|
|
rounds: Array<{ id, name }>,
|
||
|
|
}>,
|
||
|
|
specialAwards: Array<{
|
||
|
|
...award,
|
||
|
|
juryGroup: { id, name } | null,
|
||
|
|
evaluationRound: { id, name } | null,
|
||
|
|
}>,
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `simulate`
|
||
|
|
**Purpose:** Dry-run project routing through competition
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
id: z.string(),
|
||
|
|
projectIds: z.array(z.string()).min(1).max(500),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Output
|
||
|
|
{
|
||
|
|
competitionId: string,
|
||
|
|
competitionName: string,
|
||
|
|
projectCount: number,
|
||
|
|
simulations: Array<{
|
||
|
|
projectId: string,
|
||
|
|
projectTitle: string,
|
||
|
|
currentStatus: ProjectStatus,
|
||
|
|
targetRoundId: string | null,
|
||
|
|
targetRoundName: string,
|
||
|
|
}>,
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Preserved Procedures (renamed only)
|
||
|
|
|
||
|
|
- `delete` → `competition.delete` (archive)
|
||
|
|
- `getSummary` → `competition.getSummary` (aggregated stats)
|
||
|
|
- `getStageAnalytics` → `competition.getRoundAnalytics` (per-round stats)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 2. Round Router (new, split from Stage)
|
||
|
|
|
||
|
|
**File:** `src/server/routers/round.ts`
|
||
|
|
|
||
|
|
Runtime operations for rounds (open/close windows, transition status, project states).
|
||
|
|
|
||
|
|
### New Procedures
|
||
|
|
|
||
|
|
#### `get`
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({ id: z.string() })
|
||
|
|
|
||
|
|
// Output
|
||
|
|
{
|
||
|
|
...round,
|
||
|
|
competition: { id, name, programId },
|
||
|
|
juryGroup: JuryGroup | null,
|
||
|
|
submissionWindow: SubmissionWindow | null,
|
||
|
|
visibleSubmissionWindows: Array<RoundSubmissionVisibility>,
|
||
|
|
cohorts: Array<Cohort>,
|
||
|
|
advancementRules: Array<AdvancementRule>,
|
||
|
|
stateDistribution: Record<ProjectRoundStateValue, number>,
|
||
|
|
_count: { projectRoundStates, assignments, evaluationForms },
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `transition`
|
||
|
|
**Purpose:** State machine for round status
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
id: z.string(),
|
||
|
|
targetStatus: z.enum(['ROUND_DRAFT', 'ROUND_ACTIVE', 'ROUND_CLOSED', 'ROUND_ARCHIVED']),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Validation: Enforces state machine rules
|
||
|
|
// ROUND_DRAFT → ROUND_ACTIVE
|
||
|
|
// ROUND_ACTIVE → ROUND_CLOSED
|
||
|
|
// ROUND_CLOSED → ROUND_ARCHIVED | ROUND_ACTIVE (reopen)
|
||
|
|
|
||
|
|
// Output: Round with new status
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `openWindow`
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
id: z.string(),
|
||
|
|
windowCloseAt: z.date().optional(),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Sets windowOpenAt = now, optionally sets windowCloseAt
|
||
|
|
// Output: Round
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `closeWindow`
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({ id: z.string() })
|
||
|
|
|
||
|
|
// Sets windowCloseAt = now
|
||
|
|
// Output: Round
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `getProjectStates`
|
||
|
|
**Purpose:** Paginated list of project states in a round
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
roundId: z.string(),
|
||
|
|
state: z.enum([
|
||
|
|
'PENDING', 'IN_PROGRESS', 'PASSED',
|
||
|
|
'REJECTED', 'COMPLETED', 'WITHDRAWN'
|
||
|
|
]).optional(),
|
||
|
|
cursor: z.string().optional(),
|
||
|
|
limit: z.number().int().min(1).max(100).default(50),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Output
|
||
|
|
{
|
||
|
|
items: Array<{
|
||
|
|
...projectRoundState,
|
||
|
|
project: { id, title, status, tags, teamName, competitionCategory },
|
||
|
|
}>,
|
||
|
|
nextCursor: string | undefined,
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `getForJury`
|
||
|
|
**Purpose:** Jury-facing round details
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({ id: z.string() })
|
||
|
|
|
||
|
|
// Output
|
||
|
|
{
|
||
|
|
...round,
|
||
|
|
competition: { id, name, programId },
|
||
|
|
juryGroup: JuryGroup | null,
|
||
|
|
evaluationForms: Array<{ id, criteriaJson, scalesJson }>,
|
||
|
|
isWindowOpen: boolean,
|
||
|
|
windowTimeRemaining: number | null, // milliseconds
|
||
|
|
myAssignmentCount: number,
|
||
|
|
myCompletedCount: number,
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `getApplicantTimeline`
|
||
|
|
**Purpose:** Show a project's journey through rounds
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
projectId: z.string(),
|
||
|
|
competitionId: z.string(),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Output
|
||
|
|
Array<{
|
||
|
|
roundId: string,
|
||
|
|
roundName: string,
|
||
|
|
roundType: RoundType,
|
||
|
|
state: ProjectRoundStateValue,
|
||
|
|
enteredAt: DateTime,
|
||
|
|
exitedAt: DateTime | null,
|
||
|
|
isCurrent: boolean,
|
||
|
|
}>
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `getRequirements`
|
||
|
|
**Purpose:** File requirements and upload status for a round
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
roundId: z.string(),
|
||
|
|
projectId: z.string(),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Output
|
||
|
|
{
|
||
|
|
submissionWindow: {
|
||
|
|
id: string,
|
||
|
|
name: string,
|
||
|
|
windowOpenAt: DateTime | null,
|
||
|
|
windowCloseAt: DateTime | null,
|
||
|
|
deadlinePolicy: DeadlinePolicy,
|
||
|
|
} | null,
|
||
|
|
fileRequirements: Array<SubmissionFileRequirement>,
|
||
|
|
uploadedFiles: Array<ProjectFile>,
|
||
|
|
windowStatus: {
|
||
|
|
isOpen: boolean,
|
||
|
|
closesAt: DateTime | null,
|
||
|
|
isLate: boolean,
|
||
|
|
},
|
||
|
|
deadlineInfo: {
|
||
|
|
windowOpenAt: DateTime | null,
|
||
|
|
windowCloseAt: DateTime | null,
|
||
|
|
graceHours: number,
|
||
|
|
},
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 3. Jury Group Router (new)
|
||
|
|
|
||
|
|
**File:** `src/server/routers/jury-group.ts`
|
||
|
|
|
||
|
|
### New Procedures
|
||
|
|
|
||
|
|
#### `create`
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
competitionId: z.string(),
|
||
|
|
name: z.string().min(1).max(255),
|
||
|
|
slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/),
|
||
|
|
description: z.string().optional(),
|
||
|
|
sortOrder: z.number().int().min(0).optional(),
|
||
|
|
|
||
|
|
// Default assignment configuration
|
||
|
|
defaultMaxAssignments: z.number().int().min(1).default(20),
|
||
|
|
defaultCapMode: z.enum(['HARD', 'SOFT', 'NONE']).default('SOFT'),
|
||
|
|
softCapBuffer: z.number().int().min(0).default(2),
|
||
|
|
|
||
|
|
// Category quotas
|
||
|
|
categoryQuotasEnabled: z.boolean().default(false),
|
||
|
|
defaultCategoryQuotas: z.record(z.object({
|
||
|
|
min: z.number().int().min(0),
|
||
|
|
max: z.number().int().min(1),
|
||
|
|
})).optional(),
|
||
|
|
|
||
|
|
// Onboarding settings
|
||
|
|
allowJurorCapAdjustment: z.boolean().default(false),
|
||
|
|
allowJurorRatioAdjustment: z.boolean().default(false),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Output: JuryGroup
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `update`
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
id: z.string(),
|
||
|
|
name: z.string().optional(),
|
||
|
|
description: z.string().optional(),
|
||
|
|
sortOrder: z.number().int().optional(),
|
||
|
|
defaultMaxAssignments: z.number().int().optional(),
|
||
|
|
defaultCapMode: z.enum(['HARD', 'SOFT', 'NONE']).optional(),
|
||
|
|
softCapBuffer: z.number().int().optional(),
|
||
|
|
categoryQuotasEnabled: z.boolean().optional(),
|
||
|
|
defaultCategoryQuotas: z.record(z.object({
|
||
|
|
min: z.number().int(),
|
||
|
|
max: z.number().int(),
|
||
|
|
})).optional(),
|
||
|
|
allowJurorCapAdjustment: z.boolean().optional(),
|
||
|
|
allowJurorRatioAdjustment: z.boolean().optional(),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Output: JuryGroup
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `delete`
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({ id: z.string() })
|
||
|
|
|
||
|
|
// Validation: Cannot delete if linked to any rounds or assignments
|
||
|
|
// Output: { success: true }
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `get`
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({ id: z.string() })
|
||
|
|
|
||
|
|
// Output
|
||
|
|
{
|
||
|
|
...juryGroup,
|
||
|
|
competition: { id, name },
|
||
|
|
members: Array<{
|
||
|
|
...juryGroupMember,
|
||
|
|
user: { id, name, email, expertiseTags },
|
||
|
|
}>,
|
||
|
|
rounds: Array<{ id, name, roundType }>,
|
||
|
|
_count: { members, assignments },
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `list`
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({ competitionId: z.string() })
|
||
|
|
|
||
|
|
// Output
|
||
|
|
Array<{
|
||
|
|
...juryGroup,
|
||
|
|
_count: { members, rounds, assignments },
|
||
|
|
}>
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `addMember`
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
juryGroupId: z.string(),
|
||
|
|
userId: z.string(),
|
||
|
|
isLead: z.boolean().default(false),
|
||
|
|
|
||
|
|
// Per-juror overrides
|
||
|
|
maxAssignmentsOverride: z.number().int().optional(),
|
||
|
|
capModeOverride: z.enum(['HARD', 'SOFT', 'NONE']).optional(),
|
||
|
|
categoryQuotasOverride: z.record(z.object({
|
||
|
|
min: z.number().int(),
|
||
|
|
max: z.number().int(),
|
||
|
|
})).optional(),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Validation: User must have role JURY_MEMBER
|
||
|
|
// Output: JuryGroupMember
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `updateMember`
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
id: z.string(), // JuryGroupMember ID
|
||
|
|
isLead: z.boolean().optional(),
|
||
|
|
maxAssignmentsOverride: z.number().int().optional().nullable(),
|
||
|
|
capModeOverride: z.enum(['HARD', 'SOFT', 'NONE']).optional().nullable(),
|
||
|
|
categoryQuotasOverride: z.record(z.object({
|
||
|
|
min: z.number().int(),
|
||
|
|
max: z.number().int(),
|
||
|
|
})).optional().nullable(),
|
||
|
|
|
||
|
|
// Juror preferences (self-service during onboarding)
|
||
|
|
preferredStartupRatio: z.number().min(0).max(1).optional(),
|
||
|
|
availabilityNotes: z.string().optional(),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Output: JuryGroupMember
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `removeMember`
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({ id: z.string() })
|
||
|
|
|
||
|
|
// Validation: Cannot remove if has active assignments
|
||
|
|
// Output: { success: true }
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `bulkAddMembers`
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
juryGroupId: z.string(),
|
||
|
|
userIds: z.array(z.string()).min(1).max(100),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Creates JuryGroupMember for each user with default settings
|
||
|
|
// Output: { created: number, skipped: number }
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `getOnboarding`
|
||
|
|
**Purpose:** Jury member views their onboarding settings for a group
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema (protectedProcedure)
|
||
|
|
z.object({ juryGroupId: z.string() })
|
||
|
|
|
||
|
|
// Output
|
||
|
|
{
|
||
|
|
juryGroup: JuryGroup,
|
||
|
|
membership: JuryGroupMember,
|
||
|
|
allowCapAdjustment: boolean,
|
||
|
|
allowRatioAdjustment: boolean,
|
||
|
|
hasCompleted: boolean, // Has filled preferences
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `submitOnboarding`
|
||
|
|
**Purpose:** Jury member submits preferences
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema (protectedProcedure)
|
||
|
|
z.object({
|
||
|
|
juryGroupId: z.string(),
|
||
|
|
|
||
|
|
// Conditionally allowed based on juryGroup.allowJurorCapAdjustment
|
||
|
|
maxAssignmentsOverride: z.number().int().optional(),
|
||
|
|
|
||
|
|
// Conditionally allowed based on juryGroup.allowJurorRatioAdjustment
|
||
|
|
preferredStartupRatio: z.number().min(0).max(1).optional(),
|
||
|
|
|
||
|
|
availabilityNotes: z.string().optional(),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Output: JuryGroupMember
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 4. Submission Window Router (new)
|
||
|
|
|
||
|
|
**File:** `src/server/routers/submission-window.ts`
|
||
|
|
|
||
|
|
### New Procedures
|
||
|
|
|
||
|
|
#### `create`
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
competitionId: z.string(),
|
||
|
|
name: z.string().min(1),
|
||
|
|
slug: z.string().regex(/^[a-z0-9-]+$/),
|
||
|
|
roundNumber: z.number().int().min(1),
|
||
|
|
sortOrder: z.number().int().optional(),
|
||
|
|
|
||
|
|
// Window timing
|
||
|
|
windowOpenAt: z.date().optional(),
|
||
|
|
windowCloseAt: z.date().optional(),
|
||
|
|
|
||
|
|
// Deadline behavior
|
||
|
|
deadlinePolicy: z.enum(['HARD', 'FLAG', 'GRACE']).default('FLAG'),
|
||
|
|
graceHours: z.number().int().min(0).optional(),
|
||
|
|
lockOnClose: z.boolean().default(true),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Output: SubmissionWindow
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `update`
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
id: z.string(),
|
||
|
|
name: z.string().optional(),
|
||
|
|
windowOpenAt: z.date().optional().nullable(),
|
||
|
|
windowCloseAt: z.date().optional().nullable(),
|
||
|
|
deadlinePolicy: z.enum(['HARD', 'FLAG', 'GRACE']).optional(),
|
||
|
|
graceHours: z.number().int().optional(),
|
||
|
|
lockOnClose: z.boolean().optional(),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Output: SubmissionWindow
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `delete`
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({ id: z.string() })
|
||
|
|
|
||
|
|
// Validation: Cannot delete if has uploaded files
|
||
|
|
// Output: { success: true }
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `get`
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({ id: z.string() })
|
||
|
|
|
||
|
|
// Output
|
||
|
|
{
|
||
|
|
...submissionWindow,
|
||
|
|
competition: { id, name },
|
||
|
|
fileRequirements: Array<SubmissionFileRequirement>,
|
||
|
|
rounds: Array<{ id, name, roundType }>,
|
||
|
|
_count: { fileRequirements, projectFiles },
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `list`
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({ competitionId: z.string() })
|
||
|
|
|
||
|
|
// Output
|
||
|
|
Array<{
|
||
|
|
...submissionWindow,
|
||
|
|
_count: { fileRequirements, projectFiles, rounds },
|
||
|
|
}>
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `addFileRequirement`
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
submissionWindowId: z.string(),
|
||
|
|
name: z.string().min(1),
|
||
|
|
description: z.string().optional(),
|
||
|
|
acceptedMimeTypes: z.array(z.string()).min(1),
|
||
|
|
maxSizeMB: z.number().int().min(1).optional(),
|
||
|
|
isRequired: z.boolean().default(true),
|
||
|
|
sortOrder: z.number().int().optional(),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Output: SubmissionFileRequirement
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `updateFileRequirement`
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
id: z.string(),
|
||
|
|
name: z.string().optional(),
|
||
|
|
description: z.string().optional(),
|
||
|
|
acceptedMimeTypes: z.array(z.string()).optional(),
|
||
|
|
maxSizeMB: z.number().int().optional(),
|
||
|
|
isRequired: z.boolean().optional(),
|
||
|
|
sortOrder: z.number().int().optional(),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Output: SubmissionFileRequirement
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `deleteFileRequirement`
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({ id: z.string() })
|
||
|
|
|
||
|
|
// Validation: Cannot delete if has uploaded files against this requirement
|
||
|
|
// Output: { success: true }
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `setRoundVisibility`
|
||
|
|
**Purpose:** Configure which submission windows a jury round can see
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
roundId: z.string(),
|
||
|
|
submissionWindowIds: z.array(z.string()), // Windows jury can see
|
||
|
|
displayLabels: z.record(z.string()).optional(), // submissionWindowId → label
|
||
|
|
})
|
||
|
|
|
||
|
|
// Creates/updates RoundSubmissionVisibility records
|
||
|
|
// Output: Array<RoundSubmissionVisibility>
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 5. Winner Confirmation Router (new)
|
||
|
|
|
||
|
|
**File:** `src/server/routers/winner-confirmation.ts`
|
||
|
|
|
||
|
|
### New Procedures
|
||
|
|
|
||
|
|
#### `createProposal`
|
||
|
|
**Purpose:** Admin or system generates a winner proposal
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
competitionId: z.string(),
|
||
|
|
category: z.enum(['STARTUP', 'BUSINESS_CONCEPT']),
|
||
|
|
|
||
|
|
// Proposed rankings (ordered project IDs)
|
||
|
|
rankedProjectIds: z.array(z.string()).min(1).max(10),
|
||
|
|
|
||
|
|
// Evidence/basis
|
||
|
|
sourceRoundId: z.string(),
|
||
|
|
selectionBasis: z.object({
|
||
|
|
method: z.enum(['SCORE_BASED', 'VOTE_BASED', 'AI_RECOMMENDED', 'ADMIN_MANUAL']),
|
||
|
|
scores: z.record(z.number()).optional(),
|
||
|
|
aiRecommendation: z.string().optional(),
|
||
|
|
reasoning: z.string().optional(),
|
||
|
|
}),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Output: WinnerProposal (status: PENDING)
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `getProposal`
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({ id: z.string() })
|
||
|
|
|
||
|
|
// Output
|
||
|
|
{
|
||
|
|
...winnerProposal,
|
||
|
|
competition: { id, name },
|
||
|
|
sourceRound: { id, name, roundType },
|
||
|
|
proposedBy: { id, name, email },
|
||
|
|
frozenBy: { id, name } | null,
|
||
|
|
overrideBy: { id, name } | null,
|
||
|
|
approvals: Array<{
|
||
|
|
...winnerApproval,
|
||
|
|
user: { id, name, email },
|
||
|
|
}>,
|
||
|
|
projects: Array<{ // Ordered by rankedProjectIds
|
||
|
|
id, title, teamName, rank,
|
||
|
|
}>,
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `listProposals`
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
competitionId: z.string(),
|
||
|
|
status: z.enum([
|
||
|
|
'PENDING', 'APPROVED', 'REJECTED', 'OVERRIDDEN', 'FROZEN'
|
||
|
|
]).optional(),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Output
|
||
|
|
Array<{
|
||
|
|
...winnerProposal,
|
||
|
|
approvalProgress: {
|
||
|
|
total: number,
|
||
|
|
approved: number,
|
||
|
|
rejected: number,
|
||
|
|
pending: number,
|
||
|
|
},
|
||
|
|
}>
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `requestApproval`
|
||
|
|
**Purpose:** Admin adds jury members to approval list
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
winnerProposalId: z.string(),
|
||
|
|
userIds: z.array(z.string()).min(1), // Jury members to request approval from
|
||
|
|
role: z.enum(['JURY_MEMBER', 'ADMIN']).default('JURY_MEMBER'),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Creates WinnerApproval records (approved: null)
|
||
|
|
// Output: Array<WinnerApproval>
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `approve`
|
||
|
|
**Purpose:** Jury member or admin approves the proposal
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema (protectedProcedure)
|
||
|
|
z.object({
|
||
|
|
winnerProposalId: z.string(),
|
||
|
|
approved: z.boolean(),
|
||
|
|
comments: z.string().optional(),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Updates WinnerApproval.approved = true/false, respondedAt = now
|
||
|
|
// If all approvals received and all true → sets WinnerProposal.status = APPROVED
|
||
|
|
// If any approval is false → sets WinnerProposal.status = REJECTED
|
||
|
|
// Output: WinnerApproval
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `getMyPendingApprovals`
|
||
|
|
**Purpose:** Jury member sees proposals awaiting their approval
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema (protectedProcedure)
|
||
|
|
z.object({ competitionId: z.string().optional() })
|
||
|
|
|
||
|
|
// Output
|
||
|
|
Array<{
|
||
|
|
...winnerApproval,
|
||
|
|
proposal: {
|
||
|
|
...winnerProposal,
|
||
|
|
competition: { id, name },
|
||
|
|
projects: Array<{ id, title, rank }>,
|
||
|
|
},
|
||
|
|
}>
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `override`
|
||
|
|
**Purpose:** Admin forces a result (bypasses jury approval)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
winnerProposalId: z.string(),
|
||
|
|
mode: z.enum(['FORCE_MAJORITY', 'ADMIN_DECISION']),
|
||
|
|
reason: z.string().min(10),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Sets WinnerProposal.status = OVERRIDDEN
|
||
|
|
// Sets overrideUsed = true, overrideMode, overrideReason, overrideById
|
||
|
|
// Output: WinnerProposal
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `freeze`
|
||
|
|
**Purpose:** Lock the results (official, no further changes)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({ winnerProposalId: z.string() })
|
||
|
|
|
||
|
|
// Validation: Must be status APPROVED or OVERRIDDEN
|
||
|
|
// Sets WinnerProposal.status = FROZEN, frozenAt = now, frozenById
|
||
|
|
// Updates Competition with winnerProjectIds
|
||
|
|
// Output: WinnerProposal
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `unfreeze`
|
||
|
|
**Purpose:** Admin unlocks results (rare, exceptional circumstances)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
winnerProposalId: z.string(),
|
||
|
|
reason: z.string().min(10),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Sets status = PENDING, frozenAt = null
|
||
|
|
// Creates audit log with reason
|
||
|
|
// Output: WinnerProposal
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 6. Assignment Router (enhanced)
|
||
|
|
|
||
|
|
**File:** `src/server/routers/assignment.ts`
|
||
|
|
|
||
|
|
### Modified Procedures
|
||
|
|
|
||
|
|
#### `create` → Enhanced with jury group awareness
|
||
|
|
**Changes:** Add `juryGroupId` parameter, enforce per-juror caps from JuryGroupMember
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
userId: z.string(),
|
||
|
|
projectId: z.string(),
|
||
|
|
roundId: z.string(), // RENAMED from stageId
|
||
|
|
juryGroupId: z.string().optional(), // NEW: Link to jury group
|
||
|
|
isRequired: z.boolean().default(true),
|
||
|
|
forceOverride: z.boolean().default(false),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Validation:
|
||
|
|
// - Checks JuryGroupMember.maxAssignmentsOverride if set
|
||
|
|
// - Falls back to JuryGroup.defaultMaxAssignments
|
||
|
|
// - Falls back to User.maxAssignments
|
||
|
|
// - Enforces CapMode (HARD vs SOFT)
|
||
|
|
|
||
|
|
// Output: Assignment (with juryGroupId)
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `bulkCreate` → Enhanced with capacity checking
|
||
|
|
**Changes:** Respect juryGroupId, apply category quotas
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
roundId: z.string(), // RENAMED from stageId
|
||
|
|
juryGroupId: z.string().optional(), // NEW
|
||
|
|
assignments: z.array(z.object({
|
||
|
|
userId: z.string(),
|
||
|
|
projectId: z.string(),
|
||
|
|
})),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Logic:
|
||
|
|
// - For each juror, checks current count vs cap
|
||
|
|
// - If CapMode = HARD → rejects over-cap assignments
|
||
|
|
// - If CapMode = SOFT → allows up to softCapBuffer extra
|
||
|
|
// - Applies category quotas if categoryQuotasEnabled
|
||
|
|
// - Skips assignments that would violate quotas
|
||
|
|
|
||
|
|
// Output
|
||
|
|
{
|
||
|
|
created: number,
|
||
|
|
requested: number,
|
||
|
|
skipped: number,
|
||
|
|
skippedDueToCapacity: number,
|
||
|
|
skippedDueToCategoryQuota: number,
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `getSuggestions` → Enhanced with category quotas
|
||
|
|
**Changes:** Add quota scoring, penalty for over-quota categories
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
roundId: z.string(), // RENAMED from stageId
|
||
|
|
juryGroupId: z.string().optional(), // NEW
|
||
|
|
})
|
||
|
|
|
||
|
|
// Algorithm enhancements:
|
||
|
|
// - Fetches JuryGroup.defaultCategoryQuotas
|
||
|
|
// - Tracks per-juror category distribution
|
||
|
|
// - Penalizes assignments to over-quota categories (-25 score)
|
||
|
|
// - Bonuses assignments to under-quota categories (+10 score)
|
||
|
|
|
||
|
|
// Output: Array of suggestions with quota awareness
|
||
|
|
```
|
||
|
|
|
||
|
|
### New Procedures
|
||
|
|
|
||
|
|
#### `getJuryGroupStats`
|
||
|
|
**Purpose:** Per-group assignment coverage stats
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
juryGroupId: z.string(),
|
||
|
|
roundId: z.string(),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Output
|
||
|
|
{
|
||
|
|
juryGroup: { id, name },
|
||
|
|
round: { id, name },
|
||
|
|
members: Array<{
|
||
|
|
user: { id, name, email },
|
||
|
|
currentAssignments: number,
|
||
|
|
cap: number,
|
||
|
|
capMode: CapMode,
|
||
|
|
categoryDistribution: Record<string, number>, // Category → count
|
||
|
|
quotaStatus: Record<string, { current, min, max, status }>,
|
||
|
|
}>,
|
||
|
|
totalAssignments: number,
|
||
|
|
averageLoad: number,
|
||
|
|
minLoad: number,
|
||
|
|
maxLoad: number,
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 7. Evaluation Router (enhanced)
|
||
|
|
|
||
|
|
**File:** `src/server/routers/evaluation.ts`
|
||
|
|
|
||
|
|
### Modified Procedures
|
||
|
|
|
||
|
|
#### All procedures: Rename `stageId` → `roundId` in inputs/outputs
|
||
|
|
|
||
|
|
#### `get`, `start`, `autosave`, `submit` — No functional changes, just renames
|
||
|
|
|
||
|
|
### New Procedures
|
||
|
|
|
||
|
|
#### `getCrossRoundDocs`
|
||
|
|
**Purpose:** Jury sees documents from multiple submission windows
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema (protectedProcedure)
|
||
|
|
z.object({
|
||
|
|
projectId: z.string(),
|
||
|
|
roundId: z.string(),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Logic:
|
||
|
|
// - Fetches RoundSubmissionVisibility records for roundId
|
||
|
|
// - Returns files from all visible submission windows
|
||
|
|
|
||
|
|
// Output
|
||
|
|
{
|
||
|
|
round: { id, name },
|
||
|
|
submissionWindows: Array<{
|
||
|
|
window: { id, name, roundNumber },
|
||
|
|
displayLabel: string,
|
||
|
|
files: Array<ProjectFile>,
|
||
|
|
requirements: Array<SubmissionFileRequirement>,
|
||
|
|
}>,
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 8. File Router (enhanced)
|
||
|
|
|
||
|
|
**File:** `src/server/routers/file.ts`
|
||
|
|
|
||
|
|
### Modified Procedures
|
||
|
|
|
||
|
|
#### `getDownloadUrl` → Enhanced for multi-round visibility
|
||
|
|
**Changes:** Check RoundSubmissionVisibility for jury access
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema (no changes)
|
||
|
|
z.object({
|
||
|
|
bucket: z.string(),
|
||
|
|
objectKey: z.string(),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Logic changes:
|
||
|
|
// - If user is jury, checks Assignment.roundId
|
||
|
|
// - Looks up RoundSubmissionVisibility for that round
|
||
|
|
// - Grants access if file.submissionWindowId is in visible windows
|
||
|
|
|
||
|
|
// Output: { url }
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `getUploadUrl` → Enhanced for submission window association
|
||
|
|
**Changes:** Link file to submissionWindowId
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
projectId: z.string(),
|
||
|
|
fileName: z.string(),
|
||
|
|
fileType: z.enum(['EXEC_SUMMARY', 'PRESENTATION', 'VIDEO', 'OTHER']),
|
||
|
|
mimeType: z.string(),
|
||
|
|
size: z.number().int().positive(),
|
||
|
|
|
||
|
|
submissionWindowId: z.string().optional(), // NEW: Which window is this for?
|
||
|
|
requirementId: z.string().optional(), // Links to SubmissionFileRequirement
|
||
|
|
})
|
||
|
|
|
||
|
|
// Creates ProjectFile with submissionWindowId
|
||
|
|
// Output: { uploadUrl, fileId }
|
||
|
|
```
|
||
|
|
|
||
|
|
### New Procedures
|
||
|
|
|
||
|
|
#### `promoteFromMentor`
|
||
|
|
**Purpose:** Promote a MentorFile to official ProjectFile
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
mentorFileId: z.string(),
|
||
|
|
submissionWindowId: z.string(),
|
||
|
|
requirementId: z.string().optional(),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Logic:
|
||
|
|
// - Copies object in MinIO to new key
|
||
|
|
// - Creates ProjectFile linked to submissionWindow
|
||
|
|
// - Sets MentorFile.isPromoted = true, promotedToFileId
|
||
|
|
|
||
|
|
// Output: ProjectFile
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 9. Applicant Router (enhanced)
|
||
|
|
|
||
|
|
**File:** `src/server/routers/applicant.ts`
|
||
|
|
|
||
|
|
### Modified Procedures
|
||
|
|
|
||
|
|
#### `getMySubmission` → Enhanced for multi-round
|
||
|
|
**Changes:** Returns submission status for all rounds
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
competitionId: z.string().optional(), // NEW: Filter by competition
|
||
|
|
programId: z.string().optional(),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Output
|
||
|
|
{
|
||
|
|
...project,
|
||
|
|
roundStates: Array<{
|
||
|
|
round: { id, name, roundType, sortOrder },
|
||
|
|
state: ProjectRoundStateValue,
|
||
|
|
enteredAt: DateTime,
|
||
|
|
exitedAt: DateTime | null,
|
||
|
|
}>,
|
||
|
|
currentRound: { id, name, roundType } | null,
|
||
|
|
eligibleForNextSubmission: boolean,
|
||
|
|
nextSubmissionWindow: SubmissionWindow | null,
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### New Procedures
|
||
|
|
|
||
|
|
#### `getCompetitionView`
|
||
|
|
**Purpose:** Applicant's view of their journey through a competition
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema (protectedProcedure)
|
||
|
|
z.object({
|
||
|
|
competitionId: z.string(),
|
||
|
|
projectId: z.string(),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Output
|
||
|
|
{
|
||
|
|
competition: { id, name, status },
|
||
|
|
project: { id, title, teamName, status },
|
||
|
|
rounds: Array<{
|
||
|
|
round: { id, name, roundType, sortOrder, windowOpenAt, windowCloseAt },
|
||
|
|
projectState: ProjectRoundState | null,
|
||
|
|
isCurrent: boolean,
|
||
|
|
isPassed: boolean,
|
||
|
|
canSubmit: boolean,
|
||
|
|
submissionWindow: SubmissionWindow | null,
|
||
|
|
}>,
|
||
|
|
timeline: Array<{
|
||
|
|
date: DateTime,
|
||
|
|
event: string,
|
||
|
|
description: string,
|
||
|
|
}>,
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `submitToWindow`
|
||
|
|
**Purpose:** Submit files for a specific submission window
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema (protectedProcedure)
|
||
|
|
z.object({
|
||
|
|
projectId: z.string(),
|
||
|
|
submissionWindowId: z.string(),
|
||
|
|
fileIds: z.array(z.string()), // ProjectFile IDs to mark as submitted
|
||
|
|
})
|
||
|
|
|
||
|
|
// Validation:
|
||
|
|
// - Window must be open
|
||
|
|
// - All required FileRequirements must have files
|
||
|
|
// - User must own project or be team member
|
||
|
|
|
||
|
|
// Output: { success: true, submittedAt: DateTime }
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 10. Live Control Router (enhanced)
|
||
|
|
|
||
|
|
**File:** `src/server/routers/live-control.ts` (or `live.ts`)
|
||
|
|
|
||
|
|
### Modified Procedures
|
||
|
|
|
||
|
|
#### All procedures: Rename `stageId` → `roundId`
|
||
|
|
|
||
|
|
### New Procedures
|
||
|
|
|
||
|
|
#### `getStageManager`
|
||
|
|
**Purpose:** Admin view of live ceremony control panel
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({ roundId: z.string() })
|
||
|
|
|
||
|
|
// Output
|
||
|
|
{
|
||
|
|
round: { id, name, roundType },
|
||
|
|
liveCursor: LiveProgressCursor | null,
|
||
|
|
liveVotingSession: LiveVotingSession | null,
|
||
|
|
|
||
|
|
// Cohorts (project presentation order)
|
||
|
|
cohorts: Array<{
|
||
|
|
...cohort,
|
||
|
|
projects: Array<{
|
||
|
|
project: { id, title, teamName },
|
||
|
|
presentationOrder: number,
|
||
|
|
}>,
|
||
|
|
}>,
|
||
|
|
|
||
|
|
// Jury voting status
|
||
|
|
juryVotes: Array<{
|
||
|
|
juror: { id, name },
|
||
|
|
votedProjects: Array<string>, // Project IDs
|
||
|
|
hasCompleted: boolean,
|
||
|
|
}>,
|
||
|
|
|
||
|
|
// Audience voting status
|
||
|
|
audienceVotes: {
|
||
|
|
total: number,
|
||
|
|
byProject: Record<string, number>,
|
||
|
|
},
|
||
|
|
|
||
|
|
// Controls
|
||
|
|
canStartVoting: boolean,
|
||
|
|
canCloseVoting: boolean,
|
||
|
|
canRevealResults: boolean,
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `setCursor`
|
||
|
|
**Purpose:** Move live ceremony to a specific project/cohort
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
roundId: z.string(),
|
||
|
|
cohortId: z.string().optional(),
|
||
|
|
projectId: z.string().optional(),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Updates LiveProgressCursor.currentCohortId, currentProjectId
|
||
|
|
// Output: LiveProgressCursor
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 11. Mentor Workspace Router (new)
|
||
|
|
|
||
|
|
**File:** `src/server/routers/mentor-workspace.ts`
|
||
|
|
|
||
|
|
### New Procedures
|
||
|
|
|
||
|
|
#### `getWorkspace`
|
||
|
|
**Purpose:** Mentor or applicant views the shared workspace
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema (protectedProcedure)
|
||
|
|
z.object({ mentorAssignmentId: z.string() })
|
||
|
|
|
||
|
|
// Validation: User must be mentor or project team member
|
||
|
|
// Output
|
||
|
|
{
|
||
|
|
mentorAssignment: {
|
||
|
|
...assignment,
|
||
|
|
mentor: { id, name, email },
|
||
|
|
project: { id, title, teamName },
|
||
|
|
workspaceEnabled: boolean,
|
||
|
|
workspaceOpenAt: DateTime | null,
|
||
|
|
workspaceCloseAt: DateTime | null,
|
||
|
|
},
|
||
|
|
files: Array<{
|
||
|
|
...mentorFile,
|
||
|
|
uploadedBy: { id, name },
|
||
|
|
comments: Array<MentorFileComment>,
|
||
|
|
isPromoted: boolean,
|
||
|
|
promotedToFile: { id, fileName } | null,
|
||
|
|
}>,
|
||
|
|
canUpload: boolean,
|
||
|
|
canComment: boolean,
|
||
|
|
canPromote: boolean,
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `uploadFile`
|
||
|
|
```typescript
|
||
|
|
// Input schema (protectedProcedure)
|
||
|
|
z.object({
|
||
|
|
mentorAssignmentId: z.string(),
|
||
|
|
fileName: z.string(),
|
||
|
|
mimeType: z.string(),
|
||
|
|
size: z.number().int().positive(),
|
||
|
|
description: z.string().optional(),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Creates MentorFile, returns pre-signed upload URL
|
||
|
|
// Output: { uploadUrl, fileId }
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `addComment`
|
||
|
|
```typescript
|
||
|
|
// Input schema (protectedProcedure)
|
||
|
|
z.object({
|
||
|
|
mentorFileId: z.string(),
|
||
|
|
content: z.string().min(1).max(2000),
|
||
|
|
parentCommentId: z.string().optional(), // For threading
|
||
|
|
})
|
||
|
|
|
||
|
|
// Output: MentorFileComment
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `promoteFile`
|
||
|
|
**Purpose:** Mentor promotes a workspace file to official submission
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema (mentorProcedure)
|
||
|
|
z.object({
|
||
|
|
mentorFileId: z.string(),
|
||
|
|
submissionWindowId: z.string(),
|
||
|
|
requirementId: z.string().optional(),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Calls file.promoteFromMentor internally
|
||
|
|
// Output: ProjectFile
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 12. Special Award Router (renamed to Award, enhanced)
|
||
|
|
|
||
|
|
**File:** `src/server/routers/award.ts` (renamed from `specialAward.ts`)
|
||
|
|
|
||
|
|
### Modified Procedures
|
||
|
|
|
||
|
|
#### `create` → Enhanced for two-mode awards
|
||
|
|
**Changes:** Add `eligibilityMode`, `juryGroupId`, `evaluationRoundId`
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
competitionId: z.string(), // CHANGED from programId
|
||
|
|
name: z.string().min(1),
|
||
|
|
description: z.string().optional(),
|
||
|
|
criteriaText: z.string().optional(),
|
||
|
|
|
||
|
|
// NEW: Award mode
|
||
|
|
eligibilityMode: z.enum(['SEPARATE_POOL', 'STAY_IN_MAIN']).default('STAY_IN_MAIN'),
|
||
|
|
|
||
|
|
// Scoring/voting
|
||
|
|
scoringMode: z.enum(['PICK_WINNER', 'RANKED', 'SCORED']),
|
||
|
|
maxRankedPicks: z.number().int().optional(),
|
||
|
|
|
||
|
|
// Decision
|
||
|
|
decisionMode: z.enum(['JURY_VOTE', 'AWARD_MASTER_DECISION', 'ADMIN_DECISION']).default('JURY_VOTE'),
|
||
|
|
|
||
|
|
// NEW: Links to other entities
|
||
|
|
evaluationRoundId: z.string().optional(), // Which round this award runs alongside
|
||
|
|
juryGroupId: z.string().optional(), // Dedicated or shared jury group
|
||
|
|
|
||
|
|
// AI eligibility
|
||
|
|
useAiEligibility: z.boolean().default(false),
|
||
|
|
autoTagRulesJson: z.record(z.unknown()).optional(),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Output: SpecialAward
|
||
|
|
```
|
||
|
|
|
||
|
|
#### `update` → Add new fields
|
||
|
|
```typescript
|
||
|
|
// Input schema (all fields optional)
|
||
|
|
z.object({
|
||
|
|
id: z.string(),
|
||
|
|
name: z.string().optional(),
|
||
|
|
description: z.string().optional(),
|
||
|
|
criteriaText: z.string().optional(),
|
||
|
|
eligibilityMode: z.enum(['SEPARATE_POOL', 'STAY_IN_MAIN']).optional(), // NEW
|
||
|
|
scoringMode: z.enum(['PICK_WINNER', 'RANKED', 'SCORED']).optional(),
|
||
|
|
maxRankedPicks: z.number().int().optional(),
|
||
|
|
decisionMode: z.enum(['JURY_VOTE', 'AWARD_MASTER_DECISION', 'ADMIN_DECISION']).optional(),
|
||
|
|
evaluationRoundId: z.string().optional(), // NEW
|
||
|
|
juryGroupId: z.string().optional(), // NEW
|
||
|
|
useAiEligibility: z.boolean().optional(),
|
||
|
|
votingStartAt: z.date().optional(),
|
||
|
|
votingEndAt: z.date().optional(),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Output: SpecialAward
|
||
|
|
```
|
||
|
|
|
||
|
|
### New Procedures
|
||
|
|
|
||
|
|
#### `getEligibilityByMode`
|
||
|
|
**Purpose:** View eligible projects with mode-specific filtering
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({ awardId: z.string() })
|
||
|
|
|
||
|
|
// Output
|
||
|
|
{
|
||
|
|
award: { ...specialAward, eligibilityMode },
|
||
|
|
eligibleProjects: Array<{
|
||
|
|
project: { id, title, teamName, competitionCategory },
|
||
|
|
eligibility: AwardEligibility,
|
||
|
|
|
||
|
|
// For SEPARATE_POOL mode
|
||
|
|
wasRemovedFromMain: boolean,
|
||
|
|
|
||
|
|
// For STAY_IN_MAIN mode
|
||
|
|
mainRoundStatus: ProjectRoundStateValue,
|
||
|
|
}>,
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## 13. Auth & User Router
|
||
|
|
|
||
|
|
**File:** `src/server/routers/user.ts`
|
||
|
|
|
||
|
|
### New Procedures
|
||
|
|
|
||
|
|
#### `listJuryMembers`
|
||
|
|
**Purpose:** Admin lists all jury members for jury group assignment
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Input schema
|
||
|
|
z.object({
|
||
|
|
programId: z.string().optional(),
|
||
|
|
onlyActive: z.boolean().default(true),
|
||
|
|
})
|
||
|
|
|
||
|
|
// Output
|
||
|
|
Array<{
|
||
|
|
...user,
|
||
|
|
juryGroupMemberships: Array<{
|
||
|
|
juryGroup: { id, name, competition: { id, name } },
|
||
|
|
isLead: boolean,
|
||
|
|
}>,
|
||
|
|
_count: { assignments, juryGroupMemberships },
|
||
|
|
}>
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Procedure Access Matrix
|
||
|
|
|
||
|
|
| Router | Procedure | Public | Applicant | Jury | Mentor | Observer | Admin | Super Admin |
|
||
|
|
|--------|-----------|--------|-----------|------|--------|----------|-------|-------------|
|
||
|
|
| **competition** | create | | | | | | ✓ | ✓ |
|
||
|
|
| | update | | | | | | ✓ | ✓ |
|
||
|
|
| | delete | | | | | | ✓ | ✓ |
|
||
|
|
| | get | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||
|
|
| | list | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||
|
|
| | publish | | | | | | ✓ | ✓ |
|
||
|
|
| | createStructure | | | | | | ✓ | ✓ |
|
||
|
|
| | updateStructure | | | | | | ✓ | ✓ |
|
||
|
|
| | getDraft | | | | | | ✓ | ✓ |
|
||
|
|
| | simulate | | | | | | ✓ | ✓ |
|
||
|
|
| | getRoundAnalytics | | | | | ✓ | ✓ | ✓ |
|
||
|
|
| **round** | get | | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ |
|
||
|
|
| | transition | | | | | | ✓ | ✓ |
|
||
|
|
| | openWindow | | | | | | ✓ | ✓ |
|
||
|
|
| | closeWindow | | | | | | ✓ | ✓ |
|
||
|
|
| | getProjectStates | | | | | ✓ | ✓ | ✓ |
|
||
|
|
| | getForJury | | | ✓ | | | | |
|
||
|
|
| | getApplicantTimeline | | ✓ | ✓ | ✓ | | ✓ | ✓ |
|
||
|
|
| | getRequirements | | ✓ | ✓ | ✓ | | ✓ | ✓ |
|
||
|
|
| **jury-group** | create | | | | | | ✓ | ✓ |
|
||
|
|
| | update | | | | | | ✓ | ✓ |
|
||
|
|
| | delete | | | | | | ✓ | ✓ |
|
||
|
|
| | get | | | ✓* | | ✓ | ✓ | ✓ |
|
||
|
|
| | list | | | ✓* | | ✓ | ✓ | ✓ |
|
||
|
|
| | addMember | | | | | | ✓ | ✓ |
|
||
|
|
| | updateMember | | | | | | ✓ | ✓ |
|
||
|
|
| | removeMember | | | | | | ✓ | ✓ |
|
||
|
|
| | bulkAddMembers | | | | | | ✓ | ✓ |
|
||
|
|
| | getOnboarding | | | ✓* | | | | |
|
||
|
|
| | submitOnboarding | | | ✓* | | | | |
|
||
|
|
| **submission-window** | create | | | | | | ✓ | ✓ |
|
||
|
|
| | update | | | | | | ✓ | ✓ |
|
||
|
|
| | delete | | | | | | ✓ | ✓ |
|
||
|
|
| | get | | ✓* | ✓* | ✓* | ✓ | ✓ | ✓ |
|
||
|
|
| | list | | ✓* | ✓* | ✓* | ✓ | ✓ | ✓ |
|
||
|
|
| | addFileRequirement | | | | | | ✓ | ✓ |
|
||
|
|
| | updateFileRequirement | | | | | | ✓ | ✓ |
|
||
|
|
| | deleteFileRequirement | | | | | | ✓ | ✓ |
|
||
|
|
| | setRoundVisibility | | | | | | ✓ | ✓ |
|
||
|
|
| **winner-confirmation** | createProposal | | | | | | ✓ | ✓ |
|
||
|
|
| | getProposal | | | ✓* | | ✓ | ✓ | ✓ |
|
||
|
|
| | listProposals | | | | | ✓ | ✓ | ✓ |
|
||
|
|
| | requestApproval | | | | | | ✓ | ✓ |
|
||
|
|
| | approve | | | ✓* | | | ✓* | ✓ |
|
||
|
|
| | getMyPendingApprovals | | | ✓* | | | ✓* | ✓ |
|
||
|
|
| | override | | | | | | ✓ | ✓ |
|
||
|
|
| | freeze | | | | | | ✓ | ✓ |
|
||
|
|
| | unfreeze | | | | | | | ✓ |
|
||
|
|
| **assignment** | create | | | | | | ✓ | ✓ |
|
||
|
|
| | bulkCreate | | | | | | ✓ | ✓ |
|
||
|
|
| | delete | | | | | | ✓ | ✓ |
|
||
|
|
| | listByRound | | | | | ✓ | ✓ | ✓ |
|
||
|
|
| | listByProject | | | | | ✓ | ✓ | ✓ |
|
||
|
|
| | myAssignments | | | ✓ | | | | |
|
||
|
|
| | get | | | ✓* | ✓* | | ✓ | ✓ |
|
||
|
|
| | getStats | | | | | ✓ | ✓ | ✓ |
|
||
|
|
| | getSuggestions | | | | | | ✓ | ✓ |
|
||
|
|
| | getAISuggestions | | | | | | ✓ | ✓ |
|
||
|
|
| | applySuggestions | | | | | | ✓ | ✓ |
|
||
|
|
| | getJuryGroupStats | | | | | ✓ | ✓ | ✓ |
|
||
|
|
| **evaluation** | get | | | ✓* | | | ✓ | ✓ |
|
||
|
|
| | start | | | ✓* | | | | |
|
||
|
|
| | autosave | | | ✓* | | | | |
|
||
|
|
| | submit | | | ✓* | | | | |
|
||
|
|
| | getProjectStats | | | | | ✓ | ✓ | ✓ |
|
||
|
|
| | listByRound | | | | | ✓ | ✓ | ✓ |
|
||
|
|
| | myPastEvaluations | | | ✓ | | | | |
|
||
|
|
| | declareCOI | | | ✓* | | | | |
|
||
|
|
| | getCOIStatus | | | ✓* | | | ✓ | ✓ |
|
||
|
|
| | listCOIByRound | | | | | | ✓ | ✓ |
|
||
|
|
| | reviewCOI | | | | | | ✓ | ✓ |
|
||
|
|
| | generateSummary | | | | | | ✓ | ✓ |
|
||
|
|
| | getSummary | | | | | ✓ | ✓ | ✓ |
|
||
|
|
| | getCrossRoundDocs | | | ✓* | | | | |
|
||
|
|
| **file** | getDownloadUrl | | ✓* | ✓* | ✓* | | ✓ | ✓ |
|
||
|
|
| | getUploadUrl | | | | | | ✓ | ✓ |
|
||
|
|
| | promoteFromMentor | | | | ✓* | | ✓ | ✓ |
|
||
|
|
| **applicant** | getSubmissionBySlug | ✓ | ✓ | | | | | |
|
||
|
|
| | getMySubmission | | ✓ | | | | | |
|
||
|
|
| | saveSubmission | | ✓ | | | | | |
|
||
|
|
| | getCompetitionView | | ✓* | | | | | |
|
||
|
|
| | submitToWindow | | ✓* | | | | | |
|
||
|
|
| **mentor-workspace** | getWorkspace | | ✓* | | ✓* | | ✓ | ✓ |
|
||
|
|
| | uploadFile | | ✓* | | ✓* | | | |
|
||
|
|
| | addComment | | ✓* | | ✓* | | | |
|
||
|
|
| | promoteFile | | | | ✓ | | ✓ | ✓ |
|
||
|
|
| **award** | create | | | | | | ✓ | ✓ |
|
||
|
|
| | update | | | | | | ✓ | ✓ |
|
||
|
|
| | delete | | | | | | ✓ | ✓ |
|
||
|
|
| | get | | ✓* | ✓* | ✓* | ✓ | ✓ | ✓ |
|
||
|
|
| | list | | ✓* | ✓* | ✓* | ✓ | ✓ | ✓ |
|
||
|
|
| | updateStatus | | | | | | ✓ | ✓ |
|
||
|
|
| | runEligibility | | | | | | ✓ | ✓ |
|
||
|
|
| | getEligibilityByMode | | | | | ✓ | ✓ | ✓ |
|
||
|
|
| **live-control** | getStageManager | | | | | | ✓ | ✓ |
|
||
|
|
| | setCursor | | | | | | ✓ | ✓ |
|
||
|
|
| | startVoting | | | | | | ✓ | ✓ |
|
||
|
|
| | closeVoting | | | | | | ✓ | ✓ |
|
||
|
|
| | castJuryVote | | | ✓* | | | | |
|
||
|
|
| | castAudienceVote | | ✓* | ✓* | ✓* | ✓* | ✓* | ✓* |
|
||
|
|
|
||
|
|
*Legend:*
|
||
|
|
- `✓*` = Conditional access (must be assigned, own project, be in jury group, etc.)
|
||
|
|
- `✓` = Unconditional access for that role
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Migration Checklist: tRPC Updates
|
||
|
|
|
||
|
|
### Phase 1: Rename Existing Routers
|
||
|
|
- [ ] Rename `pipeline.ts` → `competition.ts`
|
||
|
|
- [ ] Update all imports in UI components
|
||
|
|
- [ ] Update all `trpc.pipeline.*` calls → `trpc.competition.*`
|
||
|
|
|
||
|
|
### Phase 2: Split Stage Router
|
||
|
|
- [ ] Create `round.ts` for runtime operations
|
||
|
|
- [ ] Move procedures: `transition`, `openWindow`, `closeWindow`, `getProjectStates`, `getForJury`, `getApplicantTimeline`, `getRequirements`
|
||
|
|
- [ ] Update all `trpc.stage.*` calls to reference correct router
|
||
|
|
|
||
|
|
### Phase 3: Create New Routers
|
||
|
|
- [ ] Create `jury-group.ts`
|
||
|
|
- [ ] Create `winner-confirmation.ts`
|
||
|
|
- [ ] Create `submission-window.ts`
|
||
|
|
- [ ] Create `mentor-workspace.ts`
|
||
|
|
|
||
|
|
### Phase 4: Enhance Existing Routers
|
||
|
|
- [ ] Update `assignment.ts` with jury group awareness
|
||
|
|
- [ ] Update `evaluation.ts` with cross-round doc visibility
|
||
|
|
- [ ] Update `file.ts` with submission window association
|
||
|
|
- [ ] Update `applicant.ts` with multi-round support
|
||
|
|
- [ ] Rename `specialAward.ts` → `award.ts` and enhance
|
||
|
|
|
||
|
|
### Phase 5: Update App Router
|
||
|
|
- [ ] Update `src/server/routers/_app.ts` with new routers
|
||
|
|
- [ ] Export all renamed routers
|
||
|
|
- [ ] Run type generation: `npm run trpc:generate`
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
**Total Procedures:**
|
||
|
|
- **Current**: ~180
|
||
|
|
- **New**: ~60
|
||
|
|
- **Modified**: ~30
|
||
|
|
- **Removed**: ~20
|
||
|
|
- **Final**: ~250
|
||
|
|
|
||
|
|
This comprehensive reference ensures every API change is documented with complete type safety.
|