MOPC-App/src/app/(admin)/admin/projects/page.tsx

301 lines
9.7 KiB
TypeScript
Raw Normal View History

import { Suspense } from 'react'
import Link from 'next/link'
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 {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import {
Plus,
MoreHorizontal,
ClipboardList,
Eye,
Pencil,
FileUp,
Users,
} from 'lucide-react'
import { formatDateOnly, truncate } from '@/lib/utils'
import { ProjectLogo } from '@/components/shared/project-logo'
async function ProjectsContent() {
const projects = await prisma.project.findMany({
// Note: PROGRAM_ADMIN filtering should be handled via middleware or a separate relation
select: {
id: true,
title: true,
teamName: true,
status: true,
logoKey: true,
createdAt: true,
round: {
select: {
id: true,
name: true,
status: true,
program: {
select: {
name: true,
},
},
},
},
_count: {
select: {
assignments: true,
files: true,
},
},
},
orderBy: { createdAt: 'desc' },
take: 100,
})
if (projects.length === 0) {
return (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
<ClipboardList className="h-12 w-12 text-muted-foreground/50" />
<p className="mt-2 font-medium">No projects yet</p>
<p className="text-sm text-muted-foreground">
Import projects via CSV or create them manually
</p>
<div className="mt-4 flex gap-2">
<Button asChild>
<Link href="/admin/projects/import">
<FileUp className="mr-2 h-4 w-4" />
Import CSV
</Link>
</Button>
<Button variant="outline" asChild>
<Link href="/admin/projects/new">
<Plus className="mr-2 h-4 w-4" />
Add Project
</Link>
</Button>
</div>
</CardContent>
</Card>
)
}
const statusColors: Record<string, 'default' | 'success' | 'secondary' | 'destructive' | 'warning'> = {
SUBMITTED: 'secondary',
UNDER_REVIEW: 'default',
SHORTLISTED: 'success',
FINALIST: 'success',
WINNER: 'success',
REJECTED: 'destructive',
WITHDRAWN: 'secondary',
}
return (
<>
{/* Desktop table view */}
<Card className="hidden md:block">
<Table>
<TableHeader>
<TableRow>
<TableHead>Project</TableHead>
<TableHead>Round</TableHead>
<TableHead>Files</TableHead>
<TableHead>Assignments</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{projects.map((project) => (
<TableRow key={project.id} className="group relative cursor-pointer hover:bg-muted/50">
<TableCell>
<Link href={`/admin/projects/${project.id}`} className="flex items-center gap-3 after:absolute after:inset-0 after:content-['']">
<ProjectLogo
project={project}
size="sm"
fallback="initials"
/>
<div>
<p className="font-medium hover:text-primary">
{truncate(project.title, 40)}
</p>
<p className="text-sm text-muted-foreground">
{project.teamName}
</p>
</div>
</Link>
</TableCell>
<TableCell>
<div>
<p>{project.round.name}</p>
<p className="text-sm text-muted-foreground">
{project.round.program.name}
</p>
</div>
</TableCell>
<TableCell>{project._count.files}</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Users className="h-4 w-4 text-muted-foreground" />
{project._count.assignments}
</div>
</TableCell>
<TableCell>
<Badge variant={statusColors[project.status] || 'secondary'}>
{project.status.replace('_', ' ')}
</Badge>
</TableCell>
<TableCell className="relative z-10 text-right">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">Actions</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem asChild>
<Link href={`/admin/projects/${project.id}`}>
<Eye className="mr-2 h-4 w-4" />
View Details
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href={`/admin/projects/${project.id}/edit`}>
<Pencil className="mr-2 h-4 w-4" />
Edit
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href={`/admin/projects/${project.id}/assignments`}>
<Users className="mr-2 h-4 w-4" />
Manage Assignments
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Card>
{/* Mobile card view */}
<div className="space-y-4 md:hidden">
{projects.map((project) => (
<Link key={project.id} href={`/admin/projects/${project.id}`} className="block">
<Card className="transition-colors hover:bg-muted/50">
<CardHeader className="pb-3">
<div className="flex items-start gap-3">
<ProjectLogo
project={project}
size="md"
fallback="initials"
/>
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2">
<CardTitle className="text-base line-clamp-2">
{project.title}
</CardTitle>
<Badge variant={statusColors[project.status] || 'secondary'} className="shrink-0">
{project.status.replace('_', ' ')}
</Badge>
</div>
<CardDescription>{project.teamName}</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Round</span>
<span>{project.round.name}</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Assignments</span>
<span>{project._count.assignments} jurors</span>
</div>
</CardContent>
</Card>
</Link>
))}
</div>
</>
)
}
function ProjectsSkeleton() {
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-64" />
<Skeleton className="h-4 w-32" />
</div>
<Skeleton className="h-9 w-9" />
</div>
))}
</div>
</CardContent>
</Card>
)
}
export default function ProjectsPage() {
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-semibold tracking-tight">Projects</h1>
<p className="text-muted-foreground">
Manage submitted projects across all rounds
</p>
</div>
<div className="flex gap-2">
<Button variant="outline" asChild>
<Link href="/admin/projects/import">
<FileUp className="mr-2 h-4 w-4" />
Import
</Link>
</Button>
<Button asChild>
<Link href="/admin/projects/new">
<Plus className="mr-2 h-4 w-4" />
Add Project
</Link>
</Button>
</div>
</div>
{/* Content */}
<Suspense fallback={<ProjectsSkeleton />}>
<ProjectsContent />
</Suspense>
</div>
)
}