364 lines
11 KiB
TypeScript
364 lines
11 KiB
TypeScript
import { Suspense } from 'react'
|
|
import Link from 'next/link'
|
|
import { auth } from '@/lib/auth'
|
|
import { prisma } from '@/lib/prisma'
|
|
|
|
export const dynamic = 'force-dynamic'
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
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 {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table'
|
|
import {
|
|
CheckCircle2,
|
|
Clock,
|
|
FileText,
|
|
ExternalLink,
|
|
AlertCircle,
|
|
} from 'lucide-react'
|
|
import { formatDate, truncate } from '@/lib/utils'
|
|
|
|
async function AssignmentsContent({
|
|
roundId,
|
|
}: {
|
|
roundId?: string
|
|
}) {
|
|
const session = await auth()
|
|
const userId = session?.user?.id
|
|
|
|
if (!userId) {
|
|
return null
|
|
}
|
|
|
|
// Get assignments, optionally filtered by round
|
|
const assignments = await prisma.assignment.findMany({
|
|
where: {
|
|
userId,
|
|
...(roundId ? { roundId } : {}),
|
|
},
|
|
include: {
|
|
project: {
|
|
select: {
|
|
id: true,
|
|
title: true,
|
|
teamName: true,
|
|
description: true,
|
|
status: true,
|
|
files: {
|
|
select: {
|
|
id: true,
|
|
fileType: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
round: {
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
status: true,
|
|
votingStartAt: true,
|
|
votingEndAt: true,
|
|
program: {
|
|
select: {
|
|
name: true,
|
|
year: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
evaluation: {
|
|
select: {
|
|
id: true,
|
|
status: true,
|
|
submittedAt: true,
|
|
updatedAt: true,
|
|
},
|
|
},
|
|
},
|
|
orderBy: [
|
|
{ round: { votingEndAt: 'asc' } },
|
|
{ createdAt: 'asc' },
|
|
],
|
|
})
|
|
|
|
if (assignments.length === 0) {
|
|
return (
|
|
<Card>
|
|
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
|
|
<FileText className="h-12 w-12 text-muted-foreground/50" />
|
|
<p className="mt-2 font-medium">No assignments found</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
{roundId
|
|
? 'No projects assigned to you for this round'
|
|
: "You don't have any project assignments yet"}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
const now = new Date()
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Desktop table view */}
|
|
<Card className="hidden md:block">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Project</TableHead>
|
|
<TableHead>Round</TableHead>
|
|
<TableHead>Deadline</TableHead>
|
|
<TableHead>Status</TableHead>
|
|
<TableHead className="text-right">Action</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{assignments.map((assignment) => {
|
|
const evaluation = assignment.evaluation
|
|
const isCompleted = evaluation?.status === 'SUBMITTED'
|
|
const isDraft = evaluation?.status === 'DRAFT'
|
|
const isVotingOpen =
|
|
assignment.round.status === 'ACTIVE' &&
|
|
assignment.round.votingStartAt &&
|
|
assignment.round.votingEndAt &&
|
|
new Date(assignment.round.votingStartAt) <= now &&
|
|
new Date(assignment.round.votingEndAt) >= now
|
|
|
|
return (
|
|
<TableRow key={assignment.id}>
|
|
<TableCell>
|
|
<div>
|
|
<p className="font-medium">
|
|
{truncate(assignment.project.title, 40)}
|
|
</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
{assignment.project.teamName}
|
|
</p>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div>
|
|
<p>{assignment.round.name}</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
{assignment.round.program.year} Edition
|
|
</p>
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
{assignment.round.votingEndAt ? (
|
|
<span
|
|
className={
|
|
new Date(assignment.round.votingEndAt) < now
|
|
? 'text-muted-foreground'
|
|
: ''
|
|
}
|
|
>
|
|
{formatDate(assignment.round.votingEndAt)}
|
|
</span>
|
|
) : (
|
|
<span className="text-muted-foreground">No deadline</span>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
{isCompleted ? (
|
|
<Badge variant="success">
|
|
<CheckCircle2 className="mr-1 h-3 w-3" />
|
|
Completed
|
|
</Badge>
|
|
) : isDraft ? (
|
|
<Badge variant="warning">
|
|
<Clock className="mr-1 h-3 w-3" />
|
|
In Progress
|
|
</Badge>
|
|
) : (
|
|
<Badge variant="secondary">Pending</Badge>
|
|
)}
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
{isCompleted ? (
|
|
<Button variant="outline" size="sm" asChild>
|
|
<Link
|
|
href={`/jury/projects/${assignment.project.id}/evaluation`}
|
|
>
|
|
View
|
|
</Link>
|
|
</Button>
|
|
) : isVotingOpen ? (
|
|
<Button size="sm" asChild>
|
|
<Link
|
|
href={`/jury/projects/${assignment.project.id}/evaluate`}
|
|
>
|
|
{isDraft ? 'Continue' : 'Evaluate'}
|
|
</Link>
|
|
</Button>
|
|
) : (
|
|
<Button variant="outline" size="sm" asChild>
|
|
<Link href={`/jury/projects/${assignment.project.id}`}>
|
|
View
|
|
</Link>
|
|
</Button>
|
|
)}
|
|
</TableCell>
|
|
</TableRow>
|
|
)
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
</Card>
|
|
|
|
{/* Mobile card view */}
|
|
<div className="space-y-4 md:hidden">
|
|
{assignments.map((assignment) => {
|
|
const evaluation = assignment.evaluation
|
|
const isCompleted = evaluation?.status === 'SUBMITTED'
|
|
const isDraft = evaluation?.status === 'DRAFT'
|
|
const isVotingOpen =
|
|
assignment.round.status === 'ACTIVE' &&
|
|
assignment.round.votingStartAt &&
|
|
assignment.round.votingEndAt &&
|
|
new Date(assignment.round.votingStartAt) <= now &&
|
|
new Date(assignment.round.votingEndAt) >= now
|
|
|
|
return (
|
|
<Card key={assignment.id}>
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-start justify-between">
|
|
<div className="space-y-1">
|
|
<CardTitle className="text-base">
|
|
{assignment.project.title}
|
|
</CardTitle>
|
|
<CardDescription>
|
|
{assignment.project.teamName}
|
|
</CardDescription>
|
|
</div>
|
|
{isCompleted ? (
|
|
<Badge variant="success">
|
|
<CheckCircle2 className="mr-1 h-3 w-3" />
|
|
Done
|
|
</Badge>
|
|
) : isDraft ? (
|
|
<Badge variant="warning">
|
|
<Clock className="mr-1 h-3 w-3" />
|
|
Draft
|
|
</Badge>
|
|
) : (
|
|
<Badge variant="secondary">Pending</Badge>
|
|
)}
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span className="text-muted-foreground">Round</span>
|
|
<span>{assignment.round.name}</span>
|
|
</div>
|
|
{assignment.round.votingEndAt && (
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span className="text-muted-foreground">Deadline</span>
|
|
<span>{formatDate(assignment.round.votingEndAt)}</span>
|
|
</div>
|
|
)}
|
|
<div className="pt-2">
|
|
{isCompleted ? (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="w-full"
|
|
asChild
|
|
>
|
|
<Link
|
|
href={`/jury/projects/${assignment.project.id}/evaluation`}
|
|
>
|
|
View Evaluation
|
|
</Link>
|
|
</Button>
|
|
) : isVotingOpen ? (
|
|
<Button size="sm" className="w-full" asChild>
|
|
<Link
|
|
href={`/jury/projects/${assignment.project.id}/evaluate`}
|
|
>
|
|
{isDraft ? 'Continue Evaluation' : 'Start Evaluation'}
|
|
</Link>
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
className="w-full"
|
|
asChild
|
|
>
|
|
<Link href={`/jury/projects/${assignment.project.id}`}>
|
|
View Project
|
|
</Link>
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function AssignmentsSkeleton() {
|
|
return (
|
|
<Card>
|
|
<CardContent className="p-6">
|
|
<div className="space-y-4">
|
|
{[...Array(5)].map((_, i) => (
|
|
<div key={i} className="flex items-center justify-between">
|
|
<div className="space-y-2">
|
|
<Skeleton className="h-5 w-48" />
|
|
<Skeleton className="h-4 w-32" />
|
|
</div>
|
|
<Skeleton className="h-9 w-24" />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|
|
|
|
export default async function JuryAssignmentsPage({
|
|
searchParams,
|
|
}: {
|
|
searchParams: Promise<{ round?: string }>
|
|
}) {
|
|
const params = await searchParams
|
|
const roundId = params.round
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Header */}
|
|
<div>
|
|
<h1 className="text-2xl font-semibold tracking-tight">My Assignments</h1>
|
|
<p className="text-muted-foreground">
|
|
Projects assigned to you for evaluation
|
|
</p>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<Suspense fallback={<AssignmentsSkeleton />}>
|
|
<AssignmentsContent roundId={roundId} />
|
|
</Suspense>
|
|
</div>
|
|
)
|
|
}
|