Move required reviews field into evaluation settings and add live voting migration
Build and Push Docker Image / build (push) Successful in 13m22s
Details
Build and Push Docker Image / build (push) Successful in 13m22s
Details
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 <noreply@anthropic.com>
This commit is contained in:
parent
7b85fd9602
commit
52cdca1b85
|
|
@ -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
|
||||||
|
-- =============================================================================
|
||||||
|
|
@ -304,33 +304,6 @@ function EditRoundContent({ roundId }: { roundId: string }) {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{ROUND_FIELD_VISIBILITY[roundType]?.showRequiredReviews && (
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="requiredReviews"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Required Reviews per Project</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={10}
|
|
||||||
{...field}
|
|
||||||
onChange={(e) =>
|
|
||||||
field.onChange(parseInt(e.target.value) || 1)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Minimum number of evaluations each project should receive
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{ROUND_FIELD_VISIBILITY[roundType]?.showAssignmentLimits && (
|
{ROUND_FIELD_VISIBILITY[roundType]?.showAssignmentLimits && (
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<FormField
|
<FormField
|
||||||
|
|
@ -393,6 +366,32 @@ function EditRoundContent({ roundId }: { roundId: string }) {
|
||||||
onRoundTypeChange={setRoundType}
|
onRoundTypeChange={setRoundType}
|
||||||
settings={roundSettings}
|
settings={roundSettings}
|
||||||
onSettingsChange={setRoundSettings}
|
onSettingsChange={setRoundSettings}
|
||||||
|
requiredReviewsField={
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="requiredReviews"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Required Reviews per Project</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={10}
|
||||||
|
{...field}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.onChange(parseInt(e.target.value) || 1)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Minimum number of evaluations each project should receive
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Voting Window */}
|
{/* Voting Window */}
|
||||||
|
|
|
||||||
|
|
@ -294,30 +294,6 @@ function CreateRoundContent() {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{ROUND_FIELD_VISIBILITY[roundType]?.showRequiredReviews && (
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="requiredReviews"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Required Reviews per Project</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={10}
|
|
||||||
{...field}
|
|
||||||
onChange={(e) => field.onChange(parseInt(e.target.value) || 1)}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Minimum number of evaluations each project should receive
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|
@ -327,6 +303,30 @@ function CreateRoundContent() {
|
||||||
onRoundTypeChange={setRoundType}
|
onRoundTypeChange={setRoundType}
|
||||||
settings={roundSettings}
|
settings={roundSettings}
|
||||||
onSettingsChange={setRoundSettings}
|
onSettingsChange={setRoundSettings}
|
||||||
|
requiredReviewsField={
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="requiredReviews"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Required Reviews per Project</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={10}
|
||||||
|
{...field}
|
||||||
|
onChange={(e) => field.onChange(parseInt(e.target.value) || 1)}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
Minimum number of evaluations each project should receive
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{ROUND_FIELD_VISIBILITY[roundType]?.showVotingWindow && (
|
{ROUND_FIELD_VISIBILITY[roundType]?.showVotingWindow && (
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ interface RoundTypeSettingsProps {
|
||||||
onRoundTypeChange: (type: 'FILTERING' | 'EVALUATION' | 'LIVE_EVENT') => void
|
onRoundTypeChange: (type: 'FILTERING' | 'EVALUATION' | 'LIVE_EVENT') => void
|
||||||
settings: Record<string, unknown>
|
settings: Record<string, unknown>
|
||||||
onSettingsChange: (settings: Record<string, unknown>) => void
|
onSettingsChange: (settings: Record<string, unknown>) => void
|
||||||
|
requiredReviewsField?: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
const roundTypeIcons = {
|
const roundTypeIcons = {
|
||||||
|
|
@ -54,6 +55,7 @@ export function RoundTypeSettings({
|
||||||
onRoundTypeChange,
|
onRoundTypeChange,
|
||||||
settings,
|
settings,
|
||||||
onSettingsChange,
|
onSettingsChange,
|
||||||
|
requiredReviewsField,
|
||||||
}: RoundTypeSettingsProps) {
|
}: RoundTypeSettingsProps) {
|
||||||
const Icon = roundTypeIcons[roundType]
|
const Icon = roundTypeIcons[roundType]
|
||||||
|
|
||||||
|
|
@ -145,6 +147,7 @@ export function RoundTypeSettings({
|
||||||
<EvaluationSettings
|
<EvaluationSettings
|
||||||
settings={getEvaluationSettings()}
|
settings={getEvaluationSettings()}
|
||||||
onChange={(s) => onSettingsChange(s as unknown as Record<string, unknown>)}
|
onChange={(s) => onSettingsChange(s as unknown as Record<string, unknown>)}
|
||||||
|
requiredReviewsField={requiredReviewsField}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -319,14 +322,19 @@ function FilteringSettings({
|
||||||
function EvaluationSettings({
|
function EvaluationSettings({
|
||||||
settings,
|
settings,
|
||||||
onChange,
|
onChange,
|
||||||
|
requiredReviewsField,
|
||||||
}: {
|
}: {
|
||||||
settings: EvaluationRoundSettings
|
settings: EvaluationRoundSettings
|
||||||
onChange: (settings: EvaluationRoundSettings) => void
|
onChange: (settings: EvaluationRoundSettings) => void
|
||||||
|
requiredReviewsField?: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 border-t pt-4">
|
<div className="space-y-6 border-t pt-4">
|
||||||
<h4 className="font-medium">Evaluation Settings</h4>
|
<h4 className="font-medium">Evaluation Settings</h4>
|
||||||
|
|
||||||
|
{/* Required Reviews (passed from parent form) */}
|
||||||
|
{requiredReviewsField}
|
||||||
|
|
||||||
{/* Target Finalists */}
|
{/* Target Finalists */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="targetFinalists">Target Finalists</Label>
|
<Label htmlFor="targetFinalists">Target Finalists</Label>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue