fix: save roundId on admin file upload and group assignments by round
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m45s
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m45s
The admin upload flow accepted roundId but never wrote it to the ProjectFile record, causing all admin-uploaded files to appear under "General". Fixed the create call, the listByProject filter, and the listByProjectForStage grouping to also use the direct roundId field. Jury assignments on the project detail page are now grouped by round with per-round completion counts instead of a flat list. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -845,9 +845,10 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
|
|||||||
} : null,
|
} : null,
|
||||||
}))
|
}))
|
||||||
for (const f of files) {
|
for (const f of files) {
|
||||||
const roundId = f.requirement?.roundId ?? null
|
const roundId = f.requirement?.roundId ?? f.roundId ?? null
|
||||||
const roundName = f.requirement?.round?.name ?? 'General'
|
const matchedRound = roundId ? competitionRounds.find((r: any) => r.id === roundId) : null
|
||||||
const sortOrder = f.requirement?.round?.sortOrder ?? -1
|
const roundName = f.requirement?.round?.name ?? matchedRound?.name ?? 'General'
|
||||||
|
const sortOrder = f.requirement?.round?.sortOrder ?? matchedRound?.sortOrder ?? -1
|
||||||
const key = roundId ?? '_general'
|
const key = roundId ?? '_general'
|
||||||
if (!groups.has(key)) {
|
if (!groups.has(key)) {
|
||||||
groups.set(key, { roundId, roundName, sortOrder, files: [] })
|
groups.set(key, { roundId, roundName, sortOrder, files: [] })
|
||||||
@@ -864,141 +865,172 @@ function ProjectDetailContent({ projectId }: { projectId: string }) {
|
|||||||
</Card>
|
</Card>
|
||||||
</AnimatedCard>
|
</AnimatedCard>
|
||||||
|
|
||||||
{/* Assignments Section */}
|
{/* Assignments Section — grouped by round */}
|
||||||
{assignments && assignments.length > 0 && (
|
{assignments && assignments.length > 0 && (() => {
|
||||||
<AnimatedCard index={5}>
|
// Group assignments by round
|
||||||
<Card>
|
const roundGroups = new Map<string, {
|
||||||
<CardHeader>
|
roundId: string
|
||||||
<div className="flex items-center justify-between">
|
roundName: string
|
||||||
<div>
|
assignments: typeof assignments
|
||||||
<CardTitle className="flex items-center gap-2.5 text-lg">
|
}>()
|
||||||
<div className="rounded-lg bg-violet-500/10 p-1.5">
|
for (const a of assignments) {
|
||||||
<Users className="h-4 w-4 text-violet-500" />
|
const rId = a.round?.id ?? '_unknown'
|
||||||
</div>
|
const rName = a.round?.name ?? 'Unknown Round'
|
||||||
Jury Assignments
|
if (!roundGroups.has(rId)) {
|
||||||
</CardTitle>
|
roundGroups.set(rId, { roundId: rId, roundName: rName, assignments: [] })
|
||||||
<CardDescription>
|
}
|
||||||
{assignments.filter((a) => a.evaluation?.status === 'SUBMITTED')
|
roundGroups.get(rId)!.assignments.push(a)
|
||||||
.length}{' '}
|
}
|
||||||
of {assignments.length} evaluations completed
|
const groups = Array.from(roundGroups.values())
|
||||||
</CardDescription>
|
|
||||||
|
return (
|
||||||
|
<AnimatedCard index={5}>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<CardTitle className="flex items-center gap-2.5 text-lg">
|
||||||
|
<div className="rounded-lg bg-violet-500/10 p-1.5">
|
||||||
|
<Users className="h-4 w-4 text-violet-500" />
|
||||||
|
</div>
|
||||||
|
Jury Assignments
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{assignments.filter((a) => a.evaluation?.status === 'SUBMITTED')
|
||||||
|
.length}{' '}
|
||||||
|
of {assignments.length} evaluations completed across {groups.length} round{groups.length !== 1 ? 's' : ''}
|
||||||
|
</CardDescription>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" size="sm" asChild>
|
||||||
|
<Link href={`/admin/members`}>
|
||||||
|
Manage
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="sm" asChild>
|
</CardHeader>
|
||||||
<Link href={`/admin/members`}>
|
<CardContent className="space-y-6">
|
||||||
Manage
|
{groups.map((group) => {
|
||||||
</Link>
|
const submitted = group.assignments.filter((a) => a.evaluation?.status === 'SUBMITTED').length
|
||||||
</Button>
|
return (
|
||||||
</div>
|
<div key={group.roundId}>
|
||||||
</CardHeader>
|
<div className="flex items-center justify-between mb-2">
|
||||||
<CardContent>
|
<h4 className="text-sm font-semibold">{group.roundName}</h4>
|
||||||
<Table>
|
<span className="text-xs text-muted-foreground">
|
||||||
<TableHeader>
|
{submitted} of {group.assignments.length} completed
|
||||||
<TableRow>
|
</span>
|
||||||
<TableHead>Juror</TableHead>
|
</div>
|
||||||
<TableHead>Expertise</TableHead>
|
<Table>
|
||||||
<TableHead>Status</TableHead>
|
<TableHeader>
|
||||||
<TableHead>Score</TableHead>
|
<TableRow>
|
||||||
<TableHead>Decision</TableHead>
|
<TableHead>Juror</TableHead>
|
||||||
<TableHead className="w-10"></TableHead>
|
<TableHead>Expertise</TableHead>
|
||||||
</TableRow>
|
<TableHead>Status</TableHead>
|
||||||
</TableHeader>
|
<TableHead>Score</TableHead>
|
||||||
<TableBody>
|
<TableHead>Decision</TableHead>
|
||||||
{assignments.map((assignment) => (
|
<TableHead className="w-10"></TableHead>
|
||||||
<TableRow
|
</TableRow>
|
||||||
key={assignment.id}
|
</TableHeader>
|
||||||
className={assignment.evaluation?.status === 'SUBMITTED' ? 'cursor-pointer hover:bg-muted/50' : ''}
|
<TableBody>
|
||||||
onClick={() => {
|
{group.assignments.map((assignment) => (
|
||||||
if (assignment.evaluation?.status === 'SUBMITTED') {
|
<TableRow
|
||||||
setSelectedEvalAssignment(assignment)
|
key={assignment.id}
|
||||||
}
|
className={assignment.evaluation?.status === 'SUBMITTED' ? 'cursor-pointer hover:bg-muted/50' : ''}
|
||||||
}}
|
onClick={() => {
|
||||||
>
|
if (assignment.evaluation?.status === 'SUBMITTED') {
|
||||||
<TableCell>
|
setSelectedEvalAssignment(assignment)
|
||||||
<div className="flex items-center gap-2">
|
}
|
||||||
<UserAvatar
|
}}
|
||||||
user={assignment.user}
|
>
|
||||||
avatarUrl={assignment.user.avatarUrl}
|
<TableCell>
|
||||||
size="sm"
|
<div className="flex items-center gap-2">
|
||||||
/>
|
<UserAvatar
|
||||||
<div>
|
user={assignment.user}
|
||||||
<p className="font-medium text-sm">
|
avatarUrl={assignment.user.avatarUrl}
|
||||||
{assignment.user.name || 'Unnamed'}
|
size="sm"
|
||||||
</p>
|
/>
|
||||||
<p className="text-xs text-muted-foreground">
|
<div>
|
||||||
{assignment.user.email}
|
<p className="font-medium text-sm">
|
||||||
</p>
|
{assignment.user.name || 'Unnamed'}
|
||||||
</div>
|
</p>
|
||||||
</div>
|
<p className="text-xs text-muted-foreground">
|
||||||
</TableCell>
|
{assignment.user.email}
|
||||||
<TableCell>
|
</p>
|
||||||
<div className="flex flex-wrap gap-1">
|
</div>
|
||||||
{assignment.user.expertiseTags?.slice(0, 2).map((tag) => (
|
</div>
|
||||||
<Badge key={tag} variant="outline" className="text-xs">
|
</TableCell>
|
||||||
{tag}
|
<TableCell>
|
||||||
</Badge>
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{assignment.user.expertiseTags?.slice(0, 2).map((tag) => (
|
||||||
|
<Badge key={tag} variant="outline" className="text-xs">
|
||||||
|
{tag}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
{(assignment.user.expertiseTags?.length || 0) > 2 && (
|
||||||
|
<Badge variant="outline" className="text-xs">
|
||||||
|
+{(assignment.user.expertiseTags?.length || 0) - 2}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
evalStatusColors[
|
||||||
|
assignment.evaluation?.status || 'NOT_STARTED'
|
||||||
|
] || 'secondary'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(assignment.evaluation?.status || 'NOT_STARTED').replace(
|
||||||
|
'_',
|
||||||
|
' '
|
||||||
|
)}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{assignment.evaluation?.globalScore !== null &&
|
||||||
|
assignment.evaluation?.globalScore !== undefined ? (
|
||||||
|
<span className="font-medium">
|
||||||
|
{assignment.evaluation.globalScore}/10
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground">-</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{assignment.evaluation?.binaryDecision !== null &&
|
||||||
|
assignment.evaluation?.binaryDecision !== undefined ? (
|
||||||
|
assignment.evaluation.binaryDecision ? (
|
||||||
|
<div className="flex items-center gap-1 text-green-600">
|
||||||
|
<ThumbsUp className="h-4 w-4" />
|
||||||
|
<span className="text-sm">Yes</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center gap-1 text-red-600">
|
||||||
|
<ThumbsDown className="h-4 w-4" />
|
||||||
|
<span className="text-sm">No</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<span className="text-muted-foreground">-</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{assignment.evaluation?.status === 'SUBMITTED' && (
|
||||||
|
<Eye className="h-4 w-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
{(assignment.user.expertiseTags?.length || 0) > 2 && (
|
</TableBody>
|
||||||
<Badge variant="outline" className="text-xs">
|
</Table>
|
||||||
+{(assignment.user.expertiseTags?.length || 0) - 2}
|
</div>
|
||||||
</Badge>
|
)
|
||||||
)}
|
})}
|
||||||
</div>
|
</CardContent>
|
||||||
</TableCell>
|
</Card>
|
||||||
<TableCell>
|
</AnimatedCard>
|
||||||
<Badge
|
)
|
||||||
variant={
|
})()}
|
||||||
evalStatusColors[
|
|
||||||
assignment.evaluation?.status || 'NOT_STARTED'
|
|
||||||
] || 'secondary'
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{(assignment.evaluation?.status || 'NOT_STARTED').replace(
|
|
||||||
'_',
|
|
||||||
' '
|
|
||||||
)}
|
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{assignment.evaluation?.globalScore !== null &&
|
|
||||||
assignment.evaluation?.globalScore !== undefined ? (
|
|
||||||
<span className="font-medium">
|
|
||||||
{assignment.evaluation.globalScore}/10
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-muted-foreground">-</span>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{assignment.evaluation?.binaryDecision !== null &&
|
|
||||||
assignment.evaluation?.binaryDecision !== undefined ? (
|
|
||||||
assignment.evaluation.binaryDecision ? (
|
|
||||||
<div className="flex items-center gap-1 text-green-600">
|
|
||||||
<ThumbsUp className="h-4 w-4" />
|
|
||||||
<span className="text-sm">Yes</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex items-center gap-1 text-red-600">
|
|
||||||
<ThumbsDown className="h-4 w-4" />
|
|
||||||
<span className="text-sm">No</span>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<span className="text-muted-foreground">-</span>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{assignment.evaluation?.status === 'SUBMITTED' && (
|
|
||||||
<Eye className="h-4 w-4 text-muted-foreground" />
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</AnimatedCard>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Evaluation Detail Sheet */}
|
{/* Evaluation Detail Sheet */}
|
||||||
<EvaluationEditSheet
|
<EvaluationEditSheet
|
||||||
|
|||||||
@@ -54,8 +54,8 @@ export function MultiWindowDocViewer({ roundId, projectId }: MultiWindowDocViewe
|
|||||||
}> = {}
|
}> = {}
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const roundName = file.requirement?.round?.name ?? 'General'
|
const rId = file.requirement?.round?.id ?? (file as any).roundId ?? null
|
||||||
const rId = file.requirement?.round?.id ?? null
|
const roundName = file.requirement?.round?.name ?? (rId ? 'Round Files' : 'General')
|
||||||
const sortOrder = file.requirement?.round?.sortOrder ?? 999
|
const sortOrder = file.requirement?.round?.sortOrder ?? 999
|
||||||
if (!groupMap[roundName]) {
|
if (!groupMap[roundName]) {
|
||||||
groupMap[roundName] = { roundId: rId, roundName, sortOrder, files: [] }
|
groupMap[roundName] = { roundId: rId, roundName, sortOrder, files: [] }
|
||||||
|
|||||||
@@ -206,6 +206,7 @@ export const fileRouter = router({
|
|||||||
const file = await ctx.prisma.projectFile.create({
|
const file = await ctx.prisma.projectFile.create({
|
||||||
data: {
|
data: {
|
||||||
projectId: input.projectId,
|
projectId: input.projectId,
|
||||||
|
roundId: input.roundId ?? null,
|
||||||
fileType: input.fileType,
|
fileType: input.fileType,
|
||||||
fileName: input.fileName,
|
fileName: input.fileName,
|
||||||
mimeType: input.mimeType,
|
mimeType: input.mimeType,
|
||||||
@@ -341,7 +342,10 @@ export const fileRouter = router({
|
|||||||
|
|
||||||
const where: Record<string, unknown> = { projectId: input.projectId }
|
const where: Record<string, unknown> = { projectId: input.projectId }
|
||||||
if (input.roundId) {
|
if (input.roundId) {
|
||||||
where.requirement = { roundId: input.roundId }
|
where.OR = [
|
||||||
|
{ requirement: { roundId: input.roundId } },
|
||||||
|
{ roundId: input.roundId, requirementId: null },
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.prisma.projectFile.findMany({
|
return ctx.prisma.projectFile.findMany({
|
||||||
@@ -429,7 +433,8 @@ export const fileRouter = router({
|
|||||||
projectId: input.projectId,
|
projectId: input.projectId,
|
||||||
OR: [
|
OR: [
|
||||||
{ requirement: { roundId: { in: eligibleRoundIds } } },
|
{ requirement: { roundId: { in: eligibleRoundIds } } },
|
||||||
{ requirementId: null },
|
{ roundId: { in: eligibleRoundIds }, requirementId: null },
|
||||||
|
{ roundId: null, requirementId: null },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
@@ -454,7 +459,8 @@ export const fileRouter = router({
|
|||||||
files: typeof files
|
files: typeof files
|
||||||
}> = []
|
}> = []
|
||||||
|
|
||||||
const generalFiles = files.filter((f) => !f.requirementId)
|
// Files with no round association at all go to General
|
||||||
|
const generalFiles = files.filter((f) => !f.requirementId && !f.roundId)
|
||||||
if (generalFiles.length > 0) {
|
if (generalFiles.length > 0) {
|
||||||
grouped.push({
|
grouped.push({
|
||||||
roundId: null,
|
roundId: null,
|
||||||
@@ -465,7 +471,9 @@ export const fileRouter = router({
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const round of eligibleRounds) {
|
for (const round of eligibleRounds) {
|
||||||
const roundFiles = files.filter((f) => f.requirement?.roundId === round.id)
|
const roundFiles = files.filter(
|
||||||
|
(f) => f.requirement?.roundId === round.id || (!f.requirementId && f.roundId === round.id)
|
||||||
|
)
|
||||||
if (roundFiles.length > 0) {
|
if (roundFiles.length > 0) {
|
||||||
grouped.push({
|
grouped.push({
|
||||||
roundId: round.id,
|
roundId: round.id,
|
||||||
|
|||||||
Reference in New Issue
Block a user