- Make pipeline cards clickable on list page (navigate to detail view) - Fix broken nav link: applicant /messages → /mentor - Fix broken nav link: mentor /messages → /projects - Add isActive field locking to all 7 wizard sections (intake, main-track, filtering, assignment, awards, live-finals, notifications) - Add minLoad ≤ maxLoad cross-field validation in assignment section - Add duplicate stage slug detection in main track section - Add active pipeline warning banners in intake and main track sections Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
268 lines
9.3 KiB
TypeScript
268 lines
9.3 KiB
TypeScript
'use client'
|
|
|
|
import { trpc } from '@/lib/trpc/client'
|
|
import Link from 'next/link'
|
|
import type { Route } from 'next'
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardHeader,
|
|
CardTitle,
|
|
} from '@/components/ui/card'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Skeleton } from '@/components/ui/skeleton'
|
|
import {
|
|
Upload,
|
|
Users,
|
|
MessageSquare,
|
|
ArrowRight,
|
|
FileText,
|
|
Clock,
|
|
CheckCircle2,
|
|
XCircle,
|
|
Layers,
|
|
} from 'lucide-react'
|
|
import { StageTimeline } from '@/components/shared/stage-timeline'
|
|
import { StageWindowBadge } from '@/components/shared/stage-window-badge'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
const stateLabels: Record<string, string> = {
|
|
PENDING: 'Pending',
|
|
IN_PROGRESS: 'In Progress',
|
|
PASSED: 'Passed',
|
|
REJECTED: 'Not Selected',
|
|
COMPLETED: 'Completed',
|
|
WAITING: 'Waiting',
|
|
}
|
|
|
|
const stateVariants: Record<string, 'success' | 'destructive' | 'warning' | 'secondary' | 'info'> = {
|
|
PENDING: 'secondary',
|
|
IN_PROGRESS: 'info',
|
|
PASSED: 'success',
|
|
REJECTED: 'destructive',
|
|
COMPLETED: 'success',
|
|
WAITING: 'warning',
|
|
}
|
|
|
|
export default function ApplicantPipelinePage() {
|
|
// Get applicant's project via dashboard endpoint
|
|
const { data: dashboard } = trpc.applicant.getMyDashboard.useQuery()
|
|
|
|
const project = dashboard?.project
|
|
const projectId = project?.id ?? ''
|
|
const programId = project?.program?.id ?? ''
|
|
|
|
const { data: pipelineView, isLoading: pipelineLoading } =
|
|
trpc.pipeline.getApplicantView.useQuery(
|
|
{ programId, projectId },
|
|
{ enabled: !!programId && !!projectId }
|
|
)
|
|
|
|
const { data: timeline, isLoading: timelineLoading } =
|
|
trpc.stage.getApplicantTimeline.useQuery(
|
|
{ projectId, pipelineId: pipelineView?.pipelineId ?? '' },
|
|
{ enabled: !!projectId && !!pipelineView?.pipelineId }
|
|
)
|
|
|
|
const isLoading = pipelineLoading || timelineLoading
|
|
|
|
if (!project && !isLoading) {
|
|
return (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h1 className="text-2xl font-bold tracking-tight">Pipeline Progress</h1>
|
|
<p className="text-muted-foreground mt-0.5">Track your project through the selection pipeline</p>
|
|
</div>
|
|
<Card>
|
|
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
|
|
<Layers className="h-12 w-12 text-muted-foreground/50 mb-3" />
|
|
<p className="font-medium">No project found</p>
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
You don't have a project in the current edition yet.
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h1 className="text-2xl font-bold tracking-tight">Pipeline Progress</h1>
|
|
<p className="text-muted-foreground mt-0.5">Track your project through the selection pipeline</p>
|
|
</div>
|
|
<Skeleton className="h-8 w-64" />
|
|
<Skeleton className="h-16 w-full" />
|
|
<Skeleton className="h-48 w-full" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Build timeline items for StageTimeline
|
|
const timelineItems = timeline?.map((item) => ({
|
|
id: item.stageId,
|
|
name: item.stageName,
|
|
stageType: item.stageType,
|
|
isCurrent: item.isCurrent,
|
|
state: item.state,
|
|
enteredAt: item.enteredAt,
|
|
})) ?? []
|
|
|
|
// Find current stage
|
|
const currentStage = timeline?.find((item) => item.isCurrent)
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h1 className="text-2xl font-bold tracking-tight">Pipeline Progress</h1>
|
|
<p className="text-muted-foreground mt-0.5">Track your project through the selection pipeline</p>
|
|
</div>
|
|
|
|
{/* Project title + status */}
|
|
<Card>
|
|
<CardContent className="py-5">
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
|
<div>
|
|
<h2 className="text-lg font-semibold">{project?.title}</h2>
|
|
<p className="text-sm text-muted-foreground">{(project as { teamName?: string } | undefined)?.teamName}</p>
|
|
</div>
|
|
{currentStage && (
|
|
<Badge variant={stateVariants[currentStage.state] ?? 'secondary'}>
|
|
{stateLabels[currentStage.state] ?? currentStage.state}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Stage Timeline visualization */}
|
|
{timelineItems.length > 0 && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">Pipeline Progress</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<StageTimeline stages={timelineItems} orientation="horizontal" />
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Current stage details */}
|
|
{currentStage && (
|
|
<Card className="border-brand-blue/30 dark:border-brand-teal/30">
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<CardTitle className="text-lg">Current Stage</CardTitle>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
<div>
|
|
<h3 className="font-semibold">{currentStage.stageName}</h3>
|
|
<p className="text-sm text-muted-foreground capitalize">
|
|
{currentStage.stageType.toLowerCase().replace(/_/g, ' ')}
|
|
</p>
|
|
</div>
|
|
{currentStage.enteredAt && (
|
|
<p className="text-xs text-muted-foreground">
|
|
Entered {new Date(currentStage.enteredAt).toLocaleDateString('en-US', {
|
|
month: 'long',
|
|
day: 'numeric',
|
|
year: 'numeric',
|
|
})}
|
|
</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Decision history */}
|
|
{timeline && timeline.length > 0 && (
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="text-lg">Stage History</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-3">
|
|
{timeline.map((item) => (
|
|
<div
|
|
key={item.stageId}
|
|
className="flex items-center justify-between py-2 border-b last:border-0"
|
|
>
|
|
<div className="flex items-center gap-3">
|
|
<div className={cn(
|
|
'h-2 w-2 rounded-full',
|
|
item.state === 'PASSED' || item.state === 'COMPLETED'
|
|
? 'bg-emerald-500'
|
|
: item.state === 'REJECTED'
|
|
? 'bg-destructive'
|
|
: item.isCurrent
|
|
? 'bg-blue-500'
|
|
: 'bg-muted-foreground'
|
|
)} />
|
|
<div>
|
|
<p className="text-sm font-medium">{item.stageName}</p>
|
|
{item.enteredAt && (
|
|
<p className="text-xs text-muted-foreground">
|
|
{new Date(item.enteredAt).toLocaleDateString()}
|
|
</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<Badge variant={stateVariants[item.state] ?? 'secondary'} className="text-xs">
|
|
{stateLabels[item.state] ?? item.state}
|
|
</Badge>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{/* Quick actions */}
|
|
<div className="grid gap-3 sm:grid-cols-3">
|
|
{currentStage && (
|
|
<Link
|
|
href={`/applicant/pipeline/${currentStage.stageId}/documents` as Route}
|
|
className="group flex items-center gap-3 rounded-xl border p-4 transition-all hover:border-brand-blue/30 hover:bg-brand-blue/5 hover:-translate-y-0.5 hover:shadow-md"
|
|
>
|
|
<div className="rounded-lg bg-blue-50 p-2 dark:bg-blue-950/40">
|
|
<Upload className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
|
</div>
|
|
<div>
|
|
<p className="font-semibold text-sm">Upload Documents</p>
|
|
<p className="text-xs text-muted-foreground">Submit required files</p>
|
|
</div>
|
|
</Link>
|
|
)}
|
|
<Link
|
|
href={"/applicant/team" as Route}
|
|
className="group flex items-center gap-3 rounded-xl border p-4 transition-all hover:border-brand-teal/30 hover:bg-brand-teal/5 hover:-translate-y-0.5 hover:shadow-md"
|
|
>
|
|
<div className="rounded-lg bg-teal-50 p-2 dark:bg-teal-950/40">
|
|
<Users className="h-4 w-4 text-brand-teal" />
|
|
</div>
|
|
<div>
|
|
<p className="font-semibold text-sm">View Team</p>
|
|
<p className="text-xs text-muted-foreground">Team members</p>
|
|
</div>
|
|
</Link>
|
|
<Link
|
|
href={"/applicant/mentor" as Route}
|
|
className="group flex items-center gap-3 rounded-xl border p-4 transition-all hover:border-amber-500/30 hover:bg-amber-50/50 hover:-translate-y-0.5 hover:shadow-md"
|
|
>
|
|
<div className="rounded-lg bg-amber-50 p-2 dark:bg-amber-950/40">
|
|
<MessageSquare className="h-4 w-4 text-amber-600 dark:text-amber-400" />
|
|
</div>
|
|
<div>
|
|
<p className="font-semibold text-sm">Contact Mentor</p>
|
|
<p className="text-xs text-muted-foreground">Send a message</p>
|
|
</div>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|