Competition/Round architecture: full platform rewrite (Phases 1-9)
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m45s
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m45s
Replace Pipeline/Stage system with Competition/Round architecture. New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy, ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow. New services: round-engine, round-assignment, deliberation, result-lock, submission-manager, competition-context, ai-prompt-guard. Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with structured prompts, retry logic, and injection detection. All legacy pipeline/stage code removed. 4 new migrations + seed aligned. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
330
src/types/competition-configs.ts
Normal file
330
src/types/competition-configs.ts
Normal file
@@ -0,0 +1,330 @@
|
||||
import { z } from 'zod'
|
||||
import type { RoundType, AwardEligibilityMode, AwardScoringMode, AwardStatus } from '@prisma/client'
|
||||
|
||||
// ============================================================================
|
||||
// Round-Type-Specific Zod Config Schemas
|
||||
// ============================================================================
|
||||
// Each RoundType has a dedicated config schema stored in Round.configJson.
|
||||
// These replace the loosely-typed pipeline-wizard.ts configs with
|
||||
// Zod-validated, compile-time-safe contracts.
|
||||
|
||||
// ─── 1. IntakeConfig ─────────────────────────────────────────────────────────
|
||||
|
||||
export const IntakeConfigSchema = z.object({
|
||||
allowDrafts: z.boolean().default(true),
|
||||
draftExpiryDays: z.number().int().positive().default(30),
|
||||
|
||||
acceptedCategories: z
|
||||
.array(z.enum(['STARTUP', 'BUSINESS_CONCEPT']))
|
||||
.default(['STARTUP', 'BUSINESS_CONCEPT']),
|
||||
|
||||
maxFileSizeMB: z.number().int().positive().default(50),
|
||||
maxFilesPerSlot: z.number().int().positive().default(1),
|
||||
allowedMimeTypes: z.array(z.string()).default(['application/pdf']),
|
||||
|
||||
lateSubmissionNotification: z.boolean().default(true),
|
||||
|
||||
publicFormEnabled: z.boolean().default(false),
|
||||
customFields: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
label: z.string(),
|
||||
type: z.enum(['text', 'textarea', 'select', 'checkbox', 'date']),
|
||||
required: z.boolean().default(false),
|
||||
options: z.array(z.string()).optional(),
|
||||
}),
|
||||
)
|
||||
.default([]),
|
||||
})
|
||||
|
||||
export type IntakeConfig = z.infer<typeof IntakeConfigSchema>
|
||||
|
||||
// ─── 2. FilteringConfig ──────────────────────────────────────────────────────
|
||||
|
||||
export const FilteringConfigSchema = z.object({
|
||||
rules: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
ruleType: z.enum(['FIELD_BASED', 'DOCUMENT_CHECK']),
|
||||
conditions: z.any(),
|
||||
action: z.enum(['PASS', 'REJECT', 'FLAG']),
|
||||
}),
|
||||
)
|
||||
.default([]),
|
||||
|
||||
aiScreeningEnabled: z.boolean().default(true),
|
||||
aiCriteriaText: z.string().optional(),
|
||||
aiConfidenceThresholds: z
|
||||
.object({
|
||||
high: z.number().min(0).max(1).default(0.85),
|
||||
medium: z.number().min(0).max(1).default(0.6),
|
||||
low: z.number().min(0).max(1).default(0.4),
|
||||
})
|
||||
.default({ high: 0.85, medium: 0.6, low: 0.4 }),
|
||||
|
||||
manualReviewEnabled: z.boolean().default(true),
|
||||
autoAdvanceEligible: z.boolean().default(false),
|
||||
duplicateDetectionEnabled: z.boolean().default(true),
|
||||
batchSize: z.number().int().positive().default(20),
|
||||
})
|
||||
|
||||
export type FilteringConfig = z.infer<typeof FilteringConfigSchema>
|
||||
|
||||
// ─── 3. EvaluationConfig ─────────────────────────────────────────────────────
|
||||
|
||||
export const EvaluationConfigSchema = z.object({
|
||||
requiredReviewsPerProject: z.number().int().positive().default(3),
|
||||
|
||||
scoringMode: z.enum(['criteria', 'global', 'binary']).default('criteria'),
|
||||
requireFeedback: z.boolean().default(true),
|
||||
feedbackMinLength: z.number().int().nonnegative().default(0),
|
||||
requireAllCriteriaScored: z.boolean().default(true),
|
||||
|
||||
coiRequired: z.boolean().default(true),
|
||||
|
||||
peerReviewEnabled: z.boolean().default(false),
|
||||
anonymizationLevel: z
|
||||
.enum(['fully_anonymous', 'show_initials', 'named'])
|
||||
.default('fully_anonymous'),
|
||||
|
||||
aiSummaryEnabled: z.boolean().default(false),
|
||||
generateAiShortlist: z.boolean().default(false),
|
||||
|
||||
advancementMode: z
|
||||
.enum(['auto_top_n', 'admin_selection', 'ai_recommended'])
|
||||
.default('admin_selection'),
|
||||
advancementConfig: z
|
||||
.object({
|
||||
perCategory: z.boolean().default(true),
|
||||
startupCount: z.number().int().nonnegative().default(10),
|
||||
conceptCount: z.number().int().nonnegative().default(10),
|
||||
tieBreaker: z
|
||||
.enum(['admin_decides', 'highest_individual', 'revote'])
|
||||
.default('admin_decides'),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
|
||||
export type EvaluationConfig = z.infer<typeof EvaluationConfigSchema>
|
||||
|
||||
// ─── 4. SubmissionConfig ─────────────────────────────────────────────────────
|
||||
|
||||
export const SubmissionConfigSchema = z.object({
|
||||
eligibleStatuses: z
|
||||
.array(
|
||||
z.enum([
|
||||
'PENDING',
|
||||
'IN_PROGRESS',
|
||||
'PASSED',
|
||||
'REJECTED',
|
||||
'COMPLETED',
|
||||
'WITHDRAWN',
|
||||
]),
|
||||
)
|
||||
.default(['PASSED']),
|
||||
|
||||
notifyEligibleTeams: z.boolean().default(true),
|
||||
lockPreviousWindows: z.boolean().default(true),
|
||||
})
|
||||
|
||||
export type SubmissionConfig = z.infer<typeof SubmissionConfigSchema>
|
||||
|
||||
// ─── 5. MentoringConfig ──────────────────────────────────────────────────────
|
||||
|
||||
export const MentoringConfigSchema = z.object({
|
||||
eligibility: z
|
||||
.enum(['all_advancing', 'requested_only', 'admin_selected'])
|
||||
.default('requested_only'),
|
||||
|
||||
chatEnabled: z.boolean().default(true),
|
||||
fileUploadEnabled: z.boolean().default(true),
|
||||
fileCommentsEnabled: z.boolean().default(true),
|
||||
filePromotionEnabled: z.boolean().default(true),
|
||||
|
||||
promotionTargetWindowId: z.string().optional(),
|
||||
autoAssignMentors: z.boolean().default(false),
|
||||
})
|
||||
|
||||
export type MentoringConfig = z.infer<typeof MentoringConfigSchema>
|
||||
|
||||
// ─── 6. LiveFinalConfig ──────────────────────────────────────────────────────
|
||||
|
||||
export const LiveFinalConfigSchema = z.object({
|
||||
juryVotingEnabled: z.boolean().default(true),
|
||||
votingMode: z.enum(['simple', 'criteria']).default('simple'),
|
||||
|
||||
audienceVotingEnabled: z.boolean().default(false),
|
||||
audienceVoteWeight: z.number().min(0).max(1).default(0),
|
||||
audienceVotingMode: z
|
||||
.enum(['per_project', 'per_category', 'favorites'])
|
||||
.default('per_project'),
|
||||
audienceMaxFavorites: z.number().int().positive().default(3),
|
||||
audienceRequireIdentification: z.boolean().default(false),
|
||||
audienceRevealTiming: z
|
||||
.enum(['real_time', 'after_jury_scores', 'at_deliberation', 'never'])
|
||||
.default('at_deliberation'),
|
||||
|
||||
deliberationEnabled: z.boolean().default(false),
|
||||
deliberationDurationMinutes: z.number().int().positive().default(30),
|
||||
showAudienceVotesToJury: z.boolean().default(false),
|
||||
|
||||
presentationOrderMode: z
|
||||
.enum(['manual', 'random', 'score_based'])
|
||||
.default('manual'),
|
||||
presentationDurationMinutes: z.number().int().positive().default(15),
|
||||
qaDurationMinutes: z.number().int().nonnegative().default(5),
|
||||
|
||||
revealPolicy: z.enum(['immediate', 'delayed', 'ceremony']).default('ceremony'),
|
||||
})
|
||||
|
||||
export type LiveFinalConfig = z.infer<typeof LiveFinalConfigSchema>
|
||||
|
||||
// ─── 7. DeliberationConfig ───────────────────────────────────────────────────
|
||||
|
||||
export const DeliberationConfigSchema = z.object({
|
||||
juryGroupId: z.string(),
|
||||
|
||||
mode: z
|
||||
.enum(['SINGLE_WINNER_VOTE', 'FULL_RANKING'])
|
||||
.default('SINGLE_WINNER_VOTE'),
|
||||
|
||||
showCollectiveRankings: z.boolean().default(false),
|
||||
showPriorJuryData: z.boolean().default(false),
|
||||
|
||||
tieBreakMethod: z
|
||||
.enum(['RUNOFF', 'ADMIN_DECIDES', 'SCORE_FALLBACK'])
|
||||
.default('ADMIN_DECIDES'),
|
||||
|
||||
votingDuration: z.number().int().positive().default(60),
|
||||
topN: z.number().int().positive().default(3),
|
||||
allowAdminOverride: z.boolean().default(true),
|
||||
})
|
||||
|
||||
export type DeliberationConfig = z.infer<typeof DeliberationConfigSchema>
|
||||
|
||||
// ============================================================================
|
||||
// Config Map & Helpers
|
||||
// ============================================================================
|
||||
|
||||
export type RoundConfigMap = {
|
||||
INTAKE: IntakeConfig
|
||||
FILTERING: FilteringConfig
|
||||
EVALUATION: EvaluationConfig
|
||||
SUBMISSION: SubmissionConfig
|
||||
MENTORING: MentoringConfig
|
||||
LIVE_FINAL: LiveFinalConfig
|
||||
DELIBERATION: DeliberationConfig
|
||||
}
|
||||
|
||||
const schemaMap: Record<RoundType, z.ZodSchema> = {
|
||||
INTAKE: IntakeConfigSchema,
|
||||
FILTERING: FilteringConfigSchema,
|
||||
EVALUATION: EvaluationConfigSchema,
|
||||
SUBMISSION: SubmissionConfigSchema,
|
||||
MENTORING: MentoringConfigSchema,
|
||||
LIVE_FINAL: LiveFinalConfigSchema,
|
||||
DELIBERATION: DeliberationConfigSchema,
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a configJson object against the Zod schema for the given RoundType.
|
||||
* Returns the parsed (with defaults applied) config or throws a ZodError.
|
||||
*/
|
||||
export function validateRoundConfig<T extends RoundType>(
|
||||
roundType: T,
|
||||
config: unknown,
|
||||
): RoundConfigMap[T] {
|
||||
const schema = schemaMap[roundType]
|
||||
return schema.parse(config) as RoundConfigMap[T]
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns safe-parsed result (no throw) for config validation.
|
||||
*/
|
||||
export function safeValidateRoundConfig<T extends RoundType>(
|
||||
roundType: T,
|
||||
config: unknown,
|
||||
): z.SafeParseReturnType<unknown, RoundConfigMap[T]> {
|
||||
const schema = schemaMap[roundType]
|
||||
return schema.safeParse(config) as z.SafeParseReturnType<
|
||||
unknown,
|
||||
RoundConfigMap[T]
|
||||
>
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns default config for the given RoundType with all defaults applied.
|
||||
* For DELIBERATION, a placeholder juryGroupId is used — caller must replace.
|
||||
*/
|
||||
export function defaultRoundConfig<T extends RoundType>(
|
||||
roundType: T,
|
||||
): RoundConfigMap[T] {
|
||||
const defaults: Record<RoundType, () => unknown> = {
|
||||
INTAKE: () => IntakeConfigSchema.parse({}),
|
||||
FILTERING: () => FilteringConfigSchema.parse({}),
|
||||
EVALUATION: () => EvaluationConfigSchema.parse({}),
|
||||
SUBMISSION: () => SubmissionConfigSchema.parse({}),
|
||||
MENTORING: () => MentoringConfigSchema.parse({}),
|
||||
LIVE_FINAL: () => LiveFinalConfigSchema.parse({}),
|
||||
DELIBERATION: () =>
|
||||
DeliberationConfigSchema.parse({ juryGroupId: 'PLACEHOLDER' }),
|
||||
}
|
||||
return defaults[roundType]() as RoundConfigMap[T]
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Award Config Types
|
||||
// ============================================================================
|
||||
// Typed interfaces matching Prisma enums for award configuration.
|
||||
|
||||
export const AwardEligibilityModeSchema = z.enum(['SEPARATE_POOL', 'STAY_IN_MAIN'])
|
||||
export const AwardScoringModeSchema = z.enum(['PICK_WINNER', 'RANKED', 'SCORED'])
|
||||
export const AwardStatusSchema = z.enum([
|
||||
'DRAFT',
|
||||
'NOMINATIONS_OPEN',
|
||||
'VOTING_OPEN',
|
||||
'VOTING_CLOSED',
|
||||
'DECIDED',
|
||||
'ANNOUNCED',
|
||||
])
|
||||
export const AwardDecisionModeSchema = z.enum([
|
||||
'JURY_VOTE',
|
||||
'AWARD_MASTER_DECISION',
|
||||
'ADMIN_DECISION',
|
||||
])
|
||||
|
||||
export const AwardConfigSchema = z.object({
|
||||
/** How eligible projects relate to the main competition track */
|
||||
eligibilityMode: AwardEligibilityModeSchema.default('STAY_IN_MAIN'),
|
||||
/** How the winner is determined */
|
||||
scoringMode: AwardScoringModeSchema.default('PICK_WINNER'),
|
||||
/** Who makes the final decision */
|
||||
decisionMode: AwardDecisionModeSchema.default('JURY_VOTE'),
|
||||
/** Max ranked picks for RANKED scoring mode */
|
||||
maxRankedPicks: z.number().int().positive().optional(),
|
||||
/** Whether AI evaluates eligibility */
|
||||
useAiEligibility: z.boolean().default(true),
|
||||
/** Plain-language criteria for AI interpretation */
|
||||
criteriaText: z.string().optional(),
|
||||
/** Structured auto-tag rules for deterministic eligibility */
|
||||
autoTagRules: z
|
||||
.array(
|
||||
z.object({
|
||||
field: z.enum([
|
||||
'competitionCategory',
|
||||
'country',
|
||||
'geographicZone',
|
||||
'tags',
|
||||
'oceanIssue',
|
||||
]),
|
||||
operator: z.enum(['equals', 'contains', 'in']),
|
||||
value: z.union([z.string(), z.array(z.string())]),
|
||||
}),
|
||||
)
|
||||
.default([]),
|
||||
})
|
||||
|
||||
export type AwardConfig = z.infer<typeof AwardConfigSchema>
|
||||
171
src/types/competition.ts
Normal file
171
src/types/competition.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import type {
|
||||
Competition,
|
||||
Round,
|
||||
JuryGroup,
|
||||
JuryGroupMember,
|
||||
SubmissionWindow,
|
||||
SubmissionFileRequirement,
|
||||
AdvancementRule,
|
||||
RoundSubmissionVisibility,
|
||||
ProjectRoundState,
|
||||
DeliberationSession,
|
||||
ResultLock,
|
||||
RoundType,
|
||||
CompetitionStatus,
|
||||
RoundStatus,
|
||||
CapMode,
|
||||
JuryGroupMemberRole,
|
||||
} from '@prisma/client'
|
||||
import type { RoundConfigMap } from './competition-configs'
|
||||
|
||||
// ============================================================================
|
||||
// Composite Types (models with nested relations)
|
||||
// ============================================================================
|
||||
|
||||
export type CompetitionWithRounds = Competition & {
|
||||
rounds: RoundSummary[]
|
||||
juryGroups: JuryGroupSummary[]
|
||||
submissionWindows: SubmissionWindowSummary[]
|
||||
}
|
||||
|
||||
export type RoundSummary = Pick<
|
||||
Round,
|
||||
'id' | 'name' | 'slug' | 'roundType' | 'status' | 'sortOrder' | 'windowOpenAt' | 'windowCloseAt'
|
||||
>
|
||||
|
||||
export type RoundWithRelations = Round & {
|
||||
juryGroup: (JuryGroup & { members: JuryGroupMember[] }) | null
|
||||
submissionWindow: (SubmissionWindow & { fileRequirements: SubmissionFileRequirement[] }) | null
|
||||
advancementRules: AdvancementRule[]
|
||||
visibleSubmissionWindows: (RoundSubmissionVisibility & { submissionWindow: SubmissionWindow })[]
|
||||
_count?: { projectRoundStates: number; assignments: number }
|
||||
}
|
||||
|
||||
export type JuryGroupSummary = Pick<
|
||||
JuryGroup,
|
||||
'id' | 'name' | 'slug' | 'sortOrder' | 'defaultMaxAssignments' | 'defaultCapMode'
|
||||
> & {
|
||||
_count: { members: number }
|
||||
}
|
||||
|
||||
export type JuryGroupWithMembers = JuryGroup & {
|
||||
members: (JuryGroupMember & {
|
||||
user: { id: string; name: string | null; email: string; role: string }
|
||||
})[]
|
||||
}
|
||||
|
||||
export type SubmissionWindowSummary = Pick<
|
||||
SubmissionWindow,
|
||||
'id' | 'name' | 'slug' | 'roundNumber' | 'windowOpenAt' | 'windowCloseAt' | 'isLocked'
|
||||
> & {
|
||||
_count: { fileRequirements: number; projectFiles: number }
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Policy Resolution
|
||||
// ============================================================================
|
||||
|
||||
export type PolicySource =
|
||||
| 'system'
|
||||
| 'program'
|
||||
| 'jury_group'
|
||||
| 'member'
|
||||
| 'admin_override'
|
||||
|
||||
export type PolicyResolution<T> = {
|
||||
value: T
|
||||
source: PolicySource
|
||||
explanation: string
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// tRPC Input Types
|
||||
// ============================================================================
|
||||
|
||||
export type CreateCompetitionInput = {
|
||||
programId: string
|
||||
name: string
|
||||
slug: string
|
||||
categoryMode?: string
|
||||
startupFinalistCount?: number
|
||||
conceptFinalistCount?: number
|
||||
notifyOnRoundAdvance?: boolean
|
||||
notifyOnDeadlineApproach?: boolean
|
||||
deadlineReminderDays?: number[]
|
||||
}
|
||||
|
||||
export type UpdateCompetitionInput = {
|
||||
id: string
|
||||
name?: string
|
||||
slug?: string
|
||||
status?: CompetitionStatus
|
||||
categoryMode?: string
|
||||
startupFinalistCount?: number
|
||||
conceptFinalistCount?: number
|
||||
notifyOnRoundAdvance?: boolean
|
||||
notifyOnDeadlineApproach?: boolean
|
||||
deadlineReminderDays?: number[]
|
||||
}
|
||||
|
||||
export type CreateRoundInput = {
|
||||
competitionId: string
|
||||
name: string
|
||||
slug: string
|
||||
roundType: RoundType
|
||||
sortOrder: number
|
||||
configJson?: Record<string, unknown>
|
||||
windowOpenAt?: Date | null
|
||||
windowCloseAt?: Date | null
|
||||
juryGroupId?: string | null
|
||||
submissionWindowId?: string | null
|
||||
purposeKey?: string | null
|
||||
}
|
||||
|
||||
export type UpdateRoundInput = {
|
||||
id: string
|
||||
name?: string
|
||||
slug?: string
|
||||
status?: RoundStatus
|
||||
configJson?: Record<string, unknown>
|
||||
windowOpenAt?: Date | null
|
||||
windowCloseAt?: Date | null
|
||||
juryGroupId?: string | null
|
||||
submissionWindowId?: string | null
|
||||
purposeKey?: string | null
|
||||
}
|
||||
|
||||
export type CreateJuryGroupInput = {
|
||||
competitionId: string
|
||||
name: string
|
||||
slug: string
|
||||
description?: string
|
||||
sortOrder?: number
|
||||
defaultMaxAssignments?: number
|
||||
defaultCapMode?: CapMode
|
||||
softCapBuffer?: number
|
||||
categoryQuotasEnabled?: boolean
|
||||
defaultCategoryQuotas?: Record<string, { min: number; max: number }>
|
||||
allowJurorCapAdjustment?: boolean
|
||||
allowJurorRatioAdjustment?: boolean
|
||||
}
|
||||
|
||||
export type AddJuryGroupMemberInput = {
|
||||
juryGroupId: string
|
||||
userId: string
|
||||
role?: JuryGroupMemberRole
|
||||
maxAssignmentsOverride?: number | null
|
||||
capModeOverride?: CapMode | null
|
||||
categoryQuotasOverride?: Record<string, { min: number; max: number }> | null
|
||||
preferredStartupRatio?: number | null
|
||||
availabilityNotes?: string | null
|
||||
}
|
||||
|
||||
export type UpdateJuryGroupMemberInput = {
|
||||
id: string
|
||||
role?: JuryGroupMemberRole
|
||||
maxAssignmentsOverride?: number | null
|
||||
capModeOverride?: CapMode | null
|
||||
categoryQuotasOverride?: Record<string, { min: number; max: number }> | null
|
||||
preferredStartupRatio?: number | null
|
||||
availabilityNotes?: string | null
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
import type { StageType, TrackKind, RoutingMode, DecisionMode, AwardScoringMode } from '@prisma/client'
|
||||
|
||||
// ============================================================================
|
||||
// Stage Config Discriminated Unions
|
||||
// ============================================================================
|
||||
|
||||
export type IntakeConfig = {
|
||||
submissionWindowEnabled: boolean
|
||||
lateSubmissionPolicy: 'reject' | 'flag' | 'accept'
|
||||
lateGraceHours: number
|
||||
fileRequirements: FileRequirementConfig[]
|
||||
}
|
||||
|
||||
export type FileRequirementConfig = {
|
||||
name: string
|
||||
description?: string
|
||||
acceptedMimeTypes: string[]
|
||||
maxSizeMB?: number
|
||||
isRequired: boolean
|
||||
}
|
||||
|
||||
export type FilterConfig = {
|
||||
rules: FilterRuleConfig[]
|
||||
aiRubricEnabled: boolean
|
||||
aiCriteriaText: string
|
||||
aiConfidenceThresholds: {
|
||||
high: number
|
||||
medium: number
|
||||
low: number
|
||||
}
|
||||
manualQueueEnabled: boolean
|
||||
}
|
||||
|
||||
export type FilterRuleConfig = {
|
||||
field: string
|
||||
operator: string
|
||||
value: string | number | boolean
|
||||
weight: number
|
||||
}
|
||||
|
||||
export type EvaluationConfig = {
|
||||
requiredReviews: number
|
||||
maxLoadPerJuror: number
|
||||
minLoadPerJuror: number
|
||||
availabilityWeighting: boolean
|
||||
overflowPolicy: 'queue' | 'expand_pool' | 'reduce_reviews'
|
||||
categoryQuotasEnabled?: boolean
|
||||
categoryQuotas?: Record<string, { min: number; max: number }>
|
||||
}
|
||||
|
||||
export type SelectionConfig = {
|
||||
finalistCount?: number
|
||||
rankingMethod: 'score_average' | 'weighted_criteria' | 'binary_pass'
|
||||
tieBreaker: 'admin_decides' | 'highest_individual' | 'revote'
|
||||
categoryQuotasEnabled?: boolean
|
||||
categoryQuotas?: Record<string, number>
|
||||
}
|
||||
|
||||
export type LiveFinalConfig = {
|
||||
juryVotingEnabled: boolean
|
||||
audienceVotingEnabled: boolean
|
||||
audienceVoteWeight: number
|
||||
cohortSetupMode: 'auto' | 'manual'
|
||||
revealPolicy: 'immediate' | 'delayed' | 'ceremony'
|
||||
}
|
||||
|
||||
export type ResultsConfig = {
|
||||
publicationMode: 'manual' | 'auto_on_close'
|
||||
showDetailedScores: boolean
|
||||
showRankings: boolean
|
||||
}
|
||||
|
||||
export type StageConfigMap = {
|
||||
INTAKE: IntakeConfig
|
||||
FILTER: FilterConfig
|
||||
EVALUATION: EvaluationConfig
|
||||
SELECTION: SelectionConfig
|
||||
LIVE_FINAL: LiveFinalConfig
|
||||
RESULTS: ResultsConfig
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Wizard Stage / Track / State Types
|
||||
// ============================================================================
|
||||
|
||||
export type WizardStageConfig = {
|
||||
id?: string
|
||||
name: string
|
||||
slug: string
|
||||
stageType: StageType
|
||||
sortOrder: number
|
||||
configJson: Record<string, unknown>
|
||||
windowOpenAt?: Date | null
|
||||
windowCloseAt?: Date | null
|
||||
}
|
||||
|
||||
export type WizardTrackConfig = {
|
||||
id?: string
|
||||
name: string
|
||||
slug: string
|
||||
kind: TrackKind
|
||||
sortOrder: number
|
||||
routingModeDefault?: RoutingMode
|
||||
decisionMode?: DecisionMode
|
||||
stages: WizardStageConfig[]
|
||||
awardConfig?: {
|
||||
name: string
|
||||
description?: string
|
||||
scoringMode?: AwardScoringMode
|
||||
}
|
||||
}
|
||||
|
||||
export type WizardState = {
|
||||
name: string
|
||||
slug: string
|
||||
programId: string
|
||||
settingsJson: Record<string, unknown>
|
||||
tracks: WizardTrackConfig[]
|
||||
notificationConfig: Record<string, boolean>
|
||||
overridePolicy: Record<string, unknown>
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Validation Result
|
||||
// ============================================================================
|
||||
|
||||
export type ValidationResult = {
|
||||
valid: boolean
|
||||
errors: string[]
|
||||
warnings: string[]
|
||||
}
|
||||
|
||||
export type SectionValidation = {
|
||||
basics: ValidationResult
|
||||
tracks: ValidationResult
|
||||
notifications: ValidationResult
|
||||
}
|
||||
Reference in New Issue
Block a user