Round system redesign: criteria voting, audience voting, pipeline view, and admin UX improvements

- Schema: Extend LiveVotingSession with votingMode, criteriaJson, audience fields;
  add AudienceVoter model; make LiveVote.userId nullable for audience voters
- Backend: Criteria-based voting with weighted scores, audience registration/voting
  with token-based dedup, configurable jury/audience weight in results
- Jury UI: Criteria scoring with per-criterion sliders alongside simple 1-10 mode
- Public audience voting page at /vote/[sessionId] with mobile-first design
- Admin live voting: Tabbed layout (Session/Config/Results), criteria config,
  audience settings, weight-adjustable results with tie detection
- Round type settings: Visual card selector replacing dropdown, feature tags
- Round detail page: Live event status section, type-specific stats and actions
- Round pipeline view: Horizontal visualization with bottleneck detection,
  List/Pipeline toggle on rounds page
- SSE: Separate jury/audience vote events, audience vote tracking
- Field visibility: Hide irrelevant fields per round type in create/edit forms

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-12 14:27:49 +01:00
parent b5d90d3c26
commit 2a5fa463b3
14 changed files with 2518 additions and 456 deletions

View File

@@ -34,6 +34,7 @@ import {
FormMessage,
} from '@/components/ui/form'
import { RoundTypeSettings } from '@/components/forms/round-type-settings'
import { ROUND_FIELD_VISIBILITY } from '@/types/round-settings'
import { ArrowLeft, Loader2, AlertCircle, Bell, LayoutTemplate } from 'lucide-react'
import { toast } from 'sonner'
import { DateTimePicker } from '@/components/ui/datetime-picker'
@@ -124,14 +125,15 @@ function CreateRoundContent() {
})
const onSubmit = async (data: CreateRoundForm) => {
const visibility = ROUND_FIELD_VISIBILITY[roundType]
await createRound.mutateAsync({
programId: data.programId,
name: data.name,
roundType,
requiredReviews: roundType === 'FILTERING' ? 0 : data.requiredReviews,
requiredReviews: visibility?.showRequiredReviews ? data.requiredReviews : 0,
settingsJson: roundSettings,
votingStartAt: data.votingStartAt ?? undefined,
votingEndAt: data.votingEndAt ?? undefined,
votingStartAt: visibility?.showVotingWindow ? (data.votingStartAt ?? undefined) : undefined,
votingEndAt: visibility?.showVotingWindow ? (data.votingEndAt ?? undefined) : undefined,
entryNotificationType: entryNotificationType || undefined,
})
}
@@ -291,7 +293,7 @@ function CreateRoundContent() {
)}
/>
{roundType !== 'FILTERING' && (
{ROUND_FIELD_VISIBILITY[roundType]?.showRequiredReviews && (
<FormField
control={form.control}
name="requiredReviews"
@@ -326,6 +328,7 @@ function CreateRoundContent() {
onSettingsChange={setRoundSettings}
/>
{ROUND_FIELD_VISIBILITY[roundType]?.showVotingWindow && (
<Card>
<CardHeader>
<CardTitle className="text-lg">Voting Window</CardTitle>
@@ -377,6 +380,7 @@ function CreateRoundContent() {
</p>
</CardContent>
</Card>
)}
{/* Team Notification */}
<Card>