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:
@@ -228,6 +228,103 @@ export const exportRouter = router({
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Export filtering results as CSV data
|
||||
*/
|
||||
filteringResults: adminProcedure
|
||||
.input(z.object({ roundId: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const results = await ctx.prisma.filteringResult.findMany({
|
||||
where: { roundId: input.roundId },
|
||||
include: {
|
||||
project: {
|
||||
select: {
|
||||
title: true,
|
||||
teamName: true,
|
||||
competitionCategory: true,
|
||||
country: true,
|
||||
oceanIssue: true,
|
||||
tags: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { project: { title: 'asc' } },
|
||||
})
|
||||
|
||||
// Collect all unique AI screening keys across all results
|
||||
const aiKeys = new Set<string>()
|
||||
results.forEach((r) => {
|
||||
if (r.aiScreeningJson && typeof r.aiScreeningJson === 'object') {
|
||||
const screening = r.aiScreeningJson as Record<string, Record<string, unknown>>
|
||||
for (const ruleResult of Object.values(screening)) {
|
||||
if (ruleResult && typeof ruleResult === 'object') {
|
||||
Object.keys(ruleResult).forEach((k) => aiKeys.add(k))
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const sortedAiKeys = Array.from(aiKeys).sort()
|
||||
|
||||
const data = results.map((r) => {
|
||||
// Flatten AI screening - take first rule result's values
|
||||
const aiFlat: Record<string, unknown> = {}
|
||||
if (r.aiScreeningJson && typeof r.aiScreeningJson === 'object') {
|
||||
const screening = r.aiScreeningJson as Record<string, Record<string, unknown>>
|
||||
const firstEntry = Object.values(screening)[0]
|
||||
if (firstEntry && typeof firstEntry === 'object') {
|
||||
for (const key of sortedAiKeys) {
|
||||
const val = firstEntry[key]
|
||||
aiFlat[`ai_${key}`] = val !== undefined ? String(val) : ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
projectTitle: r.project.title,
|
||||
teamName: r.project.teamName ?? '',
|
||||
category: r.project.competitionCategory ?? '',
|
||||
country: r.project.country ?? '',
|
||||
oceanIssue: r.project.oceanIssue ?? '',
|
||||
tags: r.project.tags.join(', '),
|
||||
outcome: r.outcome,
|
||||
finalOutcome: r.finalOutcome ?? '',
|
||||
overrideReason: r.overrideReason ?? '',
|
||||
...aiFlat,
|
||||
}
|
||||
})
|
||||
|
||||
// Build columns list
|
||||
const baseColumns = [
|
||||
'projectTitle',
|
||||
'teamName',
|
||||
'category',
|
||||
'country',
|
||||
'oceanIssue',
|
||||
'tags',
|
||||
'outcome',
|
||||
'finalOutcome',
|
||||
'overrideReason',
|
||||
]
|
||||
const aiColumns = sortedAiKeys.map((k) => `ai_${k}`)
|
||||
|
||||
// Audit log
|
||||
await logAudit({
|
||||
prisma: ctx.prisma,
|
||||
userId: ctx.user.id,
|
||||
action: 'EXPORT',
|
||||
entityType: 'FilteringResult',
|
||||
detailsJson: { roundId: input.roundId, count: data.length },
|
||||
ipAddress: ctx.ip,
|
||||
userAgent: ctx.userAgent,
|
||||
})
|
||||
|
||||
return {
|
||||
data,
|
||||
columns: [...baseColumns, ...aiColumns],
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Export audit logs as CSV data
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user