Implement 10 platform features: evaluation UX, admin tools, AI summaries, applicant portal

Batch 1 - Quick Wins:
- F1: Evaluation progress indicator with touch tracking in sticky status bar
- F2: Export filtering results as CSV with dynamic AI column flattening
- F3: Observer access to analytics dashboards (8 procedures changed to observerProcedure)

Batch 2 - Jury Experience:
- F4: Countdown timer component with urgency colors + email reminder service with cron endpoint
- F5: Conflict of interest declaration system (dialog, admin management, review workflow)

Batch 3 - Admin & AI Enhancements:
- F6: Bulk status update UI with selection checkboxes, floating toolbar, status history recording
- F7: AI-powered evaluation summary with anonymized data, OpenAI integration, scoring patterns
- F8: Smart assignment improvements (geo diversity penalty, round familiarity bonus, COI blocking)

Batch 4 - Form Flexibility & Applicant Portal:
- F9: Evaluation form flexibility (text, boolean, section_header types, conditional visibility)
- F10: Applicant portal (status timeline, per-round documents, mentor messaging)

Schema: 5 new models (ReminderLog, ConflictOfInterest, EvaluationSummary, ProjectStatusHistory, MentorMessage), ProjectFile extended with roundId + isLate.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-05 21:58:27 +01:00
parent 002a9dbfc3
commit 699248e40b
38 changed files with 5437 additions and 533 deletions

View File

@@ -256,6 +256,19 @@ model User {
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")
// NextAuth relations
accounts Account[]
sessions Session[]
@@ -377,6 +390,8 @@ model Round {
filteringJobs FilteringJob[]
assignmentJobs AssignmentJob[]
taggingJobs TaggingJob[]
reminderLogs ReminderLog[]
projectFiles ProjectFile[]
@@index([programId])
@@index([status])
@@ -480,6 +495,9 @@ model Project {
awardVotes AwardVote[]
wonAwards SpecialAward[] @relation("AwardWinner")
projectTags ProjectTag[]
statusHistory ProjectStatusHistory[]
mentorMessages MentorMessage[]
evaluationSummaries EvaluationSummary[]
@@index([roundId])
@@index([status])
@@ -494,6 +512,7 @@ model Project {
model ProjectFile {
id String @id @default(cuid())
projectId String
roundId String? // Which round this file was submitted for
// File info
fileType FileType
@@ -505,13 +524,17 @@ model ProjectFile {
bucket String
objectKey String
isLate Boolean @default(false) // Uploaded after round deadline
createdAt DateTime @default(now())
// Relations
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
round Round? @relation(fields: [roundId], references: [id])
@@unique([bucket, objectKey])
@@index([projectId])
@@index([roundId])
@@index([fileType])
}
@@ -539,10 +562,11 @@ model Assignment {
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?
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])
@@ -1297,3 +1321,109 @@ model AwardVote {
@@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])
}