Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
All checks were successful
Build and Push Docker Image / build (push) Successful in 7m23s

- Phase 1: 7 round config sub-components covering all ~65 Zod schema fields across INTAKE, FILTERING, EVALUATION, SUBMISSION, MENTORING, LIVE_FINAL, DELIBERATION
- Phase 2: Replace Competitions nav with Rounds + add Juries; new /admin/rounds and /admin/rounds/[roundId] pages with tabbed detail (Config, Projects, Windows, Documents, Awards)
- Phase 3: Top-level /admin/juries with list + detail pages (members table, settings panel, self-service review)
- Phase 4: File requirements editor in round config; project detail per-requirement upload slots replacing generic drop zone
- Phase 5: Awards edit page with source round dropdown, eligibility mode, auto-tag rules builder; round detail Awards tab; specialAward router enhanced with evaluationRoundId/eligibilityMode fields
- Phase 6: Evaluation page rewrite supporting all 3 scoring modes (criteria/global/binary) with config-driven behavior; live voting UI polish
- Phase 7: UI design polish across admin pages — consistent headers, cards, hover transitions, empty states, brand colors
- Bulk upload page for admin project imports
- File router enhanced with admin upload and submission window procedures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-16 01:16:55 +01:00
parent fbb194067d
commit 4c0efb232c
23 changed files with 5745 additions and 891 deletions

View File

@@ -0,0 +1,283 @@
'use client'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
type EvaluationConfigProps = {
config: Record<string, unknown>
onChange: (config: Record<string, unknown>) => void
}
export function EvaluationConfig({ config, onChange }: EvaluationConfigProps) {
const update = (key: string, value: unknown) => {
onChange({ ...config, [key]: value })
}
const advancementMode = (config.advancementMode as string) ?? 'admin_selection'
const advancementConfig = (config.advancementConfig as {
perCategory?: boolean; startupCount?: number; conceptCount?: number; tieBreaker?: string
}) ?? {}
const updateAdvancement = (key: string, value: unknown) => {
update('advancementConfig', { ...advancementConfig, [key]: value })
}
return (
<div className="space-y-6">
{/* Scoring */}
<Card>
<CardHeader>
<CardTitle className="text-base">Scoring & Reviews</CardTitle>
<CardDescription>How jury members evaluate and score projects</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="requiredReviews">Required Reviews per Project</Label>
<p className="text-xs text-muted-foreground">Minimum number of jury evaluations needed</p>
<Input
id="requiredReviews"
type="number"
min={1}
className="w-32"
value={(config.requiredReviewsPerProject as number) ?? 3}
onChange={(e) => update('requiredReviewsPerProject', parseInt(e.target.value, 10) || 3)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="scoringMode">Scoring Mode</Label>
<p className="text-xs text-muted-foreground">How jurors assign scores to projects</p>
<Select
value={(config.scoringMode as string) ?? 'criteria'}
onValueChange={(v) => update('scoringMode', v)}
>
<SelectTrigger id="scoringMode" className="w-64">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="criteria">Criteria-based (multiple criteria with weights)</SelectItem>
<SelectItem value="global">Global score (single overall score)</SelectItem>
<SelectItem value="binary">Binary (pass/fail)</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="anonymizationLevel">Anonymization Level</Label>
<p className="text-xs text-muted-foreground">How much of other jurors&apos; identities are revealed</p>
<Select
value={(config.anonymizationLevel as string) ?? 'fully_anonymous'}
onValueChange={(v) => update('anonymizationLevel', v)}
>
<SelectTrigger id="anonymizationLevel" className="w-64">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="fully_anonymous">Fully Anonymous</SelectItem>
<SelectItem value="show_initials">Show Initials</SelectItem>
<SelectItem value="named">Named (full names visible)</SelectItem>
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{/* Feedback */}
<Card>
<CardHeader>
<CardTitle className="text-base">Feedback Requirements</CardTitle>
<CardDescription>What jurors must provide alongside scores</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div>
<Label htmlFor="requireFeedback">Require Written Feedback</Label>
<p className="text-xs text-muted-foreground">Jurors must write feedback text</p>
</div>
<Switch
id="requireFeedback"
checked={(config.requireFeedback as boolean) ?? true}
onCheckedChange={(v) => update('requireFeedback', v)}
/>
</div>
{(config.requireFeedback as boolean) !== false && (
<div className="pl-6 border-l-2 border-muted space-y-2">
<Label htmlFor="feedbackMinLength">Minimum Feedback Length</Label>
<p className="text-xs text-muted-foreground">Minimum characters (0 = no minimum)</p>
<Input
id="feedbackMinLength"
type="number"
min={0}
className="w-32"
value={(config.feedbackMinLength as number) ?? 0}
onChange={(e) => update('feedbackMinLength', parseInt(e.target.value, 10) || 0)}
/>
</div>
)}
<div className="flex items-center justify-between">
<div>
<Label htmlFor="requireAllCriteriaScored">Require All Criteria Scored</Label>
<p className="text-xs text-muted-foreground">Jurors must score every criterion before submitting</p>
</div>
<Switch
id="requireAllCriteriaScored"
checked={(config.requireAllCriteriaScored as boolean) ?? true}
onCheckedChange={(v) => update('requireAllCriteriaScored', v)}
/>
</div>
<div className="flex items-center justify-between">
<div>
<Label htmlFor="coiRequired">COI Declaration Required</Label>
<p className="text-xs text-muted-foreground">Jurors must declare conflicts of interest</p>
</div>
<Switch
id="coiRequired"
checked={(config.coiRequired as boolean) ?? true}
onCheckedChange={(v) => update('coiRequired', v)}
/>
</div>
<div className="flex items-center justify-between">
<div>
<Label htmlFor="peerReviewEnabled">Peer Review</Label>
<p className="text-xs text-muted-foreground">Allow jurors to see and comment on other evaluations</p>
</div>
<Switch
id="peerReviewEnabled"
checked={(config.peerReviewEnabled as boolean) ?? false}
onCheckedChange={(v) => update('peerReviewEnabled', v)}
/>
</div>
</CardContent>
</Card>
{/* AI Features */}
<Card>
<CardHeader>
<CardTitle className="text-base">AI Features</CardTitle>
<CardDescription>AI-powered evaluation assistance</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<div>
<Label htmlFor="aiSummaryEnabled">AI Evaluation Summary</Label>
<p className="text-xs text-muted-foreground">Generate AI synthesis of all jury evaluations</p>
</div>
<Switch
id="aiSummaryEnabled"
checked={(config.aiSummaryEnabled as boolean) ?? false}
onCheckedChange={(v) => update('aiSummaryEnabled', v)}
/>
</div>
<div className="flex items-center justify-between">
<div>
<Label htmlFor="generateAiShortlist">AI Shortlist Recommendations</Label>
<p className="text-xs text-muted-foreground">AI suggests which projects should advance</p>
</div>
<Switch
id="generateAiShortlist"
checked={(config.generateAiShortlist as boolean) ?? false}
onCheckedChange={(v) => update('generateAiShortlist', v)}
/>
</div>
</CardContent>
</Card>
{/* Advancement */}
<Card>
<CardHeader>
<CardTitle className="text-base">Advancement Rules</CardTitle>
<CardDescription>How projects move to the next round</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="advancementMode">Advancement Mode</Label>
<Select
value={advancementMode}
onValueChange={(v) => update('advancementMode', v)}
>
<SelectTrigger id="advancementMode" className="w-64">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="admin_selection">Admin Selection (manual)</SelectItem>
<SelectItem value="auto_top_n">Auto Top-N (by score)</SelectItem>
<SelectItem value="ai_recommended">AI Recommended</SelectItem>
</SelectContent>
</Select>
</div>
{advancementMode === 'auto_top_n' && (
<div className="pl-6 border-l-2 border-muted space-y-4">
<Label className="text-sm font-medium">Auto Top-N Settings</Label>
<div className="flex items-center justify-between">
<div>
<Label htmlFor="perCategory">Per Category</Label>
<p className="text-xs text-muted-foreground">Apply limits separately for each category</p>
</div>
<Switch
id="perCategory"
checked={advancementConfig.perCategory ?? true}
onCheckedChange={(v) => updateAdvancement('perCategory', v)}
/>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="startupCount">Startup Advancement Count</Label>
<p className="text-xs text-muted-foreground">Number of startups to advance</p>
<Input
id="startupCount"
type="number"
min={0}
className="w-32"
value={advancementConfig.startupCount ?? 10}
onChange={(e) => updateAdvancement('startupCount', parseInt(e.target.value, 10) || 0)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="conceptCount">Business Concept Advancement Count</Label>
<p className="text-xs text-muted-foreground">Number of business concepts to advance</p>
<Input
id="conceptCount"
type="number"
min={0}
className="w-32"
value={advancementConfig.conceptCount ?? 10}
onChange={(e) => updateAdvancement('conceptCount', parseInt(e.target.value, 10) || 0)}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="tieBreaker">Tie Breaker</Label>
<p className="text-xs text-muted-foreground">How to handle tied scores</p>
<Select
value={advancementConfig.tieBreaker ?? 'admin_decides'}
onValueChange={(v) => updateAdvancement('tieBreaker', v)}
>
<SelectTrigger id="tieBreaker" className="w-64">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="admin_decides">Admin Decides</SelectItem>
<SelectItem value="highest_individual">Highest Individual Score</SelectItem>
<SelectItem value="revote">Re-vote</SelectItem>
</SelectContent>
</Select>
</div>
</div>
)}
</CardContent>
</Card>
</div>
)
}