All checks were successful
Build and Push Docker Image / build (push) Successful in 8m23s
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>
151 lines
4.8 KiB
TypeScript
151 lines
4.8 KiB
TypeScript
'use client';
|
|
|
|
import { use } from 'react';
|
|
import { trpc } from '@/lib/trpc/client';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { DeliberationRankingForm } from '@/components/jury/deliberation-ranking-form';
|
|
import { CheckCircle2 } from 'lucide-react';
|
|
import { toast } from 'sonner';
|
|
|
|
export default function JuryDeliberationPage({ params: paramsPromise }: { params: Promise<{ sessionId: string }> }) {
|
|
const params = use(paramsPromise);
|
|
const utils = trpc.useUtils();
|
|
|
|
const { data: session, isLoading } = trpc.deliberation.getSession.useQuery(
|
|
{ sessionId: params.sessionId },
|
|
{ refetchInterval: 10_000 },
|
|
);
|
|
|
|
const submitVoteMutation = trpc.deliberation.submitVote.useMutation({
|
|
onSuccess: () => {
|
|
utils.deliberation.getSession.invalidate();
|
|
toast.success('Vote submitted successfully');
|
|
},
|
|
onError: (err) => {
|
|
toast.error(err.message);
|
|
}
|
|
});
|
|
|
|
const handleSubmitVote = (votes: Array<{ projectId: string; rank?: number; isWinnerPick?: boolean }>) => {
|
|
votes.forEach((vote) => {
|
|
submitVoteMutation.mutate({
|
|
sessionId: params.sessionId,
|
|
juryMemberId: '', // TODO: resolve current user's jury member ID from session participants
|
|
projectId: vote.projectId,
|
|
rank: vote.rank,
|
|
isWinnerPick: vote.isWinnerPick
|
|
});
|
|
});
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="space-y-6">
|
|
<Card>
|
|
<CardContent className="flex items-center justify-center py-12">
|
|
<p className="text-muted-foreground">Loading session...</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!session) {
|
|
return (
|
|
<div className="space-y-6">
|
|
<Card>
|
|
<CardContent className="flex items-center justify-center py-12">
|
|
<p className="text-muted-foreground">Session not found</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const hasVoted = false; // TODO: check if current user has voted in this session
|
|
|
|
if (session.status !== 'VOTING') {
|
|
return (
|
|
<div className="space-y-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Deliberation Session</CardTitle>
|
|
<CardDescription>
|
|
{session.round?.name} - {session.category}
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="flex flex-col items-center justify-center py-12">
|
|
<p className="text-muted-foreground">
|
|
{session.status === 'DELIB_OPEN'
|
|
? 'Voting has not started yet. Please wait for the admin to open voting.'
|
|
: session.status === 'TALLYING'
|
|
? 'Voting is closed. Results are being tallied.'
|
|
: 'This session is locked.'}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (hasVoted) {
|
|
return (
|
|
<div className="space-y-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-start justify-between">
|
|
<div>
|
|
<CardTitle>Deliberation Session</CardTitle>
|
|
<CardDescription className="mt-1">
|
|
{session.round?.name} - {session.category}
|
|
</CardDescription>
|
|
</div>
|
|
<Badge>{session.status}</Badge>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="flex flex-col items-center justify-center py-12">
|
|
<CheckCircle2 className="mb-4 h-12 w-12 text-green-600" />
|
|
<p className="font-medium">Vote Submitted</p>
|
|
<p className="mt-1 text-sm text-muted-foreground">
|
|
Thank you for your participation in this deliberation
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-start justify-between">
|
|
<div>
|
|
<CardTitle>Deliberation Session</CardTitle>
|
|
<CardDescription className="mt-1">
|
|
{session.round?.name} - {session.category}
|
|
</CardDescription>
|
|
</div>
|
|
<Badge>{session.status}</Badge>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-muted-foreground">
|
|
{session.mode === 'SINGLE_WINNER_VOTE'
|
|
? 'Select your top choice for this category.'
|
|
: 'Rank all projects from best to least preferred.'}
|
|
</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<DeliberationRankingForm
|
|
projects={session.results?.map((r) => r.project) ?? []}
|
|
mode={session.mode}
|
|
onSubmit={handleSubmitVote}
|
|
disabled={submitVoteMutation.isPending}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|