Initial commit: MOPC platform with Docker deployment setup
Full Next.js 15 platform with tRPC, Prisma, PostgreSQL, NextAuth. Includes production Dockerfile (multi-stage, port 7600), docker-compose with registry-based image pull, Gitea Actions CI workflow, nginx config for portal.monaco-opc.com, deployment scripts, and DEPLOYMENT.md guide. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
973
prisma/schema.prisma
Normal file
973
prisma/schema.prisma
Normal file
@@ -0,0 +1,973 @@
|
||||
// =============================================================================
|
||||
// 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])
|
||||
}
|
||||
Reference in New Issue
Block a user