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:
@@ -65,16 +65,25 @@ The MOPC Platform is a secure jury voting system built as a modern full-stack Ty
|
||||
│ PRESENTATION LAYER │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
|
||||
│ │ Admin Views │ │ Jury Views │ │ Auth Views │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ - Dashboard │ │ - Project List │ │ - Login │ │
|
||||
│ │ - Rounds │ │ - Project View │ │ - Magic Link │ │
|
||||
│ │ - Projects │ │ - Evaluation │ │ - Verify │ │
|
||||
│ │ - Jury Mgmt │ │ - My Progress │ │ │ │
|
||||
│ │ - Assignments │ │ │ │ │ │
|
||||
│ │ - Reports │ │ │ │ │ │
|
||||
│ └──────────────────┘ └──────────────────┘ └──────────────────┘ │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Admin Views │ │ Jury Views │ │Applicant View│ │ Mentor Views │ │
|
||||
│ │ │ │ │ │ │ │ │ │
|
||||
│ │ - Dashboard │ │ - Project Ls │ │ - Status │ │ - Assigned │ │
|
||||
│ │ - Rounds │ │ - Project Vw │ │ Tracker │ │ Projects │ │
|
||||
│ │ - Projects │ │ - Evaluation │ │ - Document │ │ - Messaging │ │
|
||||
│ │ - Jury Mgmt │ │ - My Progress│ │ Uploads │ │ │ │
|
||||
│ │ - Assignments│ │ - COI Decl. │ │ - Mentor │ │ │ │
|
||||
│ │ - Reports │ │ - Countdown │ │ Chat │ │ │ │
|
||||
│ │ - COI Review │ │ │ │ │ │ │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
│ │
|
||||
│ ┌──────────────────┐ ┌──────────────────┐ │
|
||||
│ │ Auth Views │ │ Observer Views │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ - Login │ │ - Reports/ │ │
|
||||
│ │ - Magic Link │ │ Analytics │ │
|
||||
│ │ - Verify │ │ - Round Stats │ │
|
||||
│ └──────────────────┘ └──────────────────┘ │
|
||||
│ │
|
||||
│ Built with: Next.js App Router + React Server Components + shadcn/ui │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -116,6 +125,11 @@ The MOPC Platform is a secure jury voting system built as a modern full-stack Ty
|
||||
│ │ Service │ │ Service │ │ Service │ │ Service │ │
|
||||
│ └────────────┘ └────────────┘ └────────────┘ └────────────┘ │
|
||||
│ │
|
||||
│ ┌────────────┐ ┌────────────┐ │
|
||||
│ │ Evaluation │ │ AI Eval │ │
|
||||
│ │ Reminders │ │ Summary │ │
|
||||
│ └────────────┘ └────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
@@ -151,8 +165,11 @@ The MOPC Platform is a secure jury voting system built as a modern full-stack Ty
|
||||
|
||||
| Component | Responsibility |
|
||||
|-----------|----------------|
|
||||
| **Admin Views** | Program/round management, project import, jury management, assignments, dashboards |
|
||||
| **Jury Views** | View assigned projects, evaluate projects, track progress |
|
||||
| **Admin Views** | Program/round management, project import, jury management, assignments, dashboards, COI review, bulk status updates, AI evaluation summaries |
|
||||
| **Jury Views** | View assigned projects, evaluate projects, track progress, COI declarations, countdown timer |
|
||||
| **Applicant Views** | Project status tracker, document uploads (per-round with deadline policy), mentor chat |
|
||||
| **Mentor Views** | View assigned projects, messaging with applicants |
|
||||
| **Observer Views** | Read-only access to all reports/analytics (round selector, tabs, all chart components) |
|
||||
| **Auth Views** | Login, magic link verification, session management |
|
||||
| **Layouts** | Responsive navigation, sidebar, mobile adaptations |
|
||||
| **UI Components** | shadcn/ui based, reusable, accessible |
|
||||
@@ -177,8 +194,10 @@ The MOPC Platform is a secure jury voting system built as a modern full-stack Ty
|
||||
| **EvaluationService** | Form submission, autosave, scoring |
|
||||
| **FileService** | MinIO uploads, pre-signed URLs |
|
||||
| **EmailService** | Magic links, notifications via Nodemailer |
|
||||
| **ExportService** | CSV/Excel generation |
|
||||
| **ExportService** | CSV/Excel generation, filtering results export with AI column flattening |
|
||||
| **AuditService** | Immutable event logging |
|
||||
| **EvaluationRemindersService** | Finds incomplete assignments, sends email reminders with countdown urgency |
|
||||
| **AIEvaluationSummaryService** | Anonymizes evaluation data, generates GPT-powered summaries with scoring patterns, strengths/weaknesses |
|
||||
|
||||
### Data Layer
|
||||
|
||||
@@ -345,8 +364,14 @@ The platform includes two assignment modes:
|
||||
```
|
||||
Score = (expertise_match × 40) + (load_balance × 25) +
|
||||
(specialty_match × 20) + (diversity × 10) - (conflict × 100)
|
||||
- (geo_diversity_penalty) + (previous_round_familiarity) - (coi_penalty)
|
||||
```
|
||||
|
||||
**Additional Scoring Factors** (added in Phase 1 enhancements):
|
||||
- **geoDiversityPenalty**: -15 per excess same-country assignment beyond threshold of 2
|
||||
- **previousRoundFamiliarity**: +10 bonus for jurors who reviewed the same project in a prior round
|
||||
- **coiPenalty**: Jurors with a declared Conflict of Interest are skipped entirely
|
||||
|
||||
## Admin Settings Panel
|
||||
|
||||
Centralized configuration for:
|
||||
@@ -363,8 +388,10 @@ Centralized configuration for:
|
||||
|------|------------|
|
||||
| **SUPER_ADMIN** | Full system access, all programs, user management |
|
||||
| **PROGRAM_ADMIN** | Manage specific programs, rounds, projects, jury |
|
||||
| **JURY_MEMBER** | View assigned projects only, submit evaluations |
|
||||
| **OBSERVER** | Read-only access to dashboards |
|
||||
| **JURY_MEMBER** | View assigned projects only, submit evaluations, declare COI |
|
||||
| **OBSERVER** | Read-only access to dashboards and all analytics/reports |
|
||||
| **MENTOR** | View assigned projects, message applicants via `mentorProcedure` |
|
||||
| **APPLICANT** | View own project status, upload documents per round, message mentor |
|
||||
|
||||
## Related Documentation
|
||||
|
||||
|
||||
@@ -119,6 +119,9 @@ export const hasRole = (...roles: UserRole[]) =>
|
||||
export const protectedProcedure = t.procedure.use(isAuthenticated)
|
||||
export const adminProcedure = t.procedure.use(hasRole('SUPER_ADMIN', 'PROGRAM_ADMIN'))
|
||||
export const juryProcedure = t.procedure.use(hasRole('JURY_MEMBER'))
|
||||
export const observerProcedure = t.procedure.use(hasRole('SUPER_ADMIN', 'PROGRAM_ADMIN', 'OBSERVER'))
|
||||
export const mentorProcedure = t.procedure.use(hasRole('MENTOR'))
|
||||
export const applicantProcedure = t.procedure.use(hasRole('APPLICANT'))
|
||||
```
|
||||
|
||||
## Router Structure
|
||||
@@ -136,7 +139,9 @@ src/server/routers/
|
||||
├── export.ts # Export operations
|
||||
├── audit.ts # Audit log access
|
||||
├── settings.ts # Platform settings (admin)
|
||||
└── gracePeriod.ts # Grace period management
|
||||
├── gracePeriod.ts # Grace period management
|
||||
├── analytics.ts # Reports & analytics (admin + observer)
|
||||
└── mentor.ts # Mentor messaging endpoints
|
||||
```
|
||||
|
||||
### Root Router
|
||||
@@ -1024,6 +1029,164 @@ export const gracePeriodRouter = router({
|
||||
})
|
||||
```
|
||||
|
||||
### Conflict of Interest Endpoints (Evaluation Router)
|
||||
|
||||
```typescript
|
||||
// Added to src/server/routers/evaluation.ts
|
||||
|
||||
// Declare COI for an assignment (jury member)
|
||||
declareCOI: protectedProcedure
|
||||
.input(z.object({
|
||||
assignmentId: z.string(),
|
||||
hasConflict: z.boolean(),
|
||||
conflictType: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
}))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
// Creates/updates ConflictOfInterest record
|
||||
// Blocks evaluation access if hasConflict = true until reviewed
|
||||
}),
|
||||
|
||||
// Get COI status for an assignment
|
||||
getCOIStatus: protectedProcedure
|
||||
.input(z.object({ assignmentId: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
// Returns COI declaration status for the assignment
|
||||
}),
|
||||
|
||||
// List all COI declarations for a round (admin only)
|
||||
listCOIByRound: adminProcedure
|
||||
.input(z.object({ roundId: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
// Returns all COI declarations with user and project info
|
||||
}),
|
||||
|
||||
// Review a COI declaration (admin only)
|
||||
reviewCOI: adminProcedure
|
||||
.input(z.object({
|
||||
id: z.string(),
|
||||
reviewNotes: z.string().optional(),
|
||||
}))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
// Marks COI as reviewed, logs audit event
|
||||
}),
|
||||
```
|
||||
|
||||
### AI Evaluation Summary Endpoints (Evaluation Router)
|
||||
|
||||
```typescript
|
||||
// Added to src/server/routers/evaluation.ts
|
||||
|
||||
// Generate AI summary for a project's evaluations
|
||||
generateSummary: adminProcedure
|
||||
.input(z.object({
|
||||
projectId: z.string(),
|
||||
roundId: z.string(),
|
||||
}))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
// Anonymizes evaluation data, sends to GPT
|
||||
// Generates strengths/weaknesses, themes, scoring patterns
|
||||
// Stores EvaluationSummary record
|
||||
}),
|
||||
|
||||
// Get existing summary for a project
|
||||
getSummary: adminProcedure
|
||||
.input(z.object({
|
||||
projectId: z.string(),
|
||||
roundId: z.string(),
|
||||
}))
|
||||
.query(async ({ ctx, input }) => {
|
||||
// Returns EvaluationSummary with parsed summaryJson
|
||||
}),
|
||||
|
||||
// Generate summaries for all projects in a round
|
||||
generateBulkSummaries: adminProcedure
|
||||
.input(z.object({
|
||||
roundId: z.string(),
|
||||
}))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
// Iterates through projects, generates summaries in batch
|
||||
// Returns count of generated summaries
|
||||
}),
|
||||
```
|
||||
|
||||
### Evaluation Reminders Endpoint
|
||||
|
||||
```typescript
|
||||
// Added to src/server/routers/evaluation.ts
|
||||
|
||||
// Trigger reminder emails for incomplete assignments (admin)
|
||||
triggerReminders: adminProcedure
|
||||
.input(z.object({
|
||||
roundId: z.string(),
|
||||
}))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
// Calls EvaluationRemindersService
|
||||
// Finds incomplete assignments, sends email reminders
|
||||
// Logs to ReminderLog table
|
||||
}),
|
||||
```
|
||||
|
||||
### Cron API Route
|
||||
|
||||
```typescript
|
||||
// src/app/api/cron/reminders/route.ts
|
||||
|
||||
// POST /api/cron/reminders
|
||||
// Protected by CRON_SECRET header validation
|
||||
// Automatically finds rounds with approaching deadlines
|
||||
// Sends reminder emails to jurors with incomplete evaluations
|
||||
// Designed to be called by external cron scheduler
|
||||
```
|
||||
|
||||
### Export Router - Filtering Results
|
||||
|
||||
```typescript
|
||||
// Added to src/server/routers/export.ts
|
||||
|
||||
// Export filtering results as CSV
|
||||
filteringResults: adminProcedure
|
||||
.input(z.object({
|
||||
roundId: z.string(),
|
||||
}))
|
||||
.query(async ({ ctx, input }) => {
|
||||
// Queries projects with evaluations and AI screening data
|
||||
// Dynamically flattens aiScreeningJson columns
|
||||
// Returns CSV-ready data
|
||||
}),
|
||||
```
|
||||
|
||||
### Evaluation Form Schema - Extended Criterion Types
|
||||
|
||||
The evaluation form `criteriaJson` now supports extended criterion types:
|
||||
|
||||
```typescript
|
||||
type CriterionType = 'numeric' | 'text' | 'boolean' | 'section_header'
|
||||
|
||||
type Criterion = {
|
||||
id: string
|
||||
label: string
|
||||
type: CriterionType // Defaults to 'numeric' for backward compatibility
|
||||
scale?: string // Only for 'numeric' type
|
||||
weight?: number // Only for 'numeric' type
|
||||
required?: boolean
|
||||
description?: string
|
||||
// Conditional visibility
|
||||
visibleWhen?: {
|
||||
criterionId: string
|
||||
value: unknown
|
||||
}
|
||||
// Section grouping
|
||||
section?: string
|
||||
}
|
||||
```
|
||||
|
||||
Field components per type:
|
||||
- **numeric**: Standard slider/number input with scale
|
||||
- **text**: Free-text textarea field (`TextCriterionField`)
|
||||
- **boolean**: Yes/No toggle (`BooleanCriterionField`)
|
||||
- **section_header**: Non-input visual divider for form organization (`SectionHeader`)
|
||||
|
||||
## Authentication Flow
|
||||
|
||||
### Magic Link Implementation
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user