All checks were successful
Build and Push Docker Image / build (push) Successful in 9m5s
- Fix criteria not showing for jurors: fetch active form independently via
getStageForm query instead of relying on existing evaluation record
- Fix scoringMode default from 'global' to 'criteria' (matching schema)
- Parse scale string format ("1-10") into minScore/maxScore for criteria display
- Fix COI dialog dismissal: prevent outside click on evaluate page Dialog
- Fix requiredReviews hardcoded to 3: read from round configJson in 4 locations
- Add jury preferences banner for unconfirmed caps on jury dashboard
- Add updateJuryPreferences tRPC procedure for self-service cap/ratio
- Simplify onboarding: always show jury step, allow cap up to 50
- Add role/ratio/availability fields to jury member invite dialog
- Simplify jury group settings (keep only defaultMaxAssignments)
- Enforce deliberation showCollectiveRankings flag for non-admin users
- Redesign dashboard stat cards: editorial data strip on mobile,
clean grid layout on desktop (no more generic card pattern)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
179 lines
6.5 KiB
TypeScript
179 lines
6.5 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { useParams, useRouter } from 'next/navigation'
|
|
import { ArrowLeft, PlayCircle } from 'lucide-react'
|
|
import { trpc } from '@/lib/trpc/client'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select'
|
|
import { Skeleton } from '@/components/ui/skeleton'
|
|
import { CoverageReport } from '@/components/admin/assignment/coverage-report'
|
|
import { AssignmentPreviewSheet } from '@/components/admin/assignment/assignment-preview-sheet'
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
|
|
|
export default function AssignmentsDashboardPage() {
|
|
const params = useParams()
|
|
const router = useRouter()
|
|
const competitionId = params.competitionId as string
|
|
|
|
const [selectedRoundId, setSelectedRoundId] = useState<string>('')
|
|
const [previewSheetOpen, setPreviewSheetOpen] = useState(false)
|
|
|
|
const { data: competition, isLoading: isLoadingCompetition } = trpc.competition.getById.useQuery({
|
|
id: competitionId,
|
|
})
|
|
|
|
const { data: selectedRound } = trpc.round.getById.useQuery(
|
|
{ id: selectedRoundId },
|
|
{ enabled: !!selectedRoundId }
|
|
)
|
|
|
|
const requiredReviews = (selectedRound?.configJson as Record<string, unknown>)?.requiredReviewsPerProject as number || 3
|
|
|
|
const { data: unassignedQueue, isLoading: isLoadingQueue } =
|
|
trpc.roundAssignment.unassignedQueue.useQuery(
|
|
{ roundId: selectedRoundId, requiredReviews },
|
|
{ enabled: !!selectedRoundId }
|
|
)
|
|
|
|
const rounds = competition?.rounds || []
|
|
const currentRound = rounds.find((r) => r.id === selectedRoundId)
|
|
|
|
if (isLoadingCompetition) {
|
|
return (
|
|
<div className="container mx-auto space-y-6 p-4 sm:p-6">
|
|
<Skeleton className="h-10 w-full" />
|
|
<Skeleton className="h-96 w-full" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (!competition) {
|
|
return (
|
|
<div className="container mx-auto space-y-6 p-4 sm:p-6">
|
|
<p>Competition not found</p>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="container mx-auto space-y-6 p-4 sm:p-6">
|
|
<Button variant="ghost" onClick={() => router.back()} className="mb-4" aria-label="Back to competition details">
|
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
Back to Competition
|
|
</Button>
|
|
|
|
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
|
<div>
|
|
<h1 className="text-3xl font-bold">Assignment Dashboard</h1>
|
|
<p className="text-muted-foreground">Manage jury assignments for rounds</p>
|
|
</div>
|
|
</div>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Select Round</CardTitle>
|
|
<CardDescription>Choose a round to view and manage assignments</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Select value={selectedRoundId} onValueChange={setSelectedRoundId}>
|
|
<SelectTrigger className="w-full sm:w-[300px]">
|
|
<SelectValue placeholder="Select a round..." />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{rounds.length === 0 ? (
|
|
<div className="px-2 py-1 text-sm text-muted-foreground">No rounds available</div>
|
|
) : (
|
|
rounds.map((round) => (
|
|
<SelectItem key={round.id} value={round.id}>
|
|
{round.name} ({round.roundType})
|
|
</SelectItem>
|
|
))
|
|
)}
|
|
</SelectContent>
|
|
</Select>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{selectedRoundId && (
|
|
<div className="space-y-6">
|
|
<div className="flex justify-end">
|
|
<Button onClick={() => setPreviewSheetOpen(true)}>
|
|
<PlayCircle className="mr-2 h-4 w-4" />
|
|
Generate Assignments
|
|
</Button>
|
|
</div>
|
|
|
|
<Tabs defaultValue="coverage" className="w-full">
|
|
<TabsList className="grid w-full grid-cols-2">
|
|
<TabsTrigger value="coverage">Coverage Report</TabsTrigger>
|
|
<TabsTrigger value="unassigned">Unassigned Queue</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="coverage" className="mt-6">
|
|
<CoverageReport roundId={selectedRoundId} requiredReviews={requiredReviews} />
|
|
</TabsContent>
|
|
|
|
<TabsContent value="unassigned" className="mt-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Unassigned Projects</CardTitle>
|
|
<CardDescription>
|
|
Projects with fewer than {requiredReviews} assignments
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{isLoadingQueue ? (
|
|
<div className="space-y-2">
|
|
{[1, 2, 3].map((i) => (
|
|
<Skeleton key={i} className="h-16 w-full" />
|
|
))}
|
|
</div>
|
|
) : unassignedQueue && unassignedQueue.length > 0 ? (
|
|
<div className="space-y-2">
|
|
{unassignedQueue.map((project: any) => (
|
|
<div
|
|
key={project.id}
|
|
className="flex justify-between items-center p-3 border rounded-md"
|
|
>
|
|
<div>
|
|
<p className="font-medium">{project.title}</p>
|
|
<p className="text-sm text-muted-foreground">
|
|
{project.competitionCategory || 'No category'}
|
|
</p>
|
|
</div>
|
|
<div className="text-sm text-muted-foreground">
|
|
{project.assignmentCount || 0} / {requiredReviews} assignments
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-sm text-muted-foreground">
|
|
All projects have sufficient assignments
|
|
</p>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
</Tabs>
|
|
|
|
<AssignmentPreviewSheet
|
|
roundId={selectedRoundId}
|
|
open={previewSheetOpen}
|
|
onOpenChange={setPreviewSheetOpen}
|
|
requiredReviews={requiredReviews}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|