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:
@@ -1,137 +1,126 @@
|
||||
import {
|
||||
type WizardConfig,
|
||||
type WizardStep,
|
||||
type WizardFieldConfig,
|
||||
type WizardStepId,
|
||||
DEFAULT_WIZARD_CONFIG,
|
||||
wizardConfigSchema,
|
||||
} from '@/types/wizard-config'
|
||||
import type { WizardConfig, WizardStepId, CustomField, WizardFieldConfig } from '@/types/wizard-config'
|
||||
|
||||
/**
|
||||
* Parse wizard config from Program.settingsJson with fallback to defaults.
|
||||
* Used by both backend (application router) and frontend (apply pages).
|
||||
* Check if a field is visible based on the wizard configuration
|
||||
*/
|
||||
export function parseWizardConfig(settingsJson: unknown): WizardConfig {
|
||||
if (!settingsJson || typeof settingsJson !== 'object') {
|
||||
return DEFAULT_WIZARD_CONFIG
|
||||
}
|
||||
const settings = settingsJson as Record<string, unknown>
|
||||
if (!settings.wizardConfig) {
|
||||
return DEFAULT_WIZARD_CONFIG
|
||||
}
|
||||
try {
|
||||
const parsed = wizardConfigSchema.parse(settings.wizardConfig)
|
||||
return mergeWizardConfig(parsed)
|
||||
} catch {
|
||||
console.error('[WizardConfig] Invalid config, using defaults')
|
||||
return DEFAULT_WIZARD_CONFIG
|
||||
}
|
||||
export function isFieldVisible(config: WizardConfig, fieldName: string): boolean {
|
||||
const fieldConfig = config.fields?.[fieldName]
|
||||
if (!fieldConfig) return true // Default visible if not configured
|
||||
return fieldConfig.visible !== false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enabled steps sorted by order.
|
||||
* Check if a field is required based on the wizard configuration
|
||||
*/
|
||||
export function getActiveSteps(config: WizardConfig): WizardStep[] {
|
||||
return config.steps.filter((step) => step.enabled).sort((a, b) => a.order - b.order)
|
||||
export function isFieldRequired(config: WizardConfig, fieldName: string): boolean {
|
||||
const fieldConfig = config.fields?.[fieldName]
|
||||
if (!fieldConfig) return false // Default not required if not configured
|
||||
return fieldConfig.required === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate conditional step visibility based on current form values.
|
||||
* Returns only steps whose conditions are met (or have no condition).
|
||||
* Get field configuration for a specific field
|
||||
*/
|
||||
export function getVisibleSteps(
|
||||
config: WizardConfig,
|
||||
formValues: Record<string, unknown>
|
||||
): WizardStep[] {
|
||||
return getActiveSteps(config).filter((step) => {
|
||||
if (!step.conditionalOn) return true
|
||||
const { field, operator, value } = step.conditionalOn
|
||||
const fieldValue = formValues[field]
|
||||
switch (operator) {
|
||||
case 'equals':
|
||||
return fieldValue === value
|
||||
case 'notEquals':
|
||||
return fieldValue !== value
|
||||
case 'in':
|
||||
return Array.isArray(value) && value.includes(String(fieldValue))
|
||||
case 'notIn':
|
||||
return Array.isArray(value) && !value.includes(String(fieldValue))
|
||||
default:
|
||||
return true
|
||||
export function getFieldConfig(config: WizardConfig, fieldName: string): WizardFieldConfig | undefined {
|
||||
return config.fields?.[fieldName]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get visible steps based on configuration and form values
|
||||
*/
|
||||
export function getVisibleSteps(config: WizardConfig, formValues: Record<string, unknown>) {
|
||||
const steps = config.steps || []
|
||||
|
||||
return steps
|
||||
.filter((step) => step.enabled !== false)
|
||||
.filter((step) => {
|
||||
// Check conditional visibility
|
||||
if (!step.conditionalOn) return true
|
||||
|
||||
const { field, operator, value } = step.conditionalOn
|
||||
const fieldValue = formValues[field]
|
||||
|
||||
switch (operator) {
|
||||
case 'equals':
|
||||
return fieldValue === value
|
||||
case 'notEquals':
|
||||
return fieldValue !== value
|
||||
case 'in':
|
||||
return Array.isArray(value) && value.includes(fieldValue as string)
|
||||
case 'notIn':
|
||||
return Array.isArray(value) && !value.includes(fieldValue as string)
|
||||
default:
|
||||
return true
|
||||
}
|
||||
})
|
||||
.sort((a, b) => a.order - b.order)
|
||||
}
|
||||
|
||||
/**
|
||||
* Build steps array with field mappings for validation
|
||||
*/
|
||||
export function buildStepsArray(config: WizardConfig) {
|
||||
const baseSteps = [
|
||||
{ id: 'welcome', title: 'Category', fields: ['competitionCategory'] },
|
||||
{ id: 'contact', title: 'Contact', fields: ['contactName', 'contactEmail', 'contactPhone', 'country', 'city'] },
|
||||
{ id: 'project', title: 'Project', fields: ['projectName', 'teamName', 'description', 'oceanIssue'] },
|
||||
{ id: 'team', title: 'Team', fields: ['teamMembers'] },
|
||||
{ id: 'additional', title: 'Details', fields: ['institution', 'startupCreatedDate', 'wantsMentorship', 'referralSource'] },
|
||||
{ id: 'review', title: 'Review', fields: ['gdprConsent'] },
|
||||
]
|
||||
|
||||
// Apply config overrides
|
||||
return baseSteps.map((step) => {
|
||||
const configStep = config.steps?.find((s) => s.id === step.id)
|
||||
return {
|
||||
...step,
|
||||
title: configStep?.title || step.title,
|
||||
enabled: configStep?.enabled !== false,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get field configuration with sensible defaults.
|
||||
* Get custom fields for a specific step
|
||||
*/
|
||||
export function getFieldConfig(config: WizardConfig, fieldName: string): WizardFieldConfig {
|
||||
return config.fields[fieldName] ?? { required: true, visible: true }
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific field should be visible based on config.
|
||||
*/
|
||||
export function isFieldVisible(config: WizardConfig, fieldName: string): boolean {
|
||||
const fieldConfig = config.fields[fieldName]
|
||||
return fieldConfig?.visible !== false
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a specific field is required based on config.
|
||||
*/
|
||||
export function isFieldRequired(config: WizardConfig, fieldName: string): boolean {
|
||||
const fieldConfig = config.fields[fieldName]
|
||||
return fieldConfig?.required !== false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get custom fields assigned to a specific step, sorted by order.
|
||||
*/
|
||||
export function getCustomFieldsForStep(
|
||||
config: WizardConfig,
|
||||
stepId: WizardStepId
|
||||
): NonNullable<WizardConfig['customFields']> {
|
||||
return (config.customFields ?? [])
|
||||
.filter((f) => f.stepId === stepId)
|
||||
export function getCustomFieldsForStep(config: WizardConfig, stepId: WizardStepId): CustomField[] {
|
||||
const customFields = config.customFields || []
|
||||
return customFields
|
||||
.filter((field) => field.stepId === stepId)
|
||||
.sort((a, b) => a.order - b.order)
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge partial config with defaults. Ensures all arrays/objects exist.
|
||||
* Parse wizard config from JSON (settingsJson) and merge with defaults
|
||||
*/
|
||||
export function mergeWizardConfig(partial: Partial<WizardConfig>): WizardConfig {
|
||||
export function parseWizardConfig(settingsJson: unknown): WizardConfig {
|
||||
const DEFAULT_WIZARD_CONFIG: WizardConfig = {
|
||||
steps: [
|
||||
{ id: 'welcome', enabled: true, order: 0, title: 'Category' },
|
||||
{ id: 'contact', enabled: true, order: 1, title: 'Contact' },
|
||||
{ id: 'project', enabled: true, order: 2, title: 'Project' },
|
||||
{ id: 'team', enabled: true, order: 3, title: 'Team' },
|
||||
{ id: 'additional', enabled: true, order: 4, title: 'Details' },
|
||||
{ id: 'review', enabled: true, order: 5, title: 'Review' },
|
||||
],
|
||||
fields: {},
|
||||
competitionCategories: [],
|
||||
oceanIssues: [],
|
||||
features: {
|
||||
enableWhatsApp: false,
|
||||
enableMentorship: true,
|
||||
enableTeamMembers: true,
|
||||
requireInstitution: false,
|
||||
},
|
||||
customFields: [],
|
||||
}
|
||||
|
||||
if (!settingsJson || typeof settingsJson !== 'object') {
|
||||
return DEFAULT_WIZARD_CONFIG
|
||||
}
|
||||
|
||||
return {
|
||||
steps: partial.steps?.length ? partial.steps : DEFAULT_WIZARD_CONFIG.steps,
|
||||
fields: partial.fields ?? DEFAULT_WIZARD_CONFIG.fields,
|
||||
competitionCategories:
|
||||
partial.competitionCategories ?? DEFAULT_WIZARD_CONFIG.competitionCategories,
|
||||
oceanIssues: partial.oceanIssues ?? DEFAULT_WIZARD_CONFIG.oceanIssues,
|
||||
features: { ...DEFAULT_WIZARD_CONFIG.features, ...partial.features },
|
||||
welcomeMessage: partial.welcomeMessage,
|
||||
customFields: partial.customFields ?? [],
|
||||
...DEFAULT_WIZARD_CONFIG,
|
||||
...(settingsJson as Partial<WizardConfig>),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the STEPS array for the wizard from config (format used by apply pages).
|
||||
* Maps step IDs to their validation fields for per-step validation.
|
||||
*/
|
||||
export function buildStepsArray(
|
||||
config: WizardConfig
|
||||
): Array<{ id: string; title: string; fields: string[] }> {
|
||||
const STEP_FIELDS_MAP: Record<string, string[]> = {
|
||||
welcome: ['competitionCategory'],
|
||||
contact: ['contactName', 'contactEmail', 'contactPhone', 'country'],
|
||||
project: ['projectName', 'description', 'oceanIssue'],
|
||||
team: [],
|
||||
additional: [],
|
||||
review: ['gdprConsent'],
|
||||
}
|
||||
|
||||
return getActiveSteps(config).map((step) => ({
|
||||
id: step.id,
|
||||
title: step.title ?? step.id.charAt(0).toUpperCase() + step.id.slice(1),
|
||||
fields: (STEP_FIELDS_MAP[step.id] ?? []).filter((f) => isFieldVisible(config, f)),
|
||||
}))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user