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:
@@ -25,6 +25,7 @@ import {
|
||||
ArrowRight,
|
||||
} from 'lucide-react'
|
||||
import { formatDateOnly } from '@/lib/utils'
|
||||
import { CountdownTimer } from '@/components/shared/countdown-timer'
|
||||
|
||||
async function JuryDashboardContent() {
|
||||
const session = await auth()
|
||||
@@ -105,6 +106,27 @@ async function JuryDashboardContent() {
|
||||
{} as Record<string, { round: (typeof assignments)[0]['round']; assignments: typeof assignments }>
|
||||
)
|
||||
|
||||
// Get grace periods for this user
|
||||
const gracePeriods = await prisma.gracePeriod.findMany({
|
||||
where: {
|
||||
userId,
|
||||
extendedUntil: { gte: new Date() },
|
||||
},
|
||||
select: {
|
||||
roundId: true,
|
||||
extendedUntil: true,
|
||||
},
|
||||
})
|
||||
|
||||
// Build a map of roundId -> latest extendedUntil
|
||||
const graceByRound = new Map<string, Date>()
|
||||
for (const gp of gracePeriods) {
|
||||
const existing = graceByRound.get(gp.roundId)
|
||||
if (!existing || gp.extendedUntil > existing) {
|
||||
graceByRound.set(gp.roundId, gp.extendedUntil)
|
||||
}
|
||||
}
|
||||
|
||||
// Get active rounds (voting window is open)
|
||||
const now = new Date()
|
||||
const activeRounds = Object.values(assignmentsByRound).filter(
|
||||
@@ -221,9 +243,15 @@ async function JuryDashboardContent() {
|
||||
</div>
|
||||
|
||||
{round.votingEndAt && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Deadline: {formatDateOnly(round.votingEndAt)}
|
||||
</p>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<CountdownTimer
|
||||
deadline={graceByRound.get(round.id) ?? new Date(round.votingEndAt)}
|
||||
label="Deadline:"
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
({formatDateOnly(round.votingEndAt)})
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button asChild size="sm" className="w-full sm:w-auto">
|
||||
|
||||
Reference in New Issue
Block a user