feat: add admin advancement summary card and advance column in assignments table
- Update listByStage query to include evaluation form criteriaJson and criterionScoresJson - Add Advance column to individual assignments table showing YES/NO badge per submitted evaluation - Create AdvancementSummaryCard component showing yes/no/pending vote counts with stacked bar - Wire AdvancementSummaryCard into the EVALUATION round overview tab Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
93
src/components/admin/round/advancement-summary-card.tsx
Normal file
93
src/components/admin/round/advancement-summary-card.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
'use client'
|
||||
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { ThumbsUp, ThumbsDown, Clock } from 'lucide-react'
|
||||
|
||||
export function AdvancementSummaryCard({ roundId }: { roundId: string }) {
|
||||
const { data: assignments, isLoading } = trpc.assignment.listByStage.useQuery(
|
||||
{ roundId },
|
||||
{ refetchInterval: 15_000 },
|
||||
)
|
||||
|
||||
if (isLoading) return <Skeleton className="h-40 w-full" />
|
||||
|
||||
if (!assignments || assignments.length === 0) return null
|
||||
|
||||
// Check if form has an advance criterion
|
||||
const firstSubmitted = assignments.find(
|
||||
(a: any) => a.evaluation?.status === 'SUBMITTED' && a.evaluation?.form?.criteriaJson
|
||||
)
|
||||
if (!firstSubmitted) return null
|
||||
|
||||
const criteria = ((firstSubmitted as any).evaluation?.form?.criteriaJson ?? []) as Array<{ id: string; type?: string }>
|
||||
const advanceCriterion = criteria.find((c) => c.type === 'advance')
|
||||
if (!advanceCriterion) return null
|
||||
|
||||
let yesCount = 0
|
||||
let noCount = 0
|
||||
let pendingCount = 0
|
||||
|
||||
for (const a of assignments as any[]) {
|
||||
const ev = a.evaluation
|
||||
if (!ev || ev.status !== 'SUBMITTED') {
|
||||
pendingCount++
|
||||
continue
|
||||
}
|
||||
const scores = (ev.criterionScoresJson ?? {}) as Record<string, unknown>
|
||||
const val = scores[advanceCriterion.id]
|
||||
if (val === true) yesCount++
|
||||
else if (val === false) noCount++
|
||||
else pendingCount++
|
||||
}
|
||||
|
||||
const total = yesCount + noCount + pendingCount
|
||||
const yesPct = total > 0 ? Math.round((yesCount / total) * 100) : 0
|
||||
const noPct = total > 0 ? Math.round((noCount / total) * 100) : 0
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-base">Advancement Votes</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-10 w-10 rounded-full bg-emerald-100 flex items-center justify-center">
|
||||
<ThumbsUp className="h-5 w-5 text-emerald-700" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-emerald-700">{yesCount}</p>
|
||||
<p className="text-xs text-muted-foreground">Yes ({yesPct}%)</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-10 w-10 rounded-full bg-red-100 flex items-center justify-center">
|
||||
<ThumbsDown className="h-5 w-5 text-red-700" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-red-700">{noCount}</p>
|
||||
<p className="text-xs text-muted-foreground">No ({noPct}%)</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-10 w-10 rounded-full bg-gray-100 flex items-center justify-center">
|
||||
<Clock className="h-5 w-5 text-gray-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl font-bold text-gray-600">{pendingCount}</p>
|
||||
<p className="text-xs text-muted-foreground">Pending</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stacked bar */}
|
||||
<div className="mt-4 h-3 rounded-full bg-gray-100 overflow-hidden flex">
|
||||
{yesPct > 0 && <div className="bg-emerald-500 transition-all" style={{ width: `${yesPct}%` }} />}
|
||||
{noPct > 0 && <div className="bg-red-500 transition-all" style={{ width: `${noPct}%` }} />}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user