2026-01-30 13:41:32 +01:00
|
|
|
'use client'
|
|
|
|
|
|
|
|
|
|
import { Suspense, useState } from 'react'
|
|
|
|
|
import Link from 'next/link'
|
|
|
|
|
import { useRouter, useSearchParams } from 'next/navigation'
|
|
|
|
|
import { useForm } from 'react-hook-form'
|
|
|
|
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
|
|
|
import { z } from 'zod'
|
|
|
|
|
import { trpc } from '@/lib/trpc/client'
|
|
|
|
|
import {
|
|
|
|
|
Card,
|
|
|
|
|
CardContent,
|
|
|
|
|
CardDescription,
|
|
|
|
|
CardHeader,
|
|
|
|
|
CardTitle,
|
|
|
|
|
} from '@/components/ui/card'
|
|
|
|
|
import { Button } from '@/components/ui/button'
|
|
|
|
|
import { Input } from '@/components/ui/input'
|
|
|
|
|
import {
|
|
|
|
|
Select,
|
|
|
|
|
SelectContent,
|
|
|
|
|
SelectItem,
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
SelectValue,
|
|
|
|
|
} from '@/components/ui/select'
|
|
|
|
|
import { Skeleton } from '@/components/ui/skeleton'
|
|
|
|
|
import {
|
|
|
|
|
Form,
|
|
|
|
|
FormControl,
|
|
|
|
|
FormDescription,
|
|
|
|
|
FormField,
|
|
|
|
|
FormItem,
|
|
|
|
|
FormLabel,
|
|
|
|
|
FormMessage,
|
|
|
|
|
} from '@/components/ui/form'
|
2026-02-02 22:33:55 +01:00
|
|
|
import { RoundTypeSettings } from '@/components/forms/round-type-settings'
|
Implement 15 platform features: digest, availability, templates, comparison, live voting SSE, file versioning, mentorship, messaging, analytics, drafts, webhooks, peer review, audit enhancements, i18n
Features implemented:
- F1: Email digest notifications with cron endpoint and per-user frequency
- F2: Jury availability windows and workload preferences in smart assignment
- F3: Round templates with save-from-round and CRUD management
- F4: Side-by-side project comparison view for jury members
- F5: Real-time voting dashboard with Server-Sent Events (SSE)
- F6: Live voting UX: QR codes, audience voting, tie-breaking, score animations
- F7: File versioning, inline preview, bulk download with presigned URLs
- F8: Mentor dashboard: milestones, private notes, activity tracking
- F9: Communication hub with broadcasts, templates, and recipient targeting
- F10: Advanced analytics: cross-round comparison, juror consistency, diversity metrics, PDF export
- F11: Applicant draft saving with magic link resume and cron cleanup
- F12: Webhook integration layer with HMAC signing, retry, and delivery logs
- F13: Peer review discussions with anonymized scores and threaded comments
- F14: Audit log enhancements: before/after diffs, session grouping, anomaly detection, retention
- F15: i18n foundation with next-intl (EN/FR), cookie-based locale, language switcher
Schema: 12 new models, field additions to User, Project, ProjectFile, LiveVotingSession, LiveVote, MentorAssignment, AuditLog, Program
New routers: roundTemplate, message, webhook (registered in _app.ts)
New services: email-digest, webhook-dispatcher
New cron endpoints: /api/cron/digest, /api/cron/draft-cleanup, /api/cron/audit-cleanup
New API routes: /api/live-voting/stream (SSE), /api/files/bulk-download
All features are admin-configurable via SystemSettings or per-model settingsJson fields.
Docker build verified successfully.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 23:31:41 +01:00
|
|
|
import { ArrowLeft, Loader2, AlertCircle, Bell, LayoutTemplate } from 'lucide-react'
|
|
|
|
|
import { toast } from 'sonner'
|
2026-02-03 19:48:41 +01:00
|
|
|
import { DateTimePicker } from '@/components/ui/datetime-picker'
|
2026-01-30 13:41:32 +01:00
|
|
|
|
2026-02-04 00:10:51 +01:00
|
|
|
// Available notification types for teams entering a round
|
|
|
|
|
const TEAM_NOTIFICATION_OPTIONS = [
|
|
|
|
|
{ value: '', label: 'No automatic notification', description: 'Teams will not receive a notification when entering this round' },
|
|
|
|
|
{ value: 'ADVANCED_SEMIFINAL', label: 'Advanced to Semi-Finals', description: 'Congratulates team for advancing to semi-finals' },
|
|
|
|
|
{ value: 'ADVANCED_FINAL', label: 'Selected as Finalist', description: 'Congratulates team for being selected as finalist' },
|
|
|
|
|
{ value: 'NOT_SELECTED', label: 'Not Selected', description: 'Informs team they were not selected to continue' },
|
|
|
|
|
{ value: 'WINNER_ANNOUNCEMENT', label: 'Winner Announcement', description: 'Announces the team as a winner' },
|
|
|
|
|
]
|
|
|
|
|
|
2026-01-30 13:41:32 +01:00
|
|
|
const createRoundSchema = z.object({
|
|
|
|
|
programId: z.string().min(1, 'Please select a program'),
|
|
|
|
|
name: z.string().min(1, 'Name is required').max(255),
|
|
|
|
|
requiredReviews: z.number().int().min(1).max(10),
|
2026-02-03 19:48:41 +01:00
|
|
|
votingStartAt: z.date().nullable().optional(),
|
|
|
|
|
votingEndAt: z.date().nullable().optional(),
|
2026-01-30 13:41:32 +01:00
|
|
|
}).refine((data) => {
|
|
|
|
|
if (data.votingStartAt && data.votingEndAt) {
|
2026-02-03 19:48:41 +01:00
|
|
|
return data.votingEndAt > data.votingStartAt
|
2026-01-30 13:41:32 +01:00
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}, {
|
|
|
|
|
message: 'End date must be after start date',
|
|
|
|
|
path: ['votingEndAt'],
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
type CreateRoundForm = z.infer<typeof createRoundSchema>
|
|
|
|
|
|
|
|
|
|
function CreateRoundContent() {
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
const searchParams = useSearchParams()
|
|
|
|
|
const programIdParam = searchParams.get('program')
|
2026-02-02 22:33:55 +01:00
|
|
|
const [roundType, setRoundType] = useState<'FILTERING' | 'EVALUATION' | 'LIVE_EVENT'>('EVALUATION')
|
|
|
|
|
const [roundSettings, setRoundSettings] = useState<Record<string, unknown>>({})
|
2026-02-04 00:10:51 +01:00
|
|
|
const [entryNotificationType, setEntryNotificationType] = useState<string>('')
|
Implement 15 platform features: digest, availability, templates, comparison, live voting SSE, file versioning, mentorship, messaging, analytics, drafts, webhooks, peer review, audit enhancements, i18n
Features implemented:
- F1: Email digest notifications with cron endpoint and per-user frequency
- F2: Jury availability windows and workload preferences in smart assignment
- F3: Round templates with save-from-round and CRUD management
- F4: Side-by-side project comparison view for jury members
- F5: Real-time voting dashboard with Server-Sent Events (SSE)
- F6: Live voting UX: QR codes, audience voting, tie-breaking, score animations
- F7: File versioning, inline preview, bulk download with presigned URLs
- F8: Mentor dashboard: milestones, private notes, activity tracking
- F9: Communication hub with broadcasts, templates, and recipient targeting
- F10: Advanced analytics: cross-round comparison, juror consistency, diversity metrics, PDF export
- F11: Applicant draft saving with magic link resume and cron cleanup
- F12: Webhook integration layer with HMAC signing, retry, and delivery logs
- F13: Peer review discussions with anonymized scores and threaded comments
- F14: Audit log enhancements: before/after diffs, session grouping, anomaly detection, retention
- F15: i18n foundation with next-intl (EN/FR), cookie-based locale, language switcher
Schema: 12 new models, field additions to User, Project, ProjectFile, LiveVotingSession, LiveVote, MentorAssignment, AuditLog, Program
New routers: roundTemplate, message, webhook (registered in _app.ts)
New services: email-digest, webhook-dispatcher
New cron endpoints: /api/cron/digest, /api/cron/draft-cleanup, /api/cron/audit-cleanup
New API routes: /api/live-voting/stream (SSE), /api/files/bulk-download
All features are admin-configurable via SystemSettings or per-model settingsJson fields.
Docker build verified successfully.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 23:31:41 +01:00
|
|
|
const [selectedTemplateId, setSelectedTemplateId] = useState<string>('')
|
2026-01-30 13:41:32 +01:00
|
|
|
|
2026-02-03 23:19:45 +01:00
|
|
|
const utils = trpc.useUtils()
|
2026-01-30 13:41:32 +01:00
|
|
|
const { data: programs, isLoading: loadingPrograms } = trpc.program.list.useQuery()
|
Implement 15 platform features: digest, availability, templates, comparison, live voting SSE, file versioning, mentorship, messaging, analytics, drafts, webhooks, peer review, audit enhancements, i18n
Features implemented:
- F1: Email digest notifications with cron endpoint and per-user frequency
- F2: Jury availability windows and workload preferences in smart assignment
- F3: Round templates with save-from-round and CRUD management
- F4: Side-by-side project comparison view for jury members
- F5: Real-time voting dashboard with Server-Sent Events (SSE)
- F6: Live voting UX: QR codes, audience voting, tie-breaking, score animations
- F7: File versioning, inline preview, bulk download with presigned URLs
- F8: Mentor dashboard: milestones, private notes, activity tracking
- F9: Communication hub with broadcasts, templates, and recipient targeting
- F10: Advanced analytics: cross-round comparison, juror consistency, diversity metrics, PDF export
- F11: Applicant draft saving with magic link resume and cron cleanup
- F12: Webhook integration layer with HMAC signing, retry, and delivery logs
- F13: Peer review discussions with anonymized scores and threaded comments
- F14: Audit log enhancements: before/after diffs, session grouping, anomaly detection, retention
- F15: i18n foundation with next-intl (EN/FR), cookie-based locale, language switcher
Schema: 12 new models, field additions to User, Project, ProjectFile, LiveVotingSession, LiveVote, MentorAssignment, AuditLog, Program
New routers: roundTemplate, message, webhook (registered in _app.ts)
New services: email-digest, webhook-dispatcher
New cron endpoints: /api/cron/digest, /api/cron/draft-cleanup, /api/cron/audit-cleanup
New API routes: /api/live-voting/stream (SSE), /api/files/bulk-download
All features are admin-configurable via SystemSettings or per-model settingsJson fields.
Docker build verified successfully.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 23:31:41 +01:00
|
|
|
const { data: templates } = trpc.roundTemplate.list.useQuery()
|
|
|
|
|
|
|
|
|
|
const loadTemplate = (templateId: string) => {
|
|
|
|
|
if (!templateId || !templates) return
|
Fix build errors: add missing Prisma models/fields and resolve TypeScript type errors
Schema: Add 11 new models (RoundTemplate, MentorNote, MentorMilestone,
MentorMilestoneCompletion, EvaluationDiscussion, DiscussionComment,
Message, MessageRecipient, MessageTemplate, Webhook, WebhookDelivery,
DigestLog) and missing fields on existing models (Project.isDraft,
ProjectFile.version, LiveVotingSession.allowAudienceVotes, User.digestFrequency,
AuditLog.sessionId, MentorAssignment.completionStatus, etc).
Add AUDIT_CONFIG/LOCALIZATION/DIGEST/ANALYTICS enum values.
Code: Fix implicit any types, route type casts, enum casts, null safety,
composite key handling, and relation field names across 11 source files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 14:04:02 +01:00
|
|
|
const template = templates.find((t: { id: string; name: string; roundType: string; settingsJson: unknown }) => t.id === templateId)
|
Implement 15 platform features: digest, availability, templates, comparison, live voting SSE, file versioning, mentorship, messaging, analytics, drafts, webhooks, peer review, audit enhancements, i18n
Features implemented:
- F1: Email digest notifications with cron endpoint and per-user frequency
- F2: Jury availability windows and workload preferences in smart assignment
- F3: Round templates with save-from-round and CRUD management
- F4: Side-by-side project comparison view for jury members
- F5: Real-time voting dashboard with Server-Sent Events (SSE)
- F6: Live voting UX: QR codes, audience voting, tie-breaking, score animations
- F7: File versioning, inline preview, bulk download with presigned URLs
- F8: Mentor dashboard: milestones, private notes, activity tracking
- F9: Communication hub with broadcasts, templates, and recipient targeting
- F10: Advanced analytics: cross-round comparison, juror consistency, diversity metrics, PDF export
- F11: Applicant draft saving with magic link resume and cron cleanup
- F12: Webhook integration layer with HMAC signing, retry, and delivery logs
- F13: Peer review discussions with anonymized scores and threaded comments
- F14: Audit log enhancements: before/after diffs, session grouping, anomaly detection, retention
- F15: i18n foundation with next-intl (EN/FR), cookie-based locale, language switcher
Schema: 12 new models, field additions to User, Project, ProjectFile, LiveVotingSession, LiveVote, MentorAssignment, AuditLog, Program
New routers: roundTemplate, message, webhook (registered in _app.ts)
New services: email-digest, webhook-dispatcher
New cron endpoints: /api/cron/digest, /api/cron/draft-cleanup, /api/cron/audit-cleanup
New API routes: /api/live-voting/stream (SSE), /api/files/bulk-download
All features are admin-configurable via SystemSettings or per-model settingsJson fields.
Docker build verified successfully.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 23:31:41 +01:00
|
|
|
if (!template) return
|
|
|
|
|
|
|
|
|
|
// Apply template settings
|
|
|
|
|
const typeMap: Record<string, 'FILTERING' | 'EVALUATION' | 'LIVE_EVENT'> = {
|
|
|
|
|
EVALUATION: 'EVALUATION',
|
|
|
|
|
SELECTION: 'EVALUATION',
|
|
|
|
|
FINAL: 'EVALUATION',
|
|
|
|
|
LIVE_VOTING: 'LIVE_EVENT',
|
|
|
|
|
FILTERING: 'FILTERING',
|
|
|
|
|
}
|
|
|
|
|
setRoundType(typeMap[template.roundType] || 'EVALUATION')
|
|
|
|
|
|
|
|
|
|
if (template.settingsJson && typeof template.settingsJson === 'object') {
|
|
|
|
|
setRoundSettings(template.settingsJson as Record<string, unknown>)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (template.name) {
|
|
|
|
|
form.setValue('name', template.name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setSelectedTemplateId(templateId)
|
|
|
|
|
toast.success(`Loaded template: ${template.name}`)
|
|
|
|
|
}
|
2026-01-30 13:41:32 +01:00
|
|
|
|
|
|
|
|
const createRound = trpc.round.create.useMutation({
|
|
|
|
|
onSuccess: (data) => {
|
2026-02-03 23:19:45 +01:00
|
|
|
utils.program.list.invalidate({ includeRounds: true })
|
2026-01-30 13:41:32 +01:00
|
|
|
router.push(`/admin/rounds/${data.id}`)
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const form = useForm<CreateRoundForm>({
|
|
|
|
|
resolver: zodResolver(createRoundSchema),
|
|
|
|
|
defaultValues: {
|
|
|
|
|
programId: programIdParam || '',
|
|
|
|
|
name: '',
|
|
|
|
|
requiredReviews: 3,
|
2026-02-03 19:48:41 +01:00
|
|
|
votingStartAt: null,
|
|
|
|
|
votingEndAt: null,
|
2026-01-30 13:41:32 +01:00
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const onSubmit = async (data: CreateRoundForm) => {
|
|
|
|
|
await createRound.mutateAsync({
|
|
|
|
|
programId: data.programId,
|
|
|
|
|
name: data.name,
|
2026-02-02 22:33:55 +01:00
|
|
|
roundType,
|
2026-01-30 13:41:32 +01:00
|
|
|
requiredReviews: data.requiredReviews,
|
2026-02-02 22:33:55 +01:00
|
|
|
settingsJson: roundSettings,
|
2026-02-03 19:48:41 +01:00
|
|
|
votingStartAt: data.votingStartAt ?? undefined,
|
|
|
|
|
votingEndAt: data.votingEndAt ?? undefined,
|
2026-02-04 00:10:51 +01:00
|
|
|
entryNotificationType: entryNotificationType || undefined,
|
2026-01-30 13:41:32 +01:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (loadingPrograms) {
|
|
|
|
|
return <CreateRoundSkeleton />
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!programs || programs.length === 0) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
<Button variant="ghost" asChild className="-ml-4">
|
|
|
|
|
<Link href="/admin/rounds">
|
|
|
|
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
|
|
|
Back to Rounds
|
|
|
|
|
</Link>
|
|
|
|
|
</Button>
|
|
|
|
|
|
|
|
|
|
<Card>
|
|
|
|
|
<CardContent className="flex flex-col items-center justify-center py-12 text-center">
|
|
|
|
|
<AlertCircle className="h-12 w-12 text-muted-foreground/50" />
|
|
|
|
|
<p className="mt-2 font-medium">No Programs Found</p>
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
Create a program first before creating rounds
|
|
|
|
|
</p>
|
|
|
|
|
<Button asChild className="mt-4">
|
|
|
|
|
<Link href="/admin/programs/new">Create Program</Link>
|
|
|
|
|
</Button>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
|
<Button variant="ghost" asChild className="-ml-4">
|
|
|
|
|
<Link href="/admin/rounds">
|
|
|
|
|
<ArrowLeft className="mr-2 h-4 w-4" />
|
|
|
|
|
Back to Rounds
|
|
|
|
|
</Link>
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
<h1 className="text-2xl font-semibold tracking-tight">Create Round</h1>
|
|
|
|
|
<p className="text-muted-foreground">
|
|
|
|
|
Set up a new selection round for project evaluation
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
Implement 15 platform features: digest, availability, templates, comparison, live voting SSE, file versioning, mentorship, messaging, analytics, drafts, webhooks, peer review, audit enhancements, i18n
Features implemented:
- F1: Email digest notifications with cron endpoint and per-user frequency
- F2: Jury availability windows and workload preferences in smart assignment
- F3: Round templates with save-from-round and CRUD management
- F4: Side-by-side project comparison view for jury members
- F5: Real-time voting dashboard with Server-Sent Events (SSE)
- F6: Live voting UX: QR codes, audience voting, tie-breaking, score animations
- F7: File versioning, inline preview, bulk download with presigned URLs
- F8: Mentor dashboard: milestones, private notes, activity tracking
- F9: Communication hub with broadcasts, templates, and recipient targeting
- F10: Advanced analytics: cross-round comparison, juror consistency, diversity metrics, PDF export
- F11: Applicant draft saving with magic link resume and cron cleanup
- F12: Webhook integration layer with HMAC signing, retry, and delivery logs
- F13: Peer review discussions with anonymized scores and threaded comments
- F14: Audit log enhancements: before/after diffs, session grouping, anomaly detection, retention
- F15: i18n foundation with next-intl (EN/FR), cookie-based locale, language switcher
Schema: 12 new models, field additions to User, Project, ProjectFile, LiveVotingSession, LiveVote, MentorAssignment, AuditLog, Program
New routers: roundTemplate, message, webhook (registered in _app.ts)
New services: email-digest, webhook-dispatcher
New cron endpoints: /api/cron/digest, /api/cron/draft-cleanup, /api/cron/audit-cleanup
New API routes: /api/live-voting/stream (SSE), /api/files/bulk-download
All features are admin-configurable via SystemSettings or per-model settingsJson fields.
Docker build verified successfully.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 23:31:41 +01:00
|
|
|
{/* Template Selector */}
|
|
|
|
|
{templates && templates.length > 0 && (
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader className="pb-3">
|
|
|
|
|
<CardTitle className="text-lg flex items-center gap-2">
|
|
|
|
|
<LayoutTemplate className="h-5 w-5" />
|
|
|
|
|
Start from Template
|
|
|
|
|
</CardTitle>
|
|
|
|
|
<CardDescription>
|
|
|
|
|
Load settings from a saved template to get started quickly
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<Select
|
|
|
|
|
value={selectedTemplateId}
|
|
|
|
|
onValueChange={loadTemplate}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger className="w-full max-w-sm">
|
|
|
|
|
<SelectValue placeholder="Select a template..." />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
Fix build errors: add missing Prisma models/fields and resolve TypeScript type errors
Schema: Add 11 new models (RoundTemplate, MentorNote, MentorMilestone,
MentorMilestoneCompletion, EvaluationDiscussion, DiscussionComment,
Message, MessageRecipient, MessageTemplate, Webhook, WebhookDelivery,
DigestLog) and missing fields on existing models (Project.isDraft,
ProjectFile.version, LiveVotingSession.allowAudienceVotes, User.digestFrequency,
AuditLog.sessionId, MentorAssignment.completionStatus, etc).
Add AUDIT_CONFIG/LOCALIZATION/DIGEST/ANALYTICS enum values.
Code: Fix implicit any types, route type casts, enum casts, null safety,
composite key handling, and relation field names across 11 source files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 14:04:02 +01:00
|
|
|
{templates.map((t: { id: string; name: string; description?: string | null }) => (
|
Implement 15 platform features: digest, availability, templates, comparison, live voting SSE, file versioning, mentorship, messaging, analytics, drafts, webhooks, peer review, audit enhancements, i18n
Features implemented:
- F1: Email digest notifications with cron endpoint and per-user frequency
- F2: Jury availability windows and workload preferences in smart assignment
- F3: Round templates with save-from-round and CRUD management
- F4: Side-by-side project comparison view for jury members
- F5: Real-time voting dashboard with Server-Sent Events (SSE)
- F6: Live voting UX: QR codes, audience voting, tie-breaking, score animations
- F7: File versioning, inline preview, bulk download with presigned URLs
- F8: Mentor dashboard: milestones, private notes, activity tracking
- F9: Communication hub with broadcasts, templates, and recipient targeting
- F10: Advanced analytics: cross-round comparison, juror consistency, diversity metrics, PDF export
- F11: Applicant draft saving with magic link resume and cron cleanup
- F12: Webhook integration layer with HMAC signing, retry, and delivery logs
- F13: Peer review discussions with anonymized scores and threaded comments
- F14: Audit log enhancements: before/after diffs, session grouping, anomaly detection, retention
- F15: i18n foundation with next-intl (EN/FR), cookie-based locale, language switcher
Schema: 12 new models, field additions to User, Project, ProjectFile, LiveVotingSession, LiveVote, MentorAssignment, AuditLog, Program
New routers: roundTemplate, message, webhook (registered in _app.ts)
New services: email-digest, webhook-dispatcher
New cron endpoints: /api/cron/digest, /api/cron/draft-cleanup, /api/cron/audit-cleanup
New API routes: /api/live-voting/stream (SSE), /api/files/bulk-download
All features are admin-configurable via SystemSettings or per-model settingsJson fields.
Docker build verified successfully.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-05 23:31:41 +01:00
|
|
|
<SelectItem key={t.id} value={t.id}>
|
|
|
|
|
{t.name}
|
|
|
|
|
{t.description ? ` - ${t.description}` : ''}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
{selectedTemplateId && (
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setSelectedTemplateId('')
|
|
|
|
|
setRoundType('EVALUATION')
|
|
|
|
|
setRoundSettings({})
|
|
|
|
|
form.reset()
|
|
|
|
|
toast.info('Template cleared')
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
Clear
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-01-30 13:41:32 +01:00
|
|
|
{/* Form */}
|
|
|
|
|
<Form {...form}>
|
|
|
|
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle className="text-lg">Basic Information</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="space-y-4">
|
|
|
|
|
<FormField
|
|
|
|
|
control={form.control}
|
|
|
|
|
name="programId"
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
<FormItem>
|
2026-02-02 19:52:52 +01:00
|
|
|
<FormLabel>Edition</FormLabel>
|
2026-01-30 13:41:32 +01:00
|
|
|
<Select
|
|
|
|
|
onValueChange={field.onChange}
|
|
|
|
|
defaultValue={field.value}
|
|
|
|
|
>
|
|
|
|
|
<FormControl>
|
|
|
|
|
<SelectTrigger>
|
2026-02-02 19:52:52 +01:00
|
|
|
<SelectValue placeholder="Select an edition" />
|
2026-01-30 13:41:32 +01:00
|
|
|
</SelectTrigger>
|
|
|
|
|
</FormControl>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{programs.map((program) => (
|
|
|
|
|
<SelectItem key={program.id} value={program.id}>
|
2026-02-02 19:52:52 +01:00
|
|
|
{program.year} Edition
|
2026-01-30 13:41:32 +01:00
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
<FormMessage />
|
|
|
|
|
</FormItem>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<FormField
|
|
|
|
|
control={form.control}
|
|
|
|
|
name="name"
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
<FormItem>
|
|
|
|
|
<FormLabel>Round Name</FormLabel>
|
|
|
|
|
<FormControl>
|
|
|
|
|
<Input
|
|
|
|
|
placeholder="e.g., Round 1 - Semi-Finalists"
|
|
|
|
|
{...field}
|
|
|
|
|
/>
|
|
|
|
|
</FormControl>
|
|
|
|
|
<FormDescription>
|
|
|
|
|
A descriptive name for this selection round
|
|
|
|
|
</FormDescription>
|
|
|
|
|
<FormMessage />
|
|
|
|
|
</FormItem>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<FormField
|
|
|
|
|
control={form.control}
|
|
|
|
|
name="requiredReviews"
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
<FormItem>
|
|
|
|
|
<FormLabel>Required Reviews per Project</FormLabel>
|
|
|
|
|
<FormControl>
|
|
|
|
|
<Input
|
|
|
|
|
type="number"
|
|
|
|
|
min={1}
|
|
|
|
|
max={10}
|
|
|
|
|
{...field}
|
|
|
|
|
onChange={(e) => field.onChange(parseInt(e.target.value) || 1)}
|
|
|
|
|
/>
|
|
|
|
|
</FormControl>
|
|
|
|
|
<FormDescription>
|
|
|
|
|
Minimum number of evaluations each project should receive
|
|
|
|
|
</FormDescription>
|
|
|
|
|
<FormMessage />
|
|
|
|
|
</FormItem>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
2026-02-02 22:33:55 +01:00
|
|
|
{/* Round Type & Settings */}
|
|
|
|
|
<RoundTypeSettings
|
|
|
|
|
roundType={roundType}
|
|
|
|
|
onRoundTypeChange={setRoundType}
|
|
|
|
|
settings={roundSettings}
|
|
|
|
|
onSettingsChange={setRoundSettings}
|
|
|
|
|
/>
|
|
|
|
|
|
2026-01-30 13:41:32 +01:00
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle className="text-lg">Voting Window</CardTitle>
|
|
|
|
|
<CardDescription>
|
|
|
|
|
Optional: Set when jury members can submit their evaluations
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="space-y-4">
|
|
|
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
|
|
|
<FormField
|
|
|
|
|
control={form.control}
|
|
|
|
|
name="votingStartAt"
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
<FormItem>
|
|
|
|
|
<FormLabel>Start Date & Time</FormLabel>
|
|
|
|
|
<FormControl>
|
2026-02-03 19:48:41 +01:00
|
|
|
<DateTimePicker
|
|
|
|
|
value={field.value}
|
|
|
|
|
onChange={field.onChange}
|
|
|
|
|
placeholder="Select start date & time"
|
|
|
|
|
/>
|
2026-01-30 13:41:32 +01:00
|
|
|
</FormControl>
|
|
|
|
|
<FormMessage />
|
|
|
|
|
</FormItem>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<FormField
|
|
|
|
|
control={form.control}
|
|
|
|
|
name="votingEndAt"
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
<FormItem>
|
|
|
|
|
<FormLabel>End Date & Time</FormLabel>
|
|
|
|
|
<FormControl>
|
2026-02-03 19:48:41 +01:00
|
|
|
<DateTimePicker
|
|
|
|
|
value={field.value}
|
|
|
|
|
onChange={field.onChange}
|
|
|
|
|
placeholder="Select end date & time"
|
|
|
|
|
/>
|
2026-01-30 13:41:32 +01:00
|
|
|
</FormControl>
|
|
|
|
|
<FormMessage />
|
|
|
|
|
</FormItem>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
2026-02-03 19:48:41 +01:00
|
|
|
Leave empty to set the voting window later. Past dates are allowed.
|
2026-01-30 13:41:32 +01:00
|
|
|
</p>
|
|
|
|
|
</CardContent>
|
2026-02-04 00:10:51 +01:00
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* Team Notification */}
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle className="text-lg flex items-center gap-2">
|
|
|
|
|
<Bell className="h-5 w-5" />
|
|
|
|
|
Team Notification
|
|
|
|
|
</CardTitle>
|
|
|
|
|
<CardDescription>
|
|
|
|
|
Notification sent to project teams when they enter this round
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
<Select
|
|
|
|
|
value={entryNotificationType || 'none'}
|
|
|
|
|
onValueChange={(val) => setEntryNotificationType(val === 'none' ? '' : val)}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
<SelectValue placeholder="No automatic notification" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{TEAM_NOTIFICATION_OPTIONS.map((option) => (
|
|
|
|
|
<SelectItem key={option.value || 'none'} value={option.value || 'none'}>
|
|
|
|
|
{option.label}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
<p className="text-sm text-muted-foreground mt-2">
|
|
|
|
|
When projects advance to this round, the selected notification will be sent to the project team automatically.
|
|
|
|
|
</p>
|
|
|
|
|
</CardContent>
|
2026-01-30 13:41:32 +01:00
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* Error */}
|
|
|
|
|
{createRound.error && (
|
|
|
|
|
<Card className="border-destructive">
|
|
|
|
|
<CardContent className="flex items-center gap-2 py-4">
|
|
|
|
|
<AlertCircle className="h-5 w-5 text-destructive" />
|
|
|
|
|
<p className="text-sm text-destructive">
|
|
|
|
|
{createRound.error.message}
|
|
|
|
|
</p>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Actions */}
|
|
|
|
|
<div className="flex justify-end gap-3">
|
|
|
|
|
<Button type="button" variant="outline" asChild>
|
|
|
|
|
<Link href="/admin/rounds">Cancel</Link>
|
|
|
|
|
</Button>
|
|
|
|
|
<Button type="submit" disabled={createRound.isPending}>
|
|
|
|
|
{createRound.isPending && (
|
|
|
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
|
|
|
)}
|
|
|
|
|
Create Round
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
</Form>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function CreateRoundSkeleton() {
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
<Skeleton className="h-9 w-36" />
|
|
|
|
|
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
<Skeleton className="h-8 w-48" />
|
|
|
|
|
<Skeleton className="h-4 w-64" />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<Skeleton className="h-5 w-40" />
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="space-y-4">
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Skeleton className="h-4 w-20" />
|
|
|
|
|
<Skeleton className="h-10 w-full" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Skeleton className="h-4 w-24" />
|
|
|
|
|
<Skeleton className="h-10 w-full" />
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Skeleton className="h-4 w-40" />
|
|
|
|
|
<Skeleton className="h-10 w-32" />
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function CreateRoundPage() {
|
|
|
|
|
return (
|
|
|
|
|
<Suspense fallback={<CreateRoundSkeleton />}>
|
|
|
|
|
<CreateRoundContent />
|
|
|
|
|
</Suspense>
|
|
|
|
|
)
|
|
|
|
|
}
|