Decouple projects from rounds with RoundProject join table

Projects now exist at the program level instead of being locked to a
single round. A new RoundProject join table enables many-to-many
relationships with per-round status tracking. Rounds have sortOrder
for configurable progression paths.

- Add RoundProject model, programId on Project, sortOrder on Round
- Migration preserves existing data (roundId -> RoundProject entries)
- Update all routers to query through RoundProject join
- Add assign/remove/advance/reorder round endpoints
- Add Assign, Advance, Remove Projects dialogs on round detail page
- Add round reorder controls (up/down arrows) on rounds list
- Show all rounds on project detail page

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-02 22:33:55 +01:00
parent 0d2bc4db7e
commit fd5e5222da
52 changed files with 1892 additions and 326 deletions

View File

@@ -329,7 +329,7 @@ export default function MemberDetailPage() {
</TableCell>
<TableCell>
<Badge variant="secondary">
{assignment.project.status}
{assignment.project.roundProjects?.[0]?.status ?? 'SUBMITTED'}
</Badge>
</TableCell>
<TableCell className="text-sm text-muted-foreground">

View File

@@ -112,11 +112,11 @@ async function DashboardStats({ editionId, sessionName }: DashboardStatsProps) {
where: { programId: editionId },
}),
prisma.project.count({
where: { round: { programId: editionId } },
where: { programId: editionId },
}),
prisma.project.count({
where: {
round: { programId: editionId },
programId: editionId,
createdAt: { gte: sevenDaysAgo },
},
}),
@@ -149,7 +149,7 @@ async function DashboardStats({ editionId, sessionName }: DashboardStatsProps) {
include: {
_count: {
select: {
projects: true,
roundProjects: true,
assignments: true,
},
},
@@ -161,31 +161,33 @@ async function DashboardStats({ editionId, sessionName }: DashboardStatsProps) {
},
}),
prisma.project.findMany({
where: { round: { programId: editionId } },
where: { programId: editionId },
orderBy: { createdAt: 'desc' },
take: 8,
select: {
id: true,
title: true,
teamName: true,
status: true,
country: true,
competitionCategory: true,
oceanIssue: true,
logoKey: true,
createdAt: true,
submittedAt: true,
round: { select: { name: true } },
roundProjects: {
select: { status: true, round: { select: { name: true } } },
take: 1,
},
},
}),
prisma.project.groupBy({
by: ['competitionCategory'],
where: { round: { programId: editionId } },
where: { programId: editionId },
_count: true,
}),
prisma.project.groupBy({
by: ['oceanIssue'],
where: { round: { programId: editionId } },
where: { programId: editionId },
_count: true,
}),
])
@@ -392,7 +394,7 @@ async function DashboardStats({ editionId, sessionName }: DashboardStatsProps) {
</Badge>
</div>
<p className="text-sm text-muted-foreground">
{round._count.projects} projects &middot; {round._count.assignments} assignments
{round._count.roundProjects} projects &middot; {round._count.assignments} assignments
{round.totalEvals > 0 && (
<> &middot; {round.evalPercent}% evaluated</>
)}
@@ -459,10 +461,10 @@ async function DashboardStats({ editionId, sessionName }: DashboardStatsProps) {
{truncate(project.title, 45)}
</p>
<Badge
variant={statusColors[project.status] || 'secondary'}
variant={statusColors[project.roundProjects[0]?.status ?? 'SUBMITTED'] || 'secondary'}
className="shrink-0 text-[10px] px-1.5 py-0"
>
{project.status.replace('_', ' ')}
{(project.roundProjects[0]?.status ?? 'SUBMITTED').replace('_', ' ')}
</Badge>
</div>
<p className="text-xs text-muted-foreground mt-0.5">

View File

@@ -138,7 +138,7 @@ export default async function ProgramDetailPage({ params }: ProgramDetailPagePro
{round.status}
</Badge>
</TableCell>
<TableCell>{round._count.projects}</TableCell>
<TableCell>{round._count.roundProjects}</TableCell>
<TableCell>{round._count.assignments}</TableCell>
<TableCell>{formatDateOnly(round.createdAt)}</TableCell>
</TableRow>

View File

@@ -96,7 +96,7 @@ export default function ProjectAssignmentsPage() {
</div>
</div>
<Button asChild>
<Link href={`/admin/rounds/${project?.roundId}/assignments`}>
<Link href={`/admin/rounds/${project?.roundProjects?.[0]?.round?.id}/assignments`}>
<Plus className="mr-2 h-4 w-4" />
Manage in Round
</Link>

View File

@@ -121,7 +121,7 @@ function EditProjectContent({ projectId }: { projectId: string }) {
// Fetch existing tags for suggestions
const { data: existingTags } = trpc.project.getTags.useQuery({
roundId: project?.roundId,
roundId: project?.roundProjects?.[0]?.round?.id,
})
// Mutations
@@ -162,7 +162,7 @@ function EditProjectContent({ projectId }: { projectId: string }) {
title: project.title,
teamName: project.teamName || '',
description: project.description || '',
status: project.status as UpdateProjectForm['status'],
status: (project.roundProjects?.[0]?.status ?? 'SUBMITTED') as UpdateProjectForm['status'],
tags: project.tags || [],
})
}
@@ -197,6 +197,7 @@ function EditProjectContent({ projectId }: { projectId: string }) {
teamName: data.teamName || null,
description: data.description || null,
status: data.status,
roundId: project?.roundProjects?.[0]?.round?.id,
tags: data.tags,
})
}

View File

@@ -139,20 +139,29 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
fallback="initials"
/>
<div className="space-y-1">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Link
href={`/admin/rounds/${project.round.id}`}
className="hover:underline"
>
{project.round.name}
</Link>
<div className="flex flex-wrap items-center gap-1 text-sm text-muted-foreground">
{project.roundProjects?.length > 0 ? (
project.roundProjects.map((rp, i) => (
<span key={rp.round.id} className="flex items-center gap-1">
{i > 0 && <span className="text-muted-foreground/50">/</span>}
<Link
href={`/admin/rounds/${rp.round.id}`}
className="hover:underline"
>
{rp.round.name}
</Link>
</span>
))
) : (
<span>No round</span>
)}
</div>
<div className="flex items-center gap-3">
<h1 className="text-2xl font-semibold tracking-tight">
{project.title}
</h1>
<Badge variant={statusColors[project.status] || 'secondary'}>
{project.status.replace('_', ' ')}
<Badge variant={statusColors[project.roundProjects?.[0]?.status ?? 'SUBMITTED'] || 'secondary'}>
{(project.roundProjects?.[0]?.status ?? 'SUBMITTED').replace('_', ' ')}
</Badge>
</div>
{project.teamName && (
@@ -504,7 +513,7 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
</CardDescription>
</div>
<Button variant="outline" size="sm" asChild>
<Link href={`/admin/rounds/${project.roundId}/assignments`}>
<Link href={`/admin/rounds/${project.roundProjects?.[0]?.round?.id}/assignments`}>
Manage
</Link>
</Button>

View File

@@ -43,6 +43,7 @@ function ImportPageContent() {
const rounds = programs?.flatMap((p) =>
(p.rounds || []).map((r) => ({
...r,
programId: p.id,
programName: `${p.year} Edition`,
}))
) || []
@@ -170,6 +171,7 @@ function ImportPageContent() {
</TabsList>
<TabsContent value="csv" className="mt-4">
<CSVImportForm
programId={selectedRound.programId}
roundId={selectedRoundId}
roundName={selectedRound.name}
onSuccess={() => {

View File

@@ -73,6 +73,7 @@ function NewProjectPageContent() {
const rounds = programs?.flatMap((p) =>
(p.rounds || []).map((r) => ({
...r,
programId: p.id,
programName: `${p.year} Edition`,
}))
) || []
@@ -117,6 +118,7 @@ function NewProjectPageContent() {
})
createProject.mutate({
programId: selectedRound!.programId,
roundId: selectedRoundId,
title: title.trim(),
teamName: teamName.trim() || undefined,

View File

@@ -350,9 +350,9 @@ export default function ProjectsPage() {
</TableCell>
<TableCell>
<div>
<p>{project.round.name}</p>
<p>{project.roundProjects?.[0]?.round?.name ?? '-'}</p>
<p className="text-sm text-muted-foreground">
{project.round.program?.name}
{project.program?.name}
</p>
</div>
</TableCell>
@@ -365,9 +365,9 @@ export default function ProjectsPage() {
</TableCell>
<TableCell>
<Badge
variant={statusColors[project.status] || 'secondary'}
variant={statusColors[project.roundProjects?.[0]?.status ?? 'SUBMITTED'] || 'secondary'}
>
{project.status.replace('_', ' ')}
{(project.roundProjects?.[0]?.status ?? 'SUBMITTED').replace('_', ' ')}
</Badge>
</TableCell>
<TableCell className="relative z-10 text-right">
@@ -431,11 +431,11 @@ export default function ProjectsPage() {
</CardTitle>
<Badge
variant={
statusColors[project.status] || 'secondary'
statusColors[project.roundProjects?.[0]?.status ?? 'SUBMITTED'] || 'secondary'
}
className="shrink-0"
>
{project.status.replace('_', ' ')}
{(project.roundProjects?.[0]?.status ?? 'SUBMITTED').replace('_', ' ')}
</Badge>
</div>
<CardDescription>{project.teamName}</CardDescription>
@@ -445,7 +445,7 @@ export default function ProjectsPage() {
<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>
<span>{project.roundProjects?.[0]?.round?.name ?? '-'}</span>
</div>
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Assignments</span>

View File

@@ -72,7 +72,7 @@ export interface ProjectFilters {
}
interface FilterOptions {
rounds: Array<{ id: string; name: string; program: { name: string; year: number } }>
rounds: Array<{ id: string; name: string; sortOrder: number; program: { name: string; year: number } }>
countries: string[]
categories: Array<{ value: string; count: number }>
issues: Array<{ value: string; count: number }>

View File

@@ -180,7 +180,7 @@ function LiveVotingContent({ roundId }: { roundId: string }) {
if (storedOrder.length > 0) {
setProjectOrder(storedOrder)
} else {
setProjectOrder(sessionData.round.projects.map((p) => p.id))
setProjectOrder(sessionData.round.roundProjects.map((rp) => rp.project.id))
}
}
}, [sessionData])
@@ -253,7 +253,7 @@ function LiveVotingContent({ roundId }: { roundId: string }) {
)
}
const projects = sessionData.round.projects
const projects = sessionData.round.roundProjects.map((rp) => rp.project)
const sortedProjects = projectOrder
.map((id) => projects.find((p) => p.id === id))
.filter((p): p is Project => !!p)

View File

@@ -1,6 +1,6 @@
'use client'
import { Suspense, use } from 'react'
import { Suspense, use, useState } from 'react'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { trpc } from '@/lib/trpc/client'
@@ -44,8 +44,14 @@ import {
Filter,
Trash2,
Loader2,
Plus,
ArrowRightCircle,
Minus,
} from 'lucide-react'
import { toast } from 'sonner'
import { AssignProjectsDialog } from '@/components/admin/assign-projects-dialog'
import { AdvanceProjectsDialog } from '@/components/admin/advance-projects-dialog'
import { RemoveProjectsDialog } from '@/components/admin/remove-projects-dialog'
import { format, formatDistanceToNow, isPast, isFuture } from 'date-fns'
interface PageProps {
@@ -54,6 +60,9 @@ interface PageProps {
function RoundDetailContent({ roundId }: { roundId: string }) {
const router = useRouter()
const [assignOpen, setAssignOpen] = useState(false)
const [advanceOpen, setAdvanceOpen] = useState(false)
const [removeOpen, setRemoveOpen] = useState(false)
const { data: round, isLoading } = trpc.round.get.useQuery({ id: roundId })
const { data: progress } = trpc.round.getProgress.useQuery({ id: roundId })
@@ -235,7 +244,7 @@ function RoundDetailContent({ roundId }: { roundId: string }) {
<FileText className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{round._count.projects}</div>
<div className="text-2xl font-bold">{round._count.roundProjects}</div>
<Button variant="link" size="sm" className="px-0" asChild>
<Link href={`/admin/projects?round=${round.id}`}>View projects</Link>
</Button>
@@ -423,9 +432,43 @@ function RoundDetailContent({ roundId }: { roundId: string }) {
View Projects
</Link>
</Button>
<Button variant="outline" onClick={() => setAssignOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
Assign Projects
</Button>
<Button variant="outline" onClick={() => setAdvanceOpen(true)}>
<ArrowRightCircle className="mr-2 h-4 w-4" />
Advance Projects
</Button>
<Button variant="outline" onClick={() => setRemoveOpen(true)}>
<Minus className="mr-2 h-4 w-4" />
Remove Projects
</Button>
</div>
</CardContent>
</Card>
{/* Dialogs */}
<AssignProjectsDialog
roundId={roundId}
programId={round.program.id}
open={assignOpen}
onOpenChange={setAssignOpen}
onSuccess={() => utils.round.get.invalidate({ id: roundId })}
/>
<AdvanceProjectsDialog
roundId={roundId}
programId={round.program.id}
open={advanceOpen}
onOpenChange={setAdvanceOpen}
onSuccess={() => utils.round.get.invalidate({ id: roundId })}
/>
<RemoveProjectsDialog
roundId={roundId}
open={removeOpen}
onOpenChange={setRemoveOpen}
onSuccess={() => utils.round.get.invalidate({ id: roundId })}
/>
</div>
)
}

View File

@@ -16,7 +16,6 @@ import {
} from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import {
Select,
SelectContent,
@@ -34,6 +33,7 @@ import {
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { RoundTypeSettings } from '@/components/forms/round-type-settings'
import { ArrowLeft, Loader2, AlertCircle } from 'lucide-react'
const createRoundSchema = z.object({
@@ -58,6 +58,8 @@ function CreateRoundContent() {
const router = useRouter()
const searchParams = useSearchParams()
const programIdParam = searchParams.get('program')
const [roundType, setRoundType] = useState<'FILTERING' | 'EVALUATION' | 'LIVE_EVENT'>('EVALUATION')
const [roundSettings, setRoundSettings] = useState<Record<string, unknown>>({})
const { data: programs, isLoading: loadingPrograms } = trpc.program.list.useQuery()
@@ -82,7 +84,9 @@ function CreateRoundContent() {
await createRound.mutateAsync({
programId: data.programId,
name: data.name,
roundType,
requiredReviews: data.requiredReviews,
settingsJson: roundSettings,
votingStartAt: data.votingStartAt ? new Date(data.votingStartAt) : undefined,
votingEndAt: data.votingEndAt ? new Date(data.votingEndAt) : undefined,
})
@@ -218,6 +222,14 @@ function CreateRoundContent() {
</CardContent>
</Card>
{/* Round Type & Settings */}
<RoundTypeSettings
roundType={roundType}
onRoundTypeChange={setRoundType}
settings={roundSettings}
onSettingsChange={setRoundSettings}
/>
<Card>
<CardHeader>
<CardTitle className="text-lg">Voting Window</CardTitle>

View File

@@ -52,6 +52,8 @@ import {
Archive,
Trash2,
Loader2,
ChevronUp,
ChevronDown,
} from 'lucide-react'
import { format, isPast, isFuture } from 'date-fns'
@@ -106,6 +108,7 @@ function RoundsContent() {
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-20">Order</TableHead>
<TableHead>Round</TableHead>
<TableHead>Status</TableHead>
<TableHead>Voting Window</TableHead>
@@ -115,8 +118,15 @@ function RoundsContent() {
</TableRow>
</TableHeader>
<TableBody>
{program.rounds.map((round) => (
<RoundRow key={round.id} round={round} />
{program.rounds.map((round, index) => (
<RoundRow
key={round.id}
round={round}
index={index}
totalRounds={program.rounds.length}
allRoundIds={program.rounds.map((r) => r.id)}
programId={program.id}
/>
))}
</TableBody>
</Table>
@@ -133,10 +143,40 @@ function RoundsContent() {
)
}
function RoundRow({ round }: { round: any }) {
function RoundRow({
round,
index,
totalRounds,
allRoundIds,
programId,
}: {
round: any
index: number
totalRounds: number
allRoundIds: string[]
programId: string
}) {
const utils = trpc.useUtils()
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const reorder = trpc.round.reorder.useMutation({
onSuccess: () => {
utils.program.list.invalidate()
},
})
const moveUp = () => {
const ids = [...allRoundIds]
;[ids[index - 1], ids[index]] = [ids[index], ids[index - 1]]
reorder.mutate({ programId, roundIds: ids })
}
const moveDown = () => {
const ids = [...allRoundIds]
;[ids[index], ids[index + 1]] = [ids[index + 1], ids[index]]
reorder.mutate({ programId, roundIds: ids })
}
const updateStatus = trpc.round.updateStatus.useMutation({
onSuccess: () => {
utils.program.list.invalidate()
@@ -229,6 +269,28 @@ function RoundRow({ round }: { round: any }) {
return (
<TableRow>
<TableCell>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={moveUp}
disabled={index === 0 || reorder.isPending}
>
<ChevronUp className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={moveDown}
disabled={index === totalRounds - 1 || reorder.isPending}
>
<ChevronDown className="h-4 w-4" />
</Button>
</div>
</TableCell>
<TableCell>
<Link
href={`/admin/rounds/${round.id}`}
@@ -242,7 +304,7 @@ function RoundRow({ round }: { round: any }) {
<TableCell>
<div className="flex items-center gap-1">
<FileText className="h-4 w-4 text-muted-foreground" />
{round._count?.projects || 0}
{round._count?.roundProjects || 0}
</div>
</TableCell>
<TableCell>
@@ -325,9 +387,9 @@ function RoundRow({ round }: { round: any }) {
<AlertDialogTitle>Delete Round</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete &quot;{round.name}&quot;? This will
permanently delete all {round._count?.projects || 0} projects,{' '}
{round._count?.assignments || 0} assignments, and all evaluations
in this round. This action cannot be undone.
remove {round._count?.roundProjects || 0} project assignments,{' '}
{round._count?.assignments || 0} reviewer assignments, and all evaluations
in this round. The projects themselves will remain in the program. This action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>

View File

@@ -56,7 +56,6 @@ async function AssignmentsContent({
title: true,
teamName: true,
description: true,
status: true,
files: {
select: {
id: true,

View File

@@ -45,7 +45,6 @@ async function JuryDashboardContent() {
id: true,
title: true,
teamName: true,
status: true,
},
},
round: {

View File

@@ -34,11 +34,18 @@ async function EvaluateContent({ projectId }: { projectId: string }) {
redirect('/login')
}
// Get project with assignment info for this user
const project = await prisma.project.findUnique({
where: { id: projectId },
// Check if user is assigned to this project
const assignment = await prisma.assignment.findFirst({
where: {
projectId,
userId,
},
include: {
files: true,
evaluation: {
include: {
form: true,
},
},
round: {
include: {
program: {
@@ -53,25 +60,18 @@ async function EvaluateContent({ projectId }: { projectId: string }) {
},
})
// Get project details
const project = await prisma.project.findUnique({
where: { id: projectId },
include: {
files: true,
},
})
if (!project) {
notFound()
}
// Check if user is assigned to this project
const assignment = await prisma.assignment.findFirst({
where: {
projectId,
userId,
},
include: {
evaluation: {
include: {
form: true,
},
},
},
})
if (!assignment) {
return (
<div className="space-y-6">
@@ -95,7 +95,7 @@ async function EvaluateContent({ projectId }: { projectId: string }) {
)
}
const round = project.round
const round = assignment.round
const now = new Date()
// Check voting window

View File

@@ -49,10 +49,18 @@ async function EvaluationContent({ projectId }: { projectId: string }) {
redirect('/login')
}
// Get project with assignment info for this user
const project = await prisma.project.findUnique({
where: { id: projectId },
// 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: {
@@ -67,25 +75,20 @@ async function EvaluationContent({ projectId }: { projectId: string }) {
},
})
// Get project details
const project = await prisma.project.findUnique({
where: { id: projectId },
select: {
id: true,
title: true,
teamName: true,
},
})
if (!project) {
notFound()
}
// Check if user is assigned to this project
const assignment = await prisma.assignment.findFirst({
where: {
projectId,
userId,
},
include: {
evaluation: {
include: {
form: true,
},
},
},
})
if (!assignment) {
return (
<div className="space-y-6">
@@ -145,7 +148,7 @@ async function EvaluationContent({ projectId }: { projectId: string }) {
const criterionScores =
(evaluation.criterionScoresJson as unknown as Record<string, number>) || {}
const round = project.round
const round = assignment.round
return (
<div className="space-y-6">

View File

@@ -43,11 +43,14 @@ async function ProjectContent({ projectId }: { projectId: string }) {
redirect('/login')
}
// Get project with assignment info for this user
const project = await prisma.project.findUnique({
where: { id: projectId },
// Check if user is assigned to this project
const assignment = await prisma.assignment.findFirst({
where: {
projectId,
userId,
},
include: {
files: true,
evaluation: true,
round: {
include: {
program: {
@@ -62,21 +65,18 @@ async function ProjectContent({ projectId }: { projectId: string }) {
},
})
// Get project details
const project = await prisma.project.findUnique({
where: { id: projectId },
include: {
files: true,
},
})
if (!project) {
notFound()
}
// Check if user is assigned to this project
const assignment = await prisma.assignment.findFirst({
where: {
projectId,
userId,
},
include: {
evaluation: true,
},
})
if (!assignment) {
// User is not assigned to this project
return (
@@ -99,7 +99,7 @@ async function ProjectContent({ projectId }: { projectId: string }) {
}
const evaluation = assignment.evaluation
const round = project.round
const round = assignment.round
const now = new Date()
// Check voting window

View File

@@ -149,18 +149,24 @@ export default function MentorDashboard() {
<div className="space-y-1">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>
{project.round.program.year} Edition
{project.program.year} Edition
</span>
<span></span>
<span>{project.round.name}</span>
{project.roundProjects?.[0]?.round && (
<>
<span></span>
<span>{project.roundProjects[0].round.name}</span>
</>
)}
</div>
<CardTitle className="flex items-center gap-2">
{project.title}
<Badge
variant={statusColors[project.status] || 'secondary'}
>
{project.status.replace('_', ' ')}
</Badge>
{project.roundProjects?.[0]?.status && (
<Badge
variant={statusColors[project.roundProjects[0].status] || 'secondary'}
>
{project.roundProjects[0].status.replace('_', ' ')}
</Badge>
)}
</CardTitle>
{project.teamName && (
<CardDescription>{project.teamName}</CardDescription>

View File

@@ -109,18 +109,24 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
<div className="space-y-1">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>
{project.round.program.year} Edition
{project.program.year} Edition
</span>
<span></span>
<span>{project.round.name}</span>
{project.roundProjects?.[0]?.round && (
<>
<span></span>
<span>{project.roundProjects[0].round.name}</span>
</>
)}
</div>
<div className="flex items-center gap-3">
<h1 className="text-2xl font-semibold tracking-tight">
{project.title}
</h1>
<Badge variant={statusColors[project.status] || 'secondary'}>
{project.status.replace('_', ' ')}
</Badge>
{project.roundProjects?.[0]?.status && (
<Badge variant={statusColors[project.roundProjects[0].status] || 'secondary'}>
{project.roundProjects[0].status.replace('_', ' ')}
</Badge>
)}
</div>
{project.teamName && (
<p className="text-muted-foreground">{project.teamName}</p>

View File

@@ -94,16 +94,22 @@ export default function MentorProjectsPage() {
<div className="space-y-1">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>
{project.round.program.year} Edition
{project.program.year} Edition
</span>
<span></span>
<span>{project.round.name}</span>
{project.roundProjects?.[0]?.round && (
<>
<span></span>
<span>{project.roundProjects[0].round.name}</span>
</>
)}
</div>
<CardTitle className="flex items-center gap-2">
{project.title}
<Badge variant={statusColors[project.status] || 'secondary'}>
{project.status.replace('_', ' ')}
</Badge>
{project.roundProjects?.[0]?.status && (
<Badge variant={statusColors[project.roundProjects[0].status] || 'secondary'}>
{project.roundProjects[0].status.replace('_', ' ')}
</Badge>
)}
</CardTitle>
{project.teamName && (
<CardDescription>{project.teamName}</CardDescription>

View File

@@ -48,7 +48,7 @@ async function ObserverDashboardContent() {
program: { select: { name: true, year: true } },
_count: {
select: {
projects: true,
roundProjects: true,
assignments: true,
},
},
@@ -176,7 +176,7 @@ async function ObserverDashboardContent() {
</p>
</div>
<div className="text-right text-sm">
<p>{round._count.projects} projects</p>
<p>{round._count.roundProjects} projects</p>
<p className="text-muted-foreground">
{round._count.assignments} assignments
</p>

View File

@@ -34,7 +34,7 @@ async function ReportsContent() {
},
_count: {
select: {
projects: true,
roundProjects: true,
assignments: true,
},
},
@@ -70,7 +70,7 @@ async function ReportsContent() {
})
// Calculate totals
const totalProjects = roundStats.reduce((acc, r) => acc + r._count.projects, 0)
const totalProjects = roundStats.reduce((acc, r) => acc + r._count.roundProjects, 0)
const totalAssignments = roundStats.reduce(
(acc, r) => acc + r.totalAssignments,
0
@@ -176,7 +176,7 @@ async function ReportsContent() {
</div>
</TableCell>
<TableCell>{round.program.name}</TableCell>
<TableCell>{round._count.projects}</TableCell>
<TableCell>{round._count.roundProjects}</TableCell>
<TableCell>
<div className="min-w-[120px] space-y-1">
<div className="flex justify-between text-sm">
@@ -237,7 +237,7 @@ async function ReportsContent() {
</p>
)}
<div className="flex items-center justify-between text-sm">
<span>{round._count.projects} projects</span>
<span>{round._count.roundProjects} projects</span>
<span className="text-muted-foreground">
{round.completedEvaluations}/{round.totalAssignments} evaluations
</span>

View File

@@ -132,7 +132,7 @@ export function SubmissionDetailClient() {
</Badge>
</div>
<p className="text-muted-foreground">
{project.round.program.year} Edition - {project.round.name}
{project.roundProjects?.[0]?.round?.program?.year ? `${project.roundProjects[0].round.program.year} Edition` : ''}{project.roundProjects?.[0]?.round?.name ? ` - ${project.roundProjects[0].round.name}` : ''}
</p>
</div>
</div>

View File

@@ -131,18 +131,24 @@ export function MySubmissionClient() {
</Card>
) : (
<div className="space-y-4">
{submissions.map((project) => (
{submissions.map((project) => {
const latestRoundProject = project.roundProjects?.[0]
const projectStatus = latestRoundProject?.status ?? 'SUBMITTED'
const roundName = latestRoundProject?.round?.name
const programYear = latestRoundProject?.round?.program?.year
return (
<Card key={project.id}>
<CardHeader className="pb-3">
<div className="flex items-start justify-between">
<div>
<CardTitle className="text-lg">{project.title}</CardTitle>
<CardDescription>
{project.round.program.year} Edition - {project.round.name}
{programYear ? `${programYear} Edition` : ''}{roundName ? ` - ${roundName}` : ''}
</CardDescription>
</div>
<Badge variant={statusColors[project.status] || 'secondary'}>
{project.status.replace('_', ' ')}
<Badge variant={statusColors[projectStatus] || 'secondary'}>
{projectStatus.replace('_', ' ')}
</Badge>
</div>
</CardHeader>
@@ -197,22 +203,22 @@ export function MySubmissionClient() {
status: 'UNDER_REVIEW',
label: 'Under Review',
date: null,
completed: ['UNDER_REVIEW', 'SEMIFINALIST', 'FINALIST', 'WINNER'].includes(project.status),
completed: ['UNDER_REVIEW', 'SEMIFINALIST', 'FINALIST', 'WINNER'].includes(projectStatus),
},
{
status: 'SEMIFINALIST',
label: 'Semi-finalist',
date: null,
completed: ['SEMIFINALIST', 'FINALIST', 'WINNER'].includes(project.status),
completed: ['SEMIFINALIST', 'FINALIST', 'WINNER'].includes(projectStatus),
},
{
status: 'FINALIST',
label: 'Finalist',
date: null,
completed: ['FINALIST', 'WINNER'].includes(project.status),
completed: ['FINALIST', 'WINNER'].includes(projectStatus),
},
]}
currentStatus={project.status}
currentStatus={projectStatus}
className="mt-4"
/>
</div>
@@ -229,7 +235,8 @@ export function MySubmissionClient() {
</div>
</CardContent>
</Card>
))}
)
})}
</div>
)}
</div>