Schema: Add 11 new models (RoundTemplate, MentorNote, MentorMilestone, MentorMilestoneCompletion, EvaluationDiscussion, DiscussionComment, Message, MessageRecipient, MessageTemplate, Webhook, WebhookDelivery, DigestLog) and missing fields on existing models (Project.isDraft, ProjectFile.version, LiveVotingSession.allowAudienceVotes, User.digestFrequency, AuditLog.sessionId, MentorAssignment.completionStatus, etc). Add AUDIT_CONFIG/LOCALIZATION/DIGEST/ANALYTICS enum values. Code: Fix implicit any types, route type casts, enum casts, null safety, composite key handling, and relation field names across 11 source files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1787 lines
53 KiB
Plaintext
1787 lines
53 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
|
|
AUDIT_CONFIG
|
|
LOCALIZATION
|
|
DIGEST
|
|
ANALYTICS
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// =============================================================================
|
|
// 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
|
|
country String? // User's home country (for mentor matching)
|
|
metadataJson Json? @db.JsonB
|
|
|
|
// Profile
|
|
bio String? // User bio for matching with project descriptions
|
|
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?
|
|
|
|
// Digest & availability preferences
|
|
digestFrequency String? // 'none' | 'daily' | 'weekly'
|
|
preferredWorkload Int?
|
|
availabilityJson Json? @db.JsonB // { startDate?: string, endDate?: string }
|
|
|
|
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")
|
|
awardWinnerOverrides SpecialAward[] @relation("AwardOverriddenBy")
|
|
|
|
// In-app notifications
|
|
notifications InAppNotification[] @relation("UserNotifications")
|
|
notificationSettingsUpdated NotificationEmailSetting[] @relation("NotificationSettingUpdater")
|
|
|
|
// Reminder logs
|
|
reminderLogs ReminderLog[]
|
|
|
|
// Conflict of interest
|
|
conflictsOfInterest ConflictOfInterest[]
|
|
coiReviews ConflictOfInterest[] @relation("COIReviewedBy")
|
|
|
|
// Evaluation summaries
|
|
generatedSummaries EvaluationSummary[] @relation("EvaluationSummaryGeneratedBy")
|
|
|
|
// Mentor messages
|
|
mentorMessages MentorMessage[] @relation("MentorMessageSender")
|
|
|
|
// Wizard templates
|
|
wizardTemplates WizardTemplate[] @relation("WizardTemplateCreatedBy")
|
|
|
|
// Round templates
|
|
roundTemplates RoundTemplate[] @relation("RoundTemplateCreatedBy")
|
|
|
|
// Mentor notes
|
|
mentorNotes MentorNote[] @relation("MentorNoteAuthor")
|
|
|
|
// Milestone completions
|
|
milestoneCompletions MentorMilestoneCompletion[] @relation("MilestoneCompletedBy")
|
|
|
|
// Evaluation discussions
|
|
closedDiscussions EvaluationDiscussion[] @relation("DiscussionClosedBy")
|
|
discussionComments DiscussionComment[] @relation("DiscussionCommentAuthor")
|
|
|
|
// Messaging
|
|
sentMessages Message[] @relation("MessageSender")
|
|
receivedMessages MessageRecipient[] @relation("MessageRecipient")
|
|
messageTemplates MessageTemplate[] @relation("MessageTemplateCreator")
|
|
|
|
// Webhooks
|
|
webhooks Webhook[] @relation("WebhookCreator")
|
|
|
|
// Digest logs
|
|
digestLogs DigestLog[] @relation("DigestLog")
|
|
|
|
// NextAuth relations
|
|
accounts Account[]
|
|
sessions Session[]
|
|
|
|
@@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"
|
|
slug String? @unique // URL-friendly identifier for edition-wide applications
|
|
year Int // e.g., 2026
|
|
status ProgramStatus @default(DRAFT)
|
|
description String?
|
|
settingsJson Json? @db.JsonB
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
projects Project[]
|
|
rounds Round[]
|
|
learningResources LearningResource[]
|
|
partners Partner[]
|
|
specialAwards SpecialAward[]
|
|
taggingJobs TaggingJob[]
|
|
wizardTemplates WizardTemplate[]
|
|
roundTemplates RoundTemplate[]
|
|
mentorMilestones MentorMilestone[]
|
|
|
|
@@unique([name, year])
|
|
@@index([status])
|
|
}
|
|
|
|
model WizardTemplate {
|
|
id String @id @default(cuid())
|
|
name String
|
|
description String?
|
|
config Json @db.JsonB
|
|
isGlobal Boolean @default(false)
|
|
programId String?
|
|
program Program? @relation(fields: [programId], references: [id], onDelete: Cascade)
|
|
createdBy String
|
|
creator User @relation("WizardTemplateCreatedBy", fields: [createdBy], references: [id])
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@index([programId])
|
|
@@index([isGlobal])
|
|
}
|
|
|
|
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) // Display order within program
|
|
|
|
// Entry notification settings
|
|
entryNotificationType String? // Type of notification to send when project enters round
|
|
|
|
// 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
|
|
minAssignmentsPerJuror Int @default(5) // Target minimum projects per judge
|
|
maxAssignmentsPerJuror Int @default(20) // Max projects per judge in this round
|
|
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?
|
|
filteringRules FilteringRule[]
|
|
filteringResults FilteringResult[]
|
|
filteringJobs FilteringJob[]
|
|
assignmentJobs AssignmentJob[]
|
|
taggingJobs TaggingJob[]
|
|
reminderLogs ReminderLog[]
|
|
projectFiles ProjectFile[]
|
|
evaluationDiscussions EvaluationDiscussion[]
|
|
messages Message[]
|
|
|
|
@@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
|
|
roundId String?
|
|
status ProjectStatus @default(SUBMITTED)
|
|
|
|
// 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'
|
|
|
|
// Draft support
|
|
isDraft Boolean @default(false)
|
|
draftDataJson Json? @db.JsonB // Form data for drafts
|
|
draftExpiresAt DateTime?
|
|
|
|
// 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)
|
|
round Round? @relation(fields: [roundId], references: [id], onDelete: SetNull)
|
|
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")
|
|
projectTags ProjectTag[]
|
|
statusHistory ProjectStatusHistory[]
|
|
mentorMessages MentorMessage[]
|
|
evaluationSummaries EvaluationSummary[]
|
|
evaluationDiscussions EvaluationDiscussion[]
|
|
|
|
@@index([programId])
|
|
@@index([roundId])
|
|
@@index([programId, roundId])
|
|
@@index([status])
|
|
@@index([tags])
|
|
@@index([submissionSource])
|
|
@@index([submittedByUserId])
|
|
@@index([competitionCategory])
|
|
@@index([oceanIssue])
|
|
@@index([country])
|
|
}
|
|
|
|
model ProjectFile {
|
|
id String @id @default(cuid())
|
|
projectId String
|
|
roundId String? // Which round this file was submitted for
|
|
|
|
// File info
|
|
fileType FileType
|
|
fileName String
|
|
mimeType String
|
|
size Int // bytes
|
|
|
|
// MinIO location
|
|
bucket String
|
|
objectKey String
|
|
|
|
isLate Boolean @default(false) // Uploaded after round deadline
|
|
|
|
// Versioning
|
|
version Int @default(1)
|
|
replacedById String? // FK to the newer file that replaced this one
|
|
|
|
createdAt DateTime @default(now())
|
|
|
|
// Relations
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
round Round? @relation(fields: [roundId], references: [id])
|
|
replacedBy ProjectFile? @relation("FileVersions", fields: [replacedById], references: [id], onDelete: SetNull)
|
|
replacements ProjectFile[] @relation("FileVersions")
|
|
|
|
@@unique([bucket, objectKey])
|
|
@@index([projectId])
|
|
@@index([roundId])
|
|
@@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?
|
|
conflictOfInterest ConflictOfInterest?
|
|
|
|
@@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 (currently unused - evaluations are updated in-place.
|
|
// TODO: Implement proper versioning by creating new rows on re-submission
|
|
// if version history is needed for audit purposes)
|
|
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])
|
|
@@index([status, 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])
|
|
@@index([roundId, userId, extendedUntil])
|
|
}
|
|
|
|
// =============================================================================
|
|
// 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?
|
|
sessionId String?
|
|
|
|
timestamp DateTime @default(now())
|
|
|
|
// Relations
|
|
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
|
|
|
|
@@index([userId])
|
|
@@index([action])
|
|
@@index([entityType, entityId])
|
|
@@index([timestamp])
|
|
@@index([entityType, entityId, timestamp])
|
|
@@index([sessionId])
|
|
}
|
|
|
|
// =============================================================================
|
|
// 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])
|
|
}
|
|
|
|
// =============================================================================
|
|
// 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
|
|
|
|
// Relations
|
|
projectTags ProjectTag[]
|
|
|
|
@@index([category])
|
|
@@index([isActive])
|
|
@@index([sortOrder])
|
|
}
|
|
|
|
// Project-Tag relationship for AI tagging
|
|
model ProjectTag {
|
|
id String @id @default(cuid())
|
|
projectId String
|
|
tagId String
|
|
confidence Float @default(1.0) // AI confidence score 0-1
|
|
source String @default("AI") // "AI" or "MANUAL"
|
|
createdAt DateTime @default(now())
|
|
|
|
// Relations
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
tag ExpertiseTag @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([projectId, tagId])
|
|
@@index([projectId])
|
|
@@index([tagId])
|
|
}
|
|
|
|
// =============================================================================
|
|
// 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
|
|
|
|
// Audience & presentation settings
|
|
allowAudienceVotes Boolean @default(false)
|
|
audienceVoteWeight Float? // 0.0 to 1.0
|
|
tieBreakerMethod String? // 'admin_decides' | 'highest_individual' | 'revote'
|
|
presentationSettingsJson Json? @db.JsonB
|
|
|
|
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
|
|
isAudienceVote Boolean @default(false)
|
|
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
|
|
|
|
// Tracking
|
|
completionStatus String @default("in_progress") // 'in_progress' | 'completed' | 'paused'
|
|
lastViewedAt DateTime?
|
|
|
|
// Relations
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
mentor User @relation("MentorAssignments", fields: [mentorId], references: [id])
|
|
notes MentorNote[]
|
|
milestoneCompletions MentorMilestoneCompletion[]
|
|
|
|
@@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
|
|
}
|
|
|
|
// Tracks progress of long-running AI assignment jobs
|
|
model AssignmentJob {
|
|
id String @id @default(cuid())
|
|
roundId String
|
|
status AssignmentJobStatus @default(PENDING)
|
|
totalProjects Int @default(0)
|
|
totalBatches Int @default(0)
|
|
currentBatch Int @default(0)
|
|
processedCount Int @default(0)
|
|
suggestionsCount Int @default(0)
|
|
suggestionsJson Json? @db.JsonB // Stores the AI-generated suggestions
|
|
errorMessage String? @db.Text
|
|
startedAt DateTime?
|
|
completedAt DateTime?
|
|
fallbackUsed Boolean @default(false)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([roundId])
|
|
@@index([status])
|
|
}
|
|
|
|
enum AssignmentJobStatus {
|
|
PENDING
|
|
RUNNING
|
|
COMPLETED
|
|
FAILED
|
|
}
|
|
|
|
// Tracks progress of long-running AI tagging jobs
|
|
model TaggingJob {
|
|
id String @id @default(cuid())
|
|
programId String? // If tagging entire program
|
|
roundId String? // If tagging single round
|
|
status TaggingJobStatus @default(PENDING)
|
|
totalProjects Int @default(0)
|
|
processedCount Int @default(0)
|
|
taggedCount Int @default(0)
|
|
skippedCount Int @default(0)
|
|
failedCount Int @default(0)
|
|
errorMessage String? @db.Text
|
|
errorsJson Json? @db.JsonB // Array of error messages
|
|
startedAt DateTime?
|
|
completedAt DateTime?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations (optional - can tag by program or round)
|
|
program Program? @relation(fields: [programId], references: [id], onDelete: Cascade)
|
|
round Round? @relation(fields: [roundId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([programId])
|
|
@@index([roundId])
|
|
@@index([status])
|
|
}
|
|
|
|
enum TaggingJobStatus {
|
|
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? // FK to User who overrode the winner
|
|
|
|
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)
|
|
overriddenByUser User? @relation("AwardOverriddenBy", fields: [winnerOverriddenBy], 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])
|
|
}
|
|
|
|
// =============================================================================
|
|
// REMINDER LOG (Evaluation Deadline Reminders)
|
|
// =============================================================================
|
|
|
|
model ReminderLog {
|
|
id String @id @default(cuid())
|
|
roundId String
|
|
userId String
|
|
type String // "3_DAYS", "24H", "1H"
|
|
sentAt DateTime @default(now())
|
|
|
|
// Relations
|
|
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([roundId, userId, type])
|
|
@@index([roundId])
|
|
}
|
|
|
|
// =============================================================================
|
|
// CONFLICT OF INTEREST
|
|
// =============================================================================
|
|
|
|
model ConflictOfInterest {
|
|
id String @id @default(cuid())
|
|
assignmentId String @unique
|
|
userId String
|
|
projectId String
|
|
roundId String
|
|
hasConflict Boolean @default(false)
|
|
conflictType String? // "financial", "personal", "organizational", "other"
|
|
description String? @db.Text
|
|
declaredAt DateTime @default(now())
|
|
|
|
// Admin review
|
|
reviewedById String?
|
|
reviewedAt DateTime?
|
|
reviewAction String? // "cleared", "reassigned", "noted"
|
|
|
|
// Relations
|
|
assignment Assignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade)
|
|
user User @relation(fields: [userId], references: [id])
|
|
reviewedBy User? @relation("COIReviewedBy", fields: [reviewedById], references: [id])
|
|
|
|
@@index([userId])
|
|
@@index([roundId, hasConflict])
|
|
}
|
|
|
|
// =============================================================================
|
|
// AI EVALUATION SUMMARY
|
|
// =============================================================================
|
|
|
|
model EvaluationSummary {
|
|
id String @id @default(cuid())
|
|
projectId String
|
|
roundId String
|
|
summaryJson Json @db.JsonB
|
|
generatedAt DateTime @default(now())
|
|
generatedById String
|
|
model String
|
|
tokensUsed Int
|
|
|
|
// Relations
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
generatedBy User @relation("EvaluationSummaryGeneratedBy", fields: [generatedById], references: [id])
|
|
|
|
@@unique([projectId, roundId])
|
|
@@index([roundId])
|
|
}
|
|
|
|
// =============================================================================
|
|
// PROJECT STATUS HISTORY
|
|
// =============================================================================
|
|
|
|
model ProjectStatusHistory {
|
|
id String @id @default(cuid())
|
|
projectId String
|
|
status ProjectStatus
|
|
changedAt DateTime @default(now())
|
|
changedBy String?
|
|
|
|
// Relations
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([projectId, changedAt])
|
|
}
|
|
|
|
// =============================================================================
|
|
// MENTOR MESSAGES
|
|
// =============================================================================
|
|
|
|
model MentorMessage {
|
|
id String @id @default(cuid())
|
|
projectId String
|
|
senderId String
|
|
message String @db.Text
|
|
isRead Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
|
|
// Relations
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
sender User @relation("MentorMessageSender", fields: [senderId], references: [id])
|
|
|
|
@@index([projectId, createdAt])
|
|
}
|
|
|
|
// =============================================================================
|
|
// ROUND TEMPLATES
|
|
// =============================================================================
|
|
|
|
model RoundTemplate {
|
|
id String @id @default(cuid())
|
|
name String
|
|
description String?
|
|
programId String?
|
|
roundType RoundType @default(EVALUATION)
|
|
criteriaJson Json @db.JsonB
|
|
settingsJson Json? @db.JsonB
|
|
assignmentConfig Json? @db.JsonB
|
|
createdBy String
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
program Program? @relation(fields: [programId], references: [id], onDelete: Cascade)
|
|
creator User @relation("RoundTemplateCreatedBy", fields: [createdBy], references: [id])
|
|
|
|
@@index([programId])
|
|
@@index([roundType])
|
|
}
|
|
|
|
// =============================================================================
|
|
// MENTOR NOTES & MILESTONES
|
|
// =============================================================================
|
|
|
|
model MentorNote {
|
|
id String @id @default(cuid())
|
|
mentorAssignmentId String
|
|
authorId String
|
|
content String @db.Text
|
|
isVisibleToAdmin Boolean @default(true)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
mentorAssignment MentorAssignment @relation(fields: [mentorAssignmentId], references: [id], onDelete: Cascade)
|
|
author User @relation("MentorNoteAuthor", fields: [authorId], references: [id])
|
|
|
|
@@index([mentorAssignmentId])
|
|
@@index([authorId])
|
|
}
|
|
|
|
model MentorMilestone {
|
|
id String @id @default(cuid())
|
|
programId String
|
|
name String
|
|
description String? @db.Text
|
|
isRequired Boolean @default(false)
|
|
deadlineOffsetDays Int?
|
|
sortOrder Int @default(0)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
program Program @relation(fields: [programId], references: [id], onDelete: Cascade)
|
|
completions MentorMilestoneCompletion[]
|
|
|
|
@@index([programId])
|
|
@@index([sortOrder])
|
|
}
|
|
|
|
model MentorMilestoneCompletion {
|
|
milestoneId String
|
|
mentorAssignmentId String
|
|
completedById String
|
|
completedAt DateTime @default(now())
|
|
|
|
// Relations
|
|
milestone MentorMilestone @relation(fields: [milestoneId], references: [id], onDelete: Cascade)
|
|
mentorAssignment MentorAssignment @relation(fields: [mentorAssignmentId], references: [id], onDelete: Cascade)
|
|
completedBy User @relation("MilestoneCompletedBy", fields: [completedById], references: [id])
|
|
|
|
@@id([milestoneId, mentorAssignmentId])
|
|
@@index([mentorAssignmentId])
|
|
@@index([completedById])
|
|
}
|
|
|
|
// =============================================================================
|
|
// EVALUATION DISCUSSIONS
|
|
// =============================================================================
|
|
|
|
model EvaluationDiscussion {
|
|
id String @id @default(cuid())
|
|
projectId String
|
|
roundId String
|
|
status String @default("open") // 'open' | 'closed'
|
|
closedAt DateTime?
|
|
closedById String?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
|
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
|
|
closedBy User? @relation("DiscussionClosedBy", fields: [closedById], references: [id], onDelete: SetNull)
|
|
comments DiscussionComment[]
|
|
|
|
@@unique([projectId, roundId])
|
|
@@index([roundId])
|
|
@@index([closedById])
|
|
}
|
|
|
|
model DiscussionComment {
|
|
id String @id @default(cuid())
|
|
discussionId String
|
|
userId String
|
|
content String @db.Text
|
|
|
|
createdAt DateTime @default(now())
|
|
|
|
// Relations
|
|
discussion EvaluationDiscussion @relation(fields: [discussionId], references: [id], onDelete: Cascade)
|
|
user User @relation("DiscussionCommentAuthor", fields: [userId], references: [id])
|
|
|
|
@@index([discussionId])
|
|
@@index([userId])
|
|
}
|
|
|
|
// =============================================================================
|
|
// MESSAGING SYSTEM
|
|
// =============================================================================
|
|
|
|
model Message {
|
|
id String @id @default(cuid())
|
|
senderId String
|
|
recipientType String // 'USER', 'ROLE', 'ROUND_JURY', 'PROGRAM_TEAM', 'ALL'
|
|
recipientFilter Json? @db.JsonB
|
|
roundId String?
|
|
templateId String?
|
|
subject String
|
|
body String @db.Text
|
|
deliveryChannels String[]
|
|
|
|
scheduledAt DateTime?
|
|
sentAt DateTime?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
sender User @relation("MessageSender", fields: [senderId], references: [id], onDelete: Cascade)
|
|
round Round? @relation(fields: [roundId], references: [id], onDelete: SetNull)
|
|
template MessageTemplate? @relation(fields: [templateId], references: [id], onDelete: SetNull)
|
|
recipients MessageRecipient[]
|
|
|
|
@@index([senderId])
|
|
@@index([roundId])
|
|
@@index([sentAt])
|
|
}
|
|
|
|
model MessageRecipient {
|
|
id String @id @default(cuid())
|
|
messageId String
|
|
userId String
|
|
channel String // 'EMAIL', 'IN_APP', etc.
|
|
isRead Boolean @default(false)
|
|
readAt DateTime?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
message Message @relation(fields: [messageId], references: [id], onDelete: Cascade)
|
|
user User @relation("MessageRecipient", fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([messageId, userId, channel])
|
|
@@index([userId])
|
|
}
|
|
|
|
model MessageTemplate {
|
|
id String @id @default(cuid())
|
|
name String
|
|
category String // 'SYSTEM', 'EVALUATION', 'ASSIGNMENT'
|
|
subject String
|
|
body String @db.Text
|
|
variables Json? @db.JsonB
|
|
createdById String
|
|
isActive Boolean @default(true)
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
createdBy User @relation("MessageTemplateCreator", fields: [createdById], references: [id], onDelete: Cascade)
|
|
messages Message[]
|
|
|
|
@@index([category])
|
|
@@index([isActive])
|
|
}
|
|
|
|
// =============================================================================
|
|
// WEBHOOKS
|
|
// =============================================================================
|
|
|
|
model Webhook {
|
|
id String @id @default(cuid())
|
|
name String
|
|
url String
|
|
secret String
|
|
events String[]
|
|
headers Json? @db.JsonB
|
|
maxRetries Int @default(3)
|
|
isActive Boolean @default(true)
|
|
createdById String
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
createdBy User @relation("WebhookCreator", fields: [createdById], references: [id], onDelete: Cascade)
|
|
deliveries WebhookDelivery[]
|
|
|
|
@@index([isActive])
|
|
@@index([createdById])
|
|
}
|
|
|
|
model WebhookDelivery {
|
|
id String @id @default(cuid())
|
|
webhookId String
|
|
event String
|
|
payload Json @db.JsonB
|
|
status String @default("PENDING") // 'PENDING', 'DELIVERED', 'FAILED'
|
|
responseStatus Int?
|
|
responseBody String? @db.Text
|
|
attempts Int @default(0)
|
|
lastAttemptAt DateTime?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
webhook Webhook @relation(fields: [webhookId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([webhookId])
|
|
@@index([status])
|
|
@@index([event])
|
|
}
|
|
|
|
// =============================================================================
|
|
// DIGEST LOGS
|
|
// =============================================================================
|
|
|
|
model DigestLog {
|
|
id String @id @default(cuid())
|
|
userId String
|
|
digestType String // 'daily' | 'weekly'
|
|
contentJson Json @db.JsonB
|
|
sentAt DateTime @default(now())
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Relations
|
|
user User @relation("DigestLog", fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@index([userId, sentAt])
|
|
}
|