Add dynamic apply wizard customization with admin settings UI

- Create wizard config types, utilities, and defaults (wizard-config.ts)
- Add admin apply settings page with drag-and-drop step ordering, dropdown
  option management, feature toggles, welcome message customization, and
  custom field builder with select/multiselect options editor
- Build dynamic apply wizard component with animated step transitions,
  mobile-first responsive design, and config-driven form validation
- Update step components to accept dynamic config (categories, ocean issues,
  field visibility, feature flags)
- Replace hardcoded enum validation with string-based validation for
  admin-configurable dropdown values, with safe enum casting at storage layer
- Add wizard template system (model, router, admin UI) with built-in
  MOPC Classic preset
- Add program wizard config CRUD procedures to program router
- Update application router getConfig to return wizardConfig, submit handler
  to store custom field data in metadataJson
- Add edition-based apply page, project pool page, and supporting routers
- Fix CSS (invalid sm:fixed-none), Enter key handler (skip textarea),
  safe area insets for notched phones, buildStepsArray field visibility

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-08 13:18:20 +01:00
parent 98fe658c33
commit e7c86a7b1b
40 changed files with 4477 additions and 1045 deletions

View File

@@ -14,35 +14,22 @@ import {
SelectValue,
} from '@/components/ui/select'
import type { ApplicationFormData } from '@/server/routers/application'
import { OceanIssue } from '@prisma/client'
interface OceanIssueOption {
value: OceanIssue
label: string
}
const oceanIssueOptions: OceanIssueOption[] = [
{ value: 'POLLUTION_REDUCTION', label: 'Reduction of pollution (plastics, chemicals, noise, light,...)' },
{ value: 'CLIMATE_MITIGATION', label: 'Mitigation of climate change and sea-level rise' },
{ value: 'TECHNOLOGY_INNOVATION', label: 'Technology & innovations' },
{ value: 'SUSTAINABLE_SHIPPING', label: 'Sustainable shipping & yachting' },
{ value: 'BLUE_CARBON', label: 'Blue carbon' },
{ value: 'HABITAT_RESTORATION', label: 'Restoration of marine habitats & ecosystems' },
{ value: 'COMMUNITY_CAPACITY', label: 'Capacity building for coastal communities' },
{ value: 'SUSTAINABLE_FISHING', label: 'Sustainable fishing and aquaculture & blue food' },
{ value: 'CONSUMER_AWARENESS', label: 'Consumer awareness and education' },
{ value: 'OCEAN_ACIDIFICATION', label: 'Mitigation of ocean acidification' },
{ value: 'OTHER', label: 'Other' },
]
import { type DropdownOption, type WizardConfig, DEFAULT_OCEAN_ISSUES } from '@/types/wizard-config'
import { isFieldVisible, getFieldConfig } from '@/lib/wizard-config'
interface StepProjectProps {
form: UseFormReturn<ApplicationFormData>
oceanIssues?: DropdownOption[]
config?: WizardConfig
}
export function StepProject({ form }: StepProjectProps) {
export function StepProject({ form, oceanIssues, config }: StepProjectProps) {
const issueOptions = oceanIssues ?? DEFAULT_OCEAN_ISSUES
const { register, formState: { errors }, setValue, watch } = form
const oceanIssue = watch('oceanIssue')
const description = watch('description') || ''
const showTeamName = !config || isFieldVisible(config, 'teamName')
const descriptionLabel = config ? getFieldConfig(config, 'description').label : undefined
return (
<WizardStepContent
@@ -71,17 +58,19 @@ export function StepProject({ form }: StepProjectProps) {
</div>
{/* Team Name (optional) */}
<div className="space-y-2">
<Label htmlFor="teamName">
Team Name <span className="text-muted-foreground text-xs">(optional)</span>
</Label>
<Input
id="teamName"
placeholder="Blue Innovation Team"
{...register('teamName')}
className="h-12 text-base"
/>
</div>
{showTeamName && (
<div className="space-y-2">
<Label htmlFor="teamName">
Team Name <span className="text-muted-foreground text-xs">(optional)</span>
</Label>
<Input
id="teamName"
placeholder="Blue Innovation Team"
{...register('teamName')}
className="h-12 text-base"
/>
</div>
)}
{/* Ocean Issue */}
<div className="space-y-2">
@@ -90,13 +79,13 @@ export function StepProject({ form }: StepProjectProps) {
</Label>
<Select
value={oceanIssue}
onValueChange={(value) => setValue('oceanIssue', value as OceanIssue)}
onValueChange={(value) => setValue('oceanIssue', value)}
>
<SelectTrigger className="h-12 text-base">
<SelectValue placeholder="Select an ocean issue" />
</SelectTrigger>
<SelectContent>
{oceanIssueOptions.map((option) => (
{issueOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>