204 lines
7.7 KiB
TypeScript
204 lines
7.7 KiB
TypeScript
'use client'
|
|
|
|
import Link from 'next/link'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from '@/components/ui/tooltip'
|
|
import {
|
|
Filter,
|
|
ClipboardCheck,
|
|
Zap,
|
|
CheckCircle2,
|
|
Clock,
|
|
Archive,
|
|
ChevronRight,
|
|
FileText,
|
|
Users,
|
|
AlertTriangle,
|
|
} from 'lucide-react'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
type PipelineRound = {
|
|
id: string
|
|
name: string
|
|
status: string
|
|
roundType: string
|
|
_count?: {
|
|
projects: number
|
|
assignments: number
|
|
}
|
|
}
|
|
|
|
interface RoundPipelineProps {
|
|
rounds: PipelineRound[]
|
|
programName?: string
|
|
}
|
|
|
|
const typeIcons: Record<string, typeof Filter> = {
|
|
FILTERING: Filter,
|
|
EVALUATION: ClipboardCheck,
|
|
LIVE_EVENT: Zap,
|
|
}
|
|
|
|
const typeColors: Record<string, { bg: string; text: string; border: string }> = {
|
|
FILTERING: {
|
|
bg: 'bg-amber-50 dark:bg-amber-950/30',
|
|
text: 'text-amber-700 dark:text-amber-300',
|
|
border: 'border-amber-200 dark:border-amber-800',
|
|
},
|
|
EVALUATION: {
|
|
bg: 'bg-blue-50 dark:bg-blue-950/30',
|
|
text: 'text-blue-700 dark:text-blue-300',
|
|
border: 'border-blue-200 dark:border-blue-800',
|
|
},
|
|
LIVE_EVENT: {
|
|
bg: 'bg-violet-50 dark:bg-violet-950/30',
|
|
text: 'text-violet-700 dark:text-violet-300',
|
|
border: 'border-violet-200 dark:border-violet-800',
|
|
},
|
|
}
|
|
|
|
const statusConfig: Record<string, { color: string; icon: typeof CheckCircle2; label: string }> = {
|
|
DRAFT: { color: 'text-muted-foreground', icon: Clock, label: 'Draft' },
|
|
ACTIVE: { color: 'text-green-600', icon: CheckCircle2, label: 'Active' },
|
|
CLOSED: { color: 'text-amber-600', icon: Archive, label: 'Closed' },
|
|
ARCHIVED: { color: 'text-muted-foreground', icon: Archive, label: 'Archived' },
|
|
}
|
|
|
|
export function RoundPipeline({ rounds }: RoundPipelineProps) {
|
|
if (rounds.length === 0) return null
|
|
|
|
// Detect bottlenecks: rounds with many more incoming projects than outgoing
|
|
const projectCounts = rounds.map((r) => r._count?.projects || 0)
|
|
|
|
return (
|
|
<div className="w-full overflow-x-auto pb-2">
|
|
<div className="flex items-stretch gap-1 min-w-max px-1 py-2">
|
|
{rounds.map((round, index) => {
|
|
const TypeIcon = typeIcons[round.roundType] || ClipboardCheck
|
|
const colors = typeColors[round.roundType] || typeColors.EVALUATION
|
|
const status = statusConfig[round.status] || statusConfig.DRAFT
|
|
const StatusIcon = status.icon
|
|
const projectCount = round._count?.projects || 0
|
|
const prevCount = index > 0 ? projectCounts[index - 1] : 0
|
|
const dropRate = prevCount > 0 ? Math.round(((prevCount - projectCount) / prevCount) * 100) : 0
|
|
const isBottleneck = dropRate > 50 && index > 0
|
|
|
|
return (
|
|
<div key={round.id} className="flex items-center">
|
|
{/* Round Card */}
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Link
|
|
href={`/admin/rounds/${round.id}`}
|
|
className={cn(
|
|
'group relative flex flex-col items-center gap-2 rounded-xl border-2 px-5 py-4 transition-all duration-200 hover:shadow-lg hover:-translate-y-1 min-w-[140px]',
|
|
colors.bg,
|
|
colors.border,
|
|
round.status === 'ACTIVE' && 'ring-2 ring-green-500/30'
|
|
)}
|
|
>
|
|
{/* Status indicator dot */}
|
|
<div className="absolute -top-1.5 -right-1.5">
|
|
<div className={cn(
|
|
'h-3.5 w-3.5 rounded-full border-2 border-background',
|
|
round.status === 'ACTIVE' ? 'bg-green-500' :
|
|
round.status === 'CLOSED' ? 'bg-amber-500' :
|
|
round.status === 'DRAFT' ? 'bg-muted-foreground/40' :
|
|
'bg-muted-foreground/20'
|
|
)} />
|
|
</div>
|
|
|
|
{/* Type Icon */}
|
|
<div className={cn(
|
|
'rounded-lg p-2',
|
|
round.status === 'ACTIVE' ? 'bg-green-100 dark:bg-green-900/30' : 'bg-background'
|
|
)}>
|
|
<TypeIcon className={cn('h-5 w-5', colors.text)} />
|
|
</div>
|
|
|
|
{/* Round Name */}
|
|
<p className="text-sm font-medium text-center line-clamp-2 leading-tight max-w-[120px]">
|
|
{round.name}
|
|
</p>
|
|
|
|
{/* Stats Row */}
|
|
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
|
<span className="flex items-center gap-1">
|
|
<FileText className="h-3 w-3" />
|
|
{projectCount}
|
|
</span>
|
|
{round._count?.assignments !== undefined && round._count.assignments > 0 && (
|
|
<span className="flex items-center gap-1">
|
|
<Users className="h-3 w-3" />
|
|
{round._count.assignments}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{/* Status Badge */}
|
|
<Badge
|
|
variant="outline"
|
|
className={cn('text-[10px] px-1.5 py-0', status.color)}
|
|
>
|
|
<StatusIcon className="mr-1 h-2.5 w-2.5" />
|
|
{status.label}
|
|
</Badge>
|
|
|
|
{/* Bottleneck indicator */}
|
|
{isBottleneck && (
|
|
<div className="absolute -bottom-2 left-1/2 -translate-x-1/2">
|
|
<AlertTriangle className="h-4 w-4 text-amber-500" />
|
|
</div>
|
|
)}
|
|
</Link>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="bottom" className="max-w-xs">
|
|
<div className="space-y-1">
|
|
<p className="font-medium">{round.name}</p>
|
|
<p className="text-xs capitalize">
|
|
{round.roundType.toLowerCase().replace('_', ' ')} · {round.status.toLowerCase()}
|
|
</p>
|
|
<p className="text-xs">
|
|
{projectCount} projects
|
|
{round._count?.assignments ? `, ${round._count.assignments} assignments` : ''}
|
|
</p>
|
|
{isBottleneck && (
|
|
<p className="text-xs text-amber-600">
|
|
{dropRate}% drop from previous round
|
|
</p>
|
|
)}
|
|
</div>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
|
|
{/* Arrow connector */}
|
|
{index < rounds.length - 1 && (
|
|
<div className="flex flex-col items-center px-2">
|
|
<ChevronRight className="h-5 w-5 text-muted-foreground/40" />
|
|
{prevCount > 0 && index > 0 && dropRate > 0 && (
|
|
<span className="text-[10px] text-muted-foreground/60 -mt-0.5">
|
|
-{dropRate}%
|
|
</span>
|
|
)}
|
|
{index === 0 && projectCounts[0] > 0 && projectCounts[1] !== undefined && (
|
|
<span className="text-[10px] text-muted-foreground/60 -mt-0.5">
|
|
{projectCounts[0]} → {projectCounts[1] || '?'}
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|