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

@@ -8,7 +8,7 @@ export const dynamic = 'force-dynamic'
import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
import { EvaluationForm } from '@/components/forms/evaluation-form'
import { EvaluationFormWithCOI } from '@/components/forms/evaluation-form-with-coi'
import { ArrowLeft, AlertCircle, Clock, FileText, Users } from 'lucide-react'
import { isFuture, isPast } from 'date-fns'
@@ -21,9 +21,20 @@ interface Criterion {
id: string
label: string
description?: string
scale: number
type?: 'numeric' | 'text' | 'boolean' | 'section_header'
scale?: number
weight?: number
required?: boolean
maxLength?: number
placeholder?: string
trueLabel?: string
falseLabel?: string
condition?: {
criterionId: string
operator: 'equals' | 'greaterThan' | 'lessThan'
value: number | string | boolean
}
sectionId?: string
}
async function EvaluateContent({ projectId }: { projectId: string }) {
@@ -133,6 +144,14 @@ async function EvaluateContent({ projectId }: { projectId: string }) {
redirect(`/jury/projects/${projectId}/evaluation`)
}
// Check COI status
const coiRecord = await prisma.conflictOfInterest.findUnique({
where: { assignmentId: assignment.id },
})
const coiStatus = coiRecord
? { hasConflict: coiRecord.hasConflict, declared: true }
: { hasConflict: false, declared: false }
// Get evaluation form criteria
const evaluationForm = round.evaluationForms[0]
if (!evaluationForm) {
@@ -247,8 +266,8 @@ async function EvaluateContent({ projectId }: { projectId: string }) {
</Card>
)}
{/* Evaluation Form */}
<EvaluationForm
{/* Evaluation Form with COI Gate */}
<EvaluationFormWithCOI
assignmentId={assignment.id}
evaluationId={evaluation?.id || null}
projectTitle={project.title}
@@ -258,7 +277,7 @@ async function EvaluateContent({ projectId }: { projectId: string }) {
? {
criterionScoresJson: evaluation.criterionScoresJson as Record<
string,
number
number | string | boolean
> | null,
globalScore: evaluation.globalScore,
binaryDecision: evaluation.binaryDecision,
@@ -269,6 +288,7 @@ async function EvaluateContent({ projectId }: { projectId: string }) {
}
isVotingOpen={effectiveVotingOpen}
deadline={round.votingEndAt}
coiStatus={coiStatus}
/>
</div>
)