From f26ee3f07655d33dd8486fb4b3dd712d4228a935 Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 22 Feb 2026 17:14:00 +0100 Subject: [PATCH] Admin dashboard & round management UX overhaul MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract round detail monolith (2900→600 lines) into 13 standalone components - Add shared round/status config (round-config.ts) replacing 4 local copies - Delete 12 legacy competition-scoped pages, merge project pool into projects page - Add round-type-specific dashboard stat panels (submission, mentoring, live final, deliberation, summary) - Add contextual header quick actions based on active round type - Improve pipeline visualization: progress bars, checkmarks, chevron connectors, overflow fix - Add config tab completion dots (green/amber/red) and inline validation warnings - Enhance juries page with round assignments, member avatars, and cap mode badges - Add context-aware project list (recent submissions vs active evaluations) - Move competition settings into Manage Editions page Co-Authored-By: Claude Opus 4.6 --- .../[competitionId]/assignments/page.tsx | 206 -- .../[competitionId]/awards/[awardId]/page.tsx | 154 - .../[competitionId]/awards/new/page.tsx | 199 -- .../[competitionId]/awards/page.tsx | 104 - .../deliberation/[sessionId]/page.tsx | 255 -- .../[competitionId]/deliberation/page.tsx | 411 --- .../juries/[juryGroupId]/page.tsx | 142 - .../[competitionId]/juries/page.tsx | 187 -- .../[competitionId]/live/[roundId]/page.tsx | 37 - .../competitions/[competitionId]/page.tsx | 585 ---- .../(admin)/admin/competitions/new/page.tsx | 307 -- src/app/(admin)/admin/competitions/page.tsx | 201 -- src/app/(admin)/admin/dashboard-content.tsx | 130 +- src/app/(admin)/admin/juries/page.tsx | 93 +- src/app/(admin)/admin/programs/[id]/page.tsx | 23 +- src/app/(admin)/admin/projects/page.tsx | 2 +- src/app/(admin)/admin/projects/pool/page.tsx | 558 ---- .../(admin)/admin/rounds/[roundId]/page.tsx | 2942 ++--------------- src/app/(admin)/admin/rounds/page.tsx | 55 +- .../admin/assignment/coi-review-section.tsx | 170 + .../individual-assignments-table.tsx | 854 +++++ .../admin/assignment/jury-progress-table.tsx | 180 + .../admin/assignment/notify-jurors-button.tsx | 61 + .../admin/assignment/reassignment-history.tsx | 111 + .../assignment/round-unassigned-queue.tsx | 66 + .../assignment/send-reminders-button.tsx | 61 + .../transfer-assignments-dialog.tsx | 328 ++ .../competition/competition-timeline.tsx | 26 +- .../competition/sections/review-section.tsx | 13 +- .../admin/jury/inline-member-cap.tsx | 164 + .../admin/program/competition-settings.tsx | 165 + .../admin/round/advance-projects-dialog.tsx | 289 ++ .../round/ai-recommendations-display.tsx | 231 ++ .../round/evaluation-criteria-editor.tsx | 124 + .../admin/round/export-evaluations-dialog.tsx | 43 + .../admin/round/project-states-table.tsx | 2 +- .../admin/round/score-distribution.tsx | 64 + .../rounds/config/config-section-header.tsx | 51 + .../dashboard/active-round-panel.tsx | 33 +- .../dashboard/competition-pipeline.tsx | 51 +- .../dashboard/pipeline-round-node.tsx | 152 +- .../dashboard/project-list-compact.tsx | 154 +- .../dashboard/round-stats-deliberation.tsx | 99 + .../dashboard/round-stats-live-final.tsx | 105 + .../dashboard/round-stats-mentoring.tsx | 99 + .../dashboard/round-stats-submission.tsx | 109 + ...ts-generic.tsx => round-stats-summary.tsx} | 45 +- src/components/dashboard/round-stats.tsx | 31 +- src/lib/round-config.ts | 264 ++ src/server/routers/dashboard.ts | 57 +- src/server/routers/juryGroup.ts | 13 + 51 files changed, 4530 insertions(+), 6276 deletions(-) delete mode 100644 src/app/(admin)/admin/competitions/[competitionId]/assignments/page.tsx delete mode 100644 src/app/(admin)/admin/competitions/[competitionId]/awards/[awardId]/page.tsx delete mode 100644 src/app/(admin)/admin/competitions/[competitionId]/awards/new/page.tsx delete mode 100644 src/app/(admin)/admin/competitions/[competitionId]/awards/page.tsx delete mode 100644 src/app/(admin)/admin/competitions/[competitionId]/deliberation/[sessionId]/page.tsx delete mode 100644 src/app/(admin)/admin/competitions/[competitionId]/deliberation/page.tsx delete mode 100644 src/app/(admin)/admin/competitions/[competitionId]/juries/[juryGroupId]/page.tsx delete mode 100644 src/app/(admin)/admin/competitions/[competitionId]/juries/page.tsx delete mode 100644 src/app/(admin)/admin/competitions/[competitionId]/live/[roundId]/page.tsx delete mode 100644 src/app/(admin)/admin/competitions/[competitionId]/page.tsx delete mode 100644 src/app/(admin)/admin/competitions/new/page.tsx delete mode 100644 src/app/(admin)/admin/competitions/page.tsx delete mode 100644 src/app/(admin)/admin/projects/pool/page.tsx create mode 100644 src/components/admin/assignment/coi-review-section.tsx create mode 100644 src/components/admin/assignment/individual-assignments-table.tsx create mode 100644 src/components/admin/assignment/jury-progress-table.tsx create mode 100644 src/components/admin/assignment/notify-jurors-button.tsx create mode 100644 src/components/admin/assignment/reassignment-history.tsx create mode 100644 src/components/admin/assignment/round-unassigned-queue.tsx create mode 100644 src/components/admin/assignment/send-reminders-button.tsx create mode 100644 src/components/admin/assignment/transfer-assignments-dialog.tsx create mode 100644 src/components/admin/jury/inline-member-cap.tsx create mode 100644 src/components/admin/program/competition-settings.tsx create mode 100644 src/components/admin/round/advance-projects-dialog.tsx create mode 100644 src/components/admin/round/ai-recommendations-display.tsx create mode 100644 src/components/admin/round/evaluation-criteria-editor.tsx create mode 100644 src/components/admin/round/export-evaluations-dialog.tsx create mode 100644 src/components/admin/round/score-distribution.tsx create mode 100644 src/components/admin/rounds/config/config-section-header.tsx create mode 100644 src/components/dashboard/round-stats-deliberation.tsx create mode 100644 src/components/dashboard/round-stats-live-final.tsx create mode 100644 src/components/dashboard/round-stats-mentoring.tsx create mode 100644 src/components/dashboard/round-stats-submission.tsx rename src/components/dashboard/{round-stats-generic.tsx => round-stats-summary.tsx} (66%) create mode 100644 src/lib/round-config.ts diff --git a/src/app/(admin)/admin/competitions/[competitionId]/assignments/page.tsx b/src/app/(admin)/admin/competitions/[competitionId]/assignments/page.tsx deleted file mode 100644 index b3edd71..0000000 --- a/src/app/(admin)/admin/competitions/[competitionId]/assignments/page.tsx +++ /dev/null @@ -1,206 +0,0 @@ -'use client' - -import { useState } from 'react' -import { useParams, useRouter } from 'next/navigation' -import { ArrowLeft, Loader2, PlayCircle, Zap } from 'lucide-react' -import { toast } from 'sonner' -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('') - const [previewSheetOpen, setPreviewSheetOpen] = useState(false) - - const aiAssignmentMutation = trpc.roundAssignment.aiPreview.useMutation({ - onSuccess: () => { - toast.success('AI assignments ready!', { - action: { label: 'Review', onClick: () => setPreviewSheetOpen(true) }, - duration: 10000, - }) - }, - onError: (err) => toast.error(`AI generation failed: ${err.message}`), - }) - - 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)?.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 ( -
- - -
- ) - } - - if (!competition) { - return ( -
-

Competition not found

-
- ) - } - - return ( -
- - -
-
-

Assignment Dashboard

-

Manage jury assignments for rounds

-
-
- - - - Select Round - Choose a round to view and manage assignments - - - - - - - {selectedRoundId && ( -
-
- - {aiAssignmentMutation.data && ( - - )} -
- - - - Coverage Report - Unassigned Queue - - - - - - - - - - Unassigned Projects - - Projects with fewer than {requiredReviews} assignments - - - - {isLoadingQueue ? ( -
- {[1, 2, 3].map((i) => ( - - ))} -
- ) : unassignedQueue && unassignedQueue.length > 0 ? ( -
- {unassignedQueue.map((project: any) => ( -
-
-

{project.title}

-

- {project.competitionCategory || 'No category'} -

-
-
- {project.assignmentCount || 0} / {requiredReviews} assignments -
-
- ))} -
- ) : ( -

- All projects have sufficient assignments -

- )} -
-
-
-
- - aiAssignmentMutation.mutate({ roundId: selectedRoundId, requiredReviews })} - onResetAI={() => aiAssignmentMutation.reset()} - /> -
- )} -
- ) -} diff --git a/src/app/(admin)/admin/competitions/[competitionId]/awards/[awardId]/page.tsx b/src/app/(admin)/admin/competitions/[competitionId]/awards/[awardId]/page.tsx deleted file mode 100644 index 63c1543..0000000 --- a/src/app/(admin)/admin/competitions/[competitionId]/awards/[awardId]/page.tsx +++ /dev/null @@ -1,154 +0,0 @@ -'use client'; - -import { use } from 'react'; -import { useRouter } from 'next/navigation'; -import { trpc } from '@/lib/trpc/client'; -import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import { Badge } from '@/components/ui/badge'; -import { ArrowLeft } from 'lucide-react'; -import type { Route } from 'next'; - -export default function AwardDetailPage({ - params: paramsPromise -}: { - params: Promise<{ competitionId: string; awardId: string }>; -}) { - const params = use(paramsPromise); - const router = useRouter(); - const { data: award, isLoading } = trpc.specialAward.get.useQuery({ - id: params.awardId - }); - - if (isLoading) { - return ( -
-
- -
-

Loading...

-
-
-
- ); - } - - if (!award) { - return ( -
-
- -
-

Award Not Found

-
-
-
- ); - } - - return ( -
-
- -
-

{award.name}

-

{award.description || 'No description'}

-
-
- - - - Overview - Eligible Projects - Winners - - - - - - Award Information - Configuration and settings - - -
-
-

Scoring Mode

- - {award.scoringMode} - -
-
-

AI Eligibility

- - {award.useAiEligibility ? 'Enabled' : 'Disabled'} - -
-
-

Status

- - {award.status} - -
-
-

Program

-

{award.program?.name}

-
-
-
-
-
- - - - - Eligible Projects - - Projects that qualify for this award ({award?.eligibleCount || 0}) - - - -

- {award?.eligibleCount || 0} eligible projects -

-
-
-
- - - - - Award Winners - Selected winners for this award - - - {award?.winnerProject ? ( -
-
-

{award.winnerProject.title}

-

{award.winnerProject.teamName}

-
- Winner -
- ) : ( -

No winner selected yet

- )} -
-
-
-
-
- ); -} diff --git a/src/app/(admin)/admin/competitions/[competitionId]/awards/new/page.tsx b/src/app/(admin)/admin/competitions/[competitionId]/awards/new/page.tsx deleted file mode 100644 index 9112d0f..0000000 --- a/src/app/(admin)/admin/competitions/[competitionId]/awards/new/page.tsx +++ /dev/null @@ -1,199 +0,0 @@ -'use client'; - -import { use, useState } from 'react'; -import { useRouter } from 'next/navigation'; -import { trpc } from '@/lib/trpc/client'; -import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Textarea } from '@/components/ui/textarea'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { Checkbox } from '@/components/ui/checkbox'; -import { ArrowLeft } from 'lucide-react'; -import { toast } from 'sonner'; -import type { Route } from 'next'; - -export default function NewAwardPage({ params: paramsPromise }: { params: Promise<{ competitionId: string }> }) { - const params = use(paramsPromise); - const router = useRouter(); - const utils = trpc.useUtils(); - - const [formData, setFormData] = useState({ - name: '', - description: '', - criteriaText: '', - useAiEligibility: false, - scoringMode: 'PICK_WINNER' as 'PICK_WINNER' | 'RANKED' | 'SCORED', - maxRankedPicks: '3', - }); - - const { data: competition } = trpc.competition.getById.useQuery({ - id: params.competitionId - }); - - const { data: juryGroups } = trpc.juryGroup.list.useQuery({ - competitionId: params.competitionId - }); - - const createMutation = trpc.specialAward.create.useMutation({ - onSuccess: () => { - utils.specialAward.list.invalidate(); - toast.success('Award created successfully'); - router.push(`/admin/competitions/${params.competitionId}/awards` as Route); - }, - onError: (err) => { - toast.error(err.message); - } - }); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - - if (!formData.name.trim()) { - toast.error('Award name is required'); - return; - } - - if (!competition?.programId) { - toast.error('Competition data not loaded'); - return; - } - - createMutation.mutate({ - programId: competition.programId, - competitionId: params.competitionId, - name: formData.name.trim(), - description: formData.description.trim() || undefined, - criteriaText: formData.criteriaText.trim() || undefined, - scoringMode: formData.scoringMode, - useAiEligibility: formData.useAiEligibility, - maxRankedPicks: formData.scoringMode === 'RANKED' ? parseInt(formData.maxRankedPicks) : undefined, - }); - }; - - return ( -
-
- -
-

Create Special Award

-

Define a new award for this competition

-
-
- -
- - - Award Details - Configure the award properties and eligibility - - -
- - setFormData({ ...formData, name: e.target.value })} - placeholder="e.g., Best Innovation Award" - required - /> -
- -
- -