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 */}