974 lines
26 KiB
Plaintext
974 lines
26 KiB
Plaintext
|
|
// =============================================================================
|
||
|
|
// MOPC Platform - Prisma Schema
|
||
|
|
// =============================================================================
|
||
|
|
// This schema defines the database structure for the Monaco Ocean Protection
|
||
|
|
// Challenge jury voting platform.
|
||
|
|
|
||
|
|
generator client {
|
||
|
|
provider = "prisma-client-js"
|
||
|
|
binaryTargets = ["native", "windows", "linux-musl-openssl-3.0.x"]
|
||
|
|
}
|
||
|
|
|
||
|
|
datasource db {
|
||
|
|
provider = "postgresql"
|
||
|
|
url = env("DATABASE_URL")
|
||
|
|
}
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// ENUMS
|
||
|
|
// =============================================================================
|
||
|
|
|
||
|
|
enum UserRole {
|
||
|
|
SUPER_ADMIN
|
||
|
|
PROGRAM_ADMIN
|
||
|
|
JURY_MEMBER
|
||
|
|
MENTOR
|
||
|
|
OBSERVER
|
||
|
|
APPLICANT
|
||
|
|
}
|
||
|
|
|
||
|
|
enum UserStatus {
|
||
|
|
INVITED
|
||
|
|
ACTIVE
|
||
|
|
SUSPENDED
|
||
|
|
}
|
||
|
|
|
||
|
|
enum ProgramStatus {
|
||
|
|
DRAFT
|
||
|
|
ACTIVE
|
||
|
|
ARCHIVED
|
||
|
|
}
|
||
|
|
|
||
|
|
enum RoundStatus {
|
||
|
|
DRAFT
|
||
|
|
ACTIVE
|
||
|
|
CLOSED
|
||
|
|
ARCHIVED
|
||
|
|
}
|
||
|
|
|
||
|
|
enum ProjectStatus {
|
||
|
|
SUBMITTED
|
||
|
|
ELIGIBLE
|
||
|
|
ASSIGNED
|
||
|
|
SEMIFINALIST
|
||
|
|
FINALIST
|
||
|
|
REJECTED
|
||
|
|
}
|
||
|
|
|
||
|
|
enum EvaluationStatus {
|
||
|
|
NOT_STARTED
|
||
|
|
DRAFT
|
||
|
|
SUBMITTED
|
||
|
|
LOCKED
|
||
|
|
}
|
||
|
|
|
||
|
|
enum AssignmentMethod {
|
||
|
|
MANUAL
|
||
|
|
BULK
|
||
|
|
AI_SUGGESTED
|
||
|
|
AI_AUTO
|
||
|
|
ALGORITHM
|
||
|
|
}
|
||
|
|
|
||
|
|
enum FileType {
|
||
|
|
EXEC_SUMMARY
|
||
|
|
PRESENTATION
|
||
|
|
VIDEO
|
||
|
|
OTHER
|
||
|
|
BUSINESS_PLAN
|
||
|
|
VIDEO_PITCH
|
||
|
|
SUPPORTING_DOC
|
||
|
|
}
|
||
|
|
|
||
|
|
enum SubmissionSource {
|
||
|
|
MANUAL
|
||
|
|
CSV
|
||
|
|
NOTION
|
||
|
|
TYPEFORM
|
||
|
|
PUBLIC_FORM
|
||
|
|
}
|
||
|
|
|
||
|
|
enum RoundType {
|
||
|
|
FILTERING
|
||
|
|
EVALUATION
|
||
|
|
LIVE_EVENT
|
||
|
|
}
|
||
|
|
|
||
|
|
enum SettingType {
|
||
|
|
STRING
|
||
|
|
NUMBER
|
||
|
|
BOOLEAN
|
||
|
|
JSON
|
||
|
|
SECRET
|
||
|
|
}
|
||
|
|
|
||
|
|
enum SettingCategory {
|
||
|
|
AI
|
||
|
|
BRANDING
|
||
|
|
EMAIL
|
||
|
|
STORAGE
|
||
|
|
SECURITY
|
||
|
|
DEFAULTS
|
||
|
|
WHATSAPP
|
||
|
|
}
|
||
|
|
|
||
|
|
enum NotificationChannel {
|
||
|
|
EMAIL
|
||
|
|
WHATSAPP
|
||
|
|
BOTH
|
||
|
|
NONE
|
||
|
|
}
|
||
|
|
|
||
|
|
enum ResourceType {
|
||
|
|
PDF
|
||
|
|
VIDEO
|
||
|
|
DOCUMENT
|
||
|
|
LINK
|
||
|
|
OTHER
|
||
|
|
}
|
||
|
|
|
||
|
|
enum CohortLevel {
|
||
|
|
ALL
|
||
|
|
SEMIFINALIST
|
||
|
|
FINALIST
|
||
|
|
}
|
||
|
|
|
||
|
|
enum PartnerVisibility {
|
||
|
|
ADMIN_ONLY
|
||
|
|
JURY_VISIBLE
|
||
|
|
PUBLIC
|
||
|
|
}
|
||
|
|
|
||
|
|
enum PartnerType {
|
||
|
|
SPONSOR
|
||
|
|
PARTNER
|
||
|
|
SUPPORTER
|
||
|
|
MEDIA
|
||
|
|
OTHER
|
||
|
|
}
|
||
|
|
|
||
|
|
enum FormFieldType {
|
||
|
|
TEXT
|
||
|
|
TEXTAREA
|
||
|
|
NUMBER
|
||
|
|
EMAIL
|
||
|
|
PHONE
|
||
|
|
URL
|
||
|
|
DATE
|
||
|
|
DATETIME
|
||
|
|
SELECT
|
||
|
|
MULTI_SELECT
|
||
|
|
RADIO
|
||
|
|
CHECKBOX
|
||
|
|
CHECKBOX_GROUP
|
||
|
|
FILE
|
||
|
|
FILE_MULTIPLE
|
||
|
|
SECTION
|
||
|
|
INSTRUCTIONS
|
||
|
|
}
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// APPLICANT SYSTEM ENUMS
|
||
|
|
// =============================================================================
|
||
|
|
|
||
|
|
enum CompetitionCategory {
|
||
|
|
STARTUP // Existing companies
|
||
|
|
BUSINESS_CONCEPT // Students/graduates
|
||
|
|
}
|
||
|
|
|
||
|
|
enum OceanIssue {
|
||
|
|
POLLUTION_REDUCTION
|
||
|
|
CLIMATE_MITIGATION
|
||
|
|
TECHNOLOGY_INNOVATION
|
||
|
|
SUSTAINABLE_SHIPPING
|
||
|
|
BLUE_CARBON
|
||
|
|
HABITAT_RESTORATION
|
||
|
|
COMMUNITY_CAPACITY
|
||
|
|
SUSTAINABLE_FISHING
|
||
|
|
CONSUMER_AWARENESS
|
||
|
|
OCEAN_ACIDIFICATION
|
||
|
|
OTHER
|
||
|
|
}
|
||
|
|
|
||
|
|
enum TeamMemberRole {
|
||
|
|
LEAD // Primary contact / team lead
|
||
|
|
MEMBER // Regular team member
|
||
|
|
ADVISOR // Advisor/mentor from team side
|
||
|
|
}
|
||
|
|
|
||
|
|
enum MentorAssignmentMethod {
|
||
|
|
MANUAL
|
||
|
|
AI_SUGGESTED
|
||
|
|
AI_AUTO
|
||
|
|
ALGORITHM
|
||
|
|
}
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// USERS & AUTHENTICATION
|
||
|
|
// =============================================================================
|
||
|
|
|
||
|
|
model User {
|
||
|
|
id String @id @default(cuid())
|
||
|
|
email String @unique
|
||
|
|
name String?
|
||
|
|
emailVerified DateTime? // Required by NextAuth Prisma adapter
|
||
|
|
role UserRole @default(JURY_MEMBER)
|
||
|
|
status UserStatus @default(INVITED)
|
||
|
|
expertiseTags String[] @default([])
|
||
|
|
maxAssignments Int? // Per-round limit
|
||
|
|
metadataJson Json? @db.JsonB
|
||
|
|
|
||
|
|
// Profile image
|
||
|
|
profileImageKey String? // Storage key (e.g., "avatars/user123/1234567890.jpg")
|
||
|
|
profileImageProvider String? // Storage provider used: 's3' or 'local'
|
||
|
|
|
||
|
|
// Phone and notification preferences (Phase 2)
|
||
|
|
phoneNumber String?
|
||
|
|
phoneNumberVerified Boolean @default(false)
|
||
|
|
notificationPreference NotificationChannel @default(EMAIL)
|
||
|
|
whatsappOptIn Boolean @default(false)
|
||
|
|
|
||
|
|
// Onboarding (Phase 2B)
|
||
|
|
onboardingCompletedAt DateTime?
|
||
|
|
|
||
|
|
// Password authentication (hybrid auth)
|
||
|
|
passwordHash String? // bcrypt hashed password
|
||
|
|
passwordSetAt DateTime? // When password was set
|
||
|
|
mustSetPassword Boolean @default(true) // Force setup on first login
|
||
|
|
|
||
|
|
createdAt DateTime @default(now())
|
||
|
|
updatedAt DateTime @updatedAt
|
||
|
|
lastLoginAt DateTime?
|
||
|
|
|
||
|
|
// Relations
|
||
|
|
assignments Assignment[]
|
||
|
|
auditLogs AuditLog[]
|
||
|
|
gracePeriods GracePeriod[]
|
||
|
|
grantedGracePeriods GracePeriod[] @relation("GrantedBy")
|
||
|
|
notificationLogs NotificationLog[]
|
||
|
|
createdResources LearningResource[] @relation("ResourceCreatedBy")
|
||
|
|
resourceAccess ResourceAccess[]
|
||
|
|
submittedProjects Project[] @relation("ProjectSubmittedBy")
|
||
|
|
liveVotes LiveVote[]
|
||
|
|
|
||
|
|
// Team membership & mentorship
|
||
|
|
teamMemberships TeamMember[]
|
||
|
|
mentorAssignments MentorAssignment[] @relation("MentorAssignments")
|
||
|
|
|
||
|
|
// NextAuth relations
|
||
|
|
accounts Account[]
|
||
|
|
sessions Session[]
|
||
|
|
|
||
|
|
@@index([email])
|
||
|
|
@@index([role])
|
||
|
|
@@index([status])
|
||
|
|
}
|
||
|
|
|
||
|
|
// NextAuth.js required models
|
||
|
|
model Account {
|
||
|
|
id String @id @default(cuid())
|
||
|
|
userId String
|
||
|
|
type String
|
||
|
|
provider String
|
||
|
|
providerAccountId String
|
||
|
|
refresh_token String? @db.Text
|
||
|
|
access_token String? @db.Text
|
||
|
|
expires_at Int?
|
||
|
|
token_type String?
|
||
|
|
scope String?
|
||
|
|
id_token String? @db.Text
|
||
|
|
session_state String?
|
||
|
|
|
||
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
|
|
|
||
|
|
@@unique([provider, providerAccountId])
|
||
|
|
@@index([userId])
|
||
|
|
}
|
||
|
|
|
||
|
|
model Session {
|
||
|
|
id String @id @default(cuid())
|
||
|
|
sessionToken String @unique
|
||
|
|
userId String
|
||
|
|
expires DateTime
|
||
|
|
|
||
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
|
|
|
||
|
|
@@index([userId])
|
||
|
|
}
|
||
|
|
|
||
|
|
model VerificationToken {
|
||
|
|
identifier String
|
||
|
|
token String @unique
|
||
|
|
expires DateTime
|
||
|
|
|
||
|
|
@@unique([identifier, token])
|
||
|
|
}
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// PROGRAMS & ROUNDS
|
||
|
|
// =============================================================================
|
||
|
|
|
||
|
|
model Program {
|
||
|
|
id String @id @default(cuid())
|
||
|
|
name String // e.g., "Monaco Ocean Protection Challenge"
|
||
|
|
year Int // e.g., 2026
|
||
|
|
status ProgramStatus @default(DRAFT)
|
||
|
|
description String?
|
||
|
|
settingsJson Json? @db.JsonB
|
||
|
|
|
||
|
|
createdAt DateTime @default(now())
|
||
|
|
updatedAt DateTime @updatedAt
|
||
|
|
|
||
|
|
// Relations
|
||
|
|
rounds Round[]
|
||
|
|
learningResources LearningResource[]
|
||
|
|
partners Partner[]
|
||
|
|
applicationForms ApplicationForm[]
|
||
|
|
|
||
|
|
@@unique([name, year])
|
||
|
|
@@index([status])
|
||
|
|
}
|
||
|
|
|
||
|
|
model Round {
|
||
|
|
id String @id @default(cuid())
|
||
|
|
programId String
|
||
|
|
name String // e.g., "Round 1 - Semi-Finalists"
|
||
|
|
slug String? @unique // URL-friendly identifier for public submissions
|
||
|
|
status RoundStatus @default(DRAFT)
|
||
|
|
roundType RoundType @default(EVALUATION)
|
||
|
|
|
||
|
|
// Submission window (for applicant portal)
|
||
|
|
submissionDeadline DateTime? // Deadline for project submissions
|
||
|
|
submissionStartDate DateTime? // When submissions open
|
||
|
|
submissionEndDate DateTime? // When submissions close (replaces submissionDeadline if set)
|
||
|
|
lateSubmissionGrace Int? // Hours of grace period after deadline
|
||
|
|
|
||
|
|
// Phase-specific deadlines
|
||
|
|
phase1Deadline DateTime?
|
||
|
|
phase2Deadline DateTime?
|
||
|
|
|
||
|
|
// Voting window
|
||
|
|
votingStartAt DateTime?
|
||
|
|
votingEndAt DateTime?
|
||
|
|
|
||
|
|
// Configuration
|
||
|
|
requiredReviews Int @default(3) // Min evaluations per project
|
||
|
|
settingsJson Json? @db.JsonB // Grace periods, visibility rules, etc.
|
||
|
|
|
||
|
|
createdAt DateTime @default(now())
|
||
|
|
updatedAt DateTime @updatedAt
|
||
|
|
|
||
|
|
// Relations
|
||
|
|
program Program @relation(fields: [programId], references: [id], onDelete: Cascade)
|
||
|
|
projects Project[]
|
||
|
|
assignments Assignment[]
|
||
|
|
evaluationForms EvaluationForm[]
|
||
|
|
gracePeriods GracePeriod[]
|
||
|
|
liveVotingSession LiveVotingSession?
|
||
|
|
|
||
|
|
@@index([programId])
|
||
|
|
@@index([status])
|
||
|
|
@@index([roundType])
|
||
|
|
@@index([votingStartAt, votingEndAt])
|
||
|
|
@@index([submissionStartDate, submissionEndDate])
|
||
|
|
}
|
||
|
|
|
||
|
|
model EvaluationForm {
|
||
|
|
id String @id @default(cuid())
|
||
|
|
roundId String
|
||
|
|
version Int @default(1)
|
||
|
|
|
||
|
|
// Form configuration
|
||
|
|
// criteriaJson: Array of { id, label, description, scale, weight, required }
|
||
|
|
criteriaJson Json @db.JsonB
|
||
|
|
// scalesJson: { "1-5": { min, max, labels }, "1-10": { min, max, labels } }
|
||
|
|
scalesJson Json? @db.JsonB
|
||
|
|
isActive Boolean @default(false)
|
||
|
|
|
||
|
|
createdAt DateTime @default(now())
|
||
|
|
updatedAt DateTime @updatedAt
|
||
|
|
|
||
|
|
// Relations
|
||
|
|
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
|
||
|
|
evaluations Evaluation[]
|
||
|
|
|
||
|
|
@@unique([roundId, version])
|
||
|
|
@@index([roundId, isActive])
|
||
|
|
}
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// PROJECTS
|
||
|
|
// =============================================================================
|
||
|
|
|
||
|
|
model Project {
|
||
|
|
id String @id @default(cuid())
|
||
|
|
roundId String
|
||
|
|
|
||
|
|
// Core fields
|
||
|
|
title String
|
||
|
|
teamName String?
|
||
|
|
description String? @db.Text
|
||
|
|
status ProjectStatus @default(SUBMITTED)
|
||
|
|
|
||
|
|
// Competition category
|
||
|
|
competitionCategory CompetitionCategory?
|
||
|
|
oceanIssue OceanIssue?
|
||
|
|
|
||
|
|
// Location
|
||
|
|
country String?
|
||
|
|
geographicZone String? // "Europe, France"
|
||
|
|
|
||
|
|
// Institution (for students/Business Concepts)
|
||
|
|
institution String?
|
||
|
|
|
||
|
|
// Mentorship
|
||
|
|
wantsMentorship Boolean @default(false)
|
||
|
|
|
||
|
|
// Submission links (external, from CSV)
|
||
|
|
phase1SubmissionUrl String?
|
||
|
|
phase2SubmissionUrl String?
|
||
|
|
|
||
|
|
// Referral tracking
|
||
|
|
referralSource String?
|
||
|
|
|
||
|
|
// Internal admin fields
|
||
|
|
internalComments String? @db.Text
|
||
|
|
applicationStatus String? // "Received", etc.
|
||
|
|
|
||
|
|
// Submission tracking
|
||
|
|
submissionSource SubmissionSource @default(MANUAL)
|
||
|
|
submittedByEmail String?
|
||
|
|
submittedAt DateTime?
|
||
|
|
submittedByUserId String?
|
||
|
|
|
||
|
|
// Project branding
|
||
|
|
logoKey String? // Storage key (e.g., "logos/project456/1234567890.png")
|
||
|
|
logoProvider String? // Storage provider used: 's3' or 'local'
|
||
|
|
|
||
|
|
// Flexible fields
|
||
|
|
tags String[] @default([]) // "Ocean Conservation", "Tech", etc.
|
||
|
|
metadataJson Json? @db.JsonB // Custom fields from Typeform, etc.
|
||
|
|
externalIdsJson Json? @db.JsonB // Typeform ID, Notion ID, etc.
|
||
|
|
|
||
|
|
createdAt DateTime @default(now())
|
||
|
|
updatedAt DateTime @updatedAt
|
||
|
|
|
||
|
|
// Relations
|
||
|
|
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
|
||
|
|
files ProjectFile[]
|
||
|
|
assignments Assignment[]
|
||
|
|
submittedBy User? @relation("ProjectSubmittedBy", fields: [submittedByUserId], references: [id], onDelete: SetNull)
|
||
|
|
teamMembers TeamMember[]
|
||
|
|
mentorAssignment MentorAssignment?
|
||
|
|
|
||
|
|
@@index([roundId])
|
||
|
|
@@index([status])
|
||
|
|
@@index([tags])
|
||
|
|
@@index([submissionSource])
|
||
|
|
@@index([submittedByUserId])
|
||
|
|
@@index([competitionCategory])
|
||
|
|
@@index([oceanIssue])
|
||
|
|
@@index([country])
|
||
|
|
}
|
||
|
|
|
||
|
|
model ProjectFile {
|
||
|
|
id String @id @default(cuid())
|
||
|
|
projectId String
|
||
|
|
|
||
|
|
// File info
|
||
|
|
fileType FileType
|
||
|
|
fileName String
|
||
|
|
mimeType String
|
||
|
|
size Int // bytes
|
||
|
|
|
||
|
|
// MinIO location
|
||
|
|
bucket String
|
||
|
|
objectKey String
|
||
|
|
|
||
|
|
createdAt DateTime @default(now())
|
||
|
|
|
||
|
|
// Relations
|
||
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||
|
|
|
||
|
|
@@unique([bucket, objectKey])
|
||
|
|
@@index([projectId])
|
||
|
|
@@index([fileType])
|
||
|
|
}
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// ASSIGNMENTS & EVALUATIONS
|
||
|
|
// =============================================================================
|
||
|
|
|
||
|
|
model Assignment {
|
||
|
|
id String @id @default(cuid())
|
||
|
|
userId String
|
||
|
|
projectId String
|
||
|
|
roundId String
|
||
|
|
|
||
|
|
// Assignment info
|
||
|
|
method AssignmentMethod @default(MANUAL)
|
||
|
|
isRequired Boolean @default(true)
|
||
|
|
isCompleted Boolean @default(false)
|
||
|
|
|
||
|
|
// AI assignment metadata
|
||
|
|
aiConfidenceScore Float? // 0-1 confidence from AI
|
||
|
|
expertiseMatchScore Float? // 0-1 match score
|
||
|
|
aiReasoning String? @db.Text
|
||
|
|
|
||
|
|
createdAt DateTime @default(now())
|
||
|
|
createdBy String? // Admin who created the assignment
|
||
|
|
|
||
|
|
// Relations
|
||
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||
|
|
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
|
||
|
|
evaluation Evaluation?
|
||
|
|
|
||
|
|
@@unique([userId, projectId, roundId])
|
||
|
|
@@index([userId])
|
||
|
|
@@index([projectId])
|
||
|
|
@@index([roundId])
|
||
|
|
@@index([isCompleted])
|
||
|
|
}
|
||
|
|
|
||
|
|
model Evaluation {
|
||
|
|
id String @id @default(cuid())
|
||
|
|
assignmentId String @unique
|
||
|
|
formId String
|
||
|
|
|
||
|
|
// Status
|
||
|
|
status EvaluationStatus @default(NOT_STARTED)
|
||
|
|
|
||
|
|
// Scores
|
||
|
|
// criterionScoresJson: { "criterion_id": score, ... }
|
||
|
|
criterionScoresJson Json? @db.JsonB
|
||
|
|
globalScore Int? // 1-10
|
||
|
|
binaryDecision Boolean? // Yes/No for semi-finalist
|
||
|
|
feedbackText String? @db.Text
|
||
|
|
|
||
|
|
// Versioning
|
||
|
|
version Int @default(1)
|
||
|
|
|
||
|
|
// Timestamps
|
||
|
|
createdAt DateTime @default(now())
|
||
|
|
updatedAt DateTime @updatedAt
|
||
|
|
submittedAt DateTime?
|
||
|
|
|
||
|
|
// Relations
|
||
|
|
assignment Assignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade)
|
||
|
|
form EvaluationForm @relation(fields: [formId], references: [id])
|
||
|
|
|
||
|
|
@@index([status])
|
||
|
|
@@index([submittedAt])
|
||
|
|
@@index([formId])
|
||
|
|
}
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// GRACE PERIODS
|
||
|
|
// =============================================================================
|
||
|
|
|
||
|
|
model GracePeriod {
|
||
|
|
id String @id @default(cuid())
|
||
|
|
roundId String
|
||
|
|
userId String
|
||
|
|
projectId String? // Optional: specific project or all projects in round
|
||
|
|
|
||
|
|
extendedUntil DateTime
|
||
|
|
reason String? @db.Text
|
||
|
|
grantedById String
|
||
|
|
|
||
|
|
createdAt DateTime @default(now())
|
||
|
|
|
||
|
|
// Relations
|
||
|
|
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
|
||
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
|
|
grantedBy User @relation("GrantedBy", fields: [grantedById], references: [id])
|
||
|
|
|
||
|
|
@@index([roundId])
|
||
|
|
@@index([userId])
|
||
|
|
@@index([extendedUntil])
|
||
|
|
@@index([grantedById])
|
||
|
|
@@index([projectId])
|
||
|
|
}
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// SYSTEM SETTINGS
|
||
|
|
// =============================================================================
|
||
|
|
|
||
|
|
model SystemSettings {
|
||
|
|
id String @id @default(cuid())
|
||
|
|
key String @unique
|
||
|
|
value String @db.Text
|
||
|
|
type SettingType @default(STRING)
|
||
|
|
category SettingCategory
|
||
|
|
|
||
|
|
description String?
|
||
|
|
isSecret Boolean @default(false) // If true, value is encrypted
|
||
|
|
|
||
|
|
updatedAt DateTime @updatedAt
|
||
|
|
updatedBy String?
|
||
|
|
|
||
|
|
@@index([category])
|
||
|
|
}
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// AUDIT LOGGING
|
||
|
|
// =============================================================================
|
||
|
|
|
||
|
|
model AuditLog {
|
||
|
|
id String @id @default(cuid())
|
||
|
|
userId String?
|
||
|
|
|
||
|
|
// Event info
|
||
|
|
action String // "CREATE", "UPDATE", "DELETE", "LOGIN", "EXPORT", etc.
|
||
|
|
entityType String // "Round", "Project", "Evaluation", etc.
|
||
|
|
entityId String?
|
||
|
|
|
||
|
|
// Details
|
||
|
|
detailsJson Json? @db.JsonB // Before/after values, additional context
|
||
|
|
|
||
|
|
// Request info
|
||
|
|
ipAddress String?
|
||
|
|
userAgent String?
|
||
|
|
|
||
|
|
timestamp DateTime @default(now())
|
||
|
|
|
||
|
|
// Relations
|
||
|
|
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
||
|
|
|
||
|
|
@@index([userId])
|
||
|
|
@@index([action])
|
||
|
|
@@index([entityType, entityId])
|
||
|
|
@@index([timestamp])
|
||
|
|
}
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// NOTIFICATION LOG (Phase 2)
|
||
|
|
// =============================================================================
|
||
|
|
|
||
|
|
model NotificationLog {
|
||
|
|
id String @id @default(cuid())
|
||
|
|
userId String
|
||
|
|
channel NotificationChannel
|
||
|
|
provider String? // META, TWILIO, SMTP
|
||
|
|
type String // MAGIC_LINK, REMINDER, ANNOUNCEMENT, JURY_INVITATION
|
||
|
|
status String // PENDING, SENT, DELIVERED, FAILED
|
||
|
|
externalId String? // Message ID from provider
|
||
|
|
errorMsg String? @db.Text
|
||
|
|
|
||
|
|
createdAt DateTime @default(now())
|
||
|
|
|
||
|
|
// Relations
|
||
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
|
|
|
||
|
|
@@index([userId])
|
||
|
|
@@index([status])
|
||
|
|
@@index([createdAt])
|
||
|
|
}
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// LEARNING HUB (Phase 2)
|
||
|
|
// =============================================================================
|
||
|
|
|
||
|
|
model LearningResource {
|
||
|
|
id String @id @default(cuid())
|
||
|
|
programId String? // null = global resource
|
||
|
|
title String
|
||
|
|
description String? @db.Text
|
||
|
|
contentJson Json? @db.JsonB // BlockNote document structure
|
||
|
|
resourceType ResourceType
|
||
|
|
cohortLevel CohortLevel @default(ALL)
|
||
|
|
|
||
|
|
// File storage (for uploaded resources)
|
||
|
|
fileName String?
|
||
|
|
mimeType String?
|
||
|
|
size Int?
|
||
|
|
bucket String?
|
||
|
|
objectKey String?
|
||
|
|
|
||
|
|
// External link
|
||
|
|
externalUrl String?
|
||
|
|
|
||
|
|
sortOrder Int @default(0)
|
||
|
|
isPublished Boolean @default(false)
|
||
|
|
|
||
|
|
createdAt DateTime @default(now())
|
||
|
|
updatedAt DateTime @updatedAt
|
||
|
|
createdById String
|
||
|
|
|
||
|
|
// Relations
|
||
|
|
program Program? @relation(fields: [programId], references: [id], onDelete: SetNull)
|
||
|
|
createdBy User @relation("ResourceCreatedBy", fields: [createdById], references: [id])
|
||
|
|
accessLogs ResourceAccess[]
|
||
|
|
|
||
|
|
@@index([programId])
|
||
|
|
@@index([cohortLevel])
|
||
|
|
@@index([isPublished])
|
||
|
|
@@index([sortOrder])
|
||
|
|
}
|
||
|
|
|
||
|
|
model ResourceAccess {
|
||
|
|
id String @id @default(cuid())
|
||
|
|
resourceId String
|
||
|
|
userId String
|
||
|
|
accessedAt DateTime @default(now())
|
||
|
|
ipAddress String?
|
||
|
|
|
||
|
|
// Relations
|
||
|
|
resource LearningResource @relation(fields: [resourceId], references: [id], onDelete: Cascade)
|
||
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
|
|
|
||
|
|
@@index([resourceId])
|
||
|
|
@@index([userId])
|
||
|
|
@@index([accessedAt])
|
||
|
|
}
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// PARTNER MANAGEMENT (Phase 2)
|
||
|
|
// =============================================================================
|
||
|
|
|
||
|
|
model Partner {
|
||
|
|
id String @id @default(cuid())
|
||
|
|
programId String? // null = global partner
|
||
|
|
name String
|
||
|
|
description String? @db.Text
|
||
|
|
website String?
|
||
|
|
partnerType PartnerType @default(PARTNER)
|
||
|
|
visibility PartnerVisibility @default(ADMIN_ONLY)
|
||
|
|
|
||
|
|
// Logo file
|
||
|
|
logoFileName String?
|
||
|
|
logoBucket String?
|
||
|
|
logoObjectKey String?
|
||
|
|
|
||
|
|
sortOrder Int @default(0)
|
||
|
|
isActive Boolean @default(true)
|
||
|
|
|
||
|
|
createdAt DateTime @default(now())
|
||
|
|
updatedAt DateTime @updatedAt
|
||
|
|
|
||
|
|
// Relations
|
||
|
|
program Program? @relation(fields: [programId], references: [id], onDelete: SetNull)
|
||
|
|
|
||
|
|
@@index([programId])
|
||
|
|
@@index([partnerType])
|
||
|
|
@@index([visibility])
|
||
|
|
@@index([isActive])
|
||
|
|
@@index([sortOrder])
|
||
|
|
}
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// APPLICATION FORMS (Phase 2)
|
||
|
|
// =============================================================================
|
||
|
|
|
||
|
|
model ApplicationForm {
|
||
|
|
id String @id @default(cuid())
|
||
|
|
programId String? // null = global form
|
||
|
|
name String
|
||
|
|
description String? @db.Text
|
||
|
|
status String @default("DRAFT") // DRAFT, PUBLISHED, CLOSED
|
||
|
|
|
||
|
|
isPublic Boolean @default(false)
|
||
|
|
publicSlug String? @unique // /apply/ocean-challenge-2026
|
||
|
|
submissionLimit Int?
|
||
|
|
opensAt DateTime?
|
||
|
|
closesAt DateTime?
|
||
|
|
|
||
|
|
confirmationMessage String? @db.Text
|
||
|
|
|
||
|
|
createdAt DateTime @default(now())
|
||
|
|
updatedAt DateTime @updatedAt
|
||
|
|
|
||
|
|
// Relations
|
||
|
|
program Program? @relation(fields: [programId], references: [id], onDelete: SetNull)
|
||
|
|
fields ApplicationFormField[]
|
||
|
|
submissions ApplicationFormSubmission[]
|
||
|
|
|
||
|
|
@@index([programId])
|
||
|
|
@@index([status])
|
||
|
|
@@index([isPublic])
|
||
|
|
}
|
||
|
|
|
||
|
|
model ApplicationFormField {
|
||
|
|
id String @id @default(cuid())
|
||
|
|
formId String
|
||
|
|
fieldType FormFieldType
|
||
|
|
name String // Internal name (e.g., "project_title")
|
||
|
|
label String // Display label (e.g., "Project Title")
|
||
|
|
description String? @db.Text
|
||
|
|
placeholder String?
|
||
|
|
|
||
|
|
required Boolean @default(false)
|
||
|
|
minLength Int?
|
||
|
|
maxLength Int?
|
||
|
|
minValue Float? // For NUMBER type
|
||
|
|
maxValue Float? // For NUMBER type
|
||
|
|
|
||
|
|
optionsJson Json? @db.JsonB // For select/radio: [{ value, label }]
|
||
|
|
conditionJson Json? @db.JsonB // Conditional logic: { fieldId, operator, value }
|
||
|
|
|
||
|
|
sortOrder Int @default(0)
|
||
|
|
width String @default("full") // full, half
|
||
|
|
|
||
|
|
createdAt DateTime @default(now())
|
||
|
|
updatedAt DateTime @updatedAt
|
||
|
|
|
||
|
|
// Relations
|
||
|
|
form ApplicationForm @relation(fields: [formId], references: [id], onDelete: Cascade)
|
||
|
|
|
||
|
|
@@index([formId])
|
||
|
|
@@index([sortOrder])
|
||
|
|
}
|
||
|
|
|
||
|
|
model ApplicationFormSubmission {
|
||
|
|
id String @id @default(cuid())
|
||
|
|
formId String
|
||
|
|
email String?
|
||
|
|
name String?
|
||
|
|
dataJson Json @db.JsonB // Field values: { fieldName: value, ... }
|
||
|
|
status String @default("SUBMITTED") // SUBMITTED, REVIEWED, APPROVED, REJECTED
|
||
|
|
|
||
|
|
createdAt DateTime @default(now())
|
||
|
|
updatedAt DateTime @updatedAt
|
||
|
|
|
||
|
|
// Relations
|
||
|
|
form ApplicationForm @relation(fields: [formId], references: [id], onDelete: Cascade)
|
||
|
|
files SubmissionFile[]
|
||
|
|
|
||
|
|
@@index([formId])
|
||
|
|
@@index([status])
|
||
|
|
@@index([email])
|
||
|
|
@@index([createdAt])
|
||
|
|
}
|
||
|
|
|
||
|
|
model SubmissionFile {
|
||
|
|
id String @id @default(cuid())
|
||
|
|
submissionId String
|
||
|
|
fieldName String
|
||
|
|
fileName String
|
||
|
|
mimeType String?
|
||
|
|
size Int?
|
||
|
|
bucket String
|
||
|
|
objectKey String
|
||
|
|
|
||
|
|
createdAt DateTime @default(now())
|
||
|
|
|
||
|
|
// Relations
|
||
|
|
submission ApplicationFormSubmission @relation(fields: [submissionId], references: [id], onDelete: Cascade)
|
||
|
|
|
||
|
|
@@index([submissionId])
|
||
|
|
@@unique([bucket, objectKey])
|
||
|
|
}
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// EXPERTISE TAGS (Phase 2B)
|
||
|
|
// =============================================================================
|
||
|
|
|
||
|
|
model ExpertiseTag {
|
||
|
|
id String @id @default(cuid())
|
||
|
|
name String @unique
|
||
|
|
description String?
|
||
|
|
category String? // "Marine Science", "Technology", "Policy"
|
||
|
|
color String? // Hex for badge
|
||
|
|
isActive Boolean @default(true)
|
||
|
|
sortOrder Int @default(0)
|
||
|
|
|
||
|
|
createdAt DateTime @default(now())
|
||
|
|
updatedAt DateTime @updatedAt
|
||
|
|
|
||
|
|
@@index([category])
|
||
|
|
@@index([isActive])
|
||
|
|
@@index([sortOrder])
|
||
|
|
}
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// LIVE VOTING (Phase 2B)
|
||
|
|
// =============================================================================
|
||
|
|
|
||
|
|
model LiveVotingSession {
|
||
|
|
id String @id @default(cuid())
|
||
|
|
roundId String @unique
|
||
|
|
status String @default("NOT_STARTED") // NOT_STARTED, IN_PROGRESS, PAUSED, COMPLETED
|
||
|
|
currentProjectIndex Int @default(0)
|
||
|
|
currentProjectId String?
|
||
|
|
votingStartedAt DateTime?
|
||
|
|
votingEndsAt DateTime?
|
||
|
|
projectOrderJson Json? @db.JsonB // Array of project IDs in presentation order
|
||
|
|
|
||
|
|
createdAt DateTime @default(now())
|
||
|
|
updatedAt DateTime @updatedAt
|
||
|
|
|
||
|
|
// Relations
|
||
|
|
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
|
||
|
|
votes LiveVote[]
|
||
|
|
|
||
|
|
@@index([status])
|
||
|
|
}
|
||
|
|
|
||
|
|
model LiveVote {
|
||
|
|
id String @id @default(cuid())
|
||
|
|
sessionId String
|
||
|
|
projectId String
|
||
|
|
userId String
|
||
|
|
score Int // 1-10
|
||
|
|
votedAt DateTime @default(now())
|
||
|
|
|
||
|
|
// Relations
|
||
|
|
session LiveVotingSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
||
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
|
|
|
||
|
|
@@unique([sessionId, projectId, userId])
|
||
|
|
@@index([sessionId])
|
||
|
|
@@index([projectId])
|
||
|
|
@@index([userId])
|
||
|
|
}
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// TEAM MEMBERSHIP
|
||
|
|
// =============================================================================
|
||
|
|
|
||
|
|
model TeamMember {
|
||
|
|
id String @id @default(cuid())
|
||
|
|
projectId String
|
||
|
|
userId String
|
||
|
|
role TeamMemberRole @default(MEMBER)
|
||
|
|
title String? // "CEO", "CTO", etc.
|
||
|
|
joinedAt DateTime @default(now())
|
||
|
|
|
||
|
|
// Relations
|
||
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||
|
|
|
||
|
|
@@unique([projectId, userId])
|
||
|
|
@@index([projectId])
|
||
|
|
@@index([userId])
|
||
|
|
@@index([role])
|
||
|
|
}
|
||
|
|
|
||
|
|
// =============================================================================
|
||
|
|
// MENTOR ASSIGNMENT
|
||
|
|
// =============================================================================
|
||
|
|
|
||
|
|
model MentorAssignment {
|
||
|
|
id String @id @default(cuid())
|
||
|
|
projectId String @unique // One mentor per project
|
||
|
|
mentorId String // User with MENTOR role or expertise
|
||
|
|
|
||
|
|
// Assignment tracking
|
||
|
|
method MentorAssignmentMethod @default(MANUAL)
|
||
|
|
assignedAt DateTime @default(now())
|
||
|
|
assignedBy String? // Admin who assigned
|
||
|
|
|
||
|
|
// AI assignment metadata
|
||
|
|
aiConfidenceScore Float?
|
||
|
|
expertiseMatchScore Float?
|
||
|
|
aiReasoning String? @db.Text
|
||
|
|
|
||
|
|
// Relations
|
||
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||
|
|
mentor User @relation("MentorAssignments", fields: [mentorId], references: [id])
|
||
|
|
|
||
|
|
@@index([mentorId])
|
||
|
|
@@index([method])
|
||
|
|
}
|