'use client' import { useState, useCallback, useRef, useEffect } from 'react' import { useRouter, useSearchParams } from 'next/navigation' import type { Route } from 'next' import { trpc } from '@/lib/trpc/client' import { toast } from 'sonner' import { Button } from '@/components/ui/button' import { ArrowLeft, Loader2, Save, Rocket } from 'lucide-react' import Link from 'next/link' import { WizardSection } from '@/components/admin/pipeline/wizard-section' import { BasicsSection } from '@/components/admin/pipeline/sections/basics-section' import { IntakeSection } from '@/components/admin/pipeline/sections/intake-section' import { MainTrackSection } from '@/components/admin/pipeline/sections/main-track-section' import { FilteringSection } from '@/components/admin/pipeline/sections/filtering-section' import { AssignmentSection } from '@/components/admin/pipeline/sections/assignment-section' import { AwardsSection } from '@/components/admin/pipeline/sections/awards-section' import { LiveFinalsSection } from '@/components/admin/pipeline/sections/live-finals-section' import { NotificationsSection } from '@/components/admin/pipeline/sections/notifications-section' import { ReviewSection } from '@/components/admin/pipeline/sections/review-section' import { defaultWizardState, defaultIntakeConfig, defaultFilterConfig, defaultEvaluationConfig, defaultLiveConfig } from '@/lib/pipeline-defaults' import { validateAll, validateBasics, validateTracks } from '@/lib/pipeline-validation' import type { WizardState, IntakeConfig, FilterConfig, EvaluationConfig, LiveFinalConfig } from '@/types/pipeline-wizard' export default function NewPipelinePage() { const router = useRouter() const searchParams = useSearchParams() const programId = searchParams.get('programId') ?? '' const [state, setState] = useState(() => defaultWizardState(programId)) const [openSection, setOpenSection] = useState(0) const initialStateRef = useRef(JSON.stringify(state)) // Dirty tracking — warn on navigate away useEffect(() => { const handleBeforeUnload = (e: BeforeUnloadEvent) => { if (JSON.stringify(state) !== initialStateRef.current) { e.preventDefault() } } window.addEventListener('beforeunload', handleBeforeUnload) return () => window.removeEventListener('beforeunload', handleBeforeUnload) }, [state]) const updateState = useCallback((updates: Partial) => { setState((prev) => ({ ...prev, ...updates })) }, []) // Get stage configs from the main track const mainTrack = state.tracks.find((t) => t.kind === 'MAIN') const intakeStage = mainTrack?.stages.find((s) => s.stageType === 'INTAKE') const filterStage = mainTrack?.stages.find((s) => s.stageType === 'FILTER') const evalStage = mainTrack?.stages.find((s) => s.stageType === 'EVALUATION') const liveStage = mainTrack?.stages.find((s) => s.stageType === 'LIVE_FINAL') const intakeConfig = (intakeStage?.configJson ?? defaultIntakeConfig()) as unknown as IntakeConfig const filterConfig = (filterStage?.configJson ?? defaultFilterConfig()) as unknown as FilterConfig const evalConfig = (evalStage?.configJson ?? defaultEvaluationConfig()) as unknown as EvaluationConfig const liveConfig = (liveStage?.configJson ?? defaultLiveConfig()) as unknown as LiveFinalConfig const updateStageConfig = useCallback( (stageType: string, configJson: Record) => { setState((prev) => ({ ...prev, tracks: prev.tracks.map((track) => { if (track.kind !== 'MAIN') return track return { ...track, stages: track.stages.map((stage) => stage.stageType === stageType ? { ...stage, configJson } : stage ), } }), })) }, [] ) const updateMainTrackStages = useCallback( (stages: WizardState['tracks'][0]['stages']) => { setState((prev) => ({ ...prev, tracks: prev.tracks.map((track) => track.kind === 'MAIN' ? { ...track, stages } : track ), })) }, [] ) // Validation const basicsValid = validateBasics(state).valid const tracksValid = validateTracks(state.tracks).valid const allValid = validateAll(state).valid // Mutations const createMutation = trpc.pipeline.createStructure.useMutation({ onSuccess: (data) => { initialStateRef.current = JSON.stringify(state) // prevent dirty warning toast.success('Pipeline created successfully') router.push(`/admin/rounds/pipeline/${data.pipeline.id}` as Route) }, onError: (err) => { toast.error(err.message) }, }) const publishMutation = trpc.pipeline.publish.useMutation({ onSuccess: () => { toast.success('Pipeline published successfully') }, onError: (err) => { toast.error(err.message) }, }) const handleSave = async (publish: boolean) => { const validation = validateAll(state) if (!validation.valid) { toast.error('Please fix validation errors before saving') // Open first section with errors if (!validation.sections.basics.valid) setOpenSection(0) else if (!validation.sections.tracks.valid) setOpenSection(2) return } const result = await createMutation.mutateAsync({ programId: state.programId, name: state.name, slug: state.slug, settingsJson: { ...state.settingsJson, notificationConfig: state.notificationConfig, overridePolicy: state.overridePolicy, }, tracks: state.tracks.map((t) => ({ name: t.name, slug: t.slug, kind: t.kind, sortOrder: t.sortOrder, routingModeDefault: t.routingModeDefault, decisionMode: t.decisionMode, stages: t.stages.map((s) => ({ name: s.name, slug: s.slug, stageType: s.stageType, sortOrder: s.sortOrder, configJson: s.configJson, })), awardConfig: t.awardConfig, })), autoTransitions: true, }) if (publish && result.pipeline.id) { await publishMutation.mutateAsync({ id: result.pipeline.id }) } } const isSaving = createMutation.isPending || publishMutation.isPending const sections = [ { title: 'Basics', description: 'Pipeline name, slug, and program', isValid: basicsValid, }, { title: 'Intake', description: 'Submission windows and file requirements', isValid: !!intakeStage, }, { title: 'Main Track Stages', description: `${mainTrack?.stages.length ?? 0} stages configured`, isValid: tracksValid, }, { title: 'Filtering', description: 'Gate rules and AI screening settings', isValid: !!filterStage, }, { title: 'Assignment', description: 'Jury evaluation assignment strategy', isValid: !!evalStage, }, { title: 'Awards', description: `${state.tracks.filter((t) => t.kind === 'AWARD').length} award tracks`, isValid: true, // Awards are optional }, { title: 'Live Finals', description: 'Voting, cohorts, and reveal settings', isValid: !!liveStage, }, { title: 'Notifications', description: 'Event notifications and override governance', isValid: true, // Always valid }, { title: 'Review & Publish', description: 'Validation summary and publish controls', isValid: allValid, }, ] return (
{/* Header */}

Create Pipeline

Configure the full pipeline structure for project evaluation

{/* Wizard Sections */}
{/* 0: Basics */} setOpenSection(openSection === 0 ? -1 : 0)} isValid={sections[0].isValid} > {/* 1: Intake */} setOpenSection(openSection === 1 ? -1 : 1)} isValid={sections[1].isValid} > updateStageConfig('INTAKE', c as unknown as Record) } /> {/* 2: Main Track Stages */} setOpenSection(openSection === 2 ? -1 : 2)} isValid={sections[2].isValid} > {/* 3: Filtering */} setOpenSection(openSection === 3 ? -1 : 3)} isValid={sections[3].isValid} > updateStageConfig('FILTER', c as unknown as Record) } /> {/* 4: Assignment */} setOpenSection(openSection === 4 ? -1 : 4)} isValid={sections[4].isValid} > updateStageConfig('EVALUATION', c as unknown as Record) } /> {/* 5: Awards */} setOpenSection(openSection === 5 ? -1 : 5)} isValid={sections[5].isValid} > updateState({ tracks })} /> {/* 6: Live Finals */} setOpenSection(openSection === 6 ? -1 : 6)} isValid={sections[6].isValid} > updateStageConfig('LIVE_FINAL', c as unknown as Record) } /> {/* 7: Notifications */} setOpenSection(openSection === 7 ? -1 : 7)} isValid={sections[7].isValid} > updateState({ notificationConfig })} overridePolicy={state.overridePolicy} onOverridePolicyChange={(overridePolicy) => updateState({ overridePolicy })} /> {/* 8: Review */} setOpenSection(openSection === 8 ? -1 : 8)} isValid={sections[8].isValid} >
) }