diff --git a/src/app/(admin)/admin/reports/page.tsx b/src/app/(admin)/admin/reports/page.tsx index 7e673ce..78014be 100644 --- a/src/app/(admin)/admin/reports/page.tsx +++ b/src/app/(admin)/admin/reports/page.tsx @@ -42,6 +42,9 @@ import { UserCheck, Globe, Layers, + Trophy, + ArrowRight, + Hash, } from 'lucide-react' import { formatDateOnly } from '@/lib/utils' import { @@ -224,25 +227,23 @@ function ReportsOverview() { )} - {/* Project Reports (default: all projects, filterable by round) */} + {/* Project Reports Summary */} - -
- +
+
+ +
+ +
+ Project Reports +
+ + Summary dashboard — optionally filter to a specific round +
- Project Reports - - - Project-wide reporting across all projects — optionally filter to a specific round - - - - {/* Scope selector */} -
- Scope:
- + + {projectsLoading ? ( - + ) : projectRankings?.length ? ( -
- - - - Project - Team - Avg - Evals - Status - - - - {projectRankings.map((p) => ( - - - - {p.title} - - - - {p.teamName || '-'} - - - {p.averageScore === null ? '-' : p.averageScore.toFixed(2)} - - {p.evaluationCount} - - {p.status} - - - ))} - -
-
+ <> + {/* Summary stats row */} + {(() => { + const evaluated = projectRankings.filter(p => p.averageScore !== null) + const scores = evaluated.map(p => p.averageScore as number) + const avgScore = scores.length ? scores.reduce((a, b) => a + b, 0) / scores.length : 0 + const minScore = scores.length ? Math.min(...scores) : 0 + const maxScore = scores.length ? Math.max(...scores) : 0 + const evalPercent = projectRankings.length ? Math.round((evaluated.length / projectRankings.length) * 100) : 0 + const statusCounts = projectRankings.reduce((acc, p) => { + acc[p.status] = (acc[p.status] || 0) + 1 + return acc + }, {} as Record) + + return ( + <> +
+
+

Total Projects

+

{projectRankings.length}

+
+
+

Avg Score

+

{avgScore ? avgScore.toFixed(1) : '-'}

+
+
+

Evaluated

+

{evalPercent}%

+
+
+

Score Range

+

+ {scores.length ? `${minScore.toFixed(1)}–${maxScore.toFixed(1)}` : '-'} +

+
+
+ + {/* Status breakdown chips */} + {Object.keys(statusCounts).length > 0 && ( +
+ {Object.entries(statusCounts) + .sort(([, a], [, b]) => b - a) + .map(([status, count]) => ( + + + {formatStatusLabel(status)} {count} + + ))} +
+ )} + + {/* Top 10 ranked table */} +
+

+ Top 10 by Average Score +

+
+ + + + # + Project + Team + Avg + Evals + Status + + + + {projectRankings.slice(0, 10).map((p, idx) => ( + + {idx + 1} + + + {p.title} + + + + {p.teamName || '-'} + + + {p.averageScore === null ? '-' : p.averageScore.toFixed(2)} + + {p.evaluationCount} + + {formatStatusLabel(p.status)} + + + ))} + +
+
+
+ + {/* Link to full analytics */} + {projectRankings.length > 10 && ( +
+ +
+ )} + + ) + })()} + ) : (
@@ -408,6 +488,30 @@ function parseSelection(value: string | null): { roundId?: string; programId?: s return { roundId: value } } +// Map raw DB status to display-friendly labels +function formatStatusLabel(status: string): string { + const labels: Record = { + ELIGIBLE: 'Special Award', + ASSIGNED: 'Assigned', + PENDING: 'Pending', + IN_PROGRESS: 'In Progress', + PASSED: 'Passed', + REJECTED: 'Rejected', + COMPLETED: 'Completed', + WITHDRAWN: 'Withdrawn', + } + return labels[status] ?? status +} + +// Find the best default round: active > last closed > first +function findDefaultRound(rounds: Array<{ id: string; status?: string }>): string | undefined { + const active = rounds.find(r => r.status === 'ROUND_ACTIVE') + if (active) return active.id + const closed = [...rounds].reverse().find(r => r.status === 'ROUND_CLOSED') + if (closed) return closed.id + return rounds[0]?.id +} + function StageAnalytics() { const [selectedValue, setSelectedValue] = useState(null) @@ -415,13 +519,13 @@ function StageAnalytics() { // Flatten stages from all programs with program name const rounds = programs?.flatMap(p => - ((p.stages ?? []) as Array<{ id: string; name: string }>).map((s: { id: string; name: string }) => ({ ...s, programId: p.id, programName: `${p.year} Edition` })) + ((p.stages ?? []) as Array<{ id: string; name: string; status: string }>).map((s: { id: string; name: string; status: string }) => ({ ...s, programId: p.id, programName: `${p.year} Edition` })) ) || [] - // Set default selected stage + // Set default selected stage — prefer active round useEffect(() => { if (rounds.length && !selectedValue) { - setSelectedValue(rounds[0].id) + setSelectedValue(findDefaultRound(rounds) ?? rounds[0].id) } }, [rounds.length, selectedValue]) @@ -702,12 +806,12 @@ function JurorConsistencyTab() { const { data: programs, isLoading: programsLoading } = trpc.program.list.useQuery({ includeStages: true }) const stages = programs?.flatMap((p) => - ((p.stages ?? []) as Array<{ id: string; name: string }>).map((s: { id: string; name: string }) => ({ id: s.id, name: s.name, programId: p.id, programName: `${p.year} Edition` })) + ((p.stages ?? []) as Array<{ id: string; name: string; status: string }>).map((s: { id: string; name: string; status: string }) => ({ id: s.id, name: s.name, status: s.status, programId: p.id, programName: `${p.year} Edition` })) ) || [] useEffect(() => { if (stages.length && !selectedValue) { - setSelectedValue(stages[0].id) + setSelectedValue(findDefaultRound(stages) ?? stages[0].id) } }, [stages.length, selectedValue]) @@ -776,12 +880,12 @@ function DiversityTab() { const { data: programs, isLoading: programsLoading } = trpc.program.list.useQuery({ includeStages: true }) const stages = programs?.flatMap((p) => - ((p.stages ?? []) as Array<{ id: string; name: string }>).map((s: { id: string; name: string }) => ({ id: s.id, name: s.name, programId: p.id, programName: `${p.year} Edition` })) + ((p.stages ?? []) as Array<{ id: string; name: string; status: string }>).map((s: { id: string; name: string; status: string }) => ({ id: s.id, name: s.name, status: s.status, programId: p.id, programName: `${p.year} Edition` })) ) || [] useEffect(() => { if (stages.length && !selectedValue) { - setSelectedValue(stages[0].id) + setSelectedValue(findDefaultRound(stages) ?? stages[0].id) } }, [stages.length, selectedValue]) @@ -934,12 +1038,12 @@ export default function ReportsPage() { const { data: pdfPrograms } = trpc.program.list.useQuery({ includeStages: true }) const pdfStages = pdfPrograms?.flatMap((p) => - ((p.stages ?? []) as Array<{ id: string; name: string }>).map((s: { id: string; name: string }) => ({ id: s.id, name: s.name, programName: `${p.year} Edition` })) + ((p.stages ?? []) as Array<{ id: string; name: string; status: string }>).map((s: { id: string; name: string; status: string }) => ({ id: s.id, name: s.name, status: s.status, programName: `${p.year} Edition` })) ) || [] useEffect(() => { if (pdfStages.length && !pdfStageId) { - setPdfStageId(pdfStages[0].id) + setPdfStageId(findDefaultRound(pdfStages) ?? pdfStages[0].id) } }, [pdfStages.length, pdfStageId])