'use client' import { Suspense, use, useState } from 'react' import Link from 'next/link' import type { Route } from 'next' import { trpc } from '@/lib/trpc/client' import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Skeleton } from '@/components/ui/skeleton' import { Separator } from '@/components/ui/separator' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from '@/components/ui/dialog' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Checkbox } from '@/components/ui/checkbox' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '@/components/ui/tooltip' import { FileViewer } from '@/components/shared/file-viewer' import { FileUpload } from '@/components/shared/file-upload' import { ProjectLogoWithUrl } from '@/components/shared/project-logo-with-url' import { UserAvatar } from '@/components/shared/user-avatar' import { EvaluationSummaryCard } from '@/components/admin/evaluation-summary-card' import { EvaluationEditSheet } from '@/components/admin/evaluation-edit-sheet' import { AnimatedCard } from '@/components/shared/animated-container' import { ArrowLeft, Edit, AlertCircle, Users, FileText, Calendar, BarChart3, ThumbsUp, ThumbsDown, MapPin, Waves, GraduationCap, Heart, Crown, UserPlus, Loader2, ScanSearch, Eye, Plus, X, } from 'lucide-react' import { toast } from 'sonner' import { formatDateOnly } from '@/lib/utils' interface PageProps { params: Promise<{ id: string }> } // Status badge colors const statusColors: Record = { SUBMITTED: 'secondary', ELIGIBLE: 'default', ASSIGNED: 'default', SEMIFINALIST: 'default', FINALIST: 'default', REJECTED: 'destructive', } // Evaluation status colors const evalStatusColors: Record = { NOT_STARTED: 'outline', DRAFT: 'secondary', SUBMITTED: 'default', LOCKED: 'default', } function ProjectDetailContent({ projectId }: { projectId: string }) { // Fetch project + assignments + stats in a single combined query const { data: fullDetail, isLoading } = trpc.project.getFullDetail.useQuery( { id: projectId }, { refetchInterval: 30_000 } ) const project = fullDetail?.project const assignments = fullDetail?.assignments const stats = fullDetail?.stats // Fetch files (flat list for backward compatibility) const { data: files } = trpc.file.listByProject.useQuery({ projectId }) // Fetch competitions for this project's program to get rounds const { data: competitions } = trpc.competition.list.useQuery( { programId: project?.programId || '' }, { enabled: !!project?.programId } ) // Get first competition ID to fetch full details with rounds const competitionId = competitions?.[0]?.id // Fetch full competition details including rounds const { data: competition } = trpc.competition.getById.useQuery( { id: competitionId || '' }, { enabled: !!competitionId } ) // Extract all rounds from the competition const competitionRounds = competition?.rounds || [] // Fetch requirements for all rounds in a single query (avoids dynamic hook violation) const roundIds = competitionRounds.map((r: { id: string }) => r.id) const { data: allRequirements = [] } = trpc.file.listRequirementsByRounds.useQuery( { roundIds }, { enabled: roundIds.length > 0 } ) const utils = trpc.useUtils() // State for evaluation detail sheet // eslint-disable-next-line @typescript-eslint/no-explicit-any const [selectedEvalAssignment, setSelectedEvalAssignment] = useState(null) // State for add member dialog const [addMemberOpen, setAddMemberOpen] = useState(false) const [addMemberForm, setAddMemberForm] = useState({ email: '', name: '', role: 'MEMBER' as 'LEAD' | 'MEMBER' | 'ADVISOR', title: '', sendInvite: true, }) // State for remove member confirmation const [removingMemberId, setRemovingMemberId] = useState(null) const addTeamMember = trpc.project.addTeamMember.useMutation({ onSuccess: () => { toast.success('Team member added') setAddMemberOpen(false) setAddMemberForm({ email: '', name: '', role: 'MEMBER', title: '', sendInvite: true }) utils.project.getFullDetail.invalidate({ id: projectId }) }, onError: (err) => { toast.error(err.message || 'Failed to add team member') }, }) const removeTeamMember = trpc.project.removeTeamMember.useMutation({ onSuccess: () => { toast.success('Team member removed') setRemovingMemberId(null) utils.project.getFullDetail.invalidate({ id: projectId }) }, onError: (err) => { toast.error(err.message || 'Failed to remove team member') }, }) if (isLoading) { return } if (!project) { return (

Project Not Found

) } return (
{/* Header */}
{project.programId ? ( Program ) : ( No program )}

{project.title}

{(() => { const prs = (project as any).projectRoundStates ?? [] if (!prs.length) return Submitted if (prs.some((p: any) => p.state === 'REJECTED')) return Rejected const latest = prs[0] return {latest.round.name} })()}
{project.teamName && (

{project.teamName}

)}
{/* Stats Grid */} {stats && (
Average Score
{stats.averageGlobalScore?.toFixed(1) || '-'}

Range: {stats.minScore || '-'} - {stats.maxScore || '-'}

Recommendations
{stats.yesPercentage?.toFixed(0) || 0}%

{stats.yesVotes} yes / {stats.noVotes} no

)} {/* Project Info */}
Project Information
{/* Category & Ocean Issue badges */}
{project.competitionCategory && ( {project.competitionCategory === 'STARTUP' ? 'Start-up' : 'Business Concept'} )} {project.oceanIssue && ( {project.oceanIssue.replace(/_/g, ' ')} )} {project.wantsMentorship && ( Wants Mentorship )}
{project.description && (

Description

{project.description}

)} {/* Location & Institution */}
{(project.country || project.geographicZone) && (

Location

{project.geographicZone || project.country}

)} {project.institution && (

Institution

{project.institution}

)} {project.foundedAt && (

Founded

{formatDateOnly(project.foundedAt)}

)}
{/* Submission URLs */} {(project.phase1SubmissionUrl || project.phase2SubmissionUrl) && (

Submission Links

{project.phase1SubmissionUrl && ( )} {project.phase2SubmissionUrl && ( )}
)} {/* AI-Assigned Expertise Tags */} {project.projectTags && project.projectTags.length > 0 && (

Expertise Tags

{project.projectTags.map((pt) => ( {pt.tag.name} {pt.confidence < 1 && ( {Math.round(pt.confidence * 100)}% )} ))}
)} {/* Simple Tags (legacy) */} {project.tags && project.tags.length > 0 && (

Tags

{project.tags.map((tag) => ( {tag} ))}
)} {/* Internal Info */} {(project.internalComments || project.applicationStatus || project.referralSource) && (

Internal Notes

{project.applicationStatus && (

Application Status

{project.applicationStatus}

)} {project.referralSource && (

Referral Source

{project.referralSource}

)}
{project.internalComments && (

Comments

{project.internalComments}

)}
)}
Created:{' '} {formatDateOnly(project.createdAt)}
Updated:{' '} {formatDateOnly(project.updatedAt)}
{/* Team Members Section */}
Team Members ({project.teamMembers?.length ?? 0})
{project.teamMembers && project.teamMembers.length > 0 ? (
{project.teamMembers.map((member: { id: string; role: string; title: string | null; user: { id: string; name: string | null; email: string; avatarUrl?: string | null } }) => { const isLastLead = member.role === 'LEAD' && project.teamMembers.filter((m: { role: string }) => m.role === 'LEAD').length <= 1 return (
{member.role === 'LEAD' ? (
) : ( )}

{member.user.name || 'Unnamed'}

{member.role === 'LEAD' ? 'Lead' : member.role === 'ADVISOR' ? 'Advisor' : 'Member'}

{member.user.email}

{member.title && (

{member.title}

)}
{isLastLead && ( Cannot remove the last team lead )}
) })}
) : (

No team members yet.

)}
{/* Add Member Dialog */} Add Team Member
setAddMemberForm((f) => ({ ...f, email: e.target.value }))} />
setAddMemberForm((f) => ({ ...f, name: e.target.value }))} />
setAddMemberForm((f) => ({ ...f, title: e.target.value }))} />
setAddMemberForm((f) => ({ ...f, sendInvite: checked === true })) } />
{/* Remove Member Confirmation Dialog */} { if (!open) setRemovingMemberId(null) }}> Remove Team Member

Are you sure you want to remove this team member? This action cannot be undone.

{/* Mentor Assignment Section */} {project.wantsMentorship && (
Mentor Assignment
{!project.mentorAssignment && ( )}
{project.mentorAssignment ? (

{project.mentorAssignment.mentor.name || 'Unnamed'}

{project.mentorAssignment.mentor.email}

{project.mentorAssignment.method.replace('_', ' ')}
) : (

No mentor assigned yet. The applicant has requested mentorship support.

)}
)} {/* Files Section */}
Files
Project documents and materials organized by competition round
utils.file.listByProject.invalidate({ projectId })} />
{/* File upload */}

Upload Files

({ id: r.id, name: r.name }))} onUploadComplete={() => { utils.file.listByProject.invalidate({ projectId }) }} />
{/* All Files list */} {files && files.length > 0 && ( <> ({ id: f.id, fileName: f.fileName, fileType: f.fileType, mimeType: f.mimeType, size: f.size, bucket: f.bucket, objectKey: f.objectKey, pageCount: f.pageCount, textPreview: f.textPreview, detectedLang: f.detectedLang, langConfidence: f.langConfidence, analyzedAt: f.analyzedAt ? String(f.analyzedAt) : null, requirementId: f.requirementId, requirement: f.requirement ? { id: f.requirement.id, name: f.requirement.name, description: f.requirement.description, isRequired: f.requirement.isRequired, } : null, }))} /> )}
{/* Assignments Section */} {assignments && assignments.length > 0 && (
Jury Assignments
{assignments.filter((a) => a.evaluation?.status === 'SUBMITTED') .length}{' '} of {assignments.length} evaluations completed
Juror Expertise Status Score Decision {assignments.map((assignment) => ( { if (assignment.evaluation?.status === 'SUBMITTED') { setSelectedEvalAssignment(assignment) } }} >

{assignment.user.name || 'Unnamed'}

{assignment.user.email}

{assignment.user.expertiseTags?.slice(0, 2).map((tag) => ( {tag} ))} {(assignment.user.expertiseTags?.length || 0) > 2 && ( +{(assignment.user.expertiseTags?.length || 0) - 2} )}
{(assignment.evaluation?.status || 'NOT_STARTED').replace( '_', ' ' )} {assignment.evaluation?.globalScore !== null && assignment.evaluation?.globalScore !== undefined ? ( {assignment.evaluation.globalScore}/10 ) : ( - )} {assignment.evaluation?.binaryDecision !== null && assignment.evaluation?.binaryDecision !== undefined ? ( assignment.evaluation.binaryDecision ? (
Yes
) : (
No
) ) : ( - )}
{assignment.evaluation?.status === 'SUBMITTED' && ( )}
))}
)} {/* Evaluation Detail Sheet */} { if (!open) setSelectedEvalAssignment(null) }} onSaved={() => utils.project.getFullDetail.invalidate({ id: projectId })} /> {/* AI Evaluation Summary */} {assignments && assignments.length > 0 && stats && stats.totalEvaluations > 0 && ( )}
) } function ProjectDetailSkeleton() { return (
{[1, 2, 3, 4].map((i) => ( ))}
) } function AnalyzeDocumentsButton({ projectId, onComplete }: { projectId: string; onComplete: () => void }) { const analyzeMutation = trpc.file.analyzeProjectFiles.useMutation({ onSuccess: (result) => { toast.success( `Analyzed ${result.analyzed} file${result.analyzed !== 1 ? 's' : ''}${result.failed > 0 ? ` (${result.failed} failed)` : ''}` ) onComplete() }, onError: (error) => { toast.error(error.message || 'Analysis failed') }, }) return ( ) } export default function ProjectDetailPage({ params }: PageProps) { const { id } = use(params) return ( }> ) }