MOPC-App/src/app/(jury)/jury/assignments/page.tsx

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>
)
}