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

@@ -112,6 +112,72 @@ The MOPC platform uses PostgreSQL as its primary database, accessed via Prisma O
│ grantedBy │
│ createdAt │
└─────────────────────┘
┌─────────────────────┐
│ ReminderLog │
│─────────────────────│
│ id │
│ roundId │
│ userId │
│ assignmentId │
│ type │
│ sentAt │
│ emailTo │
│ status │
└─────────────────────┘
┌───────────────────────┐
│ ConflictOfInterest │
│───────────────────────│
│ id │
│ assignmentId │
│ userId │
│ projectId │
│ roundId │
│ hasConflict │
│ conflictType │
│ description │
│ reviewedBy │
│ reviewedAt │
│ reviewNotes │
│ createdAt │
└───────────────────────┘
┌───────────────────────┐
│ EvaluationSummary │
│───────────────────────│
│ id │
│ projectId │
│ roundId │
│ summaryJson │
│ model │
│ tokensUsed │
│ createdAt │
└───────────────────────┘
┌───────────────────────┐
│ ProjectStatusHistory │
│───────────────────────│
│ id │
│ projectId │
│ oldStatus │
│ newStatus │
│ changedBy │
│ reason │
│ createdAt │
└───────────────────────┘
┌───────────────────────┐
│ MentorMessage │
│───────────────────────│
│ id │
│ projectId │
│ senderId │
│ recipientId │
│ content │
│ readAt │
│ createdAt │
└───────────────────────┘
```
## Prisma Schema
@@ -137,6 +203,8 @@ enum UserRole {
PROGRAM_ADMIN
JURY_MEMBER
OBSERVER
MENTOR
APPLICANT
}
enum UserStatus {
@@ -357,12 +425,14 @@ model Project {
model ProjectFile {
id String @id @default(cuid())
projectId String
roundId String? // Per-round document management (Phase B)
// File info
fileType FileType
fileName String
mimeType String
size Int // bytes
isLate Boolean @default(false) // Upload deadline policy tracking
// MinIO location
bucket String
@@ -376,6 +446,7 @@ model ProjectFile {
// Indexes
@@index([projectId])
@@index([fileType])
@@index([roundId])
@@unique([bucket, objectKey])
}
@@ -475,6 +546,119 @@ model AuditLog {
@@index([entityType, entityId])
@@index([timestamp])
}
// =============================================================================
// REMINDER LOGGING
// =============================================================================
model ReminderLog {
id String @id @default(cuid())
roundId String
userId String
assignmentId String
type String // e.g., "DEADLINE_APPROACHING", "OVERDUE"
sentAt DateTime @default(now())
emailTo String
status String // "SENT", "FAILED"
// Indexes
@@index([roundId])
@@index([userId])
@@index([assignmentId])
}
// =============================================================================
// CONFLICT OF INTEREST
// =============================================================================
model ConflictOfInterest {
id String @id @default(cuid())
assignmentId String
userId String
projectId String
roundId String
hasConflict Boolean
conflictType String? // e.g., "FINANCIAL", "PERSONAL", "ORGANIZATIONAL"
description String? @db.Text
// Review fields (admin)
reviewedBy String?
reviewedAt DateTime?
reviewNotes String? @db.Text
createdAt DateTime @default(now())
// Relations
assignment Assignment @relation(fields: [assignmentId], references: [id], onDelete: Cascade)
// Indexes
@@unique([assignmentId])
@@index([userId])
@@index([roundId])
@@index([hasConflict])
}
// =============================================================================
// AI EVALUATION SUMMARY
// =============================================================================
model EvaluationSummary {
id String @id @default(cuid())
projectId String
roundId String
summaryJson Json @db.JsonB // Strengths, weaknesses, themes, scoring stats
model String // e.g., "gpt-4o"
tokensUsed Int
createdAt DateTime @default(now())
// Indexes
@@unique([projectId, roundId])
@@index([roundId])
}
// =============================================================================
// PROJECT STATUS HISTORY
// =============================================================================
model ProjectStatusHistory {
id String @id @default(cuid())
projectId String
oldStatus String
newStatus String
changedBy String? // User who changed the status
reason String? @db.Text
createdAt DateTime @default(now())
// Relations
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
// Indexes
@@index([projectId])
@@index([createdAt])
}
// =============================================================================
// MENTOR MESSAGING
// =============================================================================
model MentorMessage {
id String @id @default(cuid())
projectId String
senderId String
recipientId String
content String @db.Text
readAt DateTime?
createdAt DateTime @default(now())
// Indexes
@@index([projectId])
@@index([senderId])
@@index([recipientId])
@@index([createdAt])
}
```
## Indexing Strategy
@@ -505,6 +689,22 @@ model AuditLog {
| Evaluation | `submittedAt` | Sort by submission time |
| AuditLog | `timestamp` | Time-based queries |
| AuditLog | `entityType, entityId` | Entity history |
| ProjectFile | `roundId` | Per-round document listing |
| ReminderLog | `roundId` | Reminders per round |
| ReminderLog | `userId` | User reminder history |
| ReminderLog | `assignmentId` | Assignment reminder tracking |
| ConflictOfInterest | `assignmentId` (unique) | COI per assignment |
| ConflictOfInterest | `userId` | User COI declarations |
| ConflictOfInterest | `roundId` | COI per round |
| ConflictOfInterest | `hasConflict` | Filter active conflicts |
| EvaluationSummary | `projectId, roundId` (unique) | One summary per project per round |
| EvaluationSummary | `roundId` | Summaries per round |
| ProjectStatusHistory | `projectId` | Status change timeline |
| ProjectStatusHistory | `createdAt` | Chronological ordering |
| MentorMessage | `projectId` | Messages per project |
| MentorMessage | `senderId` | Sent messages |
| MentorMessage | `recipientId` | Received messages |
| MentorMessage | `createdAt` | Chronological ordering |
### JSON Field Indexes