Comprehensive admin UI stats audit: fix 16 display bugs
HIGH fixes: - H1: Competition detail project count no longer double-counts across rounds - H2: Rounds page header stats use unfiltered round set - H3: Rounds page "eval" label corrected to "asgn" (assignment count) - H4: Observer reports project count uses distinct analytics count - H5: Awards eligibility count filters to only eligible=true (backend) - H6: Round detail projectCount derived from projectStates for consistency - H7: Deliberation hasVoted derived from votes array (was always undefined) MEDIUM fixes: - M1: Reports page round status badges use correct ROUND_ACTIVE/ROUND_CLOSED enums - M2: Observer reports badges use ROUND_ prefix instead of stale STAGE_ prefix - M3: Deliberation list status badges use correct VOTING/TALLYING/RUNOFF enums - M4: Competition list/detail round count excludes special-award rounds (backend) - M5: Messages page shows actual recipient count instead of hardcoded "1 user" LOW fixes: - L2: Observer analytics jurorCount scoped to round when roundId provided - L3: Analytics round-scoped project count uses ProjectRoundState not assignments - L4: JuryGroup delete audit log reports member count (not assignment count) - L5: Project rankings include unevaluated projects at bottom instead of hiding Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { use } from 'react';
|
||||
import { use, useMemo } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { trpc } from '@/lib/trpc/client';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -46,6 +46,12 @@ export default function DeliberationSessionPage({
|
||||
}
|
||||
});
|
||||
|
||||
// Derive which participants have voted from the votes array
|
||||
const voterUserIds = useMemo(() => {
|
||||
if (!session?.votes) return new Set<string>();
|
||||
return new Set(session.votes.map((v: any) => v.juryMember?.user?.id).filter(Boolean));
|
||||
}, [session?.votes]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -153,8 +159,8 @@ export default function DeliberationSessionPage({
|
||||
<p className="font-medium">{participant.user?.name}</p>
|
||||
<p className="text-sm text-muted-foreground">{participant.user?.email}</p>
|
||||
</div>
|
||||
<Badge variant={participant.hasVoted ? 'default' : 'outline'}>
|
||||
{participant.hasVoted ? 'Voted' : 'Pending'}
|
||||
<Badge variant={voterUserIds.has(participant.user?.user?.id) ? 'default' : 'outline'}>
|
||||
{voterUserIds.has(participant.user?.user?.id) ? 'Voted' : 'Pending'}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
@@ -206,8 +212,8 @@ export default function DeliberationSessionPage({
|
||||
className="flex items-center justify-between rounded-lg border p-3"
|
||||
>
|
||||
<span>{participant.user?.name}</span>
|
||||
<Badge variant={participant.hasVoted ? 'default' : 'secondary'}>
|
||||
{participant.hasVoted ? 'Submitted' : 'Not Voted'}
|
||||
<Badge variant={voterUserIds.has(participant.user?.user?.id) ? 'default' : 'secondary'}>
|
||||
{voterUserIds.has(participant.user?.user?.id) ? 'Submitted' : 'Not Voted'}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -106,11 +106,19 @@ export default function DeliberationListPage({
|
||||
const getStatusBadge = (status: string) => {
|
||||
const variants: Record<string, 'default' | 'secondary' | 'destructive' | 'outline'> = {
|
||||
DELIB_OPEN: 'outline',
|
||||
DELIB_VOTING: 'default',
|
||||
DELIB_TALLYING: 'secondary',
|
||||
DELIB_LOCKED: 'destructive'
|
||||
VOTING: 'default',
|
||||
TALLYING: 'secondary',
|
||||
RUNOFF: 'secondary',
|
||||
DELIB_LOCKED: 'destructive',
|
||||
};
|
||||
return <Badge variant={variants[status] || 'outline'}>{status}</Badge>;
|
||||
const labels: Record<string, string> = {
|
||||
DELIB_OPEN: 'Open',
|
||||
VOTING: 'Voting',
|
||||
TALLYING: 'Tallying',
|
||||
RUNOFF: 'Runoff',
|
||||
DELIB_LOCKED: 'Locked',
|
||||
};
|
||||
return <Badge variant={variants[status] || 'outline'}>{labels[status] || status}</Badge>;
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
|
||||
@@ -285,7 +285,7 @@ export default function CompetitionDetailPage() {
|
||||
<Layers className="h-4 w-4 text-blue-500" />
|
||||
<span className="text-sm font-medium">Rounds</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold mt-1">{competition.rounds.length}</p>
|
||||
<p className="text-2xl font-bold mt-1">{competition.rounds.filter((r: any) => !r.specialAwardId).length}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
@@ -304,7 +304,7 @@ export default function CompetitionDetailPage() {
|
||||
<span className="text-sm font-medium">Projects</span>
|
||||
</div>
|
||||
<p className="text-2xl font-bold mt-1">
|
||||
{competition.rounds.reduce((sum: number, r: any) => sum + (r._count?.projectRoundStates ?? 0), 0)}
|
||||
{(competition as any).distinctProjectCount ?? 0}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -592,7 +592,7 @@ export default function MessagesPage() {
|
||||
: msg.recipientType === 'ROUND_JURY'
|
||||
? 'Round jury'
|
||||
: msg.recipientType === 'USER'
|
||||
? '1 user'
|
||||
? `${recipientCount || 1} user${recipientCount > 1 ? 's' : ''}`
|
||||
: msg.recipientType}
|
||||
{recipientCount > 0 && ` (${recipientCount})`}
|
||||
</TableCell>
|
||||
|
||||
@@ -354,14 +354,14 @@ function ReportsOverview() {
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={
|
||||
round.status === 'ACTIVE'
|
||||
round.status === 'ROUND_ACTIVE'
|
||||
? 'default'
|
||||
: round.status === 'CLOSED'
|
||||
: round.status === 'ROUND_CLOSED'
|
||||
? 'secondary'
|
||||
: 'outline'
|
||||
}
|
||||
>
|
||||
{round.status}
|
||||
{round.status?.replace('ROUND_', '') || round.status}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
|
||||
@@ -441,7 +441,7 @@ export default function RoundDetailPage() {
|
||||
}, [configJson])
|
||||
|
||||
// ── Computed values ────────────────────────────────────────────────────
|
||||
const projectCount = round?._count?.projectRoundStates ?? 0
|
||||
const projectCount = projectStates?.length ?? round?._count?.projectRoundStates ?? 0
|
||||
const stateCounts = useMemo(() =>
|
||||
projectStates?.reduce((acc: Record<string, number>, ps: any) => {
|
||||
acc[ps.state] = (acc[ps.state] || 0) + 1
|
||||
|
||||
@@ -284,7 +284,8 @@ export default function RoundsPage() {
|
||||
|
||||
const activeFilter = filterType !== 'all'
|
||||
const totalProjects = (compDetail as any)?.distinctProjectCount ?? 0
|
||||
const totalAssignments = rounds.reduce((s, r) => s + r._count.assignments, 0)
|
||||
const allRounds = (compDetail?.rounds ?? []) as RoundWithStats[]
|
||||
const totalAssignments = allRounds.reduce((s, r) => s + r._count.assignments, 0)
|
||||
const activeRound = rounds.find((r) => r.status === 'ROUND_ACTIVE')
|
||||
|
||||
return (
|
||||
@@ -326,7 +327,7 @@ export default function RoundsPage() {
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 mt-1 text-sm text-muted-foreground">
|
||||
<span>{rounds.length} rounds</span>
|
||||
<span>{allRounds.length} rounds</span>
|
||||
<span className="text-muted-foreground/30">|</span>
|
||||
<span>{totalProjects} projects</span>
|
||||
<span className="text-muted-foreground/30">|</span>
|
||||
@@ -493,7 +494,7 @@ export default function RoundsPage() {
|
||||
{projectCount}
|
||||
</span>
|
||||
{assignmentCount > 0 && (
|
||||
<span className="tabular-nums">{assignmentCount} eval</span>
|
||||
<span className="tabular-nums">{assignmentCount} asgn</span>
|
||||
)}
|
||||
{(round.windowOpenAt || round.windowCloseAt) && (
|
||||
<span className="flex items-center gap-1 tabular-nums">
|
||||
|
||||
Reference in New Issue
Block a user