From 52cdca1b853816917172cd9c6d75c9fd80d3870f Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 12 Feb 2026 16:57:56 +0100 Subject: [PATCH] Move required reviews field into evaluation settings and add live voting migration Move the "Required Reviews per Project" field from the Basic Information card into the Evaluation Settings section of RoundTypeSettings, where it contextually belongs. Add missing database migration for live voting enhancements (criteria voting, audience voting, AudienceVoter table). Co-Authored-By: Claude Sonnet 4.5 --- .../migration.sql | 99 +++++++++++++++++++ .../(admin)/admin/rounds/[id]/edit/page.tsx | 53 +++++----- src/app/(admin)/admin/rounds/new/page.tsx | 48 ++++----- src/components/forms/round-type-settings.tsx | 8 ++ 4 files changed, 157 insertions(+), 51 deletions(-) create mode 100644 prisma/migrations/20260212000000_add_live_voting_enhancements/migration.sql diff --git a/prisma/migrations/20260212000000_add_live_voting_enhancements/migration.sql b/prisma/migrations/20260212000000_add_live_voting_enhancements/migration.sql new file mode 100644 index 0000000..4e0bb87 --- /dev/null +++ b/prisma/migrations/20260212000000_add_live_voting_enhancements/migration.sql @@ -0,0 +1,99 @@ +-- Migration: Add live voting enhancements (criteria voting, audience voting, AudienceVoter) +-- Brings LiveVotingSession, LiveVote, and new AudienceVoter model in sync with schema.prisma +-- Uses IF NOT EXISTS / DO $$ guards for idempotent execution + +-- ============================================================================= +-- 1. LiveVotingSession: Add criteria-based & audience voting columns +-- ============================================================================= + +ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "votingMode" TEXT NOT NULL DEFAULT 'simple'; +ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "criteriaJson" JSONB; +ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "audienceVotingMode" TEXT NOT NULL DEFAULT 'disabled'; +ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "audienceMaxFavorites" INTEGER NOT NULL DEFAULT 3; +ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "audienceRequireId" BOOLEAN NOT NULL DEFAULT false; +ALTER TABLE "LiveVotingSession" ADD COLUMN IF NOT EXISTS "audienceVotingDuration" INTEGER; + +-- ============================================================================= +-- 2. LiveVote: Add criteria scores, audience voter link, make userId nullable +-- ============================================================================= + +ALTER TABLE "LiveVote" ADD COLUMN IF NOT EXISTS "criterionScoresJson" JSONB; +ALTER TABLE "LiveVote" ADD COLUMN IF NOT EXISTS "audienceVoterId" TEXT; + +-- Make userId nullable (was NOT NULL in init migration) +ALTER TABLE "LiveVote" ALTER COLUMN "userId" DROP NOT NULL; + +-- ============================================================================= +-- 3. AudienceVoter: New table for audience participation +-- ============================================================================= + +CREATE TABLE IF NOT EXISTS "AudienceVoter" ( + "id" TEXT NOT NULL, + "sessionId" TEXT NOT NULL, + "token" TEXT NOT NULL, + "identifier" TEXT, + "identifierType" TEXT, + "ipAddress" TEXT, + "userAgent" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "AudienceVoter_pkey" PRIMARY KEY ("id") +); + +-- Unique constraint on token +DO $$ BEGIN + ALTER TABLE "AudienceVoter" ADD CONSTRAINT "AudienceVoter_token_key" UNIQUE ("token"); +EXCEPTION WHEN duplicate_object THEN NULL; END $$; + +-- Indexes +CREATE INDEX IF NOT EXISTS "AudienceVoter_sessionId_idx" ON "AudienceVoter"("sessionId"); +CREATE INDEX IF NOT EXISTS "AudienceVoter_token_idx" ON "AudienceVoter"("token"); + +-- Foreign key: AudienceVoter.sessionId -> LiveVotingSession.id +DO $$ BEGIN + ALTER TABLE "AudienceVoter" ADD CONSTRAINT "AudienceVoter_sessionId_fkey" + FOREIGN KEY ("sessionId") REFERENCES "LiveVotingSession"("id") ON DELETE CASCADE ON UPDATE CASCADE; +EXCEPTION WHEN duplicate_object THEN NULL; END $$; + +-- ============================================================================= +-- 4. LiveVote: Foreign key and indexes for audienceVoterId +-- ============================================================================= + +CREATE INDEX IF NOT EXISTS "LiveVote_audienceVoterId_idx" ON "LiveVote"("audienceVoterId"); + +-- Foreign key: LiveVote.audienceVoterId -> AudienceVoter.id +DO $$ BEGIN + ALTER TABLE "LiveVote" ADD CONSTRAINT "LiveVote_audienceVoterId_fkey" + FOREIGN KEY ("audienceVoterId") REFERENCES "AudienceVoter"("id") ON DELETE CASCADE ON UPDATE CASCADE; +EXCEPTION WHEN duplicate_object THEN NULL; END $$; + +-- Unique constraint: sessionId + projectId + audienceVoterId +DO $$ BEGIN + ALTER TABLE "LiveVote" ADD CONSTRAINT "LiveVote_sessionId_projectId_audienceVoterId_key" + UNIQUE ("sessionId", "projectId", "audienceVoterId"); +EXCEPTION WHEN duplicate_object THEN NULL; END $$; + +-- ============================================================================= +-- SUMMARY: +-- +-- LiveVotingSession new columns: +-- - votingMode (TEXT, default 'simple') +-- - criteriaJson (JSONB, nullable) +-- - audienceVotingMode (TEXT, default 'disabled') +-- - audienceMaxFavorites (INTEGER, default 3) +-- - audienceRequireId (BOOLEAN, default false) +-- - audienceVotingDuration (INTEGER, nullable) +-- +-- LiveVote changes: +-- - criterionScoresJson (JSONB, nullable) - new column +-- - audienceVoterId (TEXT, nullable) - new column +-- - userId changed from NOT NULL to nullable +-- - New unique: (sessionId, projectId, audienceVoterId) +-- - New index: audienceVoterId +-- - New FK: audienceVoterId -> AudienceVoter(id) +-- +-- New table: AudienceVoter +-- - id, sessionId, token (unique), identifier, identifierType, +-- ipAddress, userAgent, createdAt +-- - FK: sessionId -> LiveVotingSession(id) CASCADE +-- ============================================================================= diff --git a/src/app/(admin)/admin/rounds/[id]/edit/page.tsx b/src/app/(admin)/admin/rounds/[id]/edit/page.tsx index dd83b89..e107774 100644 --- a/src/app/(admin)/admin/rounds/[id]/edit/page.tsx +++ b/src/app/(admin)/admin/rounds/[id]/edit/page.tsx @@ -304,33 +304,6 @@ function EditRoundContent({ roundId }: { roundId: string }) { )} /> - {ROUND_FIELD_VISIBILITY[roundType]?.showRequiredReviews && ( - ( - - Required Reviews per Project - - - field.onChange(parseInt(e.target.value) || 1) - } - /> - - - Minimum number of evaluations each project should receive - - - - )} - /> - )} - {ROUND_FIELD_VISIBILITY[roundType]?.showAssignmentLimits && (
( + + Required Reviews per Project + + + field.onChange(parseInt(e.target.value) || 1) + } + /> + + + Minimum number of evaluations each project should receive + + + + )} + /> + } /> {/* Voting Window */} diff --git a/src/app/(admin)/admin/rounds/new/page.tsx b/src/app/(admin)/admin/rounds/new/page.tsx index 29e2534..fa547e6 100644 --- a/src/app/(admin)/admin/rounds/new/page.tsx +++ b/src/app/(admin)/admin/rounds/new/page.tsx @@ -294,30 +294,6 @@ function CreateRoundContent() { )} /> - {ROUND_FIELD_VISIBILITY[roundType]?.showRequiredReviews && ( - ( - - Required Reviews per Project - - field.onChange(parseInt(e.target.value) || 1)} - /> - - - Minimum number of evaluations each project should receive - - - - )} - /> - )} @@ -327,6 +303,30 @@ function CreateRoundContent() { onRoundTypeChange={setRoundType} settings={roundSettings} onSettingsChange={setRoundSettings} + requiredReviewsField={ + ( + + Required Reviews per Project + + field.onChange(parseInt(e.target.value) || 1)} + /> + + + Minimum number of evaluations each project should receive + + + + )} + /> + } /> {ROUND_FIELD_VISIBILITY[roundType]?.showVotingWindow && ( diff --git a/src/components/forms/round-type-settings.tsx b/src/components/forms/round-type-settings.tsx index 9414855..5b04fa6 100644 --- a/src/components/forms/round-type-settings.tsx +++ b/src/components/forms/round-type-settings.tsx @@ -35,6 +35,7 @@ interface RoundTypeSettingsProps { onRoundTypeChange: (type: 'FILTERING' | 'EVALUATION' | 'LIVE_EVENT') => void settings: Record onSettingsChange: (settings: Record) => void + requiredReviewsField?: React.ReactNode } const roundTypeIcons = { @@ -54,6 +55,7 @@ export function RoundTypeSettings({ onRoundTypeChange, settings, onSettingsChange, + requiredReviewsField, }: RoundTypeSettingsProps) { const Icon = roundTypeIcons[roundType] @@ -145,6 +147,7 @@ export function RoundTypeSettings({ onSettingsChange(s as unknown as Record)} + requiredReviewsField={requiredReviewsField} /> )} @@ -319,14 +322,19 @@ function FilteringSettings({ function EvaluationSettings({ settings, onChange, + requiredReviewsField, }: { settings: EvaluationRoundSettings onChange: (settings: EvaluationRoundSettings) => void + requiredReviewsField?: React.ReactNode }) { return (

Evaluation Settings

+ {/* Required Reviews (passed from parent form) */} + {requiredReviewsField} + {/* Target Finalists */}