Files
MOPC-Portal/src/app/(jury)/jury/competitions/deliberation/[sessionId]/page.tsx
Matt 1308c3ba87
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m23s
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

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