Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { use, useState } from 'react';
|
|
|
|
|
import { useRouter } from 'next/navigation';
|
|
|
|
|
import Link from 'next/link';
|
|
|
|
|
import { trpc } from '@/lib/trpc/client';
|
|
|
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
|
|
|
import {
|
|
|
|
|
Dialog,
|
|
|
|
|
DialogContent,
|
|
|
|
|
DialogDescription,
|
|
|
|
|
DialogFooter,
|
|
|
|
|
DialogHeader,
|
|
|
|
|
DialogTitle
|
|
|
|
|
} from '@/components/ui/dialog';
|
|
|
|
|
import { Label } from '@/components/ui/label';
|
|
|
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
|
|
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
|
|
|
import { Badge } from '@/components/ui/badge';
|
|
|
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
|
|
|
import { ArrowLeft, Plus } from 'lucide-react';
|
|
|
|
|
import { toast } from 'sonner';
|
|
|
|
|
import type { Route } from 'next';
|
|
|
|
|
|
|
|
|
|
export default function DeliberationListPage({
|
|
|
|
|
params: paramsPromise
|
|
|
|
|
}: {
|
|
|
|
|
params: Promise<{ competitionId: string }>;
|
|
|
|
|
}) {
|
|
|
|
|
const params = use(paramsPromise);
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
const utils = trpc.useUtils();
|
|
|
|
|
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
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
|
|
|
const [selectedJuryGroupId, setSelectedJuryGroupId] = useState('');
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
const [formData, setFormData] = useState({
|
|
|
|
|
roundId: '',
|
|
|
|
|
category: 'STARTUP' as 'STARTUP' | 'BUSINESS_CONCEPT',
|
|
|
|
|
mode: 'SINGLE_WINNER_VOTE' as 'SINGLE_WINNER_VOTE' | 'FULL_RANKING',
|
|
|
|
|
tieBreakMethod: 'TIE_RUNOFF' as 'TIE_RUNOFF' | 'TIE_ADMIN_DECIDES' | 'SCORE_FALLBACK',
|
|
|
|
|
showCollectiveRankings: false,
|
|
|
|
|
showPriorJuryData: false,
|
|
|
|
|
participantUserIds: [] as string[]
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const { data: sessions = [], isLoading } = trpc.deliberation.listSessions.useQuery(
|
|
|
|
|
{ competitionId: params.competitionId },
|
|
|
|
|
{ enabled: !!params.competitionId }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Get rounds for this competition
|
|
|
|
|
const { data: competition } = trpc.competition.getById.useQuery(
|
|
|
|
|
{ id: params.competitionId },
|
|
|
|
|
{ enabled: !!params.competitionId }
|
|
|
|
|
);
|
|
|
|
|
const rounds = competition?.rounds || [];
|
|
|
|
|
|
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
|
|
|
// Jury groups & members for participant selection
|
|
|
|
|
const { data: juryGroups = [] } = trpc.juryGroup.list.useQuery(
|
|
|
|
|
{ competitionId: params.competitionId },
|
|
|
|
|
{ enabled: !!params.competitionId }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const { data: selectedJuryGroup } = trpc.juryGroup.getById.useQuery(
|
|
|
|
|
{ id: selectedJuryGroupId },
|
|
|
|
|
{ enabled: !!selectedJuryGroupId }
|
|
|
|
|
);
|
|
|
|
|
const juryMembers = selectedJuryGroup?.members ?? [];
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
|
|
|
|
|
const createSessionMutation = trpc.deliberation.createSession.useMutation({
|
|
|
|
|
onSuccess: (data) => {
|
|
|
|
|
utils.deliberation.listSessions.invalidate({ competitionId: params.competitionId });
|
|
|
|
|
toast.success('Deliberation session created');
|
|
|
|
|
setCreateDialogOpen(false);
|
|
|
|
|
router.push(
|
|
|
|
|
`/admin/competitions/${params.competitionId}/deliberation/${data.id}` as Route
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
onError: (err) => {
|
|
|
|
|
toast.error(err.message);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const handleCreateSession = () => {
|
|
|
|
|
if (!formData.roundId) {
|
|
|
|
|
toast.error('Please select a round');
|
|
|
|
|
return;
|
|
|
|
|
}
|
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
|
|
|
if (formData.participantUserIds.length === 0) {
|
|
|
|
|
toast.error('Please select at least one participant');
|
|
|
|
|
return;
|
|
|
|
|
}
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
|
|
|
|
|
createSessionMutation.mutate({
|
|
|
|
|
competitionId: params.competitionId,
|
|
|
|
|
roundId: formData.roundId,
|
|
|
|
|
category: formData.category,
|
|
|
|
|
mode: formData.mode,
|
|
|
|
|
tieBreakMethod: formData.tieBreakMethod,
|
|
|
|
|
showCollectiveRankings: formData.showCollectiveRankings,
|
|
|
|
|
showPriorJuryData: formData.showPriorJuryData,
|
|
|
|
|
participantUserIds: formData.participantUserIds
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getStatusBadge = (status: string) => {
|
|
|
|
|
const variants: Record<string, 'default' | 'secondary' | 'destructive' | 'outline'> = {
|
|
|
|
|
DELIB_OPEN: 'outline',
|
2026-02-19 09:56:09 +01:00
|
|
|
VOTING: 'default',
|
|
|
|
|
TALLYING: 'secondary',
|
|
|
|
|
RUNOFF: 'secondary',
|
|
|
|
|
DELIB_LOCKED: 'destructive',
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
};
|
2026-02-19 09:56:09 +01:00
|
|
|
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>;
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (isLoading) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6 p-4 sm:p-6">
|
|
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
|
<Skeleton className="h-8 w-8 shrink-0" />
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Skeleton className="h-8 w-64" />
|
|
|
|
|
<Skeleton className="h-4 w-96 max-w-full" />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="grid grid-cols-1 gap-4">
|
|
|
|
|
{[1, 2, 3].map((i) => (
|
|
|
|
|
<Skeleton key={i} className="h-32 w-full" />
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6 p-4 sm:p-6">
|
|
|
|
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
|
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
|
<Button variant="ghost" size="icon" onClick={() => router.back()} aria-label="Back to competition details">
|
|
|
|
|
<ArrowLeft className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
<div>
|
|
|
|
|
<h1 className="text-3xl font-bold">Deliberation Sessions</h1>
|
|
|
|
|
<p className="text-muted-foreground">
|
|
|
|
|
Manage final jury deliberations and winner selection
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<Button onClick={() => setCreateDialogOpen(true)}>
|
|
|
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
|
|
|
New Session
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 gap-4">
|
|
|
|
|
{sessions?.map((session: any) => (
|
|
|
|
|
<Link
|
|
|
|
|
key={session.id}
|
|
|
|
|
href={
|
|
|
|
|
`/admin/competitions/${params.competitionId}/deliberation/${session.id}` as Route
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<Card className="cursor-pointer transition-shadow hover:shadow-md">
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<div className="flex items-start justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<CardTitle>
|
|
|
|
|
{session.round?.name} - {session.category}
|
|
|
|
|
</CardTitle>
|
|
|
|
|
<CardDescription className="mt-1">
|
|
|
|
|
{session.mode === 'SINGLE_WINNER_VOTE' ? 'Single Winner Vote' : 'Full Ranking'}
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</div>
|
|
|
|
|
{getStatusBadge(session.status)}
|
|
|
|
|
</div>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
<div className="flex flex-wrap gap-2 text-sm text-muted-foreground">
|
|
|
|
|
<span>{session.participants?.length || 0} participants</span>
|
|
|
|
|
<span>•</span>
|
|
|
|
|
<span>Tie break: {session.tieBreakMethod}</span>
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</Link>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{sessions?.length === 0 && (
|
|
|
|
|
<Card>
|
|
|
|
|
<CardContent className="flex flex-col items-center justify-center py-12">
|
|
|
|
|
<p className="text-muted-foreground">No deliberation sessions yet</p>
|
|
|
|
|
<Button variant="link" onClick={() => setCreateDialogOpen(true)}>
|
|
|
|
|
Create your first session
|
|
|
|
|
</Button>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Create Session Dialog */}
|
|
|
|
|
<Dialog open={createDialogOpen} onOpenChange={setCreateDialogOpen}>
|
|
|
|
|
<DialogContent className="max-w-2xl">
|
|
|
|
|
<DialogHeader>
|
|
|
|
|
<DialogTitle>Create Deliberation Session</DialogTitle>
|
|
|
|
|
<DialogDescription>
|
|
|
|
|
Set up a new deliberation session for final winner selection
|
|
|
|
|
</DialogDescription>
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="round">Round *</Label>
|
|
|
|
|
<Select value={formData.roundId} onValueChange={(value) => setFormData({ ...formData, roundId: value })}>
|
|
|
|
|
<SelectTrigger id="round">
|
|
|
|
|
<SelectValue placeholder="Select round" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{rounds?.map((round: any) => (
|
|
|
|
|
<SelectItem key={round.id} value={round.id}>
|
|
|
|
|
{round.name}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="category">Category *</Label>
|
|
|
|
|
<Select
|
|
|
|
|
value={formData.category}
|
|
|
|
|
onValueChange={(value) =>
|
|
|
|
|
setFormData({ ...formData, category: value as 'STARTUP' | 'BUSINESS_CONCEPT' })
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger id="category">
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="STARTUP">Startup</SelectItem>
|
|
|
|
|
<SelectItem value="BUSINESS_CONCEPT">Business Concept</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="mode">Voting Mode *</Label>
|
|
|
|
|
<Select
|
|
|
|
|
value={formData.mode}
|
|
|
|
|
onValueChange={(value) =>
|
|
|
|
|
setFormData({
|
|
|
|
|
...formData,
|
|
|
|
|
mode: value as 'SINGLE_WINNER_VOTE' | 'FULL_RANKING'
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger id="mode">
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="SINGLE_WINNER_VOTE">Single Winner Vote</SelectItem>
|
|
|
|
|
<SelectItem value="FULL_RANKING">Full Ranking</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="tieBreak">Tie Break Method *</Label>
|
|
|
|
|
<Select
|
|
|
|
|
value={formData.tieBreakMethod}
|
|
|
|
|
onValueChange={(value) =>
|
|
|
|
|
setFormData({
|
|
|
|
|
...formData,
|
|
|
|
|
tieBreakMethod: value as 'TIE_RUNOFF' | 'TIE_ADMIN_DECIDES' | 'SCORE_FALLBACK'
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger id="tieBreak">
|
|
|
|
|
<SelectValue />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="TIE_RUNOFF">Runoff Vote</SelectItem>
|
|
|
|
|
<SelectItem value="TIE_ADMIN_DECIDES">Admin Decides</SelectItem>
|
|
|
|
|
<SelectItem value="SCORE_FALLBACK">Score Fallback</SelectItem>
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
|
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
|
|
|
{/* Participant Selection */}
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label htmlFor="juryGroup">Jury Group *</Label>
|
|
|
|
|
<Select
|
|
|
|
|
value={selectedJuryGroupId}
|
|
|
|
|
onValueChange={(value) => {
|
|
|
|
|
setSelectedJuryGroupId(value);
|
|
|
|
|
setFormData({ ...formData, participantUserIds: [] });
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger id="juryGroup">
|
|
|
|
|
<SelectValue placeholder="Select jury group" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{juryGroups.map((group: any) => (
|
|
|
|
|
<SelectItem key={group.id} value={group.id}>
|
|
|
|
|
{group.name} ({group._count?.members ?? 0} members)
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{juryMembers.length > 0 && (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<Label>Participants ({formData.participantUserIds.length}/{juryMembers.length})</Label>
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
const allIds = juryMembers.map((m: any) => m.user.id);
|
|
|
|
|
const allSelected = allIds.every((id: string) => formData.participantUserIds.includes(id));
|
|
|
|
|
setFormData({
|
|
|
|
|
...formData,
|
|
|
|
|
participantUserIds: allSelected ? [] : allIds,
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{juryMembers.every((m: any) => formData.participantUserIds.includes(m.user.id))
|
|
|
|
|
? 'Deselect All'
|
|
|
|
|
: 'Select All'}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="max-h-48 space-y-2 overflow-y-auto rounded-md border p-3">
|
|
|
|
|
{juryMembers.map((member: any) => (
|
|
|
|
|
<div key={member.id} className="flex items-center space-x-2">
|
|
|
|
|
<Checkbox
|
|
|
|
|
id={`member-${member.user.id}`}
|
|
|
|
|
checked={formData.participantUserIds.includes(member.user.id)}
|
|
|
|
|
onCheckedChange={(checked) => {
|
|
|
|
|
setFormData({
|
|
|
|
|
...formData,
|
|
|
|
|
participantUserIds: checked
|
|
|
|
|
? [...formData.participantUserIds, member.user.id]
|
|
|
|
|
: formData.participantUserIds.filter((id: string) => id !== member.user.id),
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
<Label htmlFor={`member-${member.user.id}`} className="flex-1 font-normal">
|
|
|
|
|
{member.user.name || member.user.email}
|
|
|
|
|
<span className="ml-2 text-xs text-muted-foreground">
|
|
|
|
|
{member.role === 'CHAIR' ? 'Chair' : member.role === 'OBSERVER' ? 'Observer' : 'Member'}
|
|
|
|
|
</span>
|
|
|
|
|
</Label>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
<div className="space-y-3">
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
<Checkbox
|
|
|
|
|
id="showCollective"
|
|
|
|
|
checked={formData.showCollectiveRankings}
|
|
|
|
|
onCheckedChange={(checked) =>
|
|
|
|
|
setFormData({ ...formData, showCollectiveRankings: checked as boolean })
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<Label htmlFor="showCollective" className="font-normal">
|
|
|
|
|
Show collective rankings during voting
|
|
|
|
|
</Label>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex items-center space-x-2">
|
|
|
|
|
<Checkbox
|
|
|
|
|
id="showPrior"
|
|
|
|
|
checked={formData.showPriorJuryData}
|
|
|
|
|
onCheckedChange={(checked) =>
|
|
|
|
|
setFormData({ ...formData, showPriorJuryData: checked as boolean })
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<Label htmlFor="showPrior" className="font-normal">
|
|
|
|
|
Show prior jury evaluation data
|
|
|
|
|
</Label>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<DialogFooter>
|
|
|
|
|
<Button variant="outline" onClick={() => setCreateDialogOpen(false)}>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
<Button onClick={handleCreateSession} disabled={createSessionMutation.isPending}>
|
|
|
|
|
{createSessionMutation.isPending ? 'Creating...' : 'Create Session'}
|
|
|
|
|
</Button>
|
|
|
|
|
</DialogFooter>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|