import { type WizardConfig, type WizardStep, type WizardFieldConfig, type WizardStepId, DEFAULT_WIZARD_CONFIG, wizardConfigSchema, } from '@/types/wizard-config' /** * Parse wizard config from Program.settingsJson with fallback to defaults. * Used by both backend (application router) and frontend (apply pages). */ export function parseWizardConfig(settingsJson: unknown): WizardConfig { if (!settingsJson || typeof settingsJson !== 'object') { return DEFAULT_WIZARD_CONFIG } const settings = settingsJson as Record 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 } } /** * Get enabled steps sorted by order. */ export function getActiveSteps(config: WizardConfig): WizardStep[] { return config.steps.filter((step) => step.enabled).sort((a, b) => a.order - b.order) } /** * Evaluate conditional step visibility based on current form values. * Returns only steps whose conditions are met (or have no condition). */ export function getVisibleSteps( config: WizardConfig, formValues: Record ): 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 } }) } /** * Get field configuration with sensible defaults. */ 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 { return (config.customFields ?? []) .filter((f) => f.stepId === stepId) .sort((a, b) => a.order - b.order) } /** * Merge partial config with defaults. Ensures all arrays/objects exist. */ export function mergeWizardConfig(partial: Partial): WizardConfig { 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 ?? [], } } /** * 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 = { 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)), })) }