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

@@ -1059,39 +1059,77 @@ model LiveVotingSession {
votingEndsAt DateTime?
projectOrderJson Json? @db.JsonB // Array of project IDs in presentation order
// Criteria-based voting
votingMode String @default("simple") // "simple" (1-10) | "criteria" (per-criterion scores)
criteriaJson Json? @db.JsonB // Array of { id, label, description, scale, weight }
// Audience & presentation settings
allowAudienceVotes Boolean @default(false)
audienceVoteWeight Float @default(0) // 0.0 to 1.0
tieBreakerMethod String @default("admin_decides") // 'admin_decides' | 'highest_individual' | 'revote'
presentationSettingsJson Json? @db.JsonB
// Audience voting configuration
audienceVotingMode String @default("disabled") // "disabled" | "per_project" | "per_category" | "favorites"
audienceMaxFavorites Int @default(3) // For "favorites" mode
audienceRequireId Boolean @default(false) // Require email/phone for audience
audienceVotingDuration Int? // Minutes (null = same as jury)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
votes LiveVote[]
audienceVoters AudienceVoter[]
@@index([status])
}
model LiveVote {
id String @id @default(cuid())
sessionId String
projectId String
userId String
score Int // 1-10
isAudienceVote Boolean @default(false)
votedAt DateTime @default(now())
id String @id @default(cuid())
sessionId String
projectId String
userId String? // Nullable for audience voters without accounts
score Int // 1-10 (or weighted score for criteria mode)
isAudienceVote Boolean @default(false)
votedAt DateTime @default(now())
// Criteria scores (used when votingMode="criteria")
criterionScoresJson Json? @db.JsonB // { [criterionId]: score } - null for simple mode
// Audience voter link
audienceVoterId String?
// Relations
session LiveVotingSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
session LiveVotingSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
audienceVoter AudienceVoter? @relation(fields: [audienceVoterId], references: [id], onDelete: Cascade)
@@unique([sessionId, projectId, userId])
@@unique([sessionId, projectId, audienceVoterId])
@@index([sessionId])
@@index([projectId])
@@index([userId])
@@index([audienceVoterId])
}
model AudienceVoter {
id String @id @default(cuid())
sessionId String
token String @unique // Unique voting token (UUID)
identifier String? // Optional: email, phone, or name
identifierType String? // "email" | "phone" | "name" | "anonymous"
ipAddress String?
userAgent String?
createdAt DateTime @default(now())
// Relations
session LiveVotingSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
votes LiveVote[]
@@index([sessionId])
@@index([token])
}
// =============================================================================