Files
MOPC-Portal/src/app/(admin)/admin/dashboard-content.tsx
Matt 9ce56f13fd
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m53s
Jury evaluation UX overhaul + admin review features
- Fix project documents not displaying on jury project page (rewrote MultiWindowDocViewer to use file.listByProject)
- Add working download/preview for project files via presigned URLs
- Display project tags on jury project detail page
- Add autosave for evaluation drafts (debounced 3s + save on unmount/beforeunload)
- Support mixed criterion types: numeric scores, yes/no booleans, text responses, section headers
- Replace inline criteria editor with rich EvaluationFormBuilder on admin round page
- Remove COI dialog from evaluation page
- Update AI summary service to handle boolean/text criteria (yes/no counts, text synthesis)
- Update EvaluationSummaryCard to show boolean criteria bars and text responses
- Add evaluation detail sheet on admin project page (click juror row to view full scores + feedback)
- Add Recent Evaluations dashboard widget showing latest jury reviews

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 12:43:28 +01:00

205 lines
6.4 KiB
TypeScript

'use client'
import Link from 'next/link'
import { trpc } from '@/lib/trpc/client'
import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import {
CircleDot,
AlertTriangle,
Upload,
UserPlus,
} from 'lucide-react'
import { GeographicSummaryCard } from '@/components/charts'
import { AnimatedCard } from '@/components/shared/animated-container'
import { motion } from 'motion/react'
import { CompetitionPipeline } from '@/components/dashboard/competition-pipeline'
import { RoundStats } from '@/components/dashboard/round-stats'
import { ActiveRoundPanel } from '@/components/dashboard/active-round-panel'
import { SmartActions } from '@/components/dashboard/smart-actions'
import { ProjectListCompact } from '@/components/dashboard/project-list-compact'
import { ActivityFeed } from '@/components/dashboard/activity-feed'
import { CategoryBreakdown } from '@/components/dashboard/category-breakdown'
import { DashboardSkeleton } from '@/components/dashboard/dashboard-skeleton'
import { RecentEvaluations } from '@/components/dashboard/recent-evaluations'
type DashboardContentProps = {
editionId: string
sessionName: string
}
export function DashboardContent({ editionId, sessionName }: DashboardContentProps) {
const { data, isLoading, error } = trpc.dashboard.getStats.useQuery(
{ editionId },
{ enabled: !!editionId, retry: 1, refetchInterval: 30_000 }
)
const { data: recentEvals } = trpc.dashboard.getRecentEvaluations.useQuery(
{ editionId, limit: 8 },
{ enabled: !!editionId, refetchInterval: 30_000 }
)
if (isLoading) {
return <DashboardSkeleton />
}
if (error) {
return (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
<AlertTriangle className="h-12 w-12 text-destructive/50" />
<p className="mt-2 font-medium">Failed to load dashboard</p>
<p className="text-sm text-muted-foreground">
{error.message || 'An unexpected error occurred. Please try refreshing the page.'}
</p>
</CardContent>
</Card>
)
}
if (!data) {
return (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
<CircleDot className="h-12 w-12 text-muted-foreground/50" />
<p className="mt-2 font-medium">Edition not found</p>
<p className="text-sm text-muted-foreground">
The selected edition could not be found
</p>
</CardContent>
</Card>
)
}
const {
edition,
pipelineRounds,
activeRoundId,
nextActions,
projectCount,
newProjectsThisWeek,
totalJurors,
activeJurors,
evaluationStats,
totalAssignments,
latestProjects,
categoryBreakdown,
oceanIssueBreakdown,
recentActivity,
} = data
const activeRound = activeRoundId
? pipelineRounds.find((r) => r.id === activeRoundId) ?? null
: null
return (
<>
{/* Page Header */}
<motion.div
initial={{ opacity: 0, y: -6 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"
>
<div>
<h1 className="text-xl font-bold tracking-tight md:text-2xl">
{edition.name} {edition.year}
</h1>
<p className="text-sm text-muted-foreground">
Welcome back, {sessionName}
</p>
</div>
<div className="flex gap-2">
<Link href="/admin/rounds">
<Button size="sm" variant="outline">
<CircleDot className="mr-1.5 h-3.5 w-3.5" />
Rounds
</Button>
</Link>
<Link href="/admin/projects/new">
<Button size="sm" variant="outline">
<Upload className="mr-1.5 h-3.5 w-3.5" />
Import
</Button>
</Link>
<Link href="/admin/members">
<Button size="sm" variant="outline">
<UserPlus className="mr-1.5 h-3.5 w-3.5" />
Invite
</Button>
</Link>
</div>
</motion.div>
{/* Competition Pipeline */}
<AnimatedCard index={0}>
<CompetitionPipeline rounds={pipelineRounds} />
</AnimatedCard>
{/* Round-Specific Stats */}
<AnimatedCard index={1}>
<RoundStats
activeRound={activeRound}
projectCount={projectCount}
newProjectsThisWeek={newProjectsThisWeek}
totalJurors={totalJurors}
activeJurors={activeJurors}
totalAssignments={totalAssignments}
evaluationStats={evaluationStats}
actionsCount={nextActions.length}
/>
</AnimatedCard>
{/* Two-Column Layout */}
<div className="grid gap-6 lg:grid-cols-12">
{/* Left Column */}
<div className="space-y-6 lg:col-span-8">
{activeRound && (
<AnimatedCard index={2}>
<ActiveRoundPanel round={activeRound} />
</AnimatedCard>
)}
<AnimatedCard index={3}>
<ProjectListCompact projects={latestProjects} />
</AnimatedCard>
{recentEvals && recentEvals.length > 0 && (
<AnimatedCard index={4}>
<RecentEvaluations evaluations={recentEvals} />
</AnimatedCard>
)}
</div>
{/* Right Column */}
<div className="space-y-6 lg:col-span-4">
<AnimatedCard index={5}>
<SmartActions actions={nextActions} />
</AnimatedCard>
<AnimatedCard index={6}>
<ActivityFeed activity={recentActivity} />
</AnimatedCard>
</div>
</div>
{/* Bottom Full Width */}
<div className="grid gap-6 lg:grid-cols-12">
<div className="lg:col-span-8">
<AnimatedCard index={7}>
<GeographicSummaryCard programId={editionId} />
</AnimatedCard>
</div>
<div className="lg:col-span-4">
<AnimatedCard index={8}>
<CategoryBreakdown
categories={categoryBreakdown}
issues={oceanIssueBreakdown}
/>
</AnimatedCard>
</div>
</div>
</>
)
}