Files
MOPC-Portal/src/app/(admin)/admin/reports/page.tsx

1141 lines
43 KiB
TypeScript
Raw Normal View History

'use client'
Admin UI audit round 2: fix 28 display bugs across 23 files HIGH fixes (broken features / wrong data): - H1: Fix roundAssignments → projectRoundStates in project router (7 occurrences) - H2: Fix deliberation results panel blank table (wrong field names) - H3: Fix deliberation participant names blank (wrong data path) - H4: Fix awards "Evaluated" stat duplicating "Eligible" count - H5: Fix cross-round comparison enabled at 1 round (backend requires 2) - H6: Fix setState during render anti-pattern (6 occurrences) - H7: Fix round detail jury member count always showing 0 - H8: Remove 4 invalid status values from observer dashboard filter - H9: Fix filtering progress bar always showing 100% MEDIUM fixes (misleading display): - M1: Filter special-award rounds from competition timeline - M2: Exclude special-award rounds from distinct project count - M3: Fix MENTORING pipeline node hardcoded "0 mentored" - M4: Fix DELIB_LOCKED badge using red for success state - M5: Add status label maps to deliberation session detail - M6: Humanize deliberation category + tie-break method displays - M8: Rename setStageId → setRoundId, "Select Stage" → "Select Round" - M9: Add missing INVITED/ACTIVE/SUSPENDED to members status labels - M10: Add ROUND_DRAFT/ACTIVE/CLOSED/ARCHIVED to StatusBadge - M11: Fix unsent messages showing "Scheduled" instead of "Draft" - M12: Rename misleading totalEvaluations → totalAssignments - M13: Rename "Stage" column to "Program" in projects page LOW fixes (cosmetic / edge-case): - L1: Use unfiltered rounds array for active round detection - L2: Use all rounds length for new round sort order - L3: Filter special-award rounds from header count - L4: Fix single-underscore replace in award status badges - L5: Fix score bucket boundary gaps (4.99 dropped between buckets) - L6: Title-case LIVE_FINAL pipeline metric status - L7: Fix roundType.replace only replacing first underscore - L8: Remove duplicate severity sort in smart-actions component Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 11:11:00 +01:00
import { useState, useEffect } from 'react'
import Link from 'next/link'
import { trpc } from '@/lib/trpc/client'
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Progress } from '@/components/ui/progress'
import { Skeleton } from '@/components/ui/skeleton'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import {
FileSpreadsheet,
Download,
BarChart3,
Users,
ClipboardList,
CheckCircle2,
TrendingUp,
GitCompare,
UserCheck,
Globe,
Layers,
Trophy,
ArrowRight,
Hash,
} from 'lucide-react'
import { formatDateOnly } from '@/lib/utils'
import {
ScoreDistributionChart,
EvaluationTimelineChart,
StatusBreakdownChart,
JurorWorkloadChart,
ProjectRankingsChart,
CriteriaScoresChart,
GeographicDistribution,
CrossStageComparisonChart,
JurorConsistencyChart,
DiversityMetricsChart,
} from '@/components/charts'
import { ExportPdfButton } from '@/components/shared/export-pdf-button'
import { AnimatedCard } from '@/components/shared/animated-container'
function ReportsOverview() {
const { data: programs, isLoading } = trpc.program.list.useQuery({ includeStages: true })
const { data: dashStats, isLoading: statsLoading } = trpc.analytics.getDashboardStats.useQuery()
// Flatten stages from all programs
const rounds = programs?.flatMap(p =>
((p.stages ?? []) as Array<{ id: string; name: string; status: string; votingEndAt?: string | Date | null }>).map((s: { id: string; name: string; status: string; votingEndAt?: string | Date | null }) => ({ ...s, programId: p.id, programName: `${p.year} Edition` }))
) || []
// Project reporting scope (default: latest program, all rounds)
const [selectedValue, setSelectedValue] = useState<string | null>(null)
Admin UI audit round 2: fix 28 display bugs across 23 files HIGH fixes (broken features / wrong data): - H1: Fix roundAssignments → projectRoundStates in project router (7 occurrences) - H2: Fix deliberation results panel blank table (wrong field names) - H3: Fix deliberation participant names blank (wrong data path) - H4: Fix awards "Evaluated" stat duplicating "Eligible" count - H5: Fix cross-round comparison enabled at 1 round (backend requires 2) - H6: Fix setState during render anti-pattern (6 occurrences) - H7: Fix round detail jury member count always showing 0 - H8: Remove 4 invalid status values from observer dashboard filter - H9: Fix filtering progress bar always showing 100% MEDIUM fixes (misleading display): - M1: Filter special-award rounds from competition timeline - M2: Exclude special-award rounds from distinct project count - M3: Fix MENTORING pipeline node hardcoded "0 mentored" - M4: Fix DELIB_LOCKED badge using red for success state - M5: Add status label maps to deliberation session detail - M6: Humanize deliberation category + tie-break method displays - M8: Rename setStageId → setRoundId, "Select Stage" → "Select Round" - M9: Add missing INVITED/ACTIVE/SUSPENDED to members status labels - M10: Add ROUND_DRAFT/ACTIVE/CLOSED/ARCHIVED to StatusBadge - M11: Fix unsent messages showing "Scheduled" instead of "Draft" - M12: Rename misleading totalEvaluations → totalAssignments - M13: Rename "Stage" column to "Program" in projects page LOW fixes (cosmetic / edge-case): - L1: Use unfiltered rounds array for active round detection - L2: Use all rounds length for new round sort order - L3: Filter special-award rounds from header count - L4: Fix single-underscore replace in award status badges - L5: Fix score bucket boundary gaps (4.99 dropped between buckets) - L6: Title-case LIVE_FINAL pipeline metric status - L7: Fix roundType.replace only replacing first underscore - L8: Remove duplicate severity sort in smart-actions component Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 11:11:00 +01:00
useEffect(() => {
if (programs?.length && !selectedValue) {
setSelectedValue(`all:${programs[0].id}`)
}
}, [programs, selectedValue])
const scopeInput = parseSelection(selectedValue)
const hasScope = !!scopeInput.roundId || !!scopeInput.programId
const { data: projectRankings, isLoading: projectsLoading } =
trpc.analytics.getProjectRankings.useQuery(
{ ...scopeInput, limit: 5000 },
{ enabled: hasScope }
)
if (isLoading || statsLoading) {
return (
<div className="space-y-6">
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{[...Array(4)].map((_, i) => (
<Card key={i}>
<CardHeader className="space-y-0 pb-2">
<Skeleton className="h-4 w-20" />
</CardHeader>
<CardContent>
<Skeleton className="h-8 w-16" />
<Skeleton className="mt-2 h-3 w-24" />
</CardContent>
</Card>
))}
</div>
</div>
)
}
const totalPrograms = dashStats?.programCount ?? programs?.length ?? 0
const totalProjects = dashStats?.projectCount ?? 0
const activeRounds = dashStats?.activeRoundCount ?? rounds.filter((r: { status: string }) => r.status === 'ROUND_ACTIVE').length
const jurorCount = dashStats?.jurorCount ?? 0
const submittedEvaluations = dashStats?.submittedEvaluations ?? 0
Admin UI audit round 2: fix 28 display bugs across 23 files HIGH fixes (broken features / wrong data): - H1: Fix roundAssignments → projectRoundStates in project router (7 occurrences) - H2: Fix deliberation results panel blank table (wrong field names) - H3: Fix deliberation participant names blank (wrong data path) - H4: Fix awards "Evaluated" stat duplicating "Eligible" count - H5: Fix cross-round comparison enabled at 1 round (backend requires 2) - H6: Fix setState during render anti-pattern (6 occurrences) - H7: Fix round detail jury member count always showing 0 - H8: Remove 4 invalid status values from observer dashboard filter - H9: Fix filtering progress bar always showing 100% MEDIUM fixes (misleading display): - M1: Filter special-award rounds from competition timeline - M2: Exclude special-award rounds from distinct project count - M3: Fix MENTORING pipeline node hardcoded "0 mentored" - M4: Fix DELIB_LOCKED badge using red for success state - M5: Add status label maps to deliberation session detail - M6: Humanize deliberation category + tie-break method displays - M8: Rename setStageId → setRoundId, "Select Stage" → "Select Round" - M9: Add missing INVITED/ACTIVE/SUSPENDED to members status labels - M10: Add ROUND_DRAFT/ACTIVE/CLOSED/ARCHIVED to StatusBadge - M11: Fix unsent messages showing "Scheduled" instead of "Draft" - M12: Rename misleading totalEvaluations → totalAssignments - M13: Rename "Stage" column to "Program" in projects page LOW fixes (cosmetic / edge-case): - L1: Use unfiltered rounds array for active round detection - L2: Use all rounds length for new round sort order - L3: Filter special-award rounds from header count - L4: Fix single-underscore replace in award status badges - L5: Fix score bucket boundary gaps (4.99 dropped between buckets) - L6: Title-case LIVE_FINAL pipeline metric status - L7: Fix roundType.replace only replacing first underscore - L8: Remove duplicate severity sort in smart-actions component Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 11:11:00 +01:00
const totalAssignments = dashStats?.totalAssignments ?? 0
const completionRate = dashStats?.completionRate ?? 0
return (
<div className="space-y-6">
{/* Quick Stats */}
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<AnimatedCard index={0}>
<Card className="border-l-4 border-l-blue-500 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
<CardContent className="p-5">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Programs</p>
<p className="text-2xl font-bold mt-1">{totalPrograms}</p>
<p className="text-xs text-muted-foreground mt-1">
{activeRounds} active round{activeRounds !== 1 ? 's' : ''}
</p>
</div>
<div className="rounded-xl bg-blue-50 p-3">
<CheckCircle2 className="h-5 w-5 text-blue-600" />
</div>
</div>
</CardContent>
</Card>
</AnimatedCard>
<AnimatedCard index={1}>
<Card className="border-l-4 border-l-emerald-500 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
<CardContent className="p-5">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Total Projects</p>
<p className="text-2xl font-bold mt-1">{totalProjects}</p>
<p className="text-xs text-muted-foreground mt-1">Across all programs</p>
</div>
<div className="rounded-xl bg-emerald-50 p-3">
<ClipboardList className="h-5 w-5 text-emerald-600" />
</div>
</div>
</CardContent>
</Card>
</AnimatedCard>
<AnimatedCard index={2}>
<Card className="border-l-4 border-l-violet-500 transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
<CardContent className="p-5">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Jury Members</p>
<p className="text-2xl font-bold mt-1">{jurorCount}</p>
<p className="text-xs text-muted-foreground mt-1">Active jurors</p>
</div>
<div className="rounded-xl bg-violet-50 p-3">
<Users className="h-5 w-5 text-violet-600" />
</div>
</div>
</CardContent>
</Card>
</AnimatedCard>
<AnimatedCard index={3}>
<Card className="border-l-4 border-l-brand-teal transition-all duration-200 hover:-translate-y-0.5 hover:shadow-md">
<CardContent className="p-5">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-muted-foreground">Evaluations</p>
<p className="text-2xl font-bold mt-1">{submittedEvaluations}</p>
<p className="text-xs text-muted-foreground mt-1">
Admin UI audit round 2: fix 28 display bugs across 23 files HIGH fixes (broken features / wrong data): - H1: Fix roundAssignments → projectRoundStates in project router (7 occurrences) - H2: Fix deliberation results panel blank table (wrong field names) - H3: Fix deliberation participant names blank (wrong data path) - H4: Fix awards "Evaluated" stat duplicating "Eligible" count - H5: Fix cross-round comparison enabled at 1 round (backend requires 2) - H6: Fix setState during render anti-pattern (6 occurrences) - H7: Fix round detail jury member count always showing 0 - H8: Remove 4 invalid status values from observer dashboard filter - H9: Fix filtering progress bar always showing 100% MEDIUM fixes (misleading display): - M1: Filter special-award rounds from competition timeline - M2: Exclude special-award rounds from distinct project count - M3: Fix MENTORING pipeline node hardcoded "0 mentored" - M4: Fix DELIB_LOCKED badge using red for success state - M5: Add status label maps to deliberation session detail - M6: Humanize deliberation category + tie-break method displays - M8: Rename setStageId → setRoundId, "Select Stage" → "Select Round" - M9: Add missing INVITED/ACTIVE/SUSPENDED to members status labels - M10: Add ROUND_DRAFT/ACTIVE/CLOSED/ARCHIVED to StatusBadge - M11: Fix unsent messages showing "Scheduled" instead of "Draft" - M12: Rename misleading totalEvaluations → totalAssignments - M13: Rename "Stage" column to "Program" in projects page LOW fixes (cosmetic / edge-case): - L1: Use unfiltered rounds array for active round detection - L2: Use all rounds length for new round sort order - L3: Filter special-award rounds from header count - L4: Fix single-underscore replace in award status badges - L5: Fix score bucket boundary gaps (4.99 dropped between buckets) - L6: Title-case LIVE_FINAL pipeline metric status - L7: Fix roundType.replace only replacing first underscore - L8: Remove duplicate severity sort in smart-actions component Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 11:11:00 +01:00
{totalAssignments > 0
? `${completionRate}% completion rate`
: 'No assignments yet'}
</p>
</div>
<div className="rounded-xl bg-brand-teal/10 p-3">
<BarChart3 className="h-5 w-5 text-brand-teal" />
</div>
</div>
</CardContent>
</Card>
</AnimatedCard>
</div>
{/* Score Distribution (if any evaluations exist) */}
{dashStats?.scoreDistribution && dashStats.scoreDistribution.some(b => b.count > 0) && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<div className="rounded-lg bg-blue-500/10 p-1.5">
<BarChart3 className="h-4 w-4 text-blue-600" />
</div>
Score Distribution
</CardTitle>
<CardDescription>Overall score distribution across all evaluations</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{dashStats.scoreDistribution.map((bucket) => {
const maxCount = Math.max(...dashStats.scoreDistribution.map(b => b.count), 1)
return (
<div key={bucket.label} className="flex items-center gap-3">
<span className="w-10 text-sm font-medium text-right">{bucket.label}</span>
<div className="flex-1">
<Progress value={(bucket.count / maxCount) * 100} className="h-6" gradient />
</div>
<span className="w-8 text-sm text-muted-foreground text-right">{bucket.count}</span>
</div>
)
})}
</div>
</CardContent>
</Card>
)}
{/* Project Reports Summary */}
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
<div className="rounded-lg bg-emerald-500/10 p-1.5">
<ClipboardList className="h-4 w-4 text-emerald-600" />
</div>
Project Reports
</CardTitle>
<CardDescription>
Summary dashboard optionally filter to a specific round
</CardDescription>
</div>
<Select value={selectedValue || ''} onValueChange={setSelectedValue}>
<SelectTrigger className="w-[280px]">
<SelectValue placeholder="All projects" />
</SelectTrigger>
<SelectContent>
{programs?.map((p) => (
<SelectItem key={`all:${p.id}`} value={`all:${p.id}`}>
{p.year} Edition All Rounds
</SelectItem>
))}
{rounds.map((round) => (
<SelectItem key={round.id} value={round.id}>
{round.programName} - {round.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</CardHeader>
<CardContent className="space-y-5">
{projectsLoading ? (
<Skeleton className="h-[300px]" />
) : projectRankings?.length ? (
<>
{/* 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<string, number>)
return (
<>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
<div className="rounded-lg border p-3 text-center">
<p className="text-xs text-muted-foreground">Total Projects</p>
<p className="text-xl font-bold tabular-nums">{projectRankings.length}</p>
</div>
<div className="rounded-lg border p-3 text-center">
<p className="text-xs text-muted-foreground">Avg Score</p>
<p className="text-xl font-bold tabular-nums">{avgScore ? avgScore.toFixed(1) : '-'}</p>
</div>
<div className="rounded-lg border p-3 text-center">
<p className="text-xs text-muted-foreground">Evaluated</p>
<p className="text-xl font-bold tabular-nums">{evalPercent}%</p>
</div>
<div className="rounded-lg border p-3 text-center">
<p className="text-xs text-muted-foreground">Score Range</p>
<p className="text-xl font-bold tabular-nums">
{scores.length ? `${minScore.toFixed(1)}${maxScore.toFixed(1)}` : '-'}
</p>
</div>
</div>
{/* Status breakdown chips */}
{Object.keys(statusCounts).length > 0 && (
<div className="flex flex-wrap gap-2">
{Object.entries(statusCounts)
.sort(([, a], [, b]) => b - a)
.map(([status, count]) => (
<Badge key={status} variant="outline" className="gap-1">
<Hash className="h-3 w-3" />
{formatStatusLabel(status)} <span className="font-bold tabular-nums">{count}</span>
</Badge>
))}
</div>
)}
{/* Top 10 ranked table */}
<div>
<p className="text-sm font-medium text-muted-foreground mb-2 flex items-center gap-1.5">
<Trophy className="h-3.5 w-3.5" /> Top 10 by Average Score
</p>
<div className="rounded-lg border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-10">#</TableHead>
<TableHead>Project</TableHead>
<TableHead className="hidden sm:table-cell">Team</TableHead>
<TableHead className="text-right">Avg</TableHead>
<TableHead className="text-right">Evals</TableHead>
<TableHead>Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{projectRankings.slice(0, 10).map((p, idx) => (
<TableRow key={p.id}>
<TableCell className="text-muted-foreground tabular-nums">{idx + 1}</TableCell>
<TableCell className="font-medium">
<Link href={`/admin/projects/${p.id}`} className="hover:underline">
{p.title}
</Link>
</TableCell>
<TableCell className="hidden sm:table-cell text-muted-foreground">
{p.teamName || '-'}
</TableCell>
<TableCell className="text-right tabular-nums">
{p.averageScore === null ? '-' : p.averageScore.toFixed(2)}
</TableCell>
<TableCell className="text-right tabular-nums">{p.evaluationCount}</TableCell>
<TableCell>
<Badge variant="outline">{formatStatusLabel(p.status)}</Badge>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
{/* Link to full analytics */}
{projectRankings.length > 10 && (
<div className="flex justify-end">
<Button variant="ghost" size="sm" className="gap-1 text-muted-foreground"
onClick={() => {
const tabs = document.querySelectorAll('[role="tab"]')
const analyticsTab = Array.from(tabs).find(t => t.textContent?.includes('Analytics')) as HTMLElement
analyticsTab?.click()
}}
>
View full rankings in Analytics <ArrowRight className="h-3.5 w-3.5" />
</Button>
</div>
)}
</>
)
})()}
</>
) : (
<div className="flex flex-col items-center justify-center py-10 text-center">
<ClipboardList className="h-10 w-10 text-muted-foreground/50" />
<p className="mt-2 text-sm text-muted-foreground">
No project report data available for the selected scope yet.
</p>
</div>
)}
</CardContent>
</Card>
{/* Round exports (still available) */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<div className="rounded-lg bg-blue-500/10 p-1.5">
<FileSpreadsheet className="h-4 w-4 text-blue-600" />
</div>
Round Exports
</CardTitle>
<CardDescription>
Download round-level evaluations and results
</CardDescription>
</CardHeader>
<CardContent>
{rounds.length === 0 ? (
<div className="flex flex-col items-center justify-center py-8 text-center">
<FileSpreadsheet className="h-10 w-10 text-muted-foreground/50" />
<p className="mt-2 text-sm text-muted-foreground">
No rounds created yet.
</p>
</div>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Round</TableHead>
<TableHead>Program</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Export</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{rounds.map((round) => (
<TableRow key={round.id}>
<TableCell>
<div>
<p className="font-medium">{round.name}</p>
{round.votingEndAt && (
<p className="text-sm text-muted-foreground">
Ends: {formatDateOnly(round.votingEndAt)}
</p>
)}
</div>
</TableCell>
<TableCell>{round.programName}</TableCell>
<TableCell>
<Badge
variant={
round.status === 'ROUND_ACTIVE'
? 'default'
: round.status === 'ROUND_CLOSED'
? 'secondary'
: 'outline'
}
>
{round.status?.replace('ROUND_', '') || round.status}
</Badge>
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2 flex-wrap">
<Button variant="outline" size="sm" asChild>
<a
href={`/api/export/evaluations?stageId=${round.id}`}
target="_blank"
rel="noopener noreferrer"
>
<Download className="mr-2 h-4 w-4" />
Evaluations
</a>
</Button>
<Button variant="outline" size="sm" asChild>
<a
href={`/api/export/results?stageId=${round.id}`}
target="_blank"
rel="noopener noreferrer"
>
<Download className="mr-2 h-4 w-4" />
Results
</a>
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</div>
)
}
// Parse selection value: "all:programId" for edition-wide, or roundId
function parseSelection(value: string | null): { roundId?: string; programId?: string } {
if (!value) return {}
if (value.startsWith('all:')) return { programId: value.slice(4) }
return { roundId: value }
}
// Map raw DB status to display-friendly labels
function formatStatusLabel(status: string): string {
const labels: Record<string, string> = {
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<string | null>(null)
const { data: programs, isLoading: roundsLoading } = trpc.program.list.useQuery({ includeStages: true })
// Flatten stages from all programs with program name
const rounds = programs?.flatMap(p =>
((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 — prefer active round
Admin UI audit round 2: fix 28 display bugs across 23 files HIGH fixes (broken features / wrong data): - H1: Fix roundAssignments → projectRoundStates in project router (7 occurrences) - H2: Fix deliberation results panel blank table (wrong field names) - H3: Fix deliberation participant names blank (wrong data path) - H4: Fix awards "Evaluated" stat duplicating "Eligible" count - H5: Fix cross-round comparison enabled at 1 round (backend requires 2) - H6: Fix setState during render anti-pattern (6 occurrences) - H7: Fix round detail jury member count always showing 0 - H8: Remove 4 invalid status values from observer dashboard filter - H9: Fix filtering progress bar always showing 100% MEDIUM fixes (misleading display): - M1: Filter special-award rounds from competition timeline - M2: Exclude special-award rounds from distinct project count - M3: Fix MENTORING pipeline node hardcoded "0 mentored" - M4: Fix DELIB_LOCKED badge using red for success state - M5: Add status label maps to deliberation session detail - M6: Humanize deliberation category + tie-break method displays - M8: Rename setStageId → setRoundId, "Select Stage" → "Select Round" - M9: Add missing INVITED/ACTIVE/SUSPENDED to members status labels - M10: Add ROUND_DRAFT/ACTIVE/CLOSED/ARCHIVED to StatusBadge - M11: Fix unsent messages showing "Scheduled" instead of "Draft" - M12: Rename misleading totalEvaluations → totalAssignments - M13: Rename "Stage" column to "Program" in projects page LOW fixes (cosmetic / edge-case): - L1: Use unfiltered rounds array for active round detection - L2: Use all rounds length for new round sort order - L3: Filter special-award rounds from header count - L4: Fix single-underscore replace in award status badges - L5: Fix score bucket boundary gaps (4.99 dropped between buckets) - L6: Title-case LIVE_FINAL pipeline metric status - L7: Fix roundType.replace only replacing first underscore - L8: Remove duplicate severity sort in smart-actions component Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 11:11:00 +01:00
useEffect(() => {
if (rounds.length && !selectedValue) {
setSelectedValue(findDefaultRound(rounds) ?? rounds[0].id)
Admin UI audit round 2: fix 28 display bugs across 23 files HIGH fixes (broken features / wrong data): - H1: Fix roundAssignments → projectRoundStates in project router (7 occurrences) - H2: Fix deliberation results panel blank table (wrong field names) - H3: Fix deliberation participant names blank (wrong data path) - H4: Fix awards "Evaluated" stat duplicating "Eligible" count - H5: Fix cross-round comparison enabled at 1 round (backend requires 2) - H6: Fix setState during render anti-pattern (6 occurrences) - H7: Fix round detail jury member count always showing 0 - H8: Remove 4 invalid status values from observer dashboard filter - H9: Fix filtering progress bar always showing 100% MEDIUM fixes (misleading display): - M1: Filter special-award rounds from competition timeline - M2: Exclude special-award rounds from distinct project count - M3: Fix MENTORING pipeline node hardcoded "0 mentored" - M4: Fix DELIB_LOCKED badge using red for success state - M5: Add status label maps to deliberation session detail - M6: Humanize deliberation category + tie-break method displays - M8: Rename setStageId → setRoundId, "Select Stage" → "Select Round" - M9: Add missing INVITED/ACTIVE/SUSPENDED to members status labels - M10: Add ROUND_DRAFT/ACTIVE/CLOSED/ARCHIVED to StatusBadge - M11: Fix unsent messages showing "Scheduled" instead of "Draft" - M12: Rename misleading totalEvaluations → totalAssignments - M13: Rename "Stage" column to "Program" in projects page LOW fixes (cosmetic / edge-case): - L1: Use unfiltered rounds array for active round detection - L2: Use all rounds length for new round sort order - L3: Filter special-award rounds from header count - L4: Fix single-underscore replace in award status badges - L5: Fix score bucket boundary gaps (4.99 dropped between buckets) - L6: Title-case LIVE_FINAL pipeline metric status - L7: Fix roundType.replace only replacing first underscore - L8: Remove duplicate severity sort in smart-actions component Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 11:11:00 +01:00
}
}, [rounds.length, selectedValue])
const queryInput = parseSelection(selectedValue)
const hasSelection = !!queryInput.roundId || !!queryInput.programId
const { data: scoreDistribution, isLoading: scoreLoading } =
trpc.analytics.getScoreDistribution.useQuery(
queryInput,
{ enabled: hasSelection }
)
const { data: timeline, isLoading: timelineLoading } =
trpc.analytics.getEvaluationTimeline.useQuery(
queryInput,
{ enabled: hasSelection }
)
const { data: statusBreakdown, isLoading: statusLoading } =
trpc.analytics.getStatusBreakdown.useQuery(
queryInput,
{ enabled: hasSelection }
)
const { data: jurorWorkload, isLoading: workloadLoading } =
trpc.analytics.getJurorWorkload.useQuery(
queryInput,
{ enabled: hasSelection }
)
const { data: projectRankings, isLoading: rankingsLoading } =
trpc.analytics.getProjectRankings.useQuery(
{ ...queryInput, limit: 15 },
{ enabled: hasSelection }
)
const { data: criteriaScores, isLoading: criteriaLoading } =
trpc.analytics.getCriteriaScores.useQuery(
queryInput,
{ enabled: hasSelection }
)
const selectedRound = rounds.find((r) => r.id === selectedValue)
const geoInput = queryInput.programId
? { programId: queryInput.programId }
: { programId: selectedRound?.programId || '', roundId: queryInput.roundId }
const { data: geoData, isLoading: geoLoading } =
trpc.analytics.getGeographicDistribution.useQuery(
geoInput,
{ enabled: hasSelection && !!(geoInput.programId || geoInput.roundId) }
)
if (roundsLoading) {
return (
<div className="space-y-4">
<Skeleton className="h-10 w-64" />
<div className="grid gap-6 lg:grid-cols-2">
<Skeleton className="h-[350px]" />
<Skeleton className="h-[350px]" />
</div>
</div>
)
}
if (!rounds?.length) {
return (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
<BarChart3 className="h-12 w-12 text-muted-foreground/50" />
<p className="mt-2 font-medium">No rounds available</p>
<p className="text-sm text-muted-foreground">
Create a round to view analytics
</p>
</CardContent>
</Card>
)
}
return (
<div className="space-y-6">
{/* Round Selector */}
<div className="flex items-center gap-4">
<label className="text-sm font-medium">Select Round:</label>
<Select value={selectedValue || ''} onValueChange={setSelectedValue}>
<SelectTrigger className="w-[300px]">
<SelectValue placeholder="Select a round" />
</SelectTrigger>
<SelectContent>
{programs?.map((p) => (
<SelectItem key={`all:${p.id}`} value={`all:${p.id}`}>
{p.year} Edition All Rounds
</SelectItem>
))}
{rounds.map((round) => (
<SelectItem key={round.id} value={round.id}>
{round.programName} - {round.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{hasSelection && (
<div className="space-y-6">
{/* Row 1: Score Distribution & Status Breakdown */}
<div className="grid gap-6 lg:grid-cols-2">
{scoreLoading ? (
<Skeleton className="h-[350px]" />
) : scoreDistribution ? (
<ScoreDistributionChart
data={scoreDistribution.distribution ?? []}
averageScore={scoreDistribution.averageScore ?? 0}
totalScores={scoreDistribution.totalScores ?? 0}
/>
) : null}
{statusLoading ? (
<Skeleton className="h-[350px]" />
) : statusBreakdown ? (
<StatusBreakdownChart data={statusBreakdown} />
) : null}
</div>
{/* Row 2: Evaluation Timeline */}
{timelineLoading ? (
<Skeleton className="h-[350px]" />
) : timeline?.length ? (
<EvaluationTimelineChart data={timeline} />
) : (
<Card>
<CardContent className="flex items-center justify-center py-12">
<p className="text-muted-foreground">
No evaluation data available yet
</p>
</CardContent>
</Card>
)}
{/* Row 3: Criteria Scores */}
{criteriaLoading ? (
<Skeleton className="h-[350px]" />
) : criteriaScores?.length ? (
<CriteriaScoresChart data={criteriaScores} />
) : null}
{/* Row 4: Juror Workload */}
{workloadLoading ? (
<Skeleton className="h-[450px]" />
) : jurorWorkload?.length ? (
<JurorWorkloadChart data={jurorWorkload} />
) : (
<Card>
<CardContent className="flex items-center justify-center py-12">
<p className="text-muted-foreground">
No juror assignments yet
</p>
</CardContent>
</Card>
)}
{/* Row 5: Project Rankings */}
{rankingsLoading ? (
<Skeleton className="h-[550px]" />
) : projectRankings?.length ? (
<ProjectRankingsChart data={projectRankings} limit={15} />
) : (
<Card>
<CardContent className="flex items-center justify-center py-12">
<p className="text-muted-foreground">
No project scores available yet
</p>
</CardContent>
</Card>
)}
{/* Row 6: Geographic Distribution */}
{geoLoading ? (
<Skeleton className="h-[500px]" />
) : geoData?.length ? (
<GeographicDistribution data={geoData} />
) : null}
</div>
)}
</div>
)
}
function CrossStageTab() {
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, programName: `${p.year} Edition` }))
) || []
const [selectedRoundIds, setSelectedRoundIds] = useState<string[]>([])
const { data: comparison, isLoading: comparisonLoading } =
trpc.analytics.getCrossRoundComparison.useQuery(
{ roundIds: selectedRoundIds },
{ enabled: selectedRoundIds.length >= 2 }
)
const toggleRound = (roundId: string) => {
setSelectedRoundIds((prev) =>
prev.includes(roundId)
? prev.filter((id) => id !== roundId)
: [...prev, roundId]
)
}
if (programsLoading) {
return <Skeleton className="h-[400px]" />
}
return (
<div className="space-y-6">
{/* Stage selector */}
<Card>
<CardHeader>
<CardTitle>Select Stages to Compare</CardTitle>
<CardDescription>
Choose at least 2 stages to compare metrics side by side
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
{stages.map((stage) => {
const isSelected = selectedRoundIds.includes(stage.id)
return (
<Badge
key={stage.id}
variant={isSelected ? 'default' : 'outline'}
className="cursor-pointer text-sm py-1.5 px-3"
onClick={() => toggleRound(stage.id)}
>
{stage.name}
</Badge>
)
})}
</div>
{selectedRoundIds.length < 2 && (
<p className="text-sm text-muted-foreground mt-3">
Select at least 2 stages to enable comparison
</p>
)}
</CardContent>
</Card>
{/* Comparison charts */}
{comparisonLoading && selectedRoundIds.length >= 2 && (
<div className="space-y-6">
<Skeleton className="h-[350px]" />
<div className="grid gap-6 lg:grid-cols-2">
<Skeleton className="h-[300px]" />
<Skeleton className="h-[300px]" />
</div>
</div>
)}
{comparison && (
<CrossStageComparisonChart data={comparison as Array<{
roundId: string
roundName: string
projectCount: number
evaluationCount: number
completionRate: number
averageScore: number | null
scoreDistribution: { score: number; count: number }[]
}>} />
)}
</div>
)
}
function JurorConsistencyTab() {
const [selectedValue, setSelectedValue] = useState<string | null>(null)
const { data: programs, isLoading: programsLoading } = trpc.program.list.useQuery({ includeStages: true })
const stages = programs?.flatMap((p) =>
((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` }))
) || []
Admin UI audit round 2: fix 28 display bugs across 23 files HIGH fixes (broken features / wrong data): - H1: Fix roundAssignments → projectRoundStates in project router (7 occurrences) - H2: Fix deliberation results panel blank table (wrong field names) - H3: Fix deliberation participant names blank (wrong data path) - H4: Fix awards "Evaluated" stat duplicating "Eligible" count - H5: Fix cross-round comparison enabled at 1 round (backend requires 2) - H6: Fix setState during render anti-pattern (6 occurrences) - H7: Fix round detail jury member count always showing 0 - H8: Remove 4 invalid status values from observer dashboard filter - H9: Fix filtering progress bar always showing 100% MEDIUM fixes (misleading display): - M1: Filter special-award rounds from competition timeline - M2: Exclude special-award rounds from distinct project count - M3: Fix MENTORING pipeline node hardcoded "0 mentored" - M4: Fix DELIB_LOCKED badge using red for success state - M5: Add status label maps to deliberation session detail - M6: Humanize deliberation category + tie-break method displays - M8: Rename setStageId → setRoundId, "Select Stage" → "Select Round" - M9: Add missing INVITED/ACTIVE/SUSPENDED to members status labels - M10: Add ROUND_DRAFT/ACTIVE/CLOSED/ARCHIVED to StatusBadge - M11: Fix unsent messages showing "Scheduled" instead of "Draft" - M12: Rename misleading totalEvaluations → totalAssignments - M13: Rename "Stage" column to "Program" in projects page LOW fixes (cosmetic / edge-case): - L1: Use unfiltered rounds array for active round detection - L2: Use all rounds length for new round sort order - L3: Filter special-award rounds from header count - L4: Fix single-underscore replace in award status badges - L5: Fix score bucket boundary gaps (4.99 dropped between buckets) - L6: Title-case LIVE_FINAL pipeline metric status - L7: Fix roundType.replace only replacing first underscore - L8: Remove duplicate severity sort in smart-actions component Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 11:11:00 +01:00
useEffect(() => {
if (stages.length && !selectedValue) {
setSelectedValue(findDefaultRound(stages) ?? stages[0].id)
Admin UI audit round 2: fix 28 display bugs across 23 files HIGH fixes (broken features / wrong data): - H1: Fix roundAssignments → projectRoundStates in project router (7 occurrences) - H2: Fix deliberation results panel blank table (wrong field names) - H3: Fix deliberation participant names blank (wrong data path) - H4: Fix awards "Evaluated" stat duplicating "Eligible" count - H5: Fix cross-round comparison enabled at 1 round (backend requires 2) - H6: Fix setState during render anti-pattern (6 occurrences) - H7: Fix round detail jury member count always showing 0 - H8: Remove 4 invalid status values from observer dashboard filter - H9: Fix filtering progress bar always showing 100% MEDIUM fixes (misleading display): - M1: Filter special-award rounds from competition timeline - M2: Exclude special-award rounds from distinct project count - M3: Fix MENTORING pipeline node hardcoded "0 mentored" - M4: Fix DELIB_LOCKED badge using red for success state - M5: Add status label maps to deliberation session detail - M6: Humanize deliberation category + tie-break method displays - M8: Rename setStageId → setRoundId, "Select Stage" → "Select Round" - M9: Add missing INVITED/ACTIVE/SUSPENDED to members status labels - M10: Add ROUND_DRAFT/ACTIVE/CLOSED/ARCHIVED to StatusBadge - M11: Fix unsent messages showing "Scheduled" instead of "Draft" - M12: Rename misleading totalEvaluations → totalAssignments - M13: Rename "Stage" column to "Program" in projects page LOW fixes (cosmetic / edge-case): - L1: Use unfiltered rounds array for active round detection - L2: Use all rounds length for new round sort order - L3: Filter special-award rounds from header count - L4: Fix single-underscore replace in award status badges - L5: Fix score bucket boundary gaps (4.99 dropped between buckets) - L6: Title-case LIVE_FINAL pipeline metric status - L7: Fix roundType.replace only replacing first underscore - L8: Remove duplicate severity sort in smart-actions component Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 11:11:00 +01:00
}
}, [stages.length, selectedValue])
const queryInput = parseSelection(selectedValue)
const hasSelection = !!queryInput.roundId || !!queryInput.programId
const { data: consistency, isLoading: consistencyLoading } =
trpc.analytics.getJurorConsistency.useQuery(
queryInput,
{ enabled: hasSelection }
)
if (programsLoading) {
return <Skeleton className="h-[400px]" />
}
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<label className="text-sm font-medium">Select Stage:</label>
<Select value={selectedValue || ''} onValueChange={setSelectedValue}>
<SelectTrigger className="w-[300px]">
<SelectValue placeholder="Select a stage" />
</SelectTrigger>
<SelectContent>
{programs?.map((p) => (
<SelectItem key={`all:${p.id}`} value={`all:${p.id}`}>
{p.year} Edition All Stages
</SelectItem>
))}
{stages.map((stage) => (
<SelectItem key={stage.id} value={stage.id}>
{stage.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{consistencyLoading && <Skeleton className="h-[400px]" />}
{consistency && (
<JurorConsistencyChart
data={consistency as {
overallAverage: number
jurors: Array<{
userId: string
name: string
email: string
evaluationCount: number
averageScore: number
stddev: number
deviationFromOverall: number
isOutlier: boolean
}>
}}
/>
)}
</div>
)
}
function DiversityTab() {
const [selectedValue, setSelectedValue] = useState<string | null>(null)
const { data: programs, isLoading: programsLoading } = trpc.program.list.useQuery({ includeStages: true })
const stages = programs?.flatMap((p) =>
((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` }))
) || []
Admin UI audit round 2: fix 28 display bugs across 23 files HIGH fixes (broken features / wrong data): - H1: Fix roundAssignments → projectRoundStates in project router (7 occurrences) - H2: Fix deliberation results panel blank table (wrong field names) - H3: Fix deliberation participant names blank (wrong data path) - H4: Fix awards "Evaluated" stat duplicating "Eligible" count - H5: Fix cross-round comparison enabled at 1 round (backend requires 2) - H6: Fix setState during render anti-pattern (6 occurrences) - H7: Fix round detail jury member count always showing 0 - H8: Remove 4 invalid status values from observer dashboard filter - H9: Fix filtering progress bar always showing 100% MEDIUM fixes (misleading display): - M1: Filter special-award rounds from competition timeline - M2: Exclude special-award rounds from distinct project count - M3: Fix MENTORING pipeline node hardcoded "0 mentored" - M4: Fix DELIB_LOCKED badge using red for success state - M5: Add status label maps to deliberation session detail - M6: Humanize deliberation category + tie-break method displays - M8: Rename setStageId → setRoundId, "Select Stage" → "Select Round" - M9: Add missing INVITED/ACTIVE/SUSPENDED to members status labels - M10: Add ROUND_DRAFT/ACTIVE/CLOSED/ARCHIVED to StatusBadge - M11: Fix unsent messages showing "Scheduled" instead of "Draft" - M12: Rename misleading totalEvaluations → totalAssignments - M13: Rename "Stage" column to "Program" in projects page LOW fixes (cosmetic / edge-case): - L1: Use unfiltered rounds array for active round detection - L2: Use all rounds length for new round sort order - L3: Filter special-award rounds from header count - L4: Fix single-underscore replace in award status badges - L5: Fix score bucket boundary gaps (4.99 dropped between buckets) - L6: Title-case LIVE_FINAL pipeline metric status - L7: Fix roundType.replace only replacing first underscore - L8: Remove duplicate severity sort in smart-actions component Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 11:11:00 +01:00
useEffect(() => {
if (stages.length && !selectedValue) {
setSelectedValue(findDefaultRound(stages) ?? stages[0].id)
Admin UI audit round 2: fix 28 display bugs across 23 files HIGH fixes (broken features / wrong data): - H1: Fix roundAssignments → projectRoundStates in project router (7 occurrences) - H2: Fix deliberation results panel blank table (wrong field names) - H3: Fix deliberation participant names blank (wrong data path) - H4: Fix awards "Evaluated" stat duplicating "Eligible" count - H5: Fix cross-round comparison enabled at 1 round (backend requires 2) - H6: Fix setState during render anti-pattern (6 occurrences) - H7: Fix round detail jury member count always showing 0 - H8: Remove 4 invalid status values from observer dashboard filter - H9: Fix filtering progress bar always showing 100% MEDIUM fixes (misleading display): - M1: Filter special-award rounds from competition timeline - M2: Exclude special-award rounds from distinct project count - M3: Fix MENTORING pipeline node hardcoded "0 mentored" - M4: Fix DELIB_LOCKED badge using red for success state - M5: Add status label maps to deliberation session detail - M6: Humanize deliberation category + tie-break method displays - M8: Rename setStageId → setRoundId, "Select Stage" → "Select Round" - M9: Add missing INVITED/ACTIVE/SUSPENDED to members status labels - M10: Add ROUND_DRAFT/ACTIVE/CLOSED/ARCHIVED to StatusBadge - M11: Fix unsent messages showing "Scheduled" instead of "Draft" - M12: Rename misleading totalEvaluations → totalAssignments - M13: Rename "Stage" column to "Program" in projects page LOW fixes (cosmetic / edge-case): - L1: Use unfiltered rounds array for active round detection - L2: Use all rounds length for new round sort order - L3: Filter special-award rounds from header count - L4: Fix single-underscore replace in award status badges - L5: Fix score bucket boundary gaps (4.99 dropped between buckets) - L6: Title-case LIVE_FINAL pipeline metric status - L7: Fix roundType.replace only replacing first underscore - L8: Remove duplicate severity sort in smart-actions component Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 11:11:00 +01:00
}
}, [stages.length, selectedValue])
const queryInput = parseSelection(selectedValue)
const hasSelection = !!queryInput.roundId || !!queryInput.programId
const { data: diversity, isLoading: diversityLoading } =
trpc.analytics.getDiversityMetrics.useQuery(
queryInput,
{ enabled: hasSelection }
)
if (programsLoading) {
return <Skeleton className="h-[400px]" />
}
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<label className="text-sm font-medium">Select Stage:</label>
<Select value={selectedValue || ''} onValueChange={setSelectedValue}>
<SelectTrigger className="w-[300px]">
<SelectValue placeholder="Select a stage" />
</SelectTrigger>
<SelectContent>
{programs?.map((p) => (
<SelectItem key={`all:${p.id}`} value={`all:${p.id}`}>
{p.year} Edition All Stages
</SelectItem>
))}
{stages.map((stage) => (
<SelectItem key={stage.id} value={stage.id}>
{stage.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{diversityLoading && <Skeleton className="h-[400px]" />}
{diversity && (
<DiversityMetricsChart
data={diversity as {
total: number
byCountry: { country: string; count: number; percentage: number }[]
byCategory: { category: string; count: number; percentage: number }[]
byOceanIssue: { issue: string; count: number; percentage: number }[]
byTag: { tag: string; count: number; percentage: number }[]
}}
/>
)}
</div>
)
}
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code Phase 1 — Critical bugs: - Fix deliberation participant selection (wire jury group query) - Fix reports "By Round" tab (inline content instead of 404 route) - Fix messages "Sent History" (add message.sent procedure, wire tab) - Add missing fields to competition award form (criteriaText, maxRankedPicks) - Wire LiveControlPanel buttons (cursor, voting, scores) - Fix ResultLockControls empty snapshot (fetch actual data before lock) - Fix SubmissionWindowManager losing fields on edit Phase 2 — Backend fixes: - Remove write-in-query from specialAward.get - Fix award eligibility job overwriting manual shortlist overrides - Fix filtering startJob deleting all prior results (defer cleanup to post-success) - Tighten access control: protectedProcedure → adminProcedure on 8 procedures - Add audit logging to deliberation mutations - Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete Phase 3 — Auto-refresh: - Add refetchInterval to 15+ admin pages/components (10s–30s) - Fix AI job polling: derive speed from job status for all viewers Phase 4 — Dead code cleanup: - Delete unused command-palette, pdf-report, admin-page-transition - Remove dead subItems sidebar code, unused GripVertical import - Replace redundant isGenerating state with mutation.isPending - Add Role column to jury members table - Remove misleading manual mentor assignment stub Phase 5 — UX improvements: - Fix rounds page single-competition assumption (add selector) - Remove raw UUID fallback in deliberation config - Fix programs page "Stage" → "Round" terminology Phase 6 — Backend hardening: - Complete logAudit calls (add prisma, ipAddress, userAgent) - Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear) - Batch user.bulkCreate writes (assignments, jury memberships, intents) - Remove any casts from deliberation service (typed PrismaClient + TransactionClient) - Fix stale DeliberationStatus enum values blocking build 40 files changed, 1010 insertions(+), 612 deletions(-) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
function RoundPipelineTab() {
const { data: programs, isLoading } = trpc.program.list.useQuery({ includeStages: true })
const rounds = programs?.flatMap(p =>
((p.stages ?? []) as Array<{ id: string; name: string; status: string; type?: string }>).map((s) => ({
...s,
programId: p.id,
programName: `${p.year} Edition`,
}))
) || []
const roundIds = rounds.map(r => r.id)
const { data: comparison, isLoading: comparisonLoading } =
trpc.analytics.getCrossRoundComparison.useQuery(
{ roundIds },
Admin UI audit round 2: fix 28 display bugs across 23 files HIGH fixes (broken features / wrong data): - H1: Fix roundAssignments → projectRoundStates in project router (7 occurrences) - H2: Fix deliberation results panel blank table (wrong field names) - H3: Fix deliberation participant names blank (wrong data path) - H4: Fix awards "Evaluated" stat duplicating "Eligible" count - H5: Fix cross-round comparison enabled at 1 round (backend requires 2) - H6: Fix setState during render anti-pattern (6 occurrences) - H7: Fix round detail jury member count always showing 0 - H8: Remove 4 invalid status values from observer dashboard filter - H9: Fix filtering progress bar always showing 100% MEDIUM fixes (misleading display): - M1: Filter special-award rounds from competition timeline - M2: Exclude special-award rounds from distinct project count - M3: Fix MENTORING pipeline node hardcoded "0 mentored" - M4: Fix DELIB_LOCKED badge using red for success state - M5: Add status label maps to deliberation session detail - M6: Humanize deliberation category + tie-break method displays - M8: Rename setStageId → setRoundId, "Select Stage" → "Select Round" - M9: Add missing INVITED/ACTIVE/SUSPENDED to members status labels - M10: Add ROUND_DRAFT/ACTIVE/CLOSED/ARCHIVED to StatusBadge - M11: Fix unsent messages showing "Scheduled" instead of "Draft" - M12: Rename misleading totalEvaluations → totalAssignments - M13: Rename "Stage" column to "Program" in projects page LOW fixes (cosmetic / edge-case): - L1: Use unfiltered rounds array for active round detection - L2: Use all rounds length for new round sort order - L3: Filter special-award rounds from header count - L4: Fix single-underscore replace in award status badges - L5: Fix score bucket boundary gaps (4.99 dropped between buckets) - L6: Title-case LIVE_FINAL pipeline metric status - L7: Fix roundType.replace only replacing first underscore - L8: Remove duplicate severity sort in smart-actions component Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 11:11:00 +01:00
{ enabled: roundIds.length >= 2 }
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code Phase 1 — Critical bugs: - Fix deliberation participant selection (wire jury group query) - Fix reports "By Round" tab (inline content instead of 404 route) - Fix messages "Sent History" (add message.sent procedure, wire tab) - Add missing fields to competition award form (criteriaText, maxRankedPicks) - Wire LiveControlPanel buttons (cursor, voting, scores) - Fix ResultLockControls empty snapshot (fetch actual data before lock) - Fix SubmissionWindowManager losing fields on edit Phase 2 — Backend fixes: - Remove write-in-query from specialAward.get - Fix award eligibility job overwriting manual shortlist overrides - Fix filtering startJob deleting all prior results (defer cleanup to post-success) - Tighten access control: protectedProcedure → adminProcedure on 8 procedures - Add audit logging to deliberation mutations - Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete Phase 3 — Auto-refresh: - Add refetchInterval to 15+ admin pages/components (10s–30s) - Fix AI job polling: derive speed from job status for all viewers Phase 4 — Dead code cleanup: - Delete unused command-palette, pdf-report, admin-page-transition - Remove dead subItems sidebar code, unused GripVertical import - Replace redundant isGenerating state with mutation.isPending - Add Role column to jury members table - Remove misleading manual mentor assignment stub Phase 5 — UX improvements: - Fix rounds page single-competition assumption (add selector) - Remove raw UUID fallback in deliberation config - Fix programs page "Stage" → "Round" terminology Phase 6 — Backend hardening: - Complete logAudit calls (add prisma, ipAddress, userAgent) - Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear) - Batch user.bulkCreate writes (assignments, jury memberships, intents) - Remove any casts from deliberation service (typed PrismaClient + TransactionClient) - Fix stale DeliberationStatus enum values blocking build 40 files changed, 1010 insertions(+), 612 deletions(-) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
)
if (isLoading || comparisonLoading) {
return (
<div className="space-y-4">
{[1, 2, 3].map(i => <Skeleton key={i} className="h-24" />)}
</div>
)
}
if (!rounds.length) {
return (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
<Layers className="h-12 w-12 text-muted-foreground/50" />
<p className="mt-2 font-medium">No rounds available</p>
</CardContent>
</Card>
)
}
const comparisonMap = new Map(
(comparison ?? []).map((c: any) => [c.roundId, c])
)
return (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<div className="rounded-lg bg-violet-500/10 p-1.5">
<Layers className="h-4 w-4 text-violet-600" />
</div>
Round Pipeline
</CardTitle>
<CardDescription>Project flow across competition rounds</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
{rounds.map((round, idx) => {
const stats = comparisonMap.get(round.id) as any
const isClosed = round.status === 'ROUND_CLOSED' || round.status === 'ROUND_ARCHIVED'
const progressValue = isClosed ? 100 : (stats?.completionRate ?? 0)
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code Phase 1 — Critical bugs: - Fix deliberation participant selection (wire jury group query) - Fix reports "By Round" tab (inline content instead of 404 route) - Fix messages "Sent History" (add message.sent procedure, wire tab) - Add missing fields to competition award form (criteriaText, maxRankedPicks) - Wire LiveControlPanel buttons (cursor, voting, scores) - Fix ResultLockControls empty snapshot (fetch actual data before lock) - Fix SubmissionWindowManager losing fields on edit Phase 2 — Backend fixes: - Remove write-in-query from specialAward.get - Fix award eligibility job overwriting manual shortlist overrides - Fix filtering startJob deleting all prior results (defer cleanup to post-success) - Tighten access control: protectedProcedure → adminProcedure on 8 procedures - Add audit logging to deliberation mutations - Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete Phase 3 — Auto-refresh: - Add refetchInterval to 15+ admin pages/components (10s–30s) - Fix AI job polling: derive speed from job status for all viewers Phase 4 — Dead code cleanup: - Delete unused command-palette, pdf-report, admin-page-transition - Remove dead subItems sidebar code, unused GripVertical import - Replace redundant isGenerating state with mutation.isPending - Add Role column to jury members table - Remove misleading manual mentor assignment stub Phase 5 — UX improvements: - Fix rounds page single-competition assumption (add selector) - Remove raw UUID fallback in deliberation config - Fix programs page "Stage" → "Round" terminology Phase 6 — Backend hardening: - Complete logAudit calls (add prisma, ipAddress, userAgent) - Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear) - Batch user.bulkCreate writes (assignments, jury memberships, intents) - Remove any casts from deliberation service (typed PrismaClient + TransactionClient) - Fix stale DeliberationStatus enum values blocking build 40 files changed, 1010 insertions(+), 612 deletions(-) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
return (
<div key={round.id} className="flex items-center gap-4">
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted text-sm font-medium">
{idx + 1}
</div>
<div className="flex-1">
<div className="flex items-center justify-between">
<div>
<p className="font-medium">{round.name}</p>
<p className="text-xs text-muted-foreground">{round.programName}</p>
</div>
<div className="flex items-center gap-4 text-sm">
<span className="tabular-nums">{stats?.projectCount ?? 0} projects</span>
<span className="tabular-nums">{stats?.evaluationCount ?? 0} evals</span>
<Badge variant={round.status === 'ROUND_ACTIVE' ? 'default' : isClosed ? 'secondary' : 'outline'}>
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code Phase 1 — Critical bugs: - Fix deliberation participant selection (wire jury group query) - Fix reports "By Round" tab (inline content instead of 404 route) - Fix messages "Sent History" (add message.sent procedure, wire tab) - Add missing fields to competition award form (criteriaText, maxRankedPicks) - Wire LiveControlPanel buttons (cursor, voting, scores) - Fix ResultLockControls empty snapshot (fetch actual data before lock) - Fix SubmissionWindowManager losing fields on edit Phase 2 — Backend fixes: - Remove write-in-query from specialAward.get - Fix award eligibility job overwriting manual shortlist overrides - Fix filtering startJob deleting all prior results (defer cleanup to post-success) - Tighten access control: protectedProcedure → adminProcedure on 8 procedures - Add audit logging to deliberation mutations - Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete Phase 3 — Auto-refresh: - Add refetchInterval to 15+ admin pages/components (10s–30s) - Fix AI job polling: derive speed from job status for all viewers Phase 4 — Dead code cleanup: - Delete unused command-palette, pdf-report, admin-page-transition - Remove dead subItems sidebar code, unused GripVertical import - Replace redundant isGenerating state with mutation.isPending - Add Role column to jury members table - Remove misleading manual mentor assignment stub Phase 5 — UX improvements: - Fix rounds page single-competition assumption (add selector) - Remove raw UUID fallback in deliberation config - Fix programs page "Stage" → "Round" terminology Phase 6 — Backend hardening: - Complete logAudit calls (add prisma, ipAddress, userAgent) - Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear) - Batch user.bulkCreate writes (assignments, jury memberships, intents) - Remove any casts from deliberation service (typed PrismaClient + TransactionClient) - Fix stale DeliberationStatus enum values blocking build 40 files changed, 1010 insertions(+), 612 deletions(-) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
{round.status?.replace('ROUND_', '') ?? 'DRAFT'}
</Badge>
</div>
</div>
<Progress value={progressValue} className="mt-2 h-2" />
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code Phase 1 — Critical bugs: - Fix deliberation participant selection (wire jury group query) - Fix reports "By Round" tab (inline content instead of 404 route) - Fix messages "Sent History" (add message.sent procedure, wire tab) - Add missing fields to competition award form (criteriaText, maxRankedPicks) - Wire LiveControlPanel buttons (cursor, voting, scores) - Fix ResultLockControls empty snapshot (fetch actual data before lock) - Fix SubmissionWindowManager losing fields on edit Phase 2 — Backend fixes: - Remove write-in-query from specialAward.get - Fix award eligibility job overwriting manual shortlist overrides - Fix filtering startJob deleting all prior results (defer cleanup to post-success) - Tighten access control: protectedProcedure → adminProcedure on 8 procedures - Add audit logging to deliberation mutations - Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete Phase 3 — Auto-refresh: - Add refetchInterval to 15+ admin pages/components (10s–30s) - Fix AI job polling: derive speed from job status for all viewers Phase 4 — Dead code cleanup: - Delete unused command-palette, pdf-report, admin-page-transition - Remove dead subItems sidebar code, unused GripVertical import - Replace redundant isGenerating state with mutation.isPending - Add Role column to jury members table - Remove misleading manual mentor assignment stub Phase 5 — UX improvements: - Fix rounds page single-competition assumption (add selector) - Remove raw UUID fallback in deliberation config - Fix programs page "Stage" → "Round" terminology Phase 6 — Backend hardening: - Complete logAudit calls (add prisma, ipAddress, userAgent) - Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear) - Batch user.bulkCreate writes (assignments, jury memberships, intents) - Remove any casts from deliberation service (typed PrismaClient + TransactionClient) - Fix stale DeliberationStatus enum values blocking build 40 files changed, 1010 insertions(+), 612 deletions(-) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
</div>
</div>
)
})}
</div>
</CardContent>
</Card>
</div>
)
}
export default function ReportsPage() {
const [pdfStageId, setPdfStageId] = useState<string | null>(null)
const { data: pdfPrograms } = trpc.program.list.useQuery({ includeStages: true })
const pdfStages = pdfPrograms?.flatMap((p) =>
((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` }))
) || []
Admin UI audit round 2: fix 28 display bugs across 23 files HIGH fixes (broken features / wrong data): - H1: Fix roundAssignments → projectRoundStates in project router (7 occurrences) - H2: Fix deliberation results panel blank table (wrong field names) - H3: Fix deliberation participant names blank (wrong data path) - H4: Fix awards "Evaluated" stat duplicating "Eligible" count - H5: Fix cross-round comparison enabled at 1 round (backend requires 2) - H6: Fix setState during render anti-pattern (6 occurrences) - H7: Fix round detail jury member count always showing 0 - H8: Remove 4 invalid status values from observer dashboard filter - H9: Fix filtering progress bar always showing 100% MEDIUM fixes (misleading display): - M1: Filter special-award rounds from competition timeline - M2: Exclude special-award rounds from distinct project count - M3: Fix MENTORING pipeline node hardcoded "0 mentored" - M4: Fix DELIB_LOCKED badge using red for success state - M5: Add status label maps to deliberation session detail - M6: Humanize deliberation category + tie-break method displays - M8: Rename setStageId → setRoundId, "Select Stage" → "Select Round" - M9: Add missing INVITED/ACTIVE/SUSPENDED to members status labels - M10: Add ROUND_DRAFT/ACTIVE/CLOSED/ARCHIVED to StatusBadge - M11: Fix unsent messages showing "Scheduled" instead of "Draft" - M12: Rename misleading totalEvaluations → totalAssignments - M13: Rename "Stage" column to "Program" in projects page LOW fixes (cosmetic / edge-case): - L1: Use unfiltered rounds array for active round detection - L2: Use all rounds length for new round sort order - L3: Filter special-award rounds from header count - L4: Fix single-underscore replace in award status badges - L5: Fix score bucket boundary gaps (4.99 dropped between buckets) - L6: Title-case LIVE_FINAL pipeline metric status - L7: Fix roundType.replace only replacing first underscore - L8: Remove duplicate severity sort in smart-actions component Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 11:11:00 +01:00
useEffect(() => {
if (pdfStages.length && !pdfStageId) {
setPdfStageId(findDefaultRound(pdfStages) ?? pdfStages[0].id)
Admin UI audit round 2: fix 28 display bugs across 23 files HIGH fixes (broken features / wrong data): - H1: Fix roundAssignments → projectRoundStates in project router (7 occurrences) - H2: Fix deliberation results panel blank table (wrong field names) - H3: Fix deliberation participant names blank (wrong data path) - H4: Fix awards "Evaluated" stat duplicating "Eligible" count - H5: Fix cross-round comparison enabled at 1 round (backend requires 2) - H6: Fix setState during render anti-pattern (6 occurrences) - H7: Fix round detail jury member count always showing 0 - H8: Remove 4 invalid status values from observer dashboard filter - H9: Fix filtering progress bar always showing 100% MEDIUM fixes (misleading display): - M1: Filter special-award rounds from competition timeline - M2: Exclude special-award rounds from distinct project count - M3: Fix MENTORING pipeline node hardcoded "0 mentored" - M4: Fix DELIB_LOCKED badge using red for success state - M5: Add status label maps to deliberation session detail - M6: Humanize deliberation category + tie-break method displays - M8: Rename setStageId → setRoundId, "Select Stage" → "Select Round" - M9: Add missing INVITED/ACTIVE/SUSPENDED to members status labels - M10: Add ROUND_DRAFT/ACTIVE/CLOSED/ARCHIVED to StatusBadge - M11: Fix unsent messages showing "Scheduled" instead of "Draft" - M12: Rename misleading totalEvaluations → totalAssignments - M13: Rename "Stage" column to "Program" in projects page LOW fixes (cosmetic / edge-case): - L1: Use unfiltered rounds array for active round detection - L2: Use all rounds length for new round sort order - L3: Filter special-award rounds from header count - L4: Fix single-underscore replace in award status badges - L5: Fix score bucket boundary gaps (4.99 dropped between buckets) - L6: Title-case LIVE_FINAL pipeline metric status - L7: Fix roundType.replace only replacing first underscore - L8: Remove duplicate severity sort in smart-actions component Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 11:11:00 +01:00
}
}, [pdfStages.length, pdfStageId])
const selectedPdfStage = pdfStages.find((r) => r.id === pdfStageId)
return (
<div className="space-y-6">
{/* Header */}
<div>
<h1 className="text-2xl font-semibold tracking-tight">Reports</h1>
<p className="text-muted-foreground">
View progress, analytics, and export evaluation data
</p>
</div>
{/* Tabs */}
<Tabs defaultValue="overview" className="space-y-6">
<div className="flex items-center justify-between flex-wrap gap-4">
<TabsList className="w-full sm:w-auto flex-wrap justify-start overflow-x-auto scrollbar-hide">
<TabsTrigger value="overview" className="gap-2">
<FileSpreadsheet className="h-4 w-4" />
Overview
</TabsTrigger>
<TabsTrigger value="analytics" className="gap-2">
<TrendingUp className="h-4 w-4" />
Analytics
</TabsTrigger>
<TabsTrigger value="cross-stage" className="gap-2">
<GitCompare className="h-4 w-4" />
Cross-Round
</TabsTrigger>
<TabsTrigger value="consistency" className="gap-2">
<UserCheck className="h-4 w-4" />
Juror Consistency
</TabsTrigger>
<TabsTrigger value="diversity" className="gap-2">
<Globe className="h-4 w-4" />
Diversity
</TabsTrigger>
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code Phase 1 — Critical bugs: - Fix deliberation participant selection (wire jury group query) - Fix reports "By Round" tab (inline content instead of 404 route) - Fix messages "Sent History" (add message.sent procedure, wire tab) - Add missing fields to competition award form (criteriaText, maxRankedPicks) - Wire LiveControlPanel buttons (cursor, voting, scores) - Fix ResultLockControls empty snapshot (fetch actual data before lock) - Fix SubmissionWindowManager losing fields on edit Phase 2 — Backend fixes: - Remove write-in-query from specialAward.get - Fix award eligibility job overwriting manual shortlist overrides - Fix filtering startJob deleting all prior results (defer cleanup to post-success) - Tighten access control: protectedProcedure → adminProcedure on 8 procedures - Add audit logging to deliberation mutations - Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete Phase 3 — Auto-refresh: - Add refetchInterval to 15+ admin pages/components (10s–30s) - Fix AI job polling: derive speed from job status for all viewers Phase 4 — Dead code cleanup: - Delete unused command-palette, pdf-report, admin-page-transition - Remove dead subItems sidebar code, unused GripVertical import - Replace redundant isGenerating state with mutation.isPending - Add Role column to jury members table - Remove misleading manual mentor assignment stub Phase 5 — UX improvements: - Fix rounds page single-competition assumption (add selector) - Remove raw UUID fallback in deliberation config - Fix programs page "Stage" → "Round" terminology Phase 6 — Backend hardening: - Complete logAudit calls (add prisma, ipAddress, userAgent) - Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear) - Batch user.bulkCreate writes (assignments, jury memberships, intents) - Remove any casts from deliberation service (typed PrismaClient + TransactionClient) - Fix stale DeliberationStatus enum values blocking build 40 files changed, 1010 insertions(+), 612 deletions(-) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
<TabsTrigger value="pipeline" className="gap-2">
<Layers className="h-4 w-4" />
By Round
</TabsTrigger>
</TabsList>
<div className="flex items-center gap-2 w-full sm:w-auto justify-between sm:justify-end">
<Select value={pdfStageId || ''} onValueChange={setPdfStageId}>
<SelectTrigger className="w-[220px]">
<SelectValue placeholder="Select stage for PDF" />
</SelectTrigger>
<SelectContent>
{pdfStages.map((stage) => (
<SelectItem key={stage.id} value={stage.id}>
{stage.name}
</SelectItem>
))}
</SelectContent>
</Select>
{pdfStageId && (
<ExportPdfButton
roundId={pdfStageId}
roundName={selectedPdfStage?.name}
programName={selectedPdfStage?.programName}
/>
)}
</div>
</div>
<TabsContent value="overview">
<ReportsOverview />
</TabsContent>
<TabsContent value="analytics">
<StageAnalytics />
</TabsContent>
<TabsContent value="cross-stage">
<CrossStageTab />
</TabsContent>
<TabsContent value="consistency">
<JurorConsistencyTab />
</TabsContent>
<TabsContent value="diversity">
<DiversityTab />
</TabsContent>
Admin platform audit: fix bugs, harden backend, add auto-refresh, clean dead code Phase 1 — Critical bugs: - Fix deliberation participant selection (wire jury group query) - Fix reports "By Round" tab (inline content instead of 404 route) - Fix messages "Sent History" (add message.sent procedure, wire tab) - Add missing fields to competition award form (criteriaText, maxRankedPicks) - Wire LiveControlPanel buttons (cursor, voting, scores) - Fix ResultLockControls empty snapshot (fetch actual data before lock) - Fix SubmissionWindowManager losing fields on edit Phase 2 — Backend fixes: - Remove write-in-query from specialAward.get - Fix award eligibility job overwriting manual shortlist overrides - Fix filtering startJob deleting all prior results (defer cleanup to post-success) - Tighten access control: protectedProcedure → adminProcedure on 8 procedures - Add audit logging to deliberation mutations - Add FINALIST/SEMIFINALIST delete guard on project.delete/bulkDelete Phase 3 — Auto-refresh: - Add refetchInterval to 15+ admin pages/components (10s–30s) - Fix AI job polling: derive speed from job status for all viewers Phase 4 — Dead code cleanup: - Delete unused command-palette, pdf-report, admin-page-transition - Remove dead subItems sidebar code, unused GripVertical import - Replace redundant isGenerating state with mutation.isPending - Add Role column to jury members table - Remove misleading manual mentor assignment stub Phase 5 — UX improvements: - Fix rounds page single-competition assumption (add selector) - Remove raw UUID fallback in deliberation config - Fix programs page "Stage" → "Round" terminology Phase 6 — Backend hardening: - Complete logAudit calls (add prisma, ipAddress, userAgent) - Batch analytics queries (fix N+1 in getCrossRoundComparison, getYearOverYear) - Batch user.bulkCreate writes (assignments, jury memberships, intents) - Remove any casts from deliberation service (typed PrismaClient + TransactionClient) - Fix stale DeliberationStatus enum values blocking build 40 files changed, 1010 insertions(+), 612 deletions(-) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 08:20:13 +01:00
<TabsContent value="pipeline">
<RoundPipelineTab />
</TabsContent>
</Tabs>
</div>
)
}