1385 lines
39 KiB
Plaintext
1385 lines
39 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
|
|
}
|
|
|
|
enum SpecialFieldType {
|
|
TEAM_MEMBERS // Team member repeater
|
|
COMPETITION_CATEGORY // Business Concept vs Startup
|
|
OCEAN_ISSUE // Ocean issue dropdown
|
|
FILE_UPLOAD // File upload
|
|
GDPR_CONSENT // GDPR consent checkbox
|
|
COUNTRY_SELECT // Country dropdown
|
|
}
|
|
|
|
// =============================================================================
|
|
// 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
|
|
|
|
// Invitation token for one-click invite acceptance
|
|
inviteToken String? @unique
|
|
inviteTokenExpiresAt DateTime?
|
|
|
|
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")
|
|
|
|
// Awards
|
|
awardJurorships AwardJuror[]
|
|
awardVotes AwardVote[]
|
|
|
|
// Filtering overrides
|
|
filteringOverrides FilteringResult[] @relation("FilteringOverriddenBy")
|
|
|
|
// Award overrides
|
|
awardEligibilityOverrides AwardEligibility[] @relation("AwardEligibilityOverriddenBy")
|
|
|
|
// In-app notifications
|
|
notifications InAppNotification[] @relation("UserNotifications")
|
|
notificationSettingsUpdated NotificationEmailSetting[] @relation("NotificationSettingUpdater")
|
|
|
|
// 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[]
|
|
projects Project[]
|
|
learningResources LearningResource[]
|
|
partners Partner[]
|
|
applicationForms ApplicationForm[]
|
|
specialAwards SpecialAward[]
|
|
|
|
@@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)
|
|
sortOrder Int @default(0) // Progression order within program
|
|
|
|
// 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)
|
|
roundProjects RoundProject[]
|
|
assignments Assignment[]
|
|
evaluationForms EvaluationForm[]
|
|
gracePeriods GracePeriod[]
|
|
liveVotingSession LiveVotingSession?
|
|
filteringRules FilteringRule[]
|
|
filteringResults FilteringResult[]
|
|
filteringJobs FilteringJob[]
|
|
applicationForm ApplicationForm?
|
|
|
|
@@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())
|
|
programId String
|
|
|
|
// Core fields
|
|
title String
|
|
teamName String?
|
|
description String? @db.Text
|
|
|
|
// 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)
|
|
|
|
// Founding date
|
|
foundedAt DateTime? // When the project/company was founded
|
|
|
|
// 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
|
|
program Program @relation(fields: [programId], references: [id], onDelete: Cascade)
|
|
roundProjects RoundProject[]
|
|
files ProjectFile[]
|
|
assignments Assignment[]
|
|
submittedBy User? @relation("ProjectSubmittedBy", fields: [submittedByUserId], references: [id], onDelete: SetNull)
|
|
teamMembers TeamMember[]
|
|
mentorAssignment MentorAssignment?
|
|
filteringResults FilteringResult[]
|
|
awardEligibilities AwardEligibility[]
|
|
awardVotes AwardVote[]
|
|
wonAwards SpecialAward[] @relation("AwardWinner")
|
|
|
|
@@index([programId])
|
|
@@index([tags])
|
|
@@index([submissionSource])
|
|
@@index([submittedByUserId])
|
|
@@index([competitionCategory])
|
|
@@index([oceanIssue])
|
|
@@index([country])
|
|
}
|
|
|
|
model RoundProject {
|
|
id String @id @default(cuid())
|
|
roundId String
|
|
projectId String
|
|
status ProjectStatus @default(SUBMITTED)
|
|
addedAt DateTime @default(now())
|
|
|
|
// Relations
|
|
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([roundId, projectId])
|
|
@@index([roundId])
|
|
@@index([projectId])
|
|
@@index([status])
|
|
}
|
|
|
|
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])
|
|
}
|
|
|
|
// =============================================================================
|
|
// AI USAGE TRACKING
|
|
// =============================================================================
|
|
|
|
model AIUsageLog {
|
|
id String @id @default(cuid())
|
|
createdAt DateTime @default(now())
|
|
|
|
// Who/what triggered it
|
|
userId String?
|
|
action String // ASSIGNMENT, FILTERING, AWARD_ELIGIBILITY, MENTOR_MATCHING
|
|
entityType String? // Round, Project, Award
|
|
entityId String?
|
|
|
|
// What was used
|
|
model String // gpt-4o, gpt-4o-mini, o1, etc.
|
|
promptTokens Int
|
|
completionTokens Int
|
|
totalTokens Int
|
|
|
|
// Cost tracking
|
|
estimatedCostUsd Decimal? @db.Decimal(10, 6)
|
|
|
|
// Request context
|
|
batchSize Int?
|
|
itemsProcessed Int?
|
|
|
|
// Status
|
|
status String // SUCCESS, PARTIAL, ERROR
|
|
errorMessage String?
|
|
|
|
// Detailed data (optional)
|
|
detailsJson Json? @db.JsonB
|
|
|
|
@@index([userId])
|
|
@@index([action])
|
|
@@index([createdAt])
|
|
@@index([model])
|
|
}
|
|
|
|
// =============================================================================
|
|
// 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])
|
|
}
|
|
|
|
// =============================================================================
|
|
// IN-APP NOTIFICATIONS
|
|
// =============================================================================
|
|
|
|
model InAppNotification {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
type String // FILTERING_COMPLETE, NEW_APPLICATION, ASSIGNED_TO_PROJECT, etc.
|
|
priority String @default("normal") // low, normal, high, urgent
|
|
icon String? // lucide icon name
|
|
title String
|
|
message String @db.Text
|
|
linkUrl String? // Where to navigate when clicked
|
|
linkLabel String? // CTA text
|
|
metadata Json? @db.JsonB // Extra context (projectId, roundId, etc.)
|
|
groupKey String? // For batching similar notifications
|
|
|
|
isRead Boolean @default(false)
|
|
readAt DateTime?
|
|
expiresAt DateTime? // Auto-dismiss after date
|
|
|
|
createdAt DateTime @default(now())
|
|
|
|
// Relations
|
|
user User @relation("UserNotifications", fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([userId, isRead])
|
|
@@index([userId, createdAt])
|
|
@@index([groupKey])
|
|
}
|
|
|
|
model NotificationEmailSetting {
|
|
id String @id @default(cuid())
|
|
notificationType String @unique // e.g., "ADVANCED_TO_ROUND", "ASSIGNED_TO_PROJECT"
|
|
category String // "team", "jury", "mentor", "admin"
|
|
label String // Human-readable label for admin UI
|
|
description String? // Help text
|
|
sendEmail Boolean @default(true)
|
|
emailSubject String? // Custom subject template (optional)
|
|
emailTemplate String? @db.Text // Custom body template (optional)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
updatedById String?
|
|
updatedBy User? @relation("NotificationSettingUpdater", fields: [updatedById], references: [id])
|
|
|
|
@@index([category])
|
|
}
|
|
|
|
// =============================================================================
|
|
// 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
|
|
|
|
// Round linking (for onboarding forms that create projects)
|
|
roundId String? @unique
|
|
|
|
// Email settings
|
|
sendConfirmationEmail Boolean @default(true)
|
|
sendTeamInviteEmails Boolean @default(true)
|
|
confirmationEmailSubject String?
|
|
confirmationEmailBody String? @db.Text
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
program Program? @relation(fields: [programId], references: [id], onDelete: SetNull)
|
|
round Round? @relation(fields: [roundId], references: [id], onDelete: SetNull)
|
|
fields ApplicationFormField[]
|
|
steps OnboardingStep[]
|
|
submissions ApplicationFormSubmission[]
|
|
|
|
@@index([programId])
|
|
@@index([status])
|
|
@@index([isPublic])
|
|
@@index([roundId])
|
|
}
|
|
|
|
model ApplicationFormField {
|
|
id String @id @default(cuid())
|
|
formId String
|
|
stepId String? // Which step this field belongs to (for onboarding)
|
|
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 }
|
|
|
|
// Onboarding-specific fields
|
|
projectMapping String? // Maps to Project column: "title", "description", etc.
|
|
specialType SpecialFieldType? // Special handling for complex fields
|
|
|
|
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)
|
|
step OnboardingStep? @relation(fields: [stepId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([formId])
|
|
@@index([stepId])
|
|
@@index([sortOrder])
|
|
}
|
|
|
|
model OnboardingStep {
|
|
id String @id @default(cuid())
|
|
formId String
|
|
name String // Internal identifier (e.g., "category", "contact")
|
|
title String // Display title (e.g., "Category", "Contact Information")
|
|
description String? @db.Text
|
|
sortOrder Int @default(0)
|
|
isOptional Boolean @default(false)
|
|
conditionJson Json? @db.JsonB // Conditional visibility: { fieldId, operator, value }
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
form ApplicationForm @relation(fields: [formId], references: [id], onDelete: Cascade)
|
|
fields ApplicationFormField[]
|
|
|
|
@@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])
|
|
}
|
|
|
|
// =============================================================================
|
|
// FILTERING ROUND SYSTEM
|
|
// =============================================================================
|
|
|
|
enum FilteringOutcome {
|
|
PASSED
|
|
FILTERED_OUT
|
|
FLAGGED
|
|
}
|
|
|
|
enum FilteringRuleType {
|
|
FIELD_BASED
|
|
DOCUMENT_CHECK
|
|
AI_SCREENING
|
|
}
|
|
|
|
model FilteringRule {
|
|
id String @id @default(cuid())
|
|
roundId String
|
|
name String
|
|
ruleType FilteringRuleType
|
|
configJson Json @db.JsonB // Conditions, logic, action per rule type
|
|
priority Int @default(0)
|
|
isActive Boolean @default(true)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([roundId])
|
|
@@index([priority])
|
|
}
|
|
|
|
model FilteringResult {
|
|
id String @id @default(cuid())
|
|
roundId String
|
|
projectId String
|
|
outcome FilteringOutcome
|
|
ruleResultsJson Json? @db.JsonB // Per-rule results
|
|
aiScreeningJson Json? @db.JsonB // AI screening details
|
|
|
|
// Admin override
|
|
overriddenBy String?
|
|
overriddenAt DateTime?
|
|
overrideReason String? @db.Text
|
|
finalOutcome FilteringOutcome?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
overriddenByUser User? @relation("FilteringOverriddenBy", fields: [overriddenBy], references: [id], onDelete: SetNull)
|
|
|
|
@@unique([roundId, projectId])
|
|
@@index([roundId])
|
|
@@index([projectId])
|
|
@@index([outcome])
|
|
}
|
|
|
|
// Tracks progress of long-running filtering jobs
|
|
model FilteringJob {
|
|
id String @id @default(cuid())
|
|
roundId String
|
|
status FilteringJobStatus @default(PENDING)
|
|
totalProjects Int @default(0)
|
|
totalBatches Int @default(0)
|
|
currentBatch Int @default(0)
|
|
processedCount Int @default(0)
|
|
passedCount Int @default(0)
|
|
filteredCount Int @default(0)
|
|
flaggedCount Int @default(0)
|
|
errorMessage String? @db.Text
|
|
startedAt DateTime?
|
|
completedAt DateTime?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([roundId])
|
|
@@index([status])
|
|
}
|
|
|
|
enum FilteringJobStatus {
|
|
PENDING
|
|
RUNNING
|
|
COMPLETED
|
|
FAILED
|
|
}
|
|
|
|
// =============================================================================
|
|
// SPECIAL AWARDS SYSTEM
|
|
// =============================================================================
|
|
|
|
enum AwardScoringMode {
|
|
PICK_WINNER
|
|
RANKED
|
|
SCORED
|
|
}
|
|
|
|
enum AwardStatus {
|
|
DRAFT
|
|
NOMINATIONS_OPEN
|
|
VOTING_OPEN
|
|
CLOSED
|
|
ARCHIVED
|
|
}
|
|
|
|
enum EligibilityMethod {
|
|
AUTO
|
|
MANUAL
|
|
}
|
|
|
|
model SpecialAward {
|
|
id String @id @default(cuid())
|
|
programId String
|
|
name String
|
|
description String? @db.Text
|
|
status AwardStatus @default(DRAFT)
|
|
|
|
// Criteria
|
|
criteriaText String? @db.Text // Plain-language criteria for AI
|
|
autoTagRulesJson Json? @db.JsonB // Deterministic eligibility rules
|
|
useAiEligibility Boolean @default(true) // Whether AI evaluates eligibility
|
|
|
|
// Scoring
|
|
scoringMode AwardScoringMode @default(PICK_WINNER)
|
|
maxRankedPicks Int? // For RANKED mode
|
|
|
|
// Voting window
|
|
votingStartAt DateTime?
|
|
votingEndAt DateTime?
|
|
|
|
// Evaluation form (for SCORED mode)
|
|
evaluationFormId String?
|
|
|
|
// Winner
|
|
winnerProjectId String?
|
|
winnerOverridden Boolean @default(false)
|
|
winnerOverriddenBy String?
|
|
|
|
sortOrder Int @default(0)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
program Program @relation(fields: [programId], references: [id], onDelete: Cascade)
|
|
winnerProject Project? @relation("AwardWinner", fields: [winnerProjectId], references: [id], onDelete: SetNull)
|
|
eligibilities AwardEligibility[]
|
|
jurors AwardJuror[]
|
|
votes AwardVote[]
|
|
|
|
@@index([programId])
|
|
@@index([status])
|
|
@@index([sortOrder])
|
|
}
|
|
|
|
model AwardEligibility {
|
|
id String @id @default(cuid())
|
|
awardId String
|
|
projectId String
|
|
method EligibilityMethod @default(AUTO)
|
|
eligible Boolean @default(false)
|
|
aiReasoningJson Json? @db.JsonB
|
|
|
|
// Admin override
|
|
overriddenBy String?
|
|
overriddenAt DateTime?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
award SpecialAward @relation(fields: [awardId], references: [id], onDelete: Cascade)
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
overriddenByUser User? @relation("AwardEligibilityOverriddenBy", fields: [overriddenBy], references: [id], onDelete: SetNull)
|
|
|
|
@@unique([awardId, projectId])
|
|
@@index([awardId])
|
|
@@index([projectId])
|
|
@@index([eligible])
|
|
}
|
|
|
|
model AwardJuror {
|
|
id String @id @default(cuid())
|
|
awardId String
|
|
userId String
|
|
|
|
createdAt DateTime @default(now())
|
|
|
|
// Relations
|
|
award SpecialAward @relation(fields: [awardId], references: [id], onDelete: Cascade)
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([awardId, userId])
|
|
@@index([awardId])
|
|
@@index([userId])
|
|
}
|
|
|
|
model AwardVote {
|
|
id String @id @default(cuid())
|
|
awardId String
|
|
userId String
|
|
projectId String
|
|
rank Int? // For RANKED mode
|
|
votedAt DateTime @default(now())
|
|
|
|
// Relations
|
|
award SpecialAward @relation(fields: [awardId], references: [id], onDelete: Cascade)
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([awardId, userId, projectId])
|
|
@@index([awardId])
|
|
@@index([userId])
|
|
@@index([projectId])
|
|
}
|