feat(finale): schema for phases, audience windows, favorite votes, notes, reveal
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,104 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "LivePhase" AS ENUM ('ON_DECK', 'PRESENTING', 'QA', 'SCORING');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "AudiencePhase" AS ENUM ('CLOSED', 'OPEN');
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "LiveProgressCursor" ADD COLUMN "overrideSlide" TEXT,
|
||||||
|
ADD COLUMN "phaseDurationSeconds" INTEGER,
|
||||||
|
ADD COLUMN "phasePausedAccumMs" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN "phasePausedAt" TIMESTAMP(3),
|
||||||
|
ADD COLUMN "phaseStartedAt" TIMESTAMP(3),
|
||||||
|
ADD COLUMN "projectPhase" "LivePhase" NOT NULL DEFAULT 'ON_DECK',
|
||||||
|
ADD COLUMN "timingLogJson" JSONB;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "LiveVote" ADD COLUMN "comment" TEXT;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "LiveVotingSession" ADD COLUMN "allowOverallFavorite" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
ADD COLUMN "audiencePhase" "AudiencePhase" NOT NULL DEFAULT 'CLOSED',
|
||||||
|
ADD COLUMN "audienceWindowClosesAt" TIMESTAMP(3),
|
||||||
|
ADD COLUMN "audienceWindowKey" TEXT,
|
||||||
|
ADD COLUMN "audienceWindowOpenedAt" TIMESTAMP(3);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "AudienceFavoriteVote" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"sessionId" TEXT NOT NULL,
|
||||||
|
"windowKey" TEXT NOT NULL,
|
||||||
|
"projectId" TEXT NOT NULL,
|
||||||
|
"audienceVoterId" TEXT NOT NULL,
|
||||||
|
"ipAddress" TEXT,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "AudienceFavoriteVote_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "LiveNote" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"roundId" TEXT NOT NULL,
|
||||||
|
"projectId" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"content" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "LiveNote_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "RevealState" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"sessionId" TEXT NOT NULL,
|
||||||
|
"status" TEXT NOT NULL DEFAULT 'DRAFT',
|
||||||
|
"stepsJson" JSONB NOT NULL,
|
||||||
|
"currentStepIndex" INTEGER NOT NULL DEFAULT -1,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "RevealState_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "AudienceFavoriteVote_sessionId_windowKey_ipAddress_idx" ON "AudienceFavoriteVote"("sessionId", "windowKey", "ipAddress");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "AudienceFavoriteVote_sessionId_windowKey_projectId_idx" ON "AudienceFavoriteVote"("sessionId", "windowKey", "projectId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "AudienceFavoriteVote_sessionId_windowKey_audienceVoterId_key" ON "AudienceFavoriteVote"("sessionId", "windowKey", "audienceVoterId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "LiveNote_userId_idx" ON "LiveNote"("userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "LiveNote_roundId_projectId_userId_key" ON "LiveNote"("roundId", "projectId", "userId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "RevealState_sessionId_key" ON "RevealState"("sessionId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "AudienceFavoriteVote" ADD CONSTRAINT "AudienceFavoriteVote_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "LiveVotingSession"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "AudienceFavoriteVote" ADD CONSTRAINT "AudienceFavoriteVote_audienceVoterId_fkey" FOREIGN KEY ("audienceVoterId") REFERENCES "AudienceVoter"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "AudienceFavoriteVote" ADD CONSTRAINT "AudienceFavoriteVote_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "LiveNote" ADD CONSTRAINT "LiveNote_roundId_fkey" FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "LiveNote" ADD CONSTRAINT "LiveNote_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "LiveNote" ADD CONSTRAINT "LiveNote_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "RevealState" ADD CONSTRAINT "RevealState_sessionId_fkey" FOREIGN KEY ("sessionId") REFERENCES "LiveVotingSession"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
@@ -347,6 +347,7 @@ model User {
|
|||||||
resourceAccess ResourceAccess[]
|
resourceAccess ResourceAccess[]
|
||||||
submittedProjects Project[] @relation("ProjectSubmittedBy")
|
submittedProjects Project[] @relation("ProjectSubmittedBy")
|
||||||
liveVotes LiveVote[]
|
liveVotes LiveVote[]
|
||||||
|
liveNotes LiveNote[]
|
||||||
|
|
||||||
// Team membership & mentorship
|
// Team membership & mentorship
|
||||||
teamMemberships TeamMember[]
|
teamMemberships TeamMember[]
|
||||||
@@ -657,6 +658,10 @@ model Project {
|
|||||||
finalistConfirmation FinalistConfirmation?
|
finalistConfirmation FinalistConfirmation?
|
||||||
externalLunchAttendees ExternalAttendee[]
|
externalLunchAttendees ExternalAttendee[]
|
||||||
|
|
||||||
|
// Grand-finale ceremony
|
||||||
|
audienceFavoriteVotes AudienceFavoriteVote[]
|
||||||
|
liveNotes LiveNote[]
|
||||||
|
|
||||||
@@index([programId])
|
@@index([programId])
|
||||||
@@index([status])
|
@@index([status])
|
||||||
@@index([tags])
|
@@index([tags])
|
||||||
@@ -1188,6 +1193,13 @@ model LiveVotingSession {
|
|||||||
audienceRequireId Boolean @default(false) // Require email/phone for audience
|
audienceRequireId Boolean @default(false) // Require email/phone for audience
|
||||||
audienceVotingDuration Int? // Minutes (null = same as jury)
|
audienceVotingDuration Int? // Minutes (null = same as jury)
|
||||||
|
|
||||||
|
// Audience favorite-vote window (grand finale)
|
||||||
|
audiencePhase AudiencePhase @default(CLOSED)
|
||||||
|
audienceWindowKey String? // 'CATEGORY:STARTUP' | 'CATEGORY:BUSINESS_CONCEPT' | 'OVERALL'
|
||||||
|
audienceWindowOpenedAt DateTime?
|
||||||
|
audienceWindowClosesAt DateTime?
|
||||||
|
allowOverallFavorite Boolean @default(false) // admin toggle, decided day-of
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
@@ -1195,6 +1207,8 @@ model LiveVotingSession {
|
|||||||
round Round? @relation(fields: [roundId], references: [id], onDelete: Cascade)
|
round Round? @relation(fields: [roundId], references: [id], onDelete: Cascade)
|
||||||
votes LiveVote[]
|
votes LiveVote[]
|
||||||
audienceVoters AudienceVoter[]
|
audienceVoters AudienceVoter[]
|
||||||
|
favoriteVotes AudienceFavoriteVote[]
|
||||||
|
revealState RevealState?
|
||||||
|
|
||||||
@@index([status])
|
@@index([status])
|
||||||
}
|
}
|
||||||
@@ -1211,6 +1225,9 @@ model LiveVote {
|
|||||||
// Criteria scores (used when votingMode="criteria")
|
// Criteria scores (used when votingMode="criteria")
|
||||||
criterionScoresJson Json? @db.JsonB // { [criterionId]: score } - null for simple mode
|
criterionScoresJson Json? @db.JsonB // { [criterionId]: score } - null for simple mode
|
||||||
|
|
||||||
|
// Optional overall comment from the juror (grand finale)
|
||||||
|
comment String? @db.Text
|
||||||
|
|
||||||
// Audience voter link
|
// Audience voter link
|
||||||
audienceVoterId String?
|
audienceVoterId String?
|
||||||
|
|
||||||
@@ -1240,11 +1257,79 @@ model AudienceVoter {
|
|||||||
// Relations
|
// Relations
|
||||||
session LiveVotingSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
session LiveVotingSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
||||||
votes LiveVote[]
|
votes LiveVote[]
|
||||||
|
favoriteVotes AudienceFavoriteVote[]
|
||||||
|
|
||||||
@@index([sessionId])
|
@@index([sessionId])
|
||||||
@@index([token])
|
@@index([token])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One pick-one-favorite vote per audience member per voting window.
|
||||||
|
// windowKey snapshots LiveVotingSession.audienceWindowKey at cast time so
|
||||||
|
// per-category and overall votes coexist in one table.
|
||||||
|
model AudienceFavoriteVote {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
sessionId String
|
||||||
|
windowKey String // 'CATEGORY:STARTUP' | 'CATEGORY:BUSINESS_CONCEPT' | 'OVERALL'
|
||||||
|
projectId String
|
||||||
|
audienceVoterId String
|
||||||
|
ipAddress String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
session LiveVotingSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
||||||
|
audienceVoter AudienceVoter @relation(fields: [audienceVoterId], references: [id], onDelete: Cascade)
|
||||||
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([sessionId, windowKey, audienceVoterId])
|
||||||
|
@@index([sessionId, windowKey, ipAddress])
|
||||||
|
@@index([sessionId, windowKey, projectId])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-juror per-project free-text notes taken during the live ceremony.
|
||||||
|
// Resurfaces during deliberation.
|
||||||
|
model LiveNote {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
roundId String
|
||||||
|
projectId String
|
||||||
|
userId String
|
||||||
|
content String @db.Text
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
|
||||||
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
@@unique([roundId, projectId, userId])
|
||||||
|
@@index([userId])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin-driven results reveal for the big-screen ceremony view.
|
||||||
|
// Steps beyond currentStepIndex are never exposed publicly.
|
||||||
|
model RevealState {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
sessionId String @unique
|
||||||
|
status String @default("DRAFT") // DRAFT | ARMED | REVEALING | DONE
|
||||||
|
stepsJson Json @db.JsonB // RevealStep[]
|
||||||
|
currentStepIndex Int @default(-1)
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
session LiveVotingSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LivePhase {
|
||||||
|
ON_DECK
|
||||||
|
PRESENTING
|
||||||
|
QA
|
||||||
|
SCORING
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AudiencePhase {
|
||||||
|
CLOSED
|
||||||
|
OPEN
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// TEAM MEMBERSHIP
|
// TEAM MEMBERSHIP
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@@ -2157,6 +2242,15 @@ model LiveProgressCursor {
|
|||||||
activeOrderIndex Int @default(0)
|
activeOrderIndex Int @default(0)
|
||||||
isPaused Boolean @default(false)
|
isPaused Boolean @default(false)
|
||||||
|
|
||||||
|
// Per-project ceremony phase + server-stamped timer (grand finale)
|
||||||
|
projectPhase LivePhase @default(ON_DECK)
|
||||||
|
phaseStartedAt DateTime?
|
||||||
|
phaseDurationSeconds Int?
|
||||||
|
phasePausedAt DateTime?
|
||||||
|
phasePausedAccumMs Int @default(0)
|
||||||
|
timingLogJson Json? @db.JsonB // [{projectId, phase, startedAt, endedAt, configuredSeconds, overranSeconds}]
|
||||||
|
overrideSlide String? // big-screen override: 'welcome' | 'break' | 'deliberation' | 'thanks'
|
||||||
|
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
@@ -2283,6 +2377,7 @@ model Round {
|
|||||||
notificationLogs NotificationLog[]
|
notificationLogs NotificationLog[]
|
||||||
cohorts Cohort[]
|
cohorts Cohort[]
|
||||||
liveCursor LiveProgressCursor?
|
liveCursor LiveProgressCursor?
|
||||||
|
liveNotes LiveNote[]
|
||||||
|
|
||||||
@@unique([competitionId, slug])
|
@@unique([competitionId, slug])
|
||||||
@@unique([competitionId, sortOrder])
|
@@unique([competitionId, sortOrder])
|
||||||
|
|||||||
Reference in New Issue
Block a user