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:
Matt
2026-02-19 09:56:09 +01:00
parent d117090fca
commit ae1685179c
12 changed files with 68 additions and 37 deletions

View File

@@ -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>
))}

View File

@@ -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) {