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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user