MOPC-App/src/app/(jury)/jury/projects/[id]/evaluate/page.tsx

319 lines
9.3 KiB
TypeScript

import { Suspense } from 'react'
import Link from 'next/link'
import { notFound, redirect } from 'next/navigation'
import { auth } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
export const dynamic = 'force-dynamic'
import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import { EvaluationForm } from '@/components/forms/evaluation-form'
import { ArrowLeft, AlertCircle, Clock, FileText, Users } from 'lucide-react'
import { isFuture, isPast } from 'date-fns'
interface PageProps {
params: Promise<{ id: string }>
}
// Define the criterion type for the evaluation form
interface Criterion {
id: string
label: string
description?: string
scale: number
weight?: number
required?: boolean
}
async function EvaluateContent({ projectId }: { projectId: string }) {
const session = await auth()
const userId = session?.user?.id
if (!userId) {
redirect('/login')
}
// Check if user is assigned to this project
const assignment = await prisma.assignment.findFirst({
where: {
projectId,
userId,
},
include: {
evaluation: {
include: {
form: true,
},
},
round: {
include: {
program: {
select: { name: true },
},
evaluationForms: {
where: { isActive: true },
take: 1,
},
},
},
},
})
// Get project details
const project = await prisma.project.findUnique({
where: { id: projectId },
include: {
files: true,
},
})
if (!project) {
notFound()
}
if (!assignment) {
return (
<div className="space-y-6">
<Button variant="ghost" asChild className="-ml-4">
<Link href="/jury/assignments">
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Assignments
</Link>
</Button>
<Card>
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
<AlertCircle className="h-12 w-12 text-destructive/50" />
<p className="mt-2 font-medium text-destructive">Access Denied</p>
<p className="text-sm text-muted-foreground">
You are not assigned to evaluate this project
</p>
</CardContent>
</Card>
</div>
)
}
const round = assignment.round
const now = new Date()
// Check voting window
const isVotingOpen =
round.status === 'ACTIVE' &&
round.votingStartAt &&
round.votingEndAt &&
new Date(round.votingStartAt) <= now &&
new Date(round.votingEndAt) >= now
const isVotingUpcoming =
round.votingStartAt && isFuture(new Date(round.votingStartAt))
const isVotingClosed = round.votingEndAt && isPast(new Date(round.votingEndAt))
// Check for grace period
const gracePeriod = await prisma.gracePeriod.findFirst({
where: {
roundId: round.id,
userId,
OR: [{ projectId: null }, { projectId }],
extendedUntil: { gte: now },
},
})
const hasGracePeriod = !!gracePeriod
const effectiveVotingOpen = isVotingOpen || hasGracePeriod
// Check if already submitted
const evaluation = assignment.evaluation
const isSubmitted =
evaluation?.status === 'SUBMITTED' || evaluation?.status === 'LOCKED'
if (isSubmitted) {
redirect(`/jury/projects/${projectId}/evaluation`)
}
// Get evaluation form criteria
const evaluationForm = round.evaluationForms[0]
if (!evaluationForm) {
return (
<div className="space-y-6">
<Button variant="ghost" asChild className="-ml-4">
<Link href={`/jury/projects/${projectId}`}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Project
</Link>
</Button>
<Card>
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
<AlertCircle className="h-12 w-12 text-amber-500/50" />
<p className="mt-2 font-medium">Evaluation Form Not Available</p>
<p className="text-sm text-muted-foreground">
The evaluation criteria for this round have not been configured yet.
Please check back later.
</p>
</CardContent>
</Card>
</div>
)
}
// Parse criteria from JSON
const criteria: Criterion[] = (evaluationForm.criteriaJson as unknown as Criterion[]) || []
// Handle voting not open
if (!effectiveVotingOpen) {
return (
<div className="space-y-6">
<Button variant="ghost" asChild className="-ml-4">
<Link href={`/jury/projects/${projectId}`}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Project
</Link>
</Button>
<Card>
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
<Clock className="h-12 w-12 text-amber-500/50" />
<p className="mt-2 font-medium">
{isVotingUpcoming ? 'Voting Not Yet Open' : 'Voting Period Closed'}
</p>
<p className="text-sm text-muted-foreground">
{isVotingUpcoming
? 'The voting window for this round has not started yet.'
: 'The voting window for this round has ended.'}
</p>
</CardContent>
</Card>
</div>
)
}
return (
<div className="space-y-6">
{/* Back button and project summary */}
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4">
<div>
<Button variant="ghost" asChild className="-ml-4">
<Link href={`/jury/projects/${projectId}`}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Project
</Link>
</Button>
<div className="mt-2 space-y-1">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>{round.program.name}</span>
<span>/</span>
<span>{round.name}</span>
</div>
<h1 className="text-xl font-semibold">Evaluate: {project.title}</h1>
{project.teamName && (
<div className="flex items-center gap-2 text-muted-foreground">
<Users className="h-4 w-4" />
<span>{project.teamName}</span>
</div>
)}
</div>
</div>
{/* Quick file access */}
{project.files.length > 0 && (
<div className="flex items-center gap-2">
<FileText className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">
{project.files.length} file{project.files.length !== 1 ? 's' : ''}
</span>
<Button variant="outline" size="sm" asChild>
<Link href={`/jury/projects/${projectId}`}>View Files</Link>
</Button>
</div>
)}
</div>
{/* Grace period notice */}
{hasGracePeriod && gracePeriod && (
<Card className="border-amber-500 bg-amber-500/5">
<CardContent className="py-3">
<div className="flex items-center gap-2 text-amber-600">
<Clock className="h-4 w-4" />
<span className="text-sm font-medium">
You have a grace period extension until{' '}
{new Date(gracePeriod.extendedUntil).toLocaleString()}
</span>
</div>
</CardContent>
</Card>
)}
{/* Evaluation Form */}
<EvaluationForm
assignmentId={assignment.id}
evaluationId={evaluation?.id || null}
projectTitle={project.title}
criteria={criteria}
initialData={
evaluation
? {
criterionScoresJson: evaluation.criterionScoresJson as Record<
string,
number
> | null,
globalScore: evaluation.globalScore,
binaryDecision: evaluation.binaryDecision,
feedbackText: evaluation.feedbackText,
status: evaluation.status,
}
: undefined
}
isVotingOpen={effectiveVotingOpen}
deadline={round.votingEndAt}
/>
</div>
)
}
function EvaluateSkeleton() {
return (
<div className="space-y-6">
<Skeleton className="h-9 w-36" />
<div className="space-y-2">
<Skeleton className="h-4 w-48" />
<Skeleton className="h-6 w-80" />
<Skeleton className="h-4 w-32" />
</div>
<Card>
<CardContent className="p-6 space-y-6">
{[1, 2, 3].map((i) => (
<div key={i} className="space-y-3">
<Skeleton className="h-5 w-48" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-10 w-full" />
</div>
))}
</CardContent>
</Card>
<Card>
<CardContent className="p-6 space-y-4">
<Skeleton className="h-5 w-32" />
<Skeleton className="h-12 w-full" />
</CardContent>
</Card>
</div>
)
}
export default async function EvaluatePage({ params }: PageProps) {
const { id } = await params
return (
<Suspense fallback={<EvaluateSkeleton />}>
<EvaluateContent projectId={id} />
</Suspense>
)
}