'use client' import { useState, useEffect, useMemo } from 'react' import { useParams } from 'next/navigation' import Link from 'next/link' import { trpc } from '@/lib/trpc/client' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { Card, CardContent, } from '@/components/ui/card' import { Skeleton } from '@/components/ui/skeleton' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { toast } from 'sonner' import { cn } from '@/lib/utils' import type { Route } from 'next' import { ArrowLeft, MoreHorizontal, Rocket, Archive, Layers, GitBranch, Loader2, ChevronDown, Save, Wand2, } from 'lucide-react' import { InlineEditableText } from '@/components/ui/inline-editable-text' import { PipelineFlowchart } from '@/components/admin/pipeline/pipeline-flowchart' import { StageDetailSheet } from '@/components/admin/pipeline/stage-detail-sheet' import { usePipelineInlineEdit } from '@/hooks/use-pipeline-inline-edit' import { MainTrackSection } from '@/components/admin/pipeline/sections/main-track-section' import { AwardsSection } from '@/components/admin/pipeline/sections/awards-section' import { NotificationsSection } from '@/components/admin/pipeline/sections/notifications-section' import { RoutingRulesEditor } from '@/components/admin/pipeline/routing-rules-editor' import { AwardGovernanceEditor } from '@/components/admin/pipeline/award-governance-editor' import { defaultNotificationConfig } from '@/lib/pipeline-defaults' import { toWizardTrackConfig } from '@/lib/pipeline-conversions' import type { WizardTrackConfig } from '@/types/pipeline-wizard' const statusColors: Record = { DRAFT: 'bg-gray-100 text-gray-700', ACTIVE: 'bg-emerald-100 text-emerald-700', ARCHIVED: 'bg-muted text-muted-foreground', CLOSED: 'bg-blue-100 text-blue-700', } export default function PipelineDetailPage() { const params = useParams() const pipelineId = params.id as string const utils = trpc.useUtils() const [selectedTrackId, setSelectedTrackId] = useState(null) const [selectedStageId, setSelectedStageId] = useState(null) const [sheetOpen, setSheetOpen] = useState(false) const [structureTracks, setStructureTracks] = useState([]) const [notificationConfig, setNotificationConfig] = useState>({}) const [overridePolicy, setOverridePolicy] = useState>({ allowedRoles: ['SUPER_ADMIN', 'PROGRAM_ADMIN'], }) const [structureDirty, setStructureDirty] = useState(false) const [settingsDirty, setSettingsDirty] = useState(false) const { data: pipeline, isLoading } = trpc.pipeline.getDraft.useQuery({ id: pipelineId, }) const { isUpdating, updatePipeline, updateStageConfig } = usePipelineInlineEdit(pipelineId) const publishMutation = trpc.pipeline.publish.useMutation({ onSuccess: () => toast.success('Pipeline published'), onError: (err) => toast.error(err.message), }) const updateMutation = trpc.pipeline.update.useMutation({ onSuccess: () => toast.success('Pipeline updated'), onError: (err) => toast.error(err.message), }) const updateStructureMutation = trpc.pipeline.updateStructure.useMutation({ onSuccess: async () => { await utils.pipeline.getDraft.invalidate({ id: pipelineId }) toast.success('Pipeline structure updated') setStructureDirty(false) }, onError: (err) => toast.error(err.message), }) const materializeRequirementsMutation = trpc.file.materializeRequirementsFromConfig.useMutation({ onSuccess: async (result) => { if (result.skipped && result.reason === 'already_materialized') { toast.message('Requirements already materialized') return } if (result.skipped && result.reason === 'no_config_requirements') { toast.message('No legacy config requirements found') return } await utils.file.listRequirements.invalidate() toast.success(`Materialized ${result.created} requirement(s)`) }, onError: (err) => toast.error(err.message), }) // Auto-select first track on load useEffect(() => { if (pipeline && pipeline.tracks.length > 0 && !selectedTrackId) { const firstTrack = pipeline.tracks.sort((a, b) => a.sortOrder - b.sortOrder)[0] setSelectedTrackId(firstTrack.id) } }, [pipeline, selectedTrackId]) useEffect(() => { if (!pipeline) return const nextTracks = pipeline.tracks .slice() .sort((a, b) => a.sortOrder - b.sortOrder) .map((track) => toWizardTrackConfig({ id: track.id, name: track.name, slug: track.slug, kind: track.kind, sortOrder: track.sortOrder, routingMode: track.routingMode, decisionMode: track.decisionMode, stages: track.stages.map((stage) => ({ id: stage.id, name: stage.name, slug: stage.slug, stageType: stage.stageType, sortOrder: stage.sortOrder, configJson: stage.configJson, })), specialAward: track.specialAward ? { name: track.specialAward.name, description: track.specialAward.description, scoringMode: track.specialAward.scoringMode, } : null, }) ) setStructureTracks(nextTracks) const settings = (pipeline.settingsJson as Record | null) ?? {} setNotificationConfig( ((settings.notificationConfig as Record | undefined) ?? defaultNotificationConfig()) as Record ) setOverridePolicy( ((settings.overridePolicy as Record | undefined) ?? { allowedRoles: ['SUPER_ADMIN', 'PROGRAM_ADMIN'], }) as Record ) setStructureDirty(false) setSettingsDirty(false) }, [pipeline]) const trackOptionsForEditors = useMemo( () => (pipeline?.tracks ?? []) .slice() .sort((a, b) => a.sortOrder - b.sortOrder) .map((track) => ({ id: track.id, name: track.name, stages: track.stages .slice() .sort((a, b) => a.sortOrder - b.sortOrder) .map((stage) => ({ id: stage.id, name: stage.name, sortOrder: stage.sortOrder, })), })), [pipeline] ) if (isLoading) { return (
) } if (!pipeline) { return (

Pipeline Not Found

The requested pipeline does not exist

) } const selectedTrack = pipeline.tracks.find((t) => t.id === selectedTrackId) const selectedStage = selectedTrack?.stages.find( (s) => s.id === selectedStageId ) const mainTrackDraft = structureTracks.find((track) => track.kind === 'MAIN') const hasAwardTracks = pipeline.tracks.some((t) => t.kind === 'AWARD') const hasMultipleTracks = pipeline.tracks.length > 1 const handleTrackChange = (trackId: string) => { setSelectedTrackId(trackId) setSelectedStageId(null) } const handleStageSelect = (stageId: string) => { setSelectedStageId(stageId) setSheetOpen(true) } const handleStatusChange = async (newStatus: 'DRAFT' | 'ACTIVE' | 'CLOSED' | 'ARCHIVED') => { await updateMutation.mutateAsync({ id: pipelineId, status: newStatus, }) } const updateMainTrackStages = (stages: WizardTrackConfig['stages']) => { setStructureTracks((prev) => prev.map((track) => track.kind === 'MAIN' ? { ...track, stages, } : track ) ) setStructureDirty(true) } const handleSaveStructure = async () => { await updateStructureMutation.mutateAsync({ id: pipelineId, tracks: structureTracks.map((track) => ({ id: track.id, name: track.name, slug: track.slug, kind: track.kind, sortOrder: track.sortOrder, routingModeDefault: track.routingModeDefault, decisionMode: track.decisionMode, stages: track.stages.map((stage) => ({ id: stage.id, name: stage.name, slug: stage.slug, stageType: stage.stageType, sortOrder: stage.sortOrder, configJson: stage.configJson, })), awardConfig: track.awardConfig, })), autoTransitions: false, }) } const handleSaveSettings = async () => { const currentSettings = (pipeline.settingsJson as Record | null) ?? {} await updatePipeline({ settingsJson: { ...currentSettings, notificationConfig, overridePolicy, }, }) setSettingsDirty(false) } // Prepare flowchart data for the selected track const flowchartTracks = selectedTrack ? [selectedTrack] : [] return (
{/* Header */}
updatePipeline({ name: newName })} variant="h1" placeholder="Untitled Pipeline" disabled={isUpdating} /> handleStatusChange('DRAFT')} disabled={pipeline.status === 'DRAFT' || updateMutation.isPending} > Draft handleStatusChange('ACTIVE')} disabled={pipeline.status === 'ACTIVE' || updateMutation.isPending} > Active handleStatusChange('CLOSED')} disabled={pipeline.status === 'CLOSED' || updateMutation.isPending} > Closed handleStatusChange('ARCHIVED')} disabled={pipeline.status === 'ARCHIVED' || updateMutation.isPending} > Archived
slug: updatePipeline({ slug: newSlug })} variant="mono" placeholder="pipeline-slug" disabled={isUpdating} />
Edit in Wizard {pipeline.status === 'DRAFT' && ( publishMutation.mutate({ id: pipelineId })} > {publishMutation.isPending ? ( ) : ( )} Publish )} {pipeline.status === 'ACTIVE' && ( handleStatusChange('CLOSED')} > Close Pipeline )} handleStatusChange('ARCHIVED')} > Archive
{/* Pipeline Summary */}
Tracks

{pipeline.tracks.length}

{pipeline.tracks.filter((t) => t.kind === 'MAIN').length} main,{' '} {pipeline.tracks.filter((t) => t.kind === 'AWARD').length} award

Stages

{pipeline.tracks.reduce((sum, t) => sum + t.stages.length, 0)}

across all tracks

Transitions

{pipeline.tracks.reduce( (sum, t) => sum + t.stages.reduce( (s, stage) => s + stage.transitionsFrom.length, 0 ), 0 )}

stage connections

{/* Track Switcher (only if multiple tracks) */} {hasMultipleTracks && (
{pipeline.tracks .sort((a, b) => a.sortOrder - b.sortOrder) .map((track) => ( ))}
)} {/* Pipeline Flowchart */} {flowchartTracks.length > 0 ? (

Click a stage to edit its configuration

) : ( No tracks configured for this pipeline )} {/* Stage Detail Sheet */} | null, } : null } onSaveConfig={updateStageConfig} isSaving={isUpdating} pipelineId={pipelineId} materializeRequirements={(stageId) => materializeRequirementsMutation.mutate({ stageId }) } isMaterializing={materializeRequirementsMutation.isPending} /> {/* Stage Management */}

Stage Management

Add, remove, reorder, or change stage types. Click a stage in the flowchart to edit its settings.

Pipeline Structure

{mainTrackDraft ? ( ) : (

No main track configured.

)} { setStructureTracks(tracks) setStructureDirty(true) }} />
{/* Routing Rules (only if multiple tracks) */} {hasMultipleTracks && (
)} {/* Award Governance (only if award tracks exist) */} {hasAwardTracks && (

Award Governance

Configure special awards, voting, and scoring for award tracks.

track.kind === 'AWARD') .map((track) => ({ id: track.id, name: track.name, decisionMode: track.decisionMode, specialAward: track.specialAward ? { id: track.specialAward.id, name: track.specialAward.name, description: track.specialAward.description, criteriaText: track.specialAward.criteriaText, useAiEligibility: track.specialAward.useAiEligibility, scoringMode: track.specialAward.scoringMode, maxRankedPicks: track.specialAward.maxRankedPicks, votingStartAt: track.specialAward.votingStartAt, votingEndAt: track.specialAward.votingEndAt, status: track.specialAward.status, } : null, }))} />
)} {/* Settings */}

Settings

Notifications and Overrides

{ setNotificationConfig(next) setSettingsDirty(true) }} overridePolicy={overridePolicy} onOverridePolicyChange={(next) => { setOverridePolicy(next) setSettingsDirty(true) }} />
) }