Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
'use client'
|
|
|
|
|
|
2026-02-16 09:20:02 +01:00
|
|
|
import { useState, useMemo, useCallback } from 'react'
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
import { useParams } from 'next/navigation'
|
|
|
|
|
import Link from 'next/link'
|
|
|
|
|
import type { Route } from 'next'
|
|
|
|
|
import { trpc } from '@/lib/trpc/client'
|
|
|
|
|
import { toast } from 'sonner'
|
2026-02-16 09:20:02 +01:00
|
|
|
import { cn } from '@/lib/utils'
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
import { Button } from '@/components/ui/button'
|
2026-02-16 09:20:02 +01:00
|
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
|
|
|
|
import { Skeleton } from '@/components/ui/skeleton'
|
|
|
|
|
import { Badge } from '@/components/ui/badge'
|
AI category-aware evaluation: per-round config, file parsing, shortlist, advance flow
- Per-juror cap mode (HARD/SOFT/NONE) in add-member dialog and members table
- Jury invite flow: create user + add to group + send invitation from dialog
- Per-round config: notifyOnAdvance, aiParseFiles, startupAdvanceCount, conceptAdvanceCount
- Moved notify-on-advance from competition-level to per-round setting
- AI filtering: round-tagged files with newest-first sorting, optional file content extraction
- File content extractor service (pdf-parse for PDF, utf-8 for text files)
- AI shortlist runs independently per category (STARTUP / BUSINESS_CONCEPT)
- generateAIRecommendations tRPC endpoint with per-round config integration
- AI recommendations UI: trigger button, confirmation dialog, per-category results display
- Category-aware advance dialog: select/deselect projects by category with target caps
- STAGE_ACTIVE bug fix in assignment router
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:09:52 +01:00
|
|
|
import { Checkbox } from '@/components/ui/checkbox'
|
Rounds overhaul: full CRUD submission windows, scheduling UI, analytics, design refresh
- Fix special award FK crash: replace 4x raw auditLog.create with logAudit() helper
- Add updateSubmissionWindow + deleteSubmissionWindow mutations to round router
- Add per-round analytics (_count, juryGroup) to competition.getById
- Remove redundant acceptedCategories from intake config
- Rewrite submission window manager with full CRUD, all fields, date pickers
- Add round scheduling card (open/close dates) to round detail page
- Add project count, assignment count, jury group to round list cards
- Visual redesign: pipeline view, brand colors, progress bars, enhanced cards
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:07:09 +01:00
|
|
|
import { Input } from '@/components/ui/input'
|
2026-02-16 09:20:02 +01:00
|
|
|
import { Switch } from '@/components/ui/switch'
|
|
|
|
|
import { Label } from '@/components/ui/label'
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
import {
|
|
|
|
|
DropdownMenu,
|
|
|
|
|
DropdownMenuContent,
|
|
|
|
|
DropdownMenuItem,
|
|
|
|
|
DropdownMenuSeparator,
|
|
|
|
|
DropdownMenuTrigger,
|
|
|
|
|
} from '@/components/ui/dropdown-menu'
|
2026-02-16 09:20:02 +01:00
|
|
|
import {
|
|
|
|
|
AlertDialog,
|
|
|
|
|
AlertDialogAction,
|
|
|
|
|
AlertDialogCancel,
|
|
|
|
|
AlertDialogContent,
|
|
|
|
|
AlertDialogDescription,
|
|
|
|
|
AlertDialogFooter,
|
|
|
|
|
AlertDialogHeader,
|
|
|
|
|
AlertDialogTitle,
|
|
|
|
|
AlertDialogTrigger,
|
|
|
|
|
} from '@/components/ui/alert-dialog'
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
import {
|
|
|
|
|
Dialog,
|
|
|
|
|
DialogContent,
|
|
|
|
|
DialogDescription,
|
|
|
|
|
DialogFooter,
|
|
|
|
|
DialogHeader,
|
|
|
|
|
DialogTitle,
|
|
|
|
|
} from '@/components/ui/dialog'
|
2026-02-16 09:20:02 +01:00
|
|
|
import {
|
|
|
|
|
Select,
|
|
|
|
|
SelectContent,
|
|
|
|
|
SelectItem,
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
SelectValue,
|
|
|
|
|
} from '@/components/ui/select'
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
import {
|
|
|
|
|
ArrowLeft,
|
|
|
|
|
Save,
|
|
|
|
|
Loader2,
|
|
|
|
|
ChevronDown,
|
|
|
|
|
Play,
|
|
|
|
|
Square,
|
|
|
|
|
Archive,
|
2026-02-16 09:20:02 +01:00
|
|
|
Layers,
|
Rounds overhaul: full CRUD submission windows, scheduling UI, analytics, design refresh
- Fix special award FK crash: replace 4x raw auditLog.create with logAudit() helper
- Add updateSubmissionWindow + deleteSubmissionWindow mutations to round router
- Add per-round analytics (_count, juryGroup) to competition.getById
- Remove redundant acceptedCategories from intake config
- Rewrite submission window manager with full CRUD, all fields, date pickers
- Add round scheduling card (open/close dates) to round detail page
- Add project count, assignment count, jury group to round list cards
- Visual redesign: pipeline view, brand colors, progress bars, enhanced cards
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:07:09 +01:00
|
|
|
Users,
|
2026-02-16 09:20:02 +01:00
|
|
|
CalendarDays,
|
|
|
|
|
BarChart3,
|
|
|
|
|
ClipboardList,
|
|
|
|
|
Settings,
|
|
|
|
|
Zap,
|
|
|
|
|
Shield,
|
|
|
|
|
UserPlus,
|
|
|
|
|
CheckCircle2,
|
|
|
|
|
AlertTriangle,
|
|
|
|
|
FileText,
|
|
|
|
|
Trophy,
|
|
|
|
|
Clock,
|
|
|
|
|
Send,
|
|
|
|
|
Download,
|
|
|
|
|
Plus,
|
|
|
|
|
Trash2,
|
|
|
|
|
ArrowRight,
|
2026-02-16 12:06:07 +01:00
|
|
|
RotateCcw,
|
2026-02-16 09:20:02 +01:00
|
|
|
X,
|
Platform-wide UX fixes: assignment dialog, invalidation, settings, dashboard
1. Assignment dialog overhaul: replace raw UUID inputs with searchable
juror Combobox (shows name, email, capacity) and multi-select project
checklist with bulk assignment support
2. Query invalidation sweep: fix missing invalidations in
assignment-preview-sheet (roundAssignment.execute) and
filtering-dashboard (filtering.finalizeResults) so data refreshes
without page reload
3. Rename Submissions tab to Document Windows with descriptive
header explaining upload window configuration
4. Connect 6 disconnected settings: storage_provider, local_storage_path,
avatar_max_size_mb, allowed_image_types, whatsapp_enabled,
whatsapp_provider - all now accessible in Settings UI
5. Admin dashboard redesign: branded Editorial Command Center with
Dark Blue gradient header, colored border-l-4 stat cards, staggered
animations, 2-column layout, action-required panel, activity timeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:05:25 +01:00
|
|
|
Check,
|
|
|
|
|
ChevronsUpDown,
|
|
|
|
|
Search,
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
} from 'lucide-react'
|
Platform-wide UX fixes: assignment dialog, invalidation, settings, dashboard
1. Assignment dialog overhaul: replace raw UUID inputs with searchable
juror Combobox (shows name, email, capacity) and multi-select project
checklist with bulk assignment support
2. Query invalidation sweep: fix missing invalidations in
assignment-preview-sheet (roundAssignment.execute) and
filtering-dashboard (filtering.finalizeResults) so data refreshes
without page reload
3. Rename Submissions tab to Document Windows with descriptive
header explaining upload window configuration
4. Connect 6 disconnected settings: storage_provider, local_storage_path,
avatar_max_size_mb, allowed_image_types, whatsapp_enabled,
whatsapp_provider - all now accessible in Settings UI
5. Admin dashboard redesign: branded Editorial Command Center with
Dark Blue gradient header, colored border-l-4 stat cards, staggered
animations, 2-column layout, action-required panel, activity timeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:05:25 +01:00
|
|
|
import {
|
|
|
|
|
Command,
|
|
|
|
|
CommandEmpty,
|
|
|
|
|
CommandGroup,
|
|
|
|
|
CommandInput,
|
|
|
|
|
CommandItem,
|
|
|
|
|
CommandList,
|
|
|
|
|
} from '@/components/ui/command'
|
|
|
|
|
import {
|
|
|
|
|
Popover,
|
|
|
|
|
PopoverContent,
|
|
|
|
|
PopoverTrigger,
|
|
|
|
|
} from '@/components/ui/popover'
|
|
|
|
|
import { ScrollArea } from '@/components/ui/scroll-area'
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
import { RoundConfigForm } from '@/components/admin/competition/round-config-form'
|
|
|
|
|
import { ProjectStatesTable } from '@/components/admin/round/project-states-table'
|
2026-02-16 16:43:23 +01:00
|
|
|
// SubmissionWindowManager removed — round dates + file requirements in Config are sufficient
|
2026-02-16 09:20:02 +01:00
|
|
|
import { FileRequirementsEditor } from '@/components/admin/round/file-requirements-editor'
|
|
|
|
|
import { FilteringDashboard } from '@/components/admin/round/filtering-dashboard'
|
|
|
|
|
import { CoverageReport } from '@/components/admin/assignment/coverage-report'
|
|
|
|
|
import { AssignmentPreviewSheet } from '@/components/admin/assignment/assignment-preview-sheet'
|
|
|
|
|
import { CsvExportDialog } from '@/components/shared/csv-export-dialog'
|
2026-02-16 12:38:28 +01:00
|
|
|
import { AnimatedCard } from '@/components/shared/animated-container'
|
2026-02-16 16:43:23 +01:00
|
|
|
import { DateTimePicker } from '@/components/ui/datetime-picker'
|
2026-02-16 12:46:01 +01:00
|
|
|
import { AddMemberDialog } from '@/components/admin/jury/add-member-dialog'
|
2026-02-16 12:38:28 +01:00
|
|
|
import { motion } from 'motion/react'
|
2026-02-16 09:20:02 +01:00
|
|
|
|
|
|
|
|
// ── Status & type config maps ──────────────────────────────────────────────
|
|
|
|
|
const roundStatusConfig = {
|
|
|
|
|
ROUND_DRAFT: {
|
|
|
|
|
label: 'Draft',
|
|
|
|
|
bgClass: 'bg-gray-100 text-gray-700',
|
|
|
|
|
dotClass: 'bg-gray-500',
|
|
|
|
|
description: 'Not yet active. Configure before launching.',
|
|
|
|
|
},
|
|
|
|
|
ROUND_ACTIVE: {
|
|
|
|
|
label: 'Active',
|
|
|
|
|
bgClass: 'bg-emerald-100 text-emerald-700',
|
|
|
|
|
dotClass: 'bg-emerald-500 animate-pulse',
|
|
|
|
|
description: 'Round is live. Projects can be processed.',
|
|
|
|
|
},
|
|
|
|
|
ROUND_CLOSED: {
|
|
|
|
|
label: 'Closed',
|
|
|
|
|
bgClass: 'bg-blue-100 text-blue-700',
|
|
|
|
|
dotClass: 'bg-blue-500',
|
|
|
|
|
description: 'No longer accepting changes. Results are final.',
|
|
|
|
|
},
|
|
|
|
|
ROUND_ARCHIVED: {
|
|
|
|
|
label: 'Archived',
|
|
|
|
|
bgClass: 'bg-muted text-muted-foreground',
|
|
|
|
|
dotClass: 'bg-muted-foreground',
|
|
|
|
|
description: 'Historical record only.',
|
|
|
|
|
},
|
|
|
|
|
} as const
|
|
|
|
|
|
|
|
|
|
const roundTypeConfig: Record<string, { label: string; color: string; description: string }> = {
|
|
|
|
|
INTAKE: { label: 'Intake', color: 'bg-gray-100 text-gray-700', description: 'Collecting applications' },
|
|
|
|
|
FILTERING: { label: 'Filtering', color: 'bg-amber-100 text-amber-700', description: 'AI + manual screening' },
|
|
|
|
|
EVALUATION: { label: 'Evaluation', color: 'bg-blue-100 text-blue-700', description: 'Jury evaluation & scoring' },
|
|
|
|
|
SUBMISSION: { label: 'Submission', color: 'bg-purple-100 text-purple-700', description: 'Document submission' },
|
|
|
|
|
MENTORING: { label: 'Mentoring', color: 'bg-teal-100 text-teal-700', description: 'Mentor-guided development' },
|
|
|
|
|
LIVE_FINAL: { label: 'Live Final', color: 'bg-red-100 text-red-700', description: 'Live presentations & voting' },
|
|
|
|
|
DELIBERATION: { label: 'Deliberation', color: 'bg-indigo-100 text-indigo-700', description: 'Final jury deliberation' },
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-16 09:20:02 +01:00
|
|
|
const stateColors: Record<string, string> = {
|
|
|
|
|
PENDING: 'bg-gray-400',
|
|
|
|
|
IN_PROGRESS: 'bg-blue-500',
|
|
|
|
|
PASSED: 'bg-green-500',
|
|
|
|
|
REJECTED: 'bg-red-500',
|
|
|
|
|
COMPLETED: 'bg-emerald-500',
|
|
|
|
|
WITHDRAWN: 'bg-orange-400',
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-16 09:20:02 +01:00
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
// Main Page Component
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
export default function RoundDetailPage() {
|
|
|
|
|
const params = useParams()
|
|
|
|
|
const roundId = params.roundId as string
|
|
|
|
|
|
|
|
|
|
const [config, setConfig] = useState<Record<string, unknown>>({})
|
|
|
|
|
const [hasChanges, setHasChanges] = useState(false)
|
2026-02-16 09:20:02 +01:00
|
|
|
const [activeTab, setActiveTab] = useState('overview')
|
|
|
|
|
const [previewSheetOpen, setPreviewSheetOpen] = useState(false)
|
|
|
|
|
const [exportOpen, setExportOpen] = useState(false)
|
|
|
|
|
const [advanceDialogOpen, setAdvanceDialogOpen] = useState(false)
|
AI category-aware evaluation: per-round config, file parsing, shortlist, advance flow
- Per-juror cap mode (HARD/SOFT/NONE) in add-member dialog and members table
- Jury invite flow: create user + add to group + send invitation from dialog
- Per-round config: notifyOnAdvance, aiParseFiles, startupAdvanceCount, conceptAdvanceCount
- Moved notify-on-advance from competition-level to per-round setting
- AI filtering: round-tagged files with newest-first sorting, optional file content extraction
- File content extractor service (pdf-parse for PDF, utf-8 for text files)
- AI shortlist runs independently per category (STARTUP / BUSINESS_CONCEPT)
- generateAIRecommendations tRPC endpoint with per-round config integration
- AI recommendations UI: trigger button, confirmation dialog, per-category results display
- Category-aware advance dialog: select/deselect projects by category with target caps
- STAGE_ACTIVE bug fix in assignment router
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:09:52 +01:00
|
|
|
const [aiRecommendations, setAiRecommendations] = useState<{
|
|
|
|
|
STARTUP: Array<{ projectId: string; rank: number; score: number; category: string; strengths: string[]; concerns: string[]; recommendation: string }>
|
|
|
|
|
BUSINESS_CONCEPT: Array<{ projectId: string; rank: number; score: number; category: string; strengths: string[]; concerns: string[]; recommendation: string }>
|
|
|
|
|
} | null>(null)
|
|
|
|
|
const [shortlistDialogOpen, setShortlistDialogOpen] = useState(false)
|
2026-02-16 12:46:01 +01:00
|
|
|
const [createJuryOpen, setCreateJuryOpen] = useState(false)
|
|
|
|
|
const [newJuryName, setNewJuryName] = useState('')
|
|
|
|
|
const [addMemberOpen, setAddMemberOpen] = useState(false)
|
2026-02-16 16:43:23 +01:00
|
|
|
const [closeAndAdvance, setCloseAndAdvance] = useState(false)
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
|
|
|
|
|
const utils = trpc.useUtils()
|
|
|
|
|
|
2026-02-16 09:20:02 +01:00
|
|
|
// ── Core data queries ──────────────────────────────────────────────────
|
2026-02-16 09:30:19 +01:00
|
|
|
const { data: round, isLoading } = trpc.round.getById.useQuery(
|
|
|
|
|
{ id: roundId },
|
2026-02-16 12:06:07 +01:00
|
|
|
{ refetchInterval: 15_000, refetchOnWindowFocus: true },
|
2026-02-16 09:30:19 +01:00
|
|
|
)
|
|
|
|
|
const { data: projectStates } = trpc.roundEngine.getProjectStates.useQuery(
|
|
|
|
|
{ roundId },
|
2026-02-16 12:06:07 +01:00
|
|
|
{ refetchInterval: 10_000, refetchOnWindowFocus: true },
|
2026-02-16 09:30:19 +01:00
|
|
|
)
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
|
2026-02-16 09:20:02 +01:00
|
|
|
const competitionId = round?.competitionId ?? ''
|
|
|
|
|
|
|
|
|
|
const { data: juryGroups } = trpc.juryGroup.list.useQuery(
|
|
|
|
|
{ competitionId },
|
2026-02-16 12:06:07 +01:00
|
|
|
{ enabled: !!competitionId, refetchInterval: 30_000, refetchOnWindowFocus: true },
|
2026-02-16 09:30:19 +01:00
|
|
|
)
|
|
|
|
|
const { data: fileRequirements } = trpc.file.listRequirements.useQuery(
|
|
|
|
|
{ roundId },
|
2026-02-16 12:06:07 +01:00
|
|
|
{ refetchInterval: 15_000, refetchOnWindowFocus: true },
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Fetch awards linked to this round
|
2026-02-16 09:20:02 +01:00
|
|
|
const { data: competition } = trpc.competition.getById.useQuery(
|
|
|
|
|
{ id: competitionId },
|
2026-02-16 09:30:19 +01:00
|
|
|
{ enabled: !!competitionId, refetchInterval: 60_000 },
|
2026-02-16 09:20:02 +01:00
|
|
|
)
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
const programId = competition?.programId
|
|
|
|
|
const { data: awards } = trpc.specialAward.list.useQuery(
|
|
|
|
|
{ programId: programId! },
|
2026-02-16 09:30:19 +01:00
|
|
|
{ enabled: !!programId, refetchInterval: 60_000 },
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
)
|
2026-02-16 09:20:02 +01:00
|
|
|
const roundAwards = awards?.filter((a) => a.evaluationRoundId === roundId) ?? []
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
|
2026-02-16 09:20:02 +01:00
|
|
|
// Sync config from server when not dirty
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
if (round && !hasChanges) {
|
|
|
|
|
const roundConfig = (round.configJson as Record<string, unknown>) ?? {}
|
|
|
|
|
if (JSON.stringify(roundConfig) !== JSON.stringify(config)) {
|
|
|
|
|
setConfig(roundConfig)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-16 09:20:02 +01:00
|
|
|
// ── Mutations ──────────────────────────────────────────────────────────
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
const updateMutation = trpc.round.update.useMutation({
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
utils.round.getById.invalidate({ id: roundId })
|
|
|
|
|
toast.success('Round configuration saved')
|
|
|
|
|
setHasChanges(false)
|
|
|
|
|
},
|
|
|
|
|
onError: (err) => toast.error(err.message),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const activateMutation = trpc.roundEngine.activate.useMutation({
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
utils.round.getById.invalidate({ id: roundId })
|
|
|
|
|
toast.success('Round activated')
|
|
|
|
|
},
|
|
|
|
|
onError: (err) => toast.error(err.message),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const closeMutation = trpc.roundEngine.close.useMutation({
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
utils.round.getById.invalidate({ id: roundId })
|
|
|
|
|
toast.success('Round closed')
|
2026-02-16 16:43:23 +01:00
|
|
|
if (closeAndAdvance) {
|
|
|
|
|
setCloseAndAdvance(false)
|
|
|
|
|
// Small delay to let cache invalidation complete before opening dialog
|
|
|
|
|
setTimeout(() => setAdvanceDialogOpen(true), 300)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
onError: (err) => {
|
|
|
|
|
setCloseAndAdvance(false)
|
|
|
|
|
toast.error(err.message)
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-16 12:06:07 +01:00
|
|
|
const reopenMutation = trpc.roundEngine.reopen.useMutation({
|
|
|
|
|
onSuccess: (data) => {
|
|
|
|
|
utils.round.getById.invalidate({ id: roundId })
|
|
|
|
|
utils.roundEngine.getProjectStates.invalidate({ roundId })
|
|
|
|
|
const msg = data.pausedRounds?.length
|
|
|
|
|
? `Round reopened. Paused: ${data.pausedRounds.join(', ')}`
|
|
|
|
|
: 'Round reopened'
|
|
|
|
|
toast.success(msg)
|
|
|
|
|
},
|
|
|
|
|
onError: (err) => toast.error(err.message),
|
|
|
|
|
})
|
|
|
|
|
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
const archiveMutation = trpc.roundEngine.archive.useMutation({
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
utils.round.getById.invalidate({ id: roundId })
|
|
|
|
|
toast.success('Round archived')
|
|
|
|
|
},
|
|
|
|
|
onError: (err) => toast.error(err.message),
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-16 09:20:02 +01:00
|
|
|
const assignJuryMutation = trpc.round.update.useMutation({
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
utils.round.getById.invalidate({ id: roundId })
|
2026-02-16 12:46:01 +01:00
|
|
|
utils.juryGroup.list.invalidate({ competitionId })
|
2026-02-16 09:20:02 +01:00
|
|
|
toast.success('Jury group updated')
|
|
|
|
|
},
|
|
|
|
|
onError: (err) => toast.error(err.message),
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-16 12:46:01 +01:00
|
|
|
// Jury group detail query (for the assigned group)
|
|
|
|
|
const juryGroupId = round?.juryGroupId ?? ''
|
|
|
|
|
const { data: juryGroupDetail } = trpc.juryGroup.getById.useQuery(
|
|
|
|
|
{ id: juryGroupId },
|
|
|
|
|
{ enabled: !!juryGroupId, refetchInterval: 10_000 },
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const createJuryMutation = trpc.juryGroup.create.useMutation({
|
|
|
|
|
onSuccess: (newGroup) => {
|
|
|
|
|
utils.juryGroup.list.invalidate({ competitionId })
|
|
|
|
|
// Auto-assign the new jury group to this round
|
|
|
|
|
assignJuryMutation.mutate({ id: roundId, juryGroupId: newGroup.id })
|
|
|
|
|
toast.success(`Jury "${newGroup.name}" created and assigned`)
|
|
|
|
|
setCreateJuryOpen(false)
|
|
|
|
|
setNewJuryName('')
|
|
|
|
|
},
|
|
|
|
|
onError: (err) => toast.error(err.message),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const deleteJuryMutation = trpc.juryGroup.delete.useMutation({
|
|
|
|
|
onSuccess: (result) => {
|
|
|
|
|
utils.juryGroup.list.invalidate({ competitionId })
|
|
|
|
|
utils.round.getById.invalidate({ id: roundId })
|
|
|
|
|
toast.success(`Jury "${result.name}" deleted`)
|
|
|
|
|
},
|
|
|
|
|
onError: (err) => toast.error(err.message),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const removeJuryMemberMutation = trpc.juryGroup.removeMember.useMutation({
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
if (juryGroupId) utils.juryGroup.getById.invalidate({ id: juryGroupId })
|
|
|
|
|
toast.success('Member removed')
|
|
|
|
|
},
|
|
|
|
|
onError: (err) => toast.error(err.message),
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-16 09:20:02 +01:00
|
|
|
const advanceMutation = trpc.round.advanceProjects.useMutation({
|
|
|
|
|
onSuccess: (data) => {
|
|
|
|
|
utils.round.getById.invalidate({ id: roundId })
|
|
|
|
|
utils.roundEngine.getProjectStates.invalidate({ roundId })
|
|
|
|
|
toast.success(`Advanced ${data.advancedCount} project(s) to ${data.targetRoundName}`)
|
|
|
|
|
setAdvanceDialogOpen(false)
|
|
|
|
|
},
|
|
|
|
|
onError: (err) => toast.error(err.message),
|
|
|
|
|
})
|
|
|
|
|
|
AI category-aware evaluation: per-round config, file parsing, shortlist, advance flow
- Per-juror cap mode (HARD/SOFT/NONE) in add-member dialog and members table
- Jury invite flow: create user + add to group + send invitation from dialog
- Per-round config: notifyOnAdvance, aiParseFiles, startupAdvanceCount, conceptAdvanceCount
- Moved notify-on-advance from competition-level to per-round setting
- AI filtering: round-tagged files with newest-first sorting, optional file content extraction
- File content extractor service (pdf-parse for PDF, utf-8 for text files)
- AI shortlist runs independently per category (STARTUP / BUSINESS_CONCEPT)
- generateAIRecommendations tRPC endpoint with per-round config integration
- AI recommendations UI: trigger button, confirmation dialog, per-category results display
- Category-aware advance dialog: select/deselect projects by category with target caps
- STAGE_ACTIVE bug fix in assignment router
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:09:52 +01:00
|
|
|
const shortlistMutation = trpc.round.generateAIRecommendations.useMutation({
|
|
|
|
|
onSuccess: (data) => {
|
|
|
|
|
if (data.success) {
|
|
|
|
|
setAiRecommendations(data.recommendations)
|
|
|
|
|
toast.success(
|
|
|
|
|
`AI recommendations generated: ${data.recommendations.STARTUP.length} startups, ${data.recommendations.BUSINESS_CONCEPT.length} concepts` +
|
|
|
|
|
(data.tokensUsed ? ` (${data.tokensUsed} tokens)` : ''),
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
toast.error(data.errors?.join('; ') || 'AI shortlist failed')
|
|
|
|
|
}
|
|
|
|
|
setShortlistDialogOpen(false)
|
|
|
|
|
},
|
|
|
|
|
onError: (err) => {
|
|
|
|
|
toast.error(err.message)
|
|
|
|
|
setShortlistDialogOpen(false)
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
2026-02-16 12:06:07 +01:00
|
|
|
const isTransitioning = activateMutation.isPending || closeMutation.isPending || reopenMutation.isPending || archiveMutation.isPending
|
2026-02-16 09:20:02 +01:00
|
|
|
|
|
|
|
|
const handleConfigChange = useCallback((newConfig: Record<string, unknown>) => {
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
setConfig(newConfig)
|
|
|
|
|
setHasChanges(true)
|
2026-02-16 09:20:02 +01:00
|
|
|
}, [])
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
|
2026-02-16 09:20:02 +01:00
|
|
|
const handleSave = useCallback(() => {
|
|
|
|
|
updateMutation.mutate({ id: roundId, configJson: config })
|
|
|
|
|
}, [roundId, config, updateMutation])
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
|
2026-02-16 09:20:02 +01:00
|
|
|
// ── Computed values ────────────────────────────────────────────────────
|
|
|
|
|
const projectCount = round?._count?.projectRoundStates ?? 0
|
|
|
|
|
const stateCounts = useMemo(() =>
|
|
|
|
|
projectStates?.reduce((acc: Record<string, number>, ps: any) => {
|
|
|
|
|
acc[ps.state] = (acc[ps.state] || 0) + 1
|
|
|
|
|
return acc
|
|
|
|
|
}, {} as Record<string, number>) ?? {},
|
|
|
|
|
[projectStates])
|
|
|
|
|
const passedCount = stateCounts['PASSED'] ?? 0
|
|
|
|
|
const juryGroup = round?.juryGroup
|
|
|
|
|
const juryMemberCount = juryGroup?.members?.length ?? 0
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
|
2026-02-16 09:20:02 +01:00
|
|
|
const isFiltering = round?.roundType === 'FILTERING'
|
|
|
|
|
const isEvaluation = round?.roundType === 'EVALUATION'
|
2026-02-16 16:43:23 +01:00
|
|
|
const hasJury = ['EVALUATION', 'LIVE_FINAL', 'DELIBERATION'].includes(round?.roundType ?? '')
|
|
|
|
|
const hasAwards = hasJury
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
|
2026-02-16 09:20:02 +01:00
|
|
|
const poolLink = `/admin/projects/pool?roundId=${roundId}&competitionId=${competitionId}` as Route
|
|
|
|
|
|
|
|
|
|
// ── Loading state ──────────────────────────────────────────────────────
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
if (isLoading) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
2026-02-16 12:38:28 +01:00
|
|
|
{/* Header skeleton — dark gradient placeholder */}
|
|
|
|
|
<div className="rounded-xl bg-gradient-to-r from-[#053d57]/20 to-[#0a5a7c]/20 p-6 animate-pulse">
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<Skeleton className="h-8 w-8 rounded bg-white/20" />
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Skeleton className="h-7 w-64 bg-white/20" />
|
|
|
|
|
<Skeleton className="h-4 w-40 bg-white/20" />
|
|
|
|
|
</div>
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-02-16 09:20:02 +01:00
|
|
|
<div className="grid gap-3 grid-cols-2 sm:grid-cols-4">
|
2026-02-16 12:38:28 +01:00
|
|
|
{[1, 2, 3, 4].map((i) => <Skeleton key={i} className="h-28 rounded-lg" />)}
|
2026-02-16 09:20:02 +01:00
|
|
|
</div>
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
<Skeleton className="h-10 w-full" />
|
2026-02-16 12:38:28 +01:00
|
|
|
<Skeleton className="h-96 w-full rounded-lg" />
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!round) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
<div className="flex items-center gap-3">
|
2026-02-16 09:20:02 +01:00
|
|
|
<Link href={'/admin/rounds' as Route}>
|
|
|
|
|
<Button variant="ghost" size="icon" className="h-8 w-8" aria-label="Back">
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
<ArrowLeft className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</Link>
|
|
|
|
|
<div>
|
|
|
|
|
<h1 className="text-xl font-bold">Round Not Found</h1>
|
2026-02-16 09:20:02 +01:00
|
|
|
<p className="text-sm text-muted-foreground">This round does not exist.</p>
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-16 09:20:02 +01:00
|
|
|
const status = round.status as keyof typeof roundStatusConfig
|
|
|
|
|
const statusCfg = roundStatusConfig[status] || roundStatusConfig.ROUND_DRAFT
|
|
|
|
|
const typeCfg = roundTypeConfig[round.roundType] || roundTypeConfig.INTAKE
|
|
|
|
|
|
|
|
|
|
// ── Readiness checklist ────────────────────────────────────────────────
|
|
|
|
|
const readinessItems = [
|
|
|
|
|
{
|
|
|
|
|
label: 'Projects assigned',
|
|
|
|
|
ready: projectCount > 0,
|
|
|
|
|
detail: projectCount > 0 ? `${projectCount} projects` : 'No projects yet',
|
|
|
|
|
action: projectCount === 0 ? poolLink : undefined,
|
|
|
|
|
actionLabel: 'Assign Projects',
|
|
|
|
|
},
|
2026-02-16 16:43:23 +01:00
|
|
|
...(hasJury
|
2026-02-16 09:20:02 +01:00
|
|
|
? [{
|
|
|
|
|
label: 'Jury group set',
|
|
|
|
|
ready: !!juryGroup,
|
|
|
|
|
detail: juryGroup ? `${juryGroup.name} (${juryMemberCount} members)` : 'No jury group assigned',
|
|
|
|
|
action: undefined as Route | undefined,
|
|
|
|
|
actionLabel: undefined as string | undefined,
|
|
|
|
|
}]
|
|
|
|
|
: []),
|
|
|
|
|
{
|
|
|
|
|
label: 'Dates configured',
|
|
|
|
|
ready: !!round.windowOpenAt && !!round.windowCloseAt,
|
|
|
|
|
detail:
|
|
|
|
|
round.windowOpenAt && round.windowCloseAt
|
|
|
|
|
? `${new Date(round.windowOpenAt).toLocaleDateString()} \u2014 ${new Date(round.windowCloseAt).toLocaleDateString()}`
|
|
|
|
|
: 'No dates set \u2014 configure in Config tab',
|
|
|
|
|
action: undefined as Route | undefined,
|
|
|
|
|
actionLabel: undefined as string | undefined,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
label: 'File requirements set',
|
|
|
|
|
ready: (fileRequirements?.length ?? 0) > 0,
|
|
|
|
|
detail:
|
|
|
|
|
(fileRequirements?.length ?? 0) > 0
|
|
|
|
|
? `${fileRequirements?.length} requirement(s)`
|
|
|
|
|
: 'No file requirements \u2014 configure in Config tab',
|
|
|
|
|
action: undefined as Route | undefined,
|
|
|
|
|
actionLabel: undefined as string | undefined,
|
|
|
|
|
},
|
|
|
|
|
]
|
|
|
|
|
const readyCount = readinessItems.filter((i) => i.ready).length
|
|
|
|
|
|
|
|
|
|
// ═════════════════════════════════════════════════════════════════════════
|
|
|
|
|
// Render
|
|
|
|
|
// ═════════════════════════════════════════════════════════════════════════
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
2026-02-16 12:38:28 +01:00
|
|
|
{/* ===== HEADER — Dark Blue gradient banner ===== */}
|
|
|
|
|
<motion.div
|
|
|
|
|
initial={{ opacity: 0, y: -8 }}
|
|
|
|
|
animate={{ opacity: 1, y: 0 }}
|
|
|
|
|
transition={{ duration: 0.4, ease: 'easeOut' }}
|
|
|
|
|
className="rounded-xl bg-gradient-to-r from-[#053d57] to-[#0a5a7c] p-5 sm:p-6 text-white shadow-lg"
|
|
|
|
|
>
|
|
|
|
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
|
|
|
|
<div className="flex items-start gap-3 min-w-0">
|
|
|
|
|
<Link href={'/admin/rounds' as Route} className="mt-0.5 shrink-0">
|
|
|
|
|
<Button variant="ghost" size="icon" className="h-8 w-8 text-white/80 hover:text-white hover:bg-white/10" aria-label="Back to rounds">
|
|
|
|
|
<ArrowLeft className="h-4 w-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</Link>
|
|
|
|
|
<div className="min-w-0">
|
|
|
|
|
<div className="flex flex-wrap items-center gap-2.5">
|
|
|
|
|
<h1 className="text-xl font-bold tracking-tight truncate">{round.name}</h1>
|
|
|
|
|
<Badge variant="secondary" className="text-xs shrink-0 bg-white/15 text-white border-white/20 hover:bg-white/20">
|
|
|
|
|
{typeCfg.label}
|
|
|
|
|
</Badge>
|
|
|
|
|
|
|
|
|
|
{/* Status dropdown */}
|
|
|
|
|
<DropdownMenu>
|
|
|
|
|
<DropdownMenuTrigger asChild>
|
|
|
|
|
<button
|
|
|
|
|
className={cn(
|
|
|
|
|
'inline-flex items-center gap-1.5 text-[11px] font-medium px-2.5 py-1 rounded-full transition-colors shrink-0',
|
|
|
|
|
'bg-white/15 text-white hover:bg-white/25',
|
|
|
|
|
)}
|
2026-02-16 09:20:02 +01:00
|
|
|
>
|
2026-02-16 12:38:28 +01:00
|
|
|
<span className={cn('h-1.5 w-1.5 rounded-full', statusCfg.dotClass)} />
|
|
|
|
|
{statusCfg.label}
|
|
|
|
|
<ChevronDown className="h-3 w-3" />
|
|
|
|
|
</button>
|
|
|
|
|
</DropdownMenuTrigger>
|
|
|
|
|
<DropdownMenuContent align="start">
|
|
|
|
|
{status === 'ROUND_DRAFT' && (
|
2026-02-16 09:20:02 +01:00
|
|
|
<DropdownMenuItem
|
2026-02-16 12:38:28 +01:00
|
|
|
onClick={() => activateMutation.mutate({ roundId })}
|
2026-02-16 09:20:02 +01:00
|
|
|
disabled={isTransitioning}
|
|
|
|
|
>
|
|
|
|
|
<Play className="h-4 w-4 mr-2 text-emerald-600" />
|
2026-02-16 12:38:28 +01:00
|
|
|
Activate Round
|
2026-02-16 09:20:02 +01:00
|
|
|
</DropdownMenuItem>
|
2026-02-16 12:38:28 +01:00
|
|
|
)}
|
|
|
|
|
{status === 'ROUND_ACTIVE' && (
|
2026-02-16 09:20:02 +01:00
|
|
|
<DropdownMenuItem
|
2026-02-16 12:38:28 +01:00
|
|
|
onClick={() => closeMutation.mutate({ roundId })}
|
2026-02-16 09:20:02 +01:00
|
|
|
disabled={isTransitioning}
|
|
|
|
|
>
|
2026-02-16 12:38:28 +01:00
|
|
|
<Square className="h-4 w-4 mr-2 text-blue-600" />
|
|
|
|
|
Close Round
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
</DropdownMenuItem>
|
2026-02-16 12:38:28 +01:00
|
|
|
)}
|
|
|
|
|
{status === 'ROUND_CLOSED' && (
|
|
|
|
|
<>
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
onClick={() => reopenMutation.mutate({ roundId })}
|
|
|
|
|
disabled={isTransitioning}
|
|
|
|
|
>
|
|
|
|
|
<Play className="h-4 w-4 mr-2 text-emerald-600" />
|
|
|
|
|
Reopen Round
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
<DropdownMenuSeparator />
|
|
|
|
|
<DropdownMenuItem
|
|
|
|
|
onClick={() => archiveMutation.mutate({ roundId })}
|
|
|
|
|
disabled={isTransitioning}
|
|
|
|
|
>
|
|
|
|
|
<Archive className="h-4 w-4 mr-2" />
|
|
|
|
|
Archive Round
|
|
|
|
|
</DropdownMenuItem>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
{isTransitioning && (
|
|
|
|
|
<div className="flex items-center gap-2 px-2 py-1.5 text-xs text-muted-foreground">
|
|
|
|
|
<Loader2 className="h-3 w-3 animate-spin" />
|
|
|
|
|
Updating...
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</DropdownMenuContent>
|
|
|
|
|
</DropdownMenu>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-sm text-white/60 mt-1">{typeCfg.description}</p>
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-16 12:38:28 +01:00
|
|
|
{/* Action buttons */}
|
|
|
|
|
<div className="flex items-center gap-2 shrink-0 flex-wrap">
|
|
|
|
|
{hasChanges && (
|
|
|
|
|
<Button size="sm" onClick={handleSave} disabled={updateMutation.isPending} className="bg-white text-[#053d57] hover:bg-white/90">
|
|
|
|
|
{updateMutation.isPending ? (
|
|
|
|
|
<Loader2 className="h-4 w-4 mr-1.5 animate-spin" />
|
|
|
|
|
) : (
|
|
|
|
|
<Save className="h-4 w-4 mr-1.5" />
|
|
|
|
|
)}
|
|
|
|
|
Save Config
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
<Link href={poolLink}>
|
2026-02-16 13:21:35 +01:00
|
|
|
<Button variant="outline" size="sm" className="border-white/40 bg-white/15 text-white hover:bg-white/30 hover:text-white">
|
2026-02-16 12:38:28 +01:00
|
|
|
<Layers className="h-4 w-4 mr-1.5" />
|
|
|
|
|
Project Pool
|
|
|
|
|
</Button>
|
|
|
|
|
</Link>
|
|
|
|
|
</div>
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
</div>
|
2026-02-16 12:38:28 +01:00
|
|
|
</motion.div>
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
|
2026-02-16 12:38:28 +01:00
|
|
|
{/* ===== STATS BAR — Accent-bordered cards ===== */}
|
2026-02-16 09:20:02 +01:00
|
|
|
<div className="grid gap-3 grid-cols-2 sm:grid-cols-4">
|
|
|
|
|
{/* Projects */}
|
2026-02-16 12:38:28 +01:00
|
|
|
<AnimatedCard index={0}>
|
|
|
|
|
<Card className="border-l-4 border-l-[#557f8c] hover:shadow-md transition-shadow">
|
|
|
|
|
<CardContent className="pt-4 pb-3">
|
|
|
|
|
<div className="flex items-center gap-2.5">
|
|
|
|
|
<div className="rounded-full bg-[#557f8c]/10 p-1.5">
|
|
|
|
|
<Layers className="h-4 w-4 text-[#557f8c]" />
|
|
|
|
|
</div>
|
|
|
|
|
<span className="text-sm font-medium text-muted-foreground">Projects</span>
|
Rounds overhaul: full CRUD submission windows, scheduling UI, analytics, design refresh
- Fix special award FK crash: replace 4x raw auditLog.create with logAudit() helper
- Add updateSubmissionWindow + deleteSubmissionWindow mutations to round router
- Add per-round analytics (_count, juryGroup) to competition.getById
- Remove redundant acceptedCategories from intake config
- Rewrite submission window manager with full CRUD, all fields, date pickers
- Add round scheduling card (open/close dates) to round detail page
- Add project count, assignment count, jury group to round list cards
- Visual redesign: pipeline view, brand colors, progress bars, enhanced cards
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:07:09 +01:00
|
|
|
</div>
|
2026-02-16 12:38:28 +01:00
|
|
|
<p className="text-3xl font-bold mt-2">{projectCount}</p>
|
|
|
|
|
<div className="flex flex-wrap gap-1.5 mt-1.5">
|
|
|
|
|
{Object.entries(stateCounts).map(([state, count]) => (
|
|
|
|
|
<span key={state} className="text-[10px] text-muted-foreground">
|
|
|
|
|
{String(count)} {state.toLowerCase().replace('_', ' ')}
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</AnimatedCard>
|
Rounds overhaul: full CRUD submission windows, scheduling UI, analytics, design refresh
- Fix special award FK crash: replace 4x raw auditLog.create with logAudit() helper
- Add updateSubmissionWindow + deleteSubmissionWindow mutations to round router
- Add per-round analytics (_count, juryGroup) to competition.getById
- Remove redundant acceptedCategories from intake config
- Rewrite submission window manager with full CRUD, all fields, date pickers
- Add round scheduling card (open/close dates) to round detail page
- Add project count, assignment count, jury group to round list cards
- Visual redesign: pipeline view, brand colors, progress bars, enhanced cards
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:07:09 +01:00
|
|
|
|
2026-02-16 09:20:02 +01:00
|
|
|
{/* Jury (with inline group selector) */}
|
2026-02-16 12:38:28 +01:00
|
|
|
<AnimatedCard index={1}>
|
|
|
|
|
<Card className="border-l-4 border-l-purple-500 hover:shadow-md transition-shadow">
|
|
|
|
|
<CardContent className="pt-4 pb-3">
|
|
|
|
|
<div className="flex items-center gap-2.5 mb-1" data-jury-select>
|
|
|
|
|
<div className="rounded-full bg-purple-50 p-1.5">
|
|
|
|
|
<Users className="h-4 w-4 text-purple-500" />
|
|
|
|
|
</div>
|
|
|
|
|
<span className="text-sm font-medium text-muted-foreground">Jury</span>
|
|
|
|
|
</div>
|
|
|
|
|
{juryGroups && juryGroups.length > 0 ? (
|
|
|
|
|
<Select
|
|
|
|
|
value={round.juryGroupId ?? '__none__'}
|
|
|
|
|
onValueChange={(value) => {
|
|
|
|
|
assignJuryMutation.mutate({
|
|
|
|
|
id: roundId,
|
|
|
|
|
juryGroupId: value === '__none__' ? null : value,
|
|
|
|
|
})
|
|
|
|
|
}}
|
|
|
|
|
disabled={assignJuryMutation.isPending}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger className="h-8 text-xs mt-1">
|
|
|
|
|
<SelectValue placeholder="Select jury group..." />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="__none__">No jury assigned</SelectItem>
|
|
|
|
|
{juryGroups.map((jg: any) => (
|
|
|
|
|
<SelectItem key={jg.id} value={jg.id}>
|
|
|
|
|
{jg.name} ({jg._count?.members ?? 0} members)
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
) : juryGroup ? (
|
|
|
|
|
<>
|
|
|
|
|
<p className="text-3xl font-bold mt-2">{juryMemberCount}</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground truncate">{juryGroup.name}</p>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<p className="text-3xl font-bold mt-2 text-muted-foreground">—</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground">No jury groups yet</p>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</AnimatedCard>
|
Rounds overhaul: full CRUD submission windows, scheduling UI, analytics, design refresh
- Fix special award FK crash: replace 4x raw auditLog.create with logAudit() helper
- Add updateSubmissionWindow + deleteSubmissionWindow mutations to round router
- Add per-round analytics (_count, juryGroup) to competition.getById
- Remove redundant acceptedCategories from intake config
- Rewrite submission window manager with full CRUD, all fields, date pickers
- Add round scheduling card (open/close dates) to round detail page
- Add project count, assignment count, jury group to round list cards
- Visual redesign: pipeline view, brand colors, progress bars, enhanced cards
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:07:09 +01:00
|
|
|
|
2026-02-16 09:20:02 +01:00
|
|
|
{/* Window */}
|
2026-02-16 12:38:28 +01:00
|
|
|
<AnimatedCard index={2}>
|
|
|
|
|
<Card className="border-l-4 border-l-emerald-500 hover:shadow-md transition-shadow">
|
|
|
|
|
<CardContent className="pt-4 pb-3">
|
|
|
|
|
<div className="flex items-center gap-2.5">
|
|
|
|
|
<div className="rounded-full bg-emerald-50 p-1.5">
|
|
|
|
|
<CalendarDays className="h-4 w-4 text-emerald-500" />
|
|
|
|
|
</div>
|
|
|
|
|
<span className="text-sm font-medium text-muted-foreground">Window</span>
|
|
|
|
|
</div>
|
|
|
|
|
{round.windowOpenAt || round.windowCloseAt ? (
|
|
|
|
|
<>
|
|
|
|
|
<p className="text-sm font-bold mt-2">
|
|
|
|
|
{round.windowOpenAt
|
|
|
|
|
? new Date(round.windowOpenAt).toLocaleDateString()
|
|
|
|
|
: 'No start'}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
{round.windowCloseAt
|
|
|
|
|
? `Closes ${new Date(round.windowCloseAt).toLocaleDateString()}`
|
|
|
|
|
: 'No deadline'}
|
|
|
|
|
</p>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<p className="text-3xl font-bold mt-2 text-muted-foreground">—</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground">No dates set</p>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</AnimatedCard>
|
Rounds overhaul: full CRUD submission windows, scheduling UI, analytics, design refresh
- Fix special award FK crash: replace 4x raw auditLog.create with logAudit() helper
- Add updateSubmissionWindow + deleteSubmissionWindow mutations to round router
- Add per-round analytics (_count, juryGroup) to competition.getById
- Remove redundant acceptedCategories from intake config
- Rewrite submission window manager with full CRUD, all fields, date pickers
- Add round scheduling card (open/close dates) to round detail page
- Add project count, assignment count, jury group to round list cards
- Visual redesign: pipeline view, brand colors, progress bars, enhanced cards
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:07:09 +01:00
|
|
|
|
2026-02-16 09:20:02 +01:00
|
|
|
{/* Advancement */}
|
2026-02-16 12:38:28 +01:00
|
|
|
<AnimatedCard index={3}>
|
|
|
|
|
<Card className="border-l-4 border-l-amber-500 hover:shadow-md transition-shadow">
|
|
|
|
|
<CardContent className="pt-4 pb-3">
|
|
|
|
|
<div className="flex items-center gap-2.5">
|
|
|
|
|
<div className="rounded-full bg-amber-50 p-1.5">
|
|
|
|
|
<BarChart3 className="h-4 w-4 text-amber-500" />
|
|
|
|
|
</div>
|
|
|
|
|
<span className="text-sm font-medium text-muted-foreground">Advancement</span>
|
|
|
|
|
</div>
|
|
|
|
|
{round.advancementRules && round.advancementRules.length > 0 ? (
|
|
|
|
|
<>
|
|
|
|
|
<p className="text-3xl font-bold mt-2">{round.advancementRules.length}</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
{round.advancementRules.map((r: any) => r.ruleType.replace('_', ' ').toLowerCase()).join(', ')}
|
|
|
|
|
</p>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<p className="text-3xl font-bold mt-2 text-muted-foreground">—</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground">Admin selection</p>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</AnimatedCard>
|
Rounds overhaul: full CRUD submission windows, scheduling UI, analytics, design refresh
- Fix special award FK crash: replace 4x raw auditLog.create with logAudit() helper
- Add updateSubmissionWindow + deleteSubmissionWindow mutations to round router
- Add per-round analytics (_count, juryGroup) to competition.getById
- Remove redundant acceptedCategories from intake config
- Rewrite submission window manager with full CRUD, all fields, date pickers
- Add round scheduling card (open/close dates) to round detail page
- Add project count, assignment count, jury group to round list cards
- Visual redesign: pipeline view, brand colors, progress bars, enhanced cards
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:07:09 +01:00
|
|
|
</div>
|
|
|
|
|
|
2026-02-16 12:38:28 +01:00
|
|
|
{/* ===== TABS — Underline style ===== */}
|
2026-02-16 09:20:02 +01:00
|
|
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
|
2026-02-16 12:38:28 +01:00
|
|
|
<div className="border-b overflow-x-auto">
|
|
|
|
|
<TabsList className="bg-transparent h-auto p-0 gap-0 w-full sm:w-auto">
|
|
|
|
|
{[
|
|
|
|
|
{ value: 'overview', label: 'Overview', icon: Zap },
|
|
|
|
|
{ value: 'projects', label: 'Projects', icon: Layers },
|
|
|
|
|
...(isFiltering ? [{ value: 'filtering', label: 'Filtering', icon: Shield }] : []),
|
|
|
|
|
...(isEvaluation ? [{ value: 'assignments', label: 'Assignments', icon: ClipboardList }] : []),
|
2026-02-16 16:43:23 +01:00
|
|
|
...(hasJury ? [{ value: 'jury', label: 'Jury', icon: Users }] : []),
|
2026-02-16 12:38:28 +01:00
|
|
|
{ value: 'config', label: 'Config', icon: Settings },
|
2026-02-16 16:43:23 +01:00
|
|
|
...(hasAwards ? [{ value: 'awards', label: 'Awards', icon: Trophy }] : []),
|
2026-02-16 12:38:28 +01:00
|
|
|
].map((tab) => (
|
|
|
|
|
<TabsTrigger
|
|
|
|
|
key={tab.value}
|
|
|
|
|
value={tab.value}
|
|
|
|
|
className={cn(
|
|
|
|
|
'relative rounded-none border-b-2 border-transparent px-4 py-2.5 text-sm font-medium transition-all',
|
|
|
|
|
'data-[state=active]:border-b-[#de0f1e] data-[state=active]:text-[#053d57] data-[state=active]:font-semibold data-[state=active]:shadow-none',
|
|
|
|
|
'text-muted-foreground hover:text-foreground',
|
|
|
|
|
'bg-transparent data-[state=active]:bg-transparent',
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<tab.icon className={cn('h-3.5 w-3.5 mr-1.5', activeTab === tab.value ? 'text-[#557f8c]' : '')} />
|
|
|
|
|
{tab.label}
|
|
|
|
|
{tab.value === 'awards' && roundAwards.length > 0 && (
|
|
|
|
|
<Badge variant="secondary" className="ml-1.5 h-5 min-w-5 text-[10px] px-1.5 bg-[#de0f1e] text-white">
|
|
|
|
|
{roundAwards.length}
|
|
|
|
|
</Badge>
|
|
|
|
|
)}
|
|
|
|
|
</TabsTrigger>
|
|
|
|
|
))}
|
|
|
|
|
</TabsList>
|
|
|
|
|
</div>
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
|
2026-02-16 09:20:02 +01:00
|
|
|
{/* ═══════════ OVERVIEW TAB ═══════════ */}
|
|
|
|
|
<TabsContent value="overview" className="space-y-6">
|
2026-02-16 12:38:28 +01:00
|
|
|
{/* Readiness Checklist with Progress Ring */}
|
|
|
|
|
<AnimatedCard index={0}>
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div className="flex items-center gap-4">
|
|
|
|
|
{/* SVG Progress Ring */}
|
|
|
|
|
<div className="relative h-14 w-14 shrink-0">
|
|
|
|
|
<svg className="h-14 w-14 -rotate-90" viewBox="0 0 56 56">
|
|
|
|
|
<circle cx="28" cy="28" r="24" fill="none" stroke="currentColor" strokeWidth="3" className="text-muted/30" />
|
|
|
|
|
<circle
|
|
|
|
|
cx="28" cy="28" r="24" fill="none"
|
|
|
|
|
strokeWidth="3" strokeLinecap="round"
|
|
|
|
|
stroke={readyCount === readinessItems.length ? '#10b981' : '#de0f1e'}
|
|
|
|
|
strokeDasharray={`${(readyCount / readinessItems.length) * 150.8} 150.8`}
|
|
|
|
|
className="transition-all duration-700"
|
|
|
|
|
/>
|
|
|
|
|
</svg>
|
|
|
|
|
<span className="absolute inset-0 flex items-center justify-center text-xs font-bold">
|
|
|
|
|
{readyCount}/{readinessItems.length}
|
|
|
|
|
</span>
|
2026-02-16 09:20:02 +01:00
|
|
|
</div>
|
2026-02-16 12:38:28 +01:00
|
|
|
<div>
|
|
|
|
|
<CardTitle className="text-base">Launch Readiness</CardTitle>
|
|
|
|
|
<CardDescription>
|
|
|
|
|
{readyCount === readinessItems.length
|
|
|
|
|
? 'All checks passed — ready to go'
|
|
|
|
|
: `${readinessItems.length - readyCount} item(s) remaining`}
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<Badge
|
|
|
|
|
variant={readyCount === readinessItems.length ? 'default' : 'secondary'}
|
|
|
|
|
className={cn(
|
|
|
|
|
'text-xs',
|
|
|
|
|
readyCount === readinessItems.length
|
|
|
|
|
? 'bg-emerald-100 text-emerald-700'
|
|
|
|
|
: 'bg-amber-100 text-amber-700',
|
2026-02-16 09:20:02 +01:00
|
|
|
)}
|
2026-02-16 12:38:28 +01:00
|
|
|
>
|
|
|
|
|
{readyCount === readinessItems.length ? 'Ready' : 'Incomplete'}
|
|
|
|
|
</Badge>
|
|
|
|
|
</div>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
{readinessItems.map((item) => (
|
|
|
|
|
<div key={item.label} className="flex items-start gap-3">
|
|
|
|
|
{item.ready ? (
|
|
|
|
|
<CheckCircle2 className="h-4 w-4 text-emerald-500 mt-0.5 shrink-0" />
|
|
|
|
|
) : (
|
|
|
|
|
<AlertTriangle className="h-4 w-4 text-amber-500 mt-0.5 shrink-0" />
|
|
|
|
|
)}
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
<p className={cn('text-sm font-medium', item.ready && 'text-muted-foreground line-through opacity-60')}>
|
|
|
|
|
{item.label}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground">{item.detail}</p>
|
|
|
|
|
</div>
|
|
|
|
|
{item.action && (
|
|
|
|
|
<Link href={item.action}>
|
|
|
|
|
<Button size="sm" className="shrink-0 text-xs bg-[#de0f1e] hover:bg-[#c00d1a] text-white">
|
|
|
|
|
{item.actionLabel}
|
|
|
|
|
</Button>
|
|
|
|
|
</Link>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</AnimatedCard>
|
|
|
|
|
|
|
|
|
|
{/* Quick Actions — Grouped & styled */}
|
|
|
|
|
<AnimatedCard index={1}>
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle className="text-base">Quick Actions</CardTitle>
|
|
|
|
|
<CardDescription>Common operations for this round</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="space-y-4">
|
|
|
|
|
{/* Round Control Group */}
|
|
|
|
|
{(status === 'ROUND_DRAFT' || status === 'ROUND_ACTIVE' || status === 'ROUND_CLOSED') && (
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground mb-2">Round Control</p>
|
|
|
|
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
|
|
|
{status === 'ROUND_DRAFT' && (
|
|
|
|
|
<AlertDialog>
|
|
|
|
|
<AlertDialogTrigger asChild>
|
|
|
|
|
<button className="flex items-start gap-3 p-4 rounded-lg border border-l-4 border-l-emerald-500 hover:-translate-y-0.5 hover:shadow-md transition-all text-left">
|
|
|
|
|
<Play className="h-5 w-5 text-emerald-600 mt-0.5 shrink-0" />
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-sm font-medium">Activate Round</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-0.5">
|
|
|
|
|
Start this round and allow project processing
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
</AlertDialogTrigger>
|
|
|
|
|
<AlertDialogContent>
|
|
|
|
|
<AlertDialogHeader>
|
|
|
|
|
<AlertDialogTitle>Activate this round?</AlertDialogTitle>
|
|
|
|
|
<AlertDialogDescription>
|
|
|
|
|
The round will go live. Projects can be processed and jury members will be able to see their assignments.
|
|
|
|
|
</AlertDialogDescription>
|
|
|
|
|
</AlertDialogHeader>
|
|
|
|
|
<AlertDialogFooter>
|
|
|
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
|
|
|
<AlertDialogAction onClick={() => activateMutation.mutate({ roundId })}>
|
|
|
|
|
Activate
|
|
|
|
|
</AlertDialogAction>
|
|
|
|
|
</AlertDialogFooter>
|
|
|
|
|
</AlertDialogContent>
|
|
|
|
|
</AlertDialog>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{status === 'ROUND_ACTIVE' && (
|
|
|
|
|
<AlertDialog>
|
|
|
|
|
<AlertDialogTrigger asChild>
|
|
|
|
|
<button className="flex items-start gap-3 p-4 rounded-lg border border-l-4 border-l-blue-500 hover:-translate-y-0.5 hover:shadow-md transition-all text-left">
|
|
|
|
|
<Square className="h-5 w-5 text-blue-600 mt-0.5 shrink-0" />
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-sm font-medium">Close Round</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-0.5">
|
|
|
|
|
Stop accepting changes and finalize results
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
</AlertDialogTrigger>
|
|
|
|
|
<AlertDialogContent>
|
|
|
|
|
<AlertDialogHeader>
|
|
|
|
|
<AlertDialogTitle>Close this round?</AlertDialogTitle>
|
|
|
|
|
<AlertDialogDescription>
|
|
|
|
|
No further changes will be accepted. You can reactivate later if needed.
|
|
|
|
|
{projectCount > 0 && (
|
|
|
|
|
<span className="block mt-2">
|
|
|
|
|
{projectCount} projects are currently in this round.
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</AlertDialogDescription>
|
|
|
|
|
</AlertDialogHeader>
|
|
|
|
|
<AlertDialogFooter>
|
|
|
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
|
|
|
<AlertDialogAction onClick={() => closeMutation.mutate({ roundId })}>
|
|
|
|
|
Close Round
|
|
|
|
|
</AlertDialogAction>
|
|
|
|
|
</AlertDialogFooter>
|
|
|
|
|
</AlertDialogContent>
|
|
|
|
|
</AlertDialog>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{status === 'ROUND_CLOSED' && (
|
|
|
|
|
<AlertDialog>
|
|
|
|
|
<AlertDialogTrigger asChild>
|
|
|
|
|
<button className="flex items-start gap-3 p-4 rounded-lg border border-l-4 border-l-amber-500 bg-amber-50/30 hover:-translate-y-0.5 hover:shadow-md transition-all text-left">
|
|
|
|
|
<RotateCcw className="h-5 w-5 text-amber-600 mt-0.5 shrink-0" />
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-sm font-medium">Reopen Round</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-0.5">
|
|
|
|
|
Reactivate this round. Any subsequent active rounds will be paused.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
</AlertDialogTrigger>
|
|
|
|
|
<AlertDialogContent>
|
|
|
|
|
<AlertDialogHeader>
|
|
|
|
|
<AlertDialogTitle>Reopen this round?</AlertDialogTitle>
|
|
|
|
|
<AlertDialogDescription>
|
|
|
|
|
The round will become active again. Any rounds after this one that are currently active will be paused (closed) automatically.
|
|
|
|
|
</AlertDialogDescription>
|
|
|
|
|
</AlertDialogHeader>
|
|
|
|
|
<AlertDialogFooter>
|
|
|
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
|
|
|
<AlertDialogAction onClick={() => reopenMutation.mutate({ roundId })}>
|
|
|
|
|
Reopen
|
|
|
|
|
</AlertDialogAction>
|
|
|
|
|
</AlertDialogFooter>
|
|
|
|
|
</AlertDialogContent>
|
|
|
|
|
</AlertDialog>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2026-02-16 09:20:02 +01:00
|
|
|
</div>
|
2026-02-16 12:38:28 +01:00
|
|
|
)}
|
2026-02-16 09:20:02 +01:00
|
|
|
|
2026-02-16 12:38:28 +01:00
|
|
|
{/* Project Management Group */}
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground mb-2">Project Management</p>
|
|
|
|
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
|
|
|
<Link href={poolLink}>
|
|
|
|
|
<button className="flex items-start gap-3 p-4 rounded-lg border hover:-translate-y-0.5 hover:shadow-md transition-all text-left w-full">
|
|
|
|
|
<Layers className="h-5 w-5 text-[#557f8c] mt-0.5 shrink-0" />
|
2026-02-16 09:20:02 +01:00
|
|
|
<div>
|
2026-02-16 12:38:28 +01:00
|
|
|
<p className="text-sm font-medium">Assign Projects</p>
|
2026-02-16 09:20:02 +01:00
|
|
|
<p className="text-xs text-muted-foreground mt-0.5">
|
2026-02-16 12:38:28 +01:00
|
|
|
Add projects from the pool to this round
|
2026-02-16 09:20:02 +01:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
2026-02-16 12:38:28 +01:00
|
|
|
</Link>
|
2026-02-16 09:20:02 +01:00
|
|
|
|
2026-02-16 12:38:28 +01:00
|
|
|
<button
|
|
|
|
|
onClick={() => setActiveTab('projects')}
|
|
|
|
|
className="flex items-start gap-3 p-4 rounded-lg border hover:-translate-y-0.5 hover:shadow-md transition-all text-left"
|
|
|
|
|
>
|
|
|
|
|
<BarChart3 className="h-5 w-5 text-[#557f8c] mt-0.5 shrink-0" />
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-sm font-medium">Manage Projects</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-0.5">
|
|
|
|
|
View, filter, and transition project states
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
|
2026-02-16 16:43:23 +01:00
|
|
|
{/* Advance projects (always visible when projects exist) */}
|
|
|
|
|
{projectCount > 0 && (
|
2026-02-16 12:38:28 +01:00
|
|
|
<button
|
2026-02-16 16:43:23 +01:00
|
|
|
onClick={() => passedCount > 0
|
|
|
|
|
? setAdvanceDialogOpen(true)
|
|
|
|
|
: toast.info('Mark projects as "Passed" first in the Projects tab')}
|
|
|
|
|
className={cn(
|
|
|
|
|
'flex items-start gap-3 p-4 rounded-lg border hover:-translate-y-0.5 hover:shadow-md transition-all text-left',
|
|
|
|
|
passedCount > 0
|
|
|
|
|
? 'border-l-4 border-l-emerald-500 bg-emerald-50/30'
|
|
|
|
|
: 'border-dashed opacity-60',
|
|
|
|
|
)}
|
2026-02-16 12:38:28 +01:00
|
|
|
>
|
2026-02-16 16:43:23 +01:00
|
|
|
<ArrowRight className={cn('h-5 w-5 mt-0.5 shrink-0', passedCount > 0 ? 'text-emerald-600' : 'text-muted-foreground')} />
|
2026-02-16 09:20:02 +01:00
|
|
|
<div>
|
2026-02-16 12:38:28 +01:00
|
|
|
<p className="text-sm font-medium">Advance Projects</p>
|
2026-02-16 09:20:02 +01:00
|
|
|
<p className="text-xs text-muted-foreground mt-0.5">
|
2026-02-16 16:43:23 +01:00
|
|
|
{passedCount > 0
|
|
|
|
|
? `Move ${passedCount} passed project(s) to the next round`
|
|
|
|
|
: 'Mark projects as "Passed" first, then advance'}
|
2026-02-16 09:20:02 +01:00
|
|
|
</p>
|
|
|
|
|
</div>
|
2026-02-16 16:43:23 +01:00
|
|
|
{passedCount > 0 && (
|
|
|
|
|
<Badge className="ml-auto shrink-0 bg-emerald-100 text-emerald-700 text-[10px]">{passedCount}</Badge>
|
|
|
|
|
)}
|
2026-02-16 09:20:02 +01:00
|
|
|
</button>
|
2026-02-16 12:38:28 +01:00
|
|
|
)}
|
2026-02-16 09:20:02 +01:00
|
|
|
|
2026-02-16 16:43:23 +01:00
|
|
|
{/* Close & Advance (active rounds with passed projects) */}
|
|
|
|
|
{status === 'ROUND_ACTIVE' && passedCount > 0 && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setCloseAndAdvance(true)
|
|
|
|
|
closeMutation.mutate({ roundId })
|
|
|
|
|
}}
|
|
|
|
|
disabled={isTransitioning}
|
|
|
|
|
className="flex items-start gap-3 p-4 rounded-lg border border-l-4 border-l-purple-500 bg-purple-50/30 hover:-translate-y-0.5 hover:shadow-md transition-all text-left"
|
|
|
|
|
>
|
|
|
|
|
<Square className="h-5 w-5 text-purple-600 mt-0.5 shrink-0" />
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-sm font-medium">Close & Advance</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-0.5">
|
|
|
|
|
Close this round and advance {passedCount} passed project(s) to the next round
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Jury assignment for rounds that use jury */}
|
|
|
|
|
{hasJury && !juryGroup && (
|
2026-02-16 12:38:28 +01:00
|
|
|
<button
|
|
|
|
|
onClick={() => {
|
|
|
|
|
const el = document.querySelector('[data-jury-select]')
|
|
|
|
|
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
|
|
|
}}
|
|
|
|
|
className="flex items-start gap-3 p-4 rounded-lg border border-l-4 border-l-amber-500 bg-amber-50/30 hover:-translate-y-0.5 hover:shadow-md transition-all text-left"
|
|
|
|
|
>
|
|
|
|
|
<UserPlus className="h-5 w-5 text-amber-600 mt-0.5 shrink-0" />
|
2026-02-16 12:06:07 +01:00
|
|
|
<div>
|
2026-02-16 12:38:28 +01:00
|
|
|
<p className="text-sm font-medium">Assign Jury Group</p>
|
2026-02-16 12:06:07 +01:00
|
|
|
<p className="text-xs text-muted-foreground mt-0.5">
|
2026-02-16 12:38:28 +01:00
|
|
|
No jury group assigned. Select one in the Jury card above.
|
2026-02-16 12:06:07 +01:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
2026-02-16 12:38:28 +01:00
|
|
|
)}
|
2026-02-16 09:20:02 +01:00
|
|
|
|
2026-02-16 12:38:28 +01:00
|
|
|
{/* Evaluation: manage assignments */}
|
|
|
|
|
{isEvaluation && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setActiveTab('assignments')}
|
|
|
|
|
className="flex items-start gap-3 p-4 rounded-lg border hover:-translate-y-0.5 hover:shadow-md transition-all text-left"
|
|
|
|
|
>
|
|
|
|
|
<ClipboardList className="h-5 w-5 text-[#557f8c] mt-0.5 shrink-0" />
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-sm font-medium">Manage Assignments</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-0.5">
|
|
|
|
|
Generate and review jury-project assignments
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-02-16 09:20:02 +01:00
|
|
|
|
2026-02-16 12:38:28 +01:00
|
|
|
{/* AI Tools Group */}
|
|
|
|
|
{((isFiltering || isEvaluation) && projectCount > 0) && (
|
2026-02-16 09:20:02 +01:00
|
|
|
<div>
|
2026-02-16 12:38:28 +01:00
|
|
|
<p className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground mb-2">AI Tools</p>
|
|
|
|
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
|
|
|
{isFiltering && (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setActiveTab('filtering')}
|
|
|
|
|
className="flex items-start gap-3 p-4 rounded-lg border bg-gradient-to-br from-purple-50/50 to-blue-50/50 hover:-translate-y-0.5 hover:shadow-md transition-all text-left"
|
|
|
|
|
>
|
|
|
|
|
<Shield className="h-5 w-5 text-purple-600 mt-0.5 shrink-0" />
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-sm font-medium">Run AI Filtering</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-0.5">
|
|
|
|
|
Screen projects with AI and manual review
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
AI category-aware evaluation: per-round config, file parsing, shortlist, advance flow
- Per-juror cap mode (HARD/SOFT/NONE) in add-member dialog and members table
- Jury invite flow: create user + add to group + send invitation from dialog
- Per-round config: notifyOnAdvance, aiParseFiles, startupAdvanceCount, conceptAdvanceCount
- Moved notify-on-advance from competition-level to per-round setting
- AI filtering: round-tagged files with newest-first sorting, optional file content extraction
- File content extractor service (pdf-parse for PDF, utf-8 for text files)
- AI shortlist runs independently per category (STARTUP / BUSINESS_CONCEPT)
- generateAIRecommendations tRPC endpoint with per-round config integration
- AI recommendations UI: trigger button, confirmation dialog, per-category results display
- Category-aware advance dialog: select/deselect projects by category with target caps
- STAGE_ACTIVE bug fix in assignment router
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:09:52 +01:00
|
|
|
|
2026-02-16 12:38:28 +01:00
|
|
|
<button
|
|
|
|
|
onClick={() => setShortlistDialogOpen(true)}
|
|
|
|
|
className="flex items-start gap-3 p-4 rounded-lg border bg-gradient-to-br from-purple-50/50 to-blue-50/50 hover:-translate-y-0.5 hover:shadow-md transition-all text-left"
|
|
|
|
|
disabled={shortlistMutation.isPending}
|
|
|
|
|
>
|
|
|
|
|
<Zap className="h-5 w-5 text-purple-600 mt-0.5 shrink-0" />
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-sm font-medium">
|
|
|
|
|
{shortlistMutation.isPending ? 'Generating...' : 'AI Recommendations'}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-0.5">
|
|
|
|
|
Generate ranked shortlist per category using AI analysis
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
2026-02-16 09:20:02 +01:00
|
|
|
</div>
|
2026-02-16 12:38:28 +01:00
|
|
|
</div>
|
2026-02-16 09:20:02 +01:00
|
|
|
)}
|
2026-02-16 12:38:28 +01:00
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</AnimatedCard>
|
2026-02-16 09:20:02 +01:00
|
|
|
|
AI category-aware evaluation: per-round config, file parsing, shortlist, advance flow
- Per-juror cap mode (HARD/SOFT/NONE) in add-member dialog and members table
- Jury invite flow: create user + add to group + send invitation from dialog
- Per-round config: notifyOnAdvance, aiParseFiles, startupAdvanceCount, conceptAdvanceCount
- Moved notify-on-advance from competition-level to per-round setting
- AI filtering: round-tagged files with newest-first sorting, optional file content extraction
- File content extractor service (pdf-parse for PDF, utf-8 for text files)
- AI shortlist runs independently per category (STARTUP / BUSINESS_CONCEPT)
- generateAIRecommendations tRPC endpoint with per-round config integration
- AI recommendations UI: trigger button, confirmation dialog, per-category results display
- Category-aware advance dialog: select/deselect projects by category with target caps
- STAGE_ACTIVE bug fix in assignment router
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:09:52 +01:00
|
|
|
{/* Advance Projects Dialog */}
|
|
|
|
|
<AdvanceProjectsDialog
|
|
|
|
|
open={advanceDialogOpen}
|
|
|
|
|
onOpenChange={setAdvanceDialogOpen}
|
|
|
|
|
roundId={roundId}
|
|
|
|
|
projectStates={projectStates}
|
|
|
|
|
config={config}
|
|
|
|
|
advanceMutation={advanceMutation}
|
2026-02-16 16:43:23 +01:00
|
|
|
competitionRounds={competition?.rounds?.map((r: any) => ({
|
|
|
|
|
id: r.id,
|
|
|
|
|
name: r.name,
|
|
|
|
|
sortOrder: r.sortOrder,
|
|
|
|
|
roundType: r.roundType,
|
|
|
|
|
}))}
|
|
|
|
|
currentSortOrder={round?.sortOrder}
|
AI category-aware evaluation: per-round config, file parsing, shortlist, advance flow
- Per-juror cap mode (HARD/SOFT/NONE) in add-member dialog and members table
- Jury invite flow: create user + add to group + send invitation from dialog
- Per-round config: notifyOnAdvance, aiParseFiles, startupAdvanceCount, conceptAdvanceCount
- Moved notify-on-advance from competition-level to per-round setting
- AI filtering: round-tagged files with newest-first sorting, optional file content extraction
- File content extractor service (pdf-parse for PDF, utf-8 for text files)
- AI shortlist runs independently per category (STARTUP / BUSINESS_CONCEPT)
- generateAIRecommendations tRPC endpoint with per-round config integration
- AI recommendations UI: trigger button, confirmation dialog, per-category results display
- Category-aware advance dialog: select/deselect projects by category with target caps
- STAGE_ACTIVE bug fix in assignment router
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:09:52 +01:00
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* AI Shortlist Confirmation Dialog */}
|
|
|
|
|
<AlertDialog open={shortlistDialogOpen} onOpenChange={setShortlistDialogOpen}>
|
2026-02-16 09:20:02 +01:00
|
|
|
<AlertDialogContent>
|
|
|
|
|
<AlertDialogHeader>
|
AI category-aware evaluation: per-round config, file parsing, shortlist, advance flow
- Per-juror cap mode (HARD/SOFT/NONE) in add-member dialog and members table
- Jury invite flow: create user + add to group + send invitation from dialog
- Per-round config: notifyOnAdvance, aiParseFiles, startupAdvanceCount, conceptAdvanceCount
- Moved notify-on-advance from competition-level to per-round setting
- AI filtering: round-tagged files with newest-first sorting, optional file content extraction
- File content extractor service (pdf-parse for PDF, utf-8 for text files)
- AI shortlist runs independently per category (STARTUP / BUSINESS_CONCEPT)
- generateAIRecommendations tRPC endpoint with per-round config integration
- AI recommendations UI: trigger button, confirmation dialog, per-category results display
- Category-aware advance dialog: select/deselect projects by category with target caps
- STAGE_ACTIVE bug fix in assignment router
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:09:52 +01:00
|
|
|
<AlertDialogTitle>Generate AI Recommendations?</AlertDialogTitle>
|
2026-02-16 09:20:02 +01:00
|
|
|
<AlertDialogDescription>
|
AI category-aware evaluation: per-round config, file parsing, shortlist, advance flow
- Per-juror cap mode (HARD/SOFT/NONE) in add-member dialog and members table
- Jury invite flow: create user + add to group + send invitation from dialog
- Per-round config: notifyOnAdvance, aiParseFiles, startupAdvanceCount, conceptAdvanceCount
- Moved notify-on-advance from competition-level to per-round setting
- AI filtering: round-tagged files with newest-first sorting, optional file content extraction
- File content extractor service (pdf-parse for PDF, utf-8 for text files)
- AI shortlist runs independently per category (STARTUP / BUSINESS_CONCEPT)
- generateAIRecommendations tRPC endpoint with per-round config integration
- AI recommendations UI: trigger button, confirmation dialog, per-category results display
- Category-aware advance dialog: select/deselect projects by category with target caps
- STAGE_ACTIVE bug fix in assignment router
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:09:52 +01:00
|
|
|
The AI will analyze all project evaluations and generate a ranked shortlist
|
|
|
|
|
for each category independently.
|
|
|
|
|
{config.startupAdvanceCount ? (
|
|
|
|
|
<span className="block mt-1">
|
|
|
|
|
Startup target: top {String(config.startupAdvanceCount)}
|
|
|
|
|
</span>
|
|
|
|
|
) : null}
|
|
|
|
|
{config.conceptAdvanceCount ? (
|
|
|
|
|
<span className="block">
|
|
|
|
|
Business Concept target: top {String(config.conceptAdvanceCount)}
|
|
|
|
|
</span>
|
|
|
|
|
) : null}
|
|
|
|
|
{config.aiParseFiles ? (
|
|
|
|
|
<span className="block mt-1 text-amber-600">
|
|
|
|
|
Document parsing is enabled — the AI will read uploaded file contents.
|
|
|
|
|
</span>
|
|
|
|
|
) : null}
|
2026-02-16 09:20:02 +01:00
|
|
|
</AlertDialogDescription>
|
|
|
|
|
</AlertDialogHeader>
|
|
|
|
|
<AlertDialogFooter>
|
|
|
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
|
|
|
<AlertDialogAction
|
AI category-aware evaluation: per-round config, file parsing, shortlist, advance flow
- Per-juror cap mode (HARD/SOFT/NONE) in add-member dialog and members table
- Jury invite flow: create user + add to group + send invitation from dialog
- Per-round config: notifyOnAdvance, aiParseFiles, startupAdvanceCount, conceptAdvanceCount
- Moved notify-on-advance from competition-level to per-round setting
- AI filtering: round-tagged files with newest-first sorting, optional file content extraction
- File content extractor service (pdf-parse for PDF, utf-8 for text files)
- AI shortlist runs independently per category (STARTUP / BUSINESS_CONCEPT)
- generateAIRecommendations tRPC endpoint with per-round config integration
- AI recommendations UI: trigger button, confirmation dialog, per-category results display
- Category-aware advance dialog: select/deselect projects by category with target caps
- STAGE_ACTIVE bug fix in assignment router
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:09:52 +01:00
|
|
|
onClick={() => shortlistMutation.mutate({ roundId })}
|
|
|
|
|
disabled={shortlistMutation.isPending}
|
2026-02-16 09:20:02 +01:00
|
|
|
>
|
AI category-aware evaluation: per-round config, file parsing, shortlist, advance flow
- Per-juror cap mode (HARD/SOFT/NONE) in add-member dialog and members table
- Jury invite flow: create user + add to group + send invitation from dialog
- Per-round config: notifyOnAdvance, aiParseFiles, startupAdvanceCount, conceptAdvanceCount
- Moved notify-on-advance from competition-level to per-round setting
- AI filtering: round-tagged files with newest-first sorting, optional file content extraction
- File content extractor service (pdf-parse for PDF, utf-8 for text files)
- AI shortlist runs independently per category (STARTUP / BUSINESS_CONCEPT)
- generateAIRecommendations tRPC endpoint with per-round config integration
- AI recommendations UI: trigger button, confirmation dialog, per-category results display
- Category-aware advance dialog: select/deselect projects by category with target caps
- STAGE_ACTIVE bug fix in assignment router
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:09:52 +01:00
|
|
|
{shortlistMutation.isPending && <Loader2 className="h-4 w-4 mr-1.5 animate-spin" />}
|
|
|
|
|
Generate
|
2026-02-16 09:20:02 +01:00
|
|
|
</AlertDialogAction>
|
|
|
|
|
</AlertDialogFooter>
|
|
|
|
|
</AlertDialogContent>
|
|
|
|
|
</AlertDialog>
|
|
|
|
|
|
AI category-aware evaluation: per-round config, file parsing, shortlist, advance flow
- Per-juror cap mode (HARD/SOFT/NONE) in add-member dialog and members table
- Jury invite flow: create user + add to group + send invitation from dialog
- Per-round config: notifyOnAdvance, aiParseFiles, startupAdvanceCount, conceptAdvanceCount
- Moved notify-on-advance from competition-level to per-round setting
- AI filtering: round-tagged files with newest-first sorting, optional file content extraction
- File content extractor service (pdf-parse for PDF, utf-8 for text files)
- AI shortlist runs independently per category (STARTUP / BUSINESS_CONCEPT)
- generateAIRecommendations tRPC endpoint with per-round config integration
- AI recommendations UI: trigger button, confirmation dialog, per-category results display
- Category-aware advance dialog: select/deselect projects by category with target caps
- STAGE_ACTIVE bug fix in assignment router
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:09:52 +01:00
|
|
|
{/* AI Recommendations Display */}
|
|
|
|
|
{aiRecommendations && (
|
|
|
|
|
<AIRecommendationsDisplay
|
|
|
|
|
recommendations={aiRecommendations}
|
|
|
|
|
onClear={() => setAiRecommendations(null)}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-02-16 09:20:02 +01:00
|
|
|
{/* Round Info + Project Breakdown */}
|
|
|
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
2026-02-16 12:38:28 +01:00
|
|
|
<AnimatedCard index={2}>
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle className="text-sm">Round Details</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="space-y-0 text-sm">
|
|
|
|
|
{[
|
|
|
|
|
{ label: 'Type', value: <Badge variant="secondary" className={cn('text-xs', typeCfg.color)}>{typeCfg.label}</Badge> },
|
|
|
|
|
{ label: 'Status', value: <span className="font-medium">{statusCfg.label}</span> },
|
|
|
|
|
{ label: 'Sort Order', value: <span className="font-medium font-mono">{round.sortOrder}</span> },
|
|
|
|
|
...(round.purposeKey ? [{ label: 'Purpose', value: <span className="font-medium">{round.purposeKey}</span> }] : []),
|
|
|
|
|
{ label: 'Jury Group', value: <span className="font-medium">{juryGroup ? juryGroup.name : '\u2014'}</span> },
|
|
|
|
|
{ label: 'Opens', value: <span className="font-medium">{round.windowOpenAt ? new Date(round.windowOpenAt).toLocaleString() : '\u2014'}</span> },
|
|
|
|
|
{ label: 'Closes', value: <span className="font-medium">{round.windowCloseAt ? new Date(round.windowCloseAt).toLocaleString() : '\u2014'}</span> },
|
|
|
|
|
].map((row, i) => (
|
|
|
|
|
<div key={row.label} className={cn('flex justify-between items-center py-2.5', i > 0 && 'border-t border-dotted border-muted')}>
|
|
|
|
|
<span className="text-muted-foreground">{row.label}</span>
|
|
|
|
|
{row.value}
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</AnimatedCard>
|
|
|
|
|
|
|
|
|
|
<AnimatedCard index={3}>
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<CardTitle className="text-sm">Project Breakdown</CardTitle>
|
|
|
|
|
{projectCount > 0 && (
|
|
|
|
|
<span className="text-xs font-mono text-muted-foreground">{projectCount} total</span>
|
|
|
|
|
)}
|
2026-02-16 09:20:02 +01:00
|
|
|
</div>
|
2026-02-16 12:38:28 +01:00
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
{projectCount === 0 ? (
|
|
|
|
|
<p className="text-sm text-muted-foreground py-4 text-center">
|
|
|
|
|
No projects assigned yet
|
|
|
|
|
</p>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
{['PENDING', 'IN_PROGRESS', 'PASSED', 'REJECTED', 'COMPLETED', 'WITHDRAWN'].map((state) => {
|
|
|
|
|
const count = stateCounts[state] || 0
|
|
|
|
|
if (count === 0) return null
|
|
|
|
|
const pct = ((count / projectCount) * 100).toFixed(0)
|
|
|
|
|
return (
|
|
|
|
|
<div key={state}>
|
|
|
|
|
<div className="flex justify-between text-xs mb-1.5">
|
|
|
|
|
<span className="text-muted-foreground capitalize font-medium">{state.toLowerCase().replace('_', ' ')}</span>
|
|
|
|
|
<span className="font-bold tabular-nums">{count} <span className="font-normal text-muted-foreground">({pct}%)</span></span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="h-2 bg-muted rounded-full overflow-hidden">
|
|
|
|
|
<div
|
|
|
|
|
className={cn('h-full rounded-full transition-all duration-500', stateColors[state])}
|
|
|
|
|
style={{ width: `${pct}%` }}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2026-02-16 09:20:02 +01:00
|
|
|
</div>
|
2026-02-16 12:38:28 +01:00
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</AnimatedCard>
|
2026-02-16 09:20:02 +01:00
|
|
|
</div>
|
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
|
|
|
{/* ═══════════ PROJECTS TAB ═══════════ */}
|
|
|
|
|
<TabsContent value="projects" className="space-y-4">
|
|
|
|
|
<ProjectStatesTable competitionId={competitionId} roundId={roundId} />
|
|
|
|
|
</TabsContent>
|
|
|
|
|
|
|
|
|
|
{/* ═══════════ FILTERING TAB ═══════════ */}
|
|
|
|
|
{isFiltering && (
|
|
|
|
|
<TabsContent value="filtering" className="space-y-4">
|
|
|
|
|
<FilteringDashboard competitionId={competitionId} roundId={roundId} />
|
|
|
|
|
</TabsContent>
|
|
|
|
|
)}
|
|
|
|
|
|
2026-02-16 12:46:01 +01:00
|
|
|
{/* ═══════════ JURY TAB ═══════════ */}
|
2026-02-16 16:43:23 +01:00
|
|
|
{hasJury && (
|
2026-02-16 12:46:01 +01:00
|
|
|
<TabsContent value="jury" className="space-y-6">
|
|
|
|
|
{/* Jury Group Selector + Create */}
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<CardTitle className="text-base">Jury Group</CardTitle>
|
|
|
|
|
<CardDescription>
|
|
|
|
|
Select or create a jury group for this round
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Button size="sm" variant="outline" onClick={() => setCreateJuryOpen(true)}>
|
|
|
|
|
<Plus className="h-4 w-4 mr-1.5" />
|
|
|
|
|
New Jury
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
{juryGroups && juryGroups.length > 0 ? (
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<Select
|
|
|
|
|
value={round.juryGroupId ?? '__none__'}
|
|
|
|
|
onValueChange={(value) => {
|
|
|
|
|
assignJuryMutation.mutate({
|
|
|
|
|
id: roundId,
|
|
|
|
|
juryGroupId: value === '__none__' ? null : value,
|
|
|
|
|
})
|
|
|
|
|
}}
|
|
|
|
|
disabled={assignJuryMutation.isPending}
|
|
|
|
|
>
|
|
|
|
|
<SelectTrigger className="w-full sm:w-80">
|
|
|
|
|
<SelectValue placeholder="Select jury group..." />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
<SelectItem value="__none__">No jury assigned</SelectItem>
|
|
|
|
|
{juryGroups.map((jg: any) => (
|
|
|
|
|
<SelectItem key={jg.id} value={jg.id}>
|
|
|
|
|
{jg.name} ({jg._count?.members ?? 0} members)
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
|
|
|
|
|
{/* Delete button for currently selected jury group */}
|
|
|
|
|
{round.juryGroupId && (
|
|
|
|
|
<AlertDialog>
|
|
|
|
|
<AlertDialogTrigger asChild>
|
|
|
|
|
<Button size="sm" variant="ghost" className="text-destructive hover:text-destructive">
|
|
|
|
|
<Trash2 className="h-4 w-4 mr-1.5" />
|
|
|
|
|
Delete "{juryGroup?.name}"
|
|
|
|
|
</Button>
|
|
|
|
|
</AlertDialogTrigger>
|
|
|
|
|
<AlertDialogContent>
|
|
|
|
|
<AlertDialogHeader>
|
|
|
|
|
<AlertDialogTitle>Delete jury group?</AlertDialogTitle>
|
|
|
|
|
<AlertDialogDescription>
|
|
|
|
|
This will permanently delete "{juryGroup?.name}" and remove all its members.
|
|
|
|
|
Rounds using this jury group will be unlinked. This action cannot be undone.
|
|
|
|
|
</AlertDialogDescription>
|
|
|
|
|
</AlertDialogHeader>
|
|
|
|
|
<AlertDialogFooter>
|
|
|
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
|
|
|
<AlertDialogAction
|
|
|
|
|
onClick={() => deleteJuryMutation.mutate({ id: round.juryGroupId! })}
|
|
|
|
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
|
|
|
disabled={deleteJuryMutation.isPending}
|
|
|
|
|
>
|
|
|
|
|
{deleteJuryMutation.isPending && <Loader2 className="h-4 w-4 mr-1.5 animate-spin" />}
|
|
|
|
|
Delete Jury
|
|
|
|
|
</AlertDialogAction>
|
|
|
|
|
</AlertDialogFooter>
|
|
|
|
|
</AlertDialogContent>
|
|
|
|
|
</AlertDialog>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="flex flex-col items-center justify-center py-10 text-center">
|
|
|
|
|
<div className="rounded-full bg-purple-50 p-4 mb-4">
|
|
|
|
|
<Users className="h-8 w-8 text-purple-400" />
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-sm font-medium">No Jury Groups</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-1 max-w-sm">
|
|
|
|
|
Create a jury group to assign members who will evaluate projects in this round.
|
|
|
|
|
</p>
|
|
|
|
|
<Button size="sm" className="mt-4" onClick={() => setCreateJuryOpen(true)}>
|
|
|
|
|
<Plus className="h-4 w-4 mr-1.5" />
|
|
|
|
|
Create First Jury
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* Members list (only if a jury group is assigned) */}
|
|
|
|
|
{juryGroupDetail && (
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<CardTitle className="text-base">
|
|
|
|
|
Members — {juryGroupDetail.name}
|
|
|
|
|
</CardTitle>
|
|
|
|
|
<CardDescription>
|
|
|
|
|
{juryGroupDetail.members.length} member{juryGroupDetail.members.length !== 1 ? 's' : ''}
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</div>
|
|
|
|
|
<Button size="sm" onClick={() => setAddMemberOpen(true)}>
|
|
|
|
|
<UserPlus className="h-4 w-4 mr-1.5" />
|
|
|
|
|
Add Member
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
{juryGroupDetail.members.length === 0 ? (
|
|
|
|
|
<div className="flex flex-col items-center justify-center py-10 text-center">
|
|
|
|
|
<div className="rounded-full bg-muted p-4 mb-4">
|
|
|
|
|
<UserPlus className="h-8 w-8 text-muted-foreground" />
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-sm font-medium">No Members Yet</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
|
|
|
Add jury members to start assigning projects for evaluation.
|
|
|
|
|
</p>
|
|
|
|
|
<Button size="sm" variant="outline" className="mt-4" onClick={() => setAddMemberOpen(true)}>
|
|
|
|
|
<UserPlus className="h-4 w-4 mr-1.5" />
|
|
|
|
|
Add First Member
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="space-y-1">
|
|
|
|
|
{juryGroupDetail.members.map((member: any, idx: number) => (
|
|
|
|
|
<div
|
|
|
|
|
key={member.id}
|
|
|
|
|
className={cn(
|
|
|
|
|
'flex items-center justify-between py-2.5 px-3 rounded-md transition-colors',
|
|
|
|
|
idx % 2 === 1 && 'bg-muted/30',
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<div className="min-w-0 flex-1">
|
|
|
|
|
<p className="text-sm font-medium truncate">
|
|
|
|
|
{member.user.name || 'Unnamed User'}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground truncate">{member.user.email}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<AlertDialog>
|
|
|
|
|
<AlertDialogTrigger asChild>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
className="h-7 w-7 text-destructive hover:text-destructive shrink-0"
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
|
|
|
</Button>
|
|
|
|
|
</AlertDialogTrigger>
|
|
|
|
|
<AlertDialogContent>
|
|
|
|
|
<AlertDialogHeader>
|
|
|
|
|
<AlertDialogTitle>Remove member?</AlertDialogTitle>
|
|
|
|
|
<AlertDialogDescription>
|
|
|
|
|
Remove {member.user.name || member.user.email} from {juryGroupDetail.name}?
|
|
|
|
|
</AlertDialogDescription>
|
|
|
|
|
</AlertDialogHeader>
|
|
|
|
|
<AlertDialogFooter>
|
|
|
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
|
|
|
<AlertDialogAction
|
|
|
|
|
onClick={() => removeJuryMemberMutation.mutate({ id: member.id })}
|
|
|
|
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
|
|
|
>
|
|
|
|
|
Remove
|
|
|
|
|
</AlertDialogAction>
|
|
|
|
|
</AlertDialogFooter>
|
|
|
|
|
</AlertDialogContent>
|
|
|
|
|
</AlertDialog>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Create Jury Dialog */}
|
|
|
|
|
<Dialog open={createJuryOpen} onOpenChange={setCreateJuryOpen}>
|
|
|
|
|
<DialogContent>
|
|
|
|
|
<DialogHeader>
|
|
|
|
|
<DialogTitle>Create Jury Group</DialogTitle>
|
|
|
|
|
<DialogDescription>
|
|
|
|
|
Create a new jury group for this competition. It will be automatically assigned to this round.
|
|
|
|
|
</DialogDescription>
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<label className="text-sm font-medium">Name</label>
|
|
|
|
|
<Input
|
|
|
|
|
placeholder="e.g. Round 1 Jury, Expert Panel, Final Jury"
|
|
|
|
|
value={newJuryName}
|
|
|
|
|
onChange={(e) => setNewJuryName(e.target.value)}
|
|
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
if (e.key === 'Enter' && newJuryName.trim()) {
|
|
|
|
|
createJuryMutation.mutate({
|
|
|
|
|
competitionId,
|
|
|
|
|
name: newJuryName.trim(),
|
|
|
|
|
slug: newJuryName.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<DialogFooter>
|
|
|
|
|
<Button variant="outline" onClick={() => setCreateJuryOpen(false)}>Cancel</Button>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => {
|
|
|
|
|
createJuryMutation.mutate({
|
|
|
|
|
competitionId,
|
|
|
|
|
name: newJuryName.trim(),
|
|
|
|
|
slug: newJuryName.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''),
|
|
|
|
|
})
|
|
|
|
|
}}
|
|
|
|
|
disabled={createJuryMutation.isPending || !newJuryName.trim()}
|
|
|
|
|
>
|
|
|
|
|
{createJuryMutation.isPending && <Loader2 className="h-4 w-4 mr-1.5 animate-spin" />}
|
|
|
|
|
Create
|
|
|
|
|
</Button>
|
|
|
|
|
</DialogFooter>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
|
|
|
|
{/* Add Member Dialog */}
|
|
|
|
|
{juryGroupId && (
|
|
|
|
|
<AddMemberDialog
|
|
|
|
|
juryGroupId={juryGroupId}
|
|
|
|
|
open={addMemberOpen}
|
|
|
|
|
onOpenChange={(open) => {
|
|
|
|
|
setAddMemberOpen(open)
|
|
|
|
|
if (!open) utils.juryGroup.getById.invalidate({ id: juryGroupId })
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</TabsContent>
|
2026-02-16 16:43:23 +01:00
|
|
|
)}
|
2026-02-16 12:46:01 +01:00
|
|
|
|
2026-02-16 09:20:02 +01:00
|
|
|
{/* ═══════════ ASSIGNMENTS TAB (Evaluation rounds) ═══════════ */}
|
|
|
|
|
{isEvaluation && (
|
|
|
|
|
<TabsContent value="assignments" className="space-y-6">
|
|
|
|
|
{/* Coverage Report */}
|
|
|
|
|
<CoverageReport roundId={roundId} />
|
|
|
|
|
|
|
|
|
|
{/* Generate Assignments */}
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<CardTitle className="text-base">Assignment Generation</CardTitle>
|
|
|
|
|
<CardDescription>
|
|
|
|
|
AI-suggested jury-to-project assignments based on expertise and workload
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</div>
|
|
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={() => setPreviewSheetOpen(true)}
|
|
|
|
|
disabled={projectCount === 0 || !juryGroup}
|
|
|
|
|
>
|
|
|
|
|
<Zap className="h-4 w-4 mr-1.5" />
|
|
|
|
|
Generate Assignments
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
{!juryGroup && (
|
|
|
|
|
<div className="flex items-center gap-2 p-3 rounded-lg bg-amber-50 border border-amber-200 text-sm text-amber-800">
|
|
|
|
|
<AlertTriangle className="h-4 w-4 shrink-0" />
|
|
|
|
|
Assign a jury group first before generating assignments.
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{projectCount === 0 && (
|
|
|
|
|
<div className="flex items-center gap-2 p-3 rounded-lg bg-amber-50 border border-amber-200 text-sm text-amber-800">
|
|
|
|
|
<AlertTriangle className="h-4 w-4 shrink-0" />
|
|
|
|
|
Add projects to this round first.
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{juryGroup && projectCount > 0 && (
|
|
|
|
|
<p className="text-sm text-muted-foreground">
|
|
|
|
|
Click "Generate Assignments" to preview AI-suggested assignments.
|
|
|
|
|
You can review and execute them from the preview sheet.
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* Jury Progress + Score Distribution */}
|
|
|
|
|
<div className="grid gap-4 lg:grid-cols-2">
|
|
|
|
|
<JuryProgressTable roundId={roundId} />
|
|
|
|
|
<ScoreDistribution roundId={roundId} />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Actions: Send Reminders + Export */}
|
|
|
|
|
<div className="flex flex-wrap items-center gap-3">
|
|
|
|
|
<SendRemindersButton roundId={roundId} />
|
|
|
|
|
<Button variant="outline" size="sm" onClick={() => setExportOpen(true)}>
|
|
|
|
|
<Download className="h-4 w-4 mr-1.5" />
|
|
|
|
|
Export Evaluations
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Individual Assignments Table */}
|
Platform-wide UX fixes: assignment dialog, invalidation, settings, dashboard
1. Assignment dialog overhaul: replace raw UUID inputs with searchable
juror Combobox (shows name, email, capacity) and multi-select project
checklist with bulk assignment support
2. Query invalidation sweep: fix missing invalidations in
assignment-preview-sheet (roundAssignment.execute) and
filtering-dashboard (filtering.finalizeResults) so data refreshes
without page reload
3. Rename Submissions tab to Document Windows with descriptive
header explaining upload window configuration
4. Connect 6 disconnected settings: storage_provider, local_storage_path,
avatar_max_size_mb, allowed_image_types, whatsapp_enabled,
whatsapp_provider - all now accessible in Settings UI
5. Admin dashboard redesign: branded Editorial Command Center with
Dark Blue gradient header, colored border-l-4 stat cards, staggered
animations, 2-column layout, action-required panel, activity timeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:05:25 +01:00
|
|
|
<IndividualAssignmentsTable roundId={roundId} projectStates={projectStates} />
|
2026-02-16 09:20:02 +01:00
|
|
|
|
|
|
|
|
{/* Unassigned Queue */}
|
|
|
|
|
<RoundUnassignedQueue roundId={roundId} />
|
|
|
|
|
|
|
|
|
|
{/* Assignment Preview Sheet */}
|
|
|
|
|
<AssignmentPreviewSheet
|
|
|
|
|
roundId={roundId}
|
|
|
|
|
open={previewSheetOpen}
|
|
|
|
|
onOpenChange={setPreviewSheetOpen}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* CSV Export Dialog */}
|
|
|
|
|
<ExportEvaluationsDialog roundId={roundId} open={exportOpen} onOpenChange={setExportOpen} />
|
|
|
|
|
</TabsContent>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* ═══════════ CONFIG TAB ═══════════ */}
|
|
|
|
|
<TabsContent value="config" className="space-y-6">
|
2026-02-16 16:43:23 +01:00
|
|
|
{/* Round Dates */}
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader className="border-b">
|
|
|
|
|
<CardTitle className="text-base">Round Dates</CardTitle>
|
|
|
|
|
<CardDescription>
|
|
|
|
|
When this round starts and ends. Defines the active period for document uploads and evaluations.
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="pt-4">
|
|
|
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label>Start Date</Label>
|
|
|
|
|
<DateTimePicker
|
|
|
|
|
value={round.windowOpenAt ? new Date(round.windowOpenAt) : null}
|
|
|
|
|
onChange={(date) => updateMutation.mutate({ id: roundId, windowOpenAt: date })}
|
|
|
|
|
placeholder="Select start date & time"
|
|
|
|
|
clearable
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<Label>End Date</Label>
|
|
|
|
|
<DateTimePicker
|
|
|
|
|
value={round.windowCloseAt ? new Date(round.windowCloseAt) : null}
|
|
|
|
|
onChange={(date) => updateMutation.mutate({ id: roundId, windowCloseAt: date })}
|
|
|
|
|
placeholder="Select end date & time"
|
|
|
|
|
clearable
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
2026-02-16 09:20:02 +01:00
|
|
|
{/* General Round Settings */}
|
|
|
|
|
<Card>
|
2026-02-16 12:38:28 +01:00
|
|
|
<CardHeader className="border-b">
|
2026-02-16 09:20:02 +01:00
|
|
|
<CardTitle className="text-base">General Settings</CardTitle>
|
|
|
|
|
<CardDescription>Settings that apply to this round regardless of type</CardDescription>
|
|
|
|
|
</CardHeader>
|
2026-02-16 12:38:28 +01:00
|
|
|
<CardContent className="space-y-0 pt-0">
|
|
|
|
|
<div className="flex items-center justify-between p-4 rounded-md">
|
2026-02-16 09:20:02 +01:00
|
|
|
<div className="space-y-0.5">
|
|
|
|
|
<Label htmlFor="notify-on-entry" className="text-sm font-medium">
|
|
|
|
|
Notify on round entry
|
|
|
|
|
</Label>
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
Send an automated email to project applicants when their project enters this round
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Switch
|
|
|
|
|
id="notify-on-entry"
|
|
|
|
|
checked={!!config.notifyOnEntry}
|
|
|
|
|
onCheckedChange={(checked) => {
|
|
|
|
|
handleConfigChange({ ...config, notifyOnEntry: checked })
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
AI category-aware evaluation: per-round config, file parsing, shortlist, advance flow
- Per-juror cap mode (HARD/SOFT/NONE) in add-member dialog and members table
- Jury invite flow: create user + add to group + send invitation from dialog
- Per-round config: notifyOnAdvance, aiParseFiles, startupAdvanceCount, conceptAdvanceCount
- Moved notify-on-advance from competition-level to per-round setting
- AI filtering: round-tagged files with newest-first sorting, optional file content extraction
- File content extractor service (pdf-parse for PDF, utf-8 for text files)
- AI shortlist runs independently per category (STARTUP / BUSINESS_CONCEPT)
- generateAIRecommendations tRPC endpoint with per-round config integration
- AI recommendations UI: trigger button, confirmation dialog, per-category results display
- Category-aware advance dialog: select/deselect projects by category with target caps
- STAGE_ACTIVE bug fix in assignment router
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:09:52 +01:00
|
|
|
|
2026-02-16 12:38:28 +01:00
|
|
|
<div className="flex items-center justify-between p-4 rounded-md bg-muted/30">
|
AI category-aware evaluation: per-round config, file parsing, shortlist, advance flow
- Per-juror cap mode (HARD/SOFT/NONE) in add-member dialog and members table
- Jury invite flow: create user + add to group + send invitation from dialog
- Per-round config: notifyOnAdvance, aiParseFiles, startupAdvanceCount, conceptAdvanceCount
- Moved notify-on-advance from competition-level to per-round setting
- AI filtering: round-tagged files with newest-first sorting, optional file content extraction
- File content extractor service (pdf-parse for PDF, utf-8 for text files)
- AI shortlist runs independently per category (STARTUP / BUSINESS_CONCEPT)
- generateAIRecommendations tRPC endpoint with per-round config integration
- AI recommendations UI: trigger button, confirmation dialog, per-category results display
- Category-aware advance dialog: select/deselect projects by category with target caps
- STAGE_ACTIVE bug fix in assignment router
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:09:52 +01:00
|
|
|
<div className="space-y-0.5">
|
|
|
|
|
<Label htmlFor="notify-on-advance" className="text-sm font-medium">
|
|
|
|
|
Notify on advance
|
|
|
|
|
</Label>
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
Send an email to project applicants when their project advances from this round to the next
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Switch
|
|
|
|
|
id="notify-on-advance"
|
|
|
|
|
checked={!!config.notifyOnAdvance}
|
|
|
|
|
onCheckedChange={(checked) => {
|
|
|
|
|
handleConfigChange({ ...config, notifyOnAdvance: checked })
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-16 12:38:28 +01:00
|
|
|
<div className="flex items-center justify-between p-4 rounded-md">
|
AI category-aware evaluation: per-round config, file parsing, shortlist, advance flow
- Per-juror cap mode (HARD/SOFT/NONE) in add-member dialog and members table
- Jury invite flow: create user + add to group + send invitation from dialog
- Per-round config: notifyOnAdvance, aiParseFiles, startupAdvanceCount, conceptAdvanceCount
- Moved notify-on-advance from competition-level to per-round setting
- AI filtering: round-tagged files with newest-first sorting, optional file content extraction
- File content extractor service (pdf-parse for PDF, utf-8 for text files)
- AI shortlist runs independently per category (STARTUP / BUSINESS_CONCEPT)
- generateAIRecommendations tRPC endpoint with per-round config integration
- AI recommendations UI: trigger button, confirmation dialog, per-category results display
- Category-aware advance dialog: select/deselect projects by category with target caps
- STAGE_ACTIVE bug fix in assignment router
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:09:52 +01:00
|
|
|
<div className="space-y-0.5">
|
|
|
|
|
<Label htmlFor="ai-parse-files" className="text-sm font-medium">
|
|
|
|
|
AI document parsing
|
|
|
|
|
</Label>
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
Allow AI to read the contents of uploaded files (PDF/text) for deeper analysis during filtering and evaluation
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Switch
|
|
|
|
|
id="ai-parse-files"
|
|
|
|
|
checked={!!config.aiParseFiles}
|
|
|
|
|
onCheckedChange={(checked) => {
|
|
|
|
|
handleConfigChange({ ...config, aiParseFiles: checked })
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-16 12:38:28 +01:00
|
|
|
<div className="border-t mt-2 pt-4 px-4 pb-2 bg-[#053d57]/[0.03] rounded-b-lg -mx-6 -mb-6 p-6">
|
AI category-aware evaluation: per-round config, file parsing, shortlist, advance flow
- Per-juror cap mode (HARD/SOFT/NONE) in add-member dialog and members table
- Jury invite flow: create user + add to group + send invitation from dialog
- Per-round config: notifyOnAdvance, aiParseFiles, startupAdvanceCount, conceptAdvanceCount
- Moved notify-on-advance from competition-level to per-round setting
- AI filtering: round-tagged files with newest-first sorting, optional file content extraction
- File content extractor service (pdf-parse for PDF, utf-8 for text files)
- AI shortlist runs independently per category (STARTUP / BUSINESS_CONCEPT)
- generateAIRecommendations tRPC endpoint with per-round config integration
- AI recommendations UI: trigger button, confirmation dialog, per-category results display
- Category-aware advance dialog: select/deselect projects by category with target caps
- STAGE_ACTIVE bug fix in assignment router
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:09:52 +01:00
|
|
|
<Label className="text-sm font-medium">Advancement Targets</Label>
|
|
|
|
|
<p className="text-xs text-muted-foreground mb-3">
|
|
|
|
|
Target number of projects per category to advance from this round
|
|
|
|
|
</p>
|
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<Label htmlFor="startup-advance-count" className="text-xs text-muted-foreground">
|
|
|
|
|
Startup Projects
|
|
|
|
|
</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="startup-advance-count"
|
|
|
|
|
type="number"
|
|
|
|
|
min={0}
|
|
|
|
|
className="h-9"
|
|
|
|
|
placeholder="No limit"
|
|
|
|
|
value={(config.startupAdvanceCount as number) ?? ''}
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
const val = e.target.value ? parseInt(e.target.value, 10) : undefined
|
|
|
|
|
handleConfigChange({ ...config, startupAdvanceCount: val })
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-1.5">
|
|
|
|
|
<Label htmlFor="concept-advance-count" className="text-xs text-muted-foreground">
|
|
|
|
|
Concept Projects
|
|
|
|
|
</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="concept-advance-count"
|
|
|
|
|
type="number"
|
|
|
|
|
min={0}
|
|
|
|
|
className="h-9"
|
|
|
|
|
placeholder="No limit"
|
|
|
|
|
value={(config.conceptAdvanceCount as number) ?? ''}
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
const val = e.target.value ? parseInt(e.target.value, 10) : undefined
|
|
|
|
|
handleConfigChange({ ...config, conceptAdvanceCount: val })
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
2026-02-16 09:20:02 +01:00
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* Round-type-specific config */}
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
<RoundConfigForm
|
|
|
|
|
roundType={round.roundType}
|
|
|
|
|
config={config}
|
|
|
|
|
onChange={handleConfigChange}
|
2026-02-16 09:20:02 +01:00
|
|
|
juryGroups={juryGroups?.map((jg: any) => ({ id: jg.id, name: jg.name }))}
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
/>
|
|
|
|
|
|
2026-02-16 09:20:02 +01:00
|
|
|
{/* Evaluation Criteria Editor (EVALUATION rounds only) */}
|
|
|
|
|
{isEvaluation && <EvaluationCriteriaEditor roundId={roundId} />}
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
|
2026-02-16 09:20:02 +01:00
|
|
|
{/* Document Requirements */}
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle className="text-base">Document Requirements</CardTitle>
|
|
|
|
|
<CardDescription>
|
|
|
|
|
Files applicants must submit for this round
|
|
|
|
|
{round.windowCloseAt && (
|
|
|
|
|
<> — due by {new Date(round.windowCloseAt).toLocaleDateString()}</>
|
|
|
|
|
)}
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
<FileRequirementsEditor
|
|
|
|
|
roundId={roundId}
|
|
|
|
|
windowOpenAt={round.windowOpenAt}
|
|
|
|
|
windowCloseAt={round.windowCloseAt}
|
|
|
|
|
/>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
</TabsContent>
|
|
|
|
|
|
2026-02-16 09:20:02 +01:00
|
|
|
{/* ═══════════ AWARDS TAB ═══════════ */}
|
2026-02-16 16:43:23 +01:00
|
|
|
{hasAwards && (
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
<TabsContent value="awards" className="space-y-4">
|
|
|
|
|
<Card>
|
|
|
|
|
<CardContent className="p-6">
|
|
|
|
|
{roundAwards.length === 0 ? (
|
2026-02-16 12:38:28 +01:00
|
|
|
<div className="text-center py-12 text-muted-foreground">
|
|
|
|
|
<div className="rounded-full bg-[#de0f1e]/10 p-4 w-fit mx-auto mb-4">
|
|
|
|
|
<Trophy className="h-8 w-8 text-[#de0f1e]/60" />
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-sm font-medium text-foreground">No Awards Linked</p>
|
|
|
|
|
<p className="text-xs mt-1 max-w-sm mx-auto">
|
2026-02-16 09:20:02 +01:00
|
|
|
Create an award and set this round as its evaluation round to see it here
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
{roundAwards.map((award) => {
|
|
|
|
|
const eligibleCount = award._count?.eligibilities || 0
|
|
|
|
|
const autoTagRules = award.autoTagRulesJson as { rules?: unknown[] } | null
|
|
|
|
|
const ruleCount = autoTagRules?.rules?.length || 0
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Link
|
|
|
|
|
key={award.id}
|
|
|
|
|
href={`/admin/awards/${award.id}` as Route}
|
|
|
|
|
className="block"
|
|
|
|
|
>
|
2026-02-16 12:38:28 +01:00
|
|
|
<div className="flex items-start justify-between gap-4 rounded-lg border border-l-4 border-l-[#de0f1e] p-4 transition-all hover:shadow-md hover:-translate-y-0.5">
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
<div className="flex items-center gap-2 mb-1">
|
|
|
|
|
<h3 className="font-medium truncate">{award.name}</h3>
|
|
|
|
|
<Badge
|
|
|
|
|
variant={
|
|
|
|
|
award.eligibilityMode === 'SEPARATE_POOL'
|
|
|
|
|
? 'default'
|
|
|
|
|
: 'secondary'
|
|
|
|
|
}
|
|
|
|
|
className="shrink-0"
|
|
|
|
|
>
|
|
|
|
|
{award.eligibilityMode === 'SEPARATE_POOL'
|
|
|
|
|
? 'Separate Pool'
|
|
|
|
|
: 'Stay in Main'}
|
|
|
|
|
</Badge>
|
|
|
|
|
</div>
|
|
|
|
|
{award.description && (
|
|
|
|
|
<p className="text-sm text-muted-foreground line-clamp-1">
|
|
|
|
|
{award.description}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-4 text-sm text-muted-foreground shrink-0">
|
|
|
|
|
<div className="text-right">
|
2026-02-16 09:20:02 +01:00
|
|
|
<div className="font-medium text-foreground">{ruleCount}</div>
|
|
|
|
|
<div className="text-xs">{ruleCount === 1 ? 'rule' : 'rules'}</div>
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
</div>
|
|
|
|
|
<div className="text-right">
|
2026-02-16 09:20:02 +01:00
|
|
|
<div className="font-medium text-foreground">{eligibleCount}</div>
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
<div className="text-xs">eligible</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Link>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
</TabsContent>
|
2026-02-16 16:43:23 +01:00
|
|
|
)}
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
</Tabs>
|
2026-02-16 09:20:02 +01:00
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
// Sub-components
|
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
// ── Unassigned projects queue ────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function RoundUnassignedQueue({ roundId }: { roundId: string }) {
|
|
|
|
|
const { data: unassigned, isLoading } = trpc.roundAssignment.unassignedQueue.useQuery(
|
|
|
|
|
{ roundId, requiredReviews: 3 },
|
2026-02-16 09:30:19 +01:00
|
|
|
{ refetchInterval: 15_000 },
|
2026-02-16 09:20:02 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle className="text-base">Unassigned Projects</CardTitle>
|
|
|
|
|
<CardDescription>Projects with fewer than 3 jury assignments</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
{isLoading ? (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
{[1, 2, 3].map((i) => <Skeleton key={i} className="h-14 w-full" />)}
|
|
|
|
|
</div>
|
|
|
|
|
) : unassigned && unassigned.length > 0 ? (
|
|
|
|
|
<div className="space-y-2 max-h-[400px] overflow-y-auto">
|
|
|
|
|
{unassigned.map((project: any) => (
|
|
|
|
|
<div
|
|
|
|
|
key={project.id}
|
2026-02-16 12:38:28 +01:00
|
|
|
className={cn(
|
|
|
|
|
'flex justify-between items-center p-3 border rounded-md hover:bg-muted/30 transition-colors',
|
|
|
|
|
(project.assignmentCount || 0) === 0 && 'border-l-4 border-l-red-500',
|
|
|
|
|
)}
|
2026-02-16 09:20:02 +01:00
|
|
|
>
|
|
|
|
|
<div className="min-w-0">
|
|
|
|
|
<p className="text-sm font-medium truncate">{project.title}</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground">
|
|
|
|
|
{project.competitionCategory || 'No category'}
|
|
|
|
|
{project.teamName && ` \u00b7 ${project.teamName}`}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Badge variant="outline" className={cn(
|
|
|
|
|
'text-xs shrink-0 ml-3',
|
|
|
|
|
(project.assignmentCount || 0) === 0
|
|
|
|
|
? 'bg-red-50 text-red-700 border-red-200'
|
|
|
|
|
: 'bg-amber-50 text-amber-700 border-amber-200',
|
|
|
|
|
)}>
|
|
|
|
|
{project.assignmentCount || 0} / 3
|
|
|
|
|
</Badge>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<p className="text-sm text-muted-foreground text-center py-6">
|
|
|
|
|
All projects have sufficient assignments
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Jury Progress Table ──────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function JuryProgressTable({ roundId }: { roundId: string }) {
|
2026-02-16 09:30:19 +01:00
|
|
|
const { data: workload, isLoading } = trpc.analytics.getJurorWorkload.useQuery(
|
|
|
|
|
{ roundId },
|
|
|
|
|
{ refetchInterval: 15_000 },
|
|
|
|
|
)
|
2026-02-16 09:20:02 +01:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle className="text-base">Jury Progress</CardTitle>
|
|
|
|
|
<CardDescription>Evaluation completion per juror</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
{isLoading ? (
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
{[1, 2, 3].map((i) => <Skeleton key={i} className="h-10 w-full" />)}
|
|
|
|
|
</div>
|
|
|
|
|
) : !workload || workload.length === 0 ? (
|
|
|
|
|
<p className="text-sm text-muted-foreground text-center py-6">
|
|
|
|
|
No assignments yet
|
|
|
|
|
</p>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="space-y-3 max-h-[350px] overflow-y-auto">
|
|
|
|
|
{workload.map((juror) => {
|
|
|
|
|
const pct = juror.completionRate
|
2026-02-16 12:38:28 +01:00
|
|
|
const barGradient = pct === 100
|
|
|
|
|
? 'bg-gradient-to-r from-emerald-400 to-emerald-600'
|
2026-02-16 09:20:02 +01:00
|
|
|
: pct >= 50
|
2026-02-16 12:38:28 +01:00
|
|
|
? 'bg-gradient-to-r from-blue-400 to-blue-600'
|
2026-02-16 09:20:02 +01:00
|
|
|
: pct > 0
|
2026-02-16 12:38:28 +01:00
|
|
|
? 'bg-gradient-to-r from-amber-400 to-amber-600'
|
2026-02-16 09:20:02 +01:00
|
|
|
: 'bg-gray-300'
|
|
|
|
|
|
|
|
|
|
return (
|
2026-02-16 12:38:28 +01:00
|
|
|
<div key={juror.id} className="space-y-1 hover:bg-muted/20 rounded px-1 py-0.5 -mx-1 transition-colors">
|
2026-02-16 09:20:02 +01:00
|
|
|
<div className="flex justify-between text-xs">
|
|
|
|
|
<span className="font-medium truncate max-w-[60%]">{juror.name}</span>
|
2026-02-16 12:38:28 +01:00
|
|
|
<span className="text-muted-foreground shrink-0 tabular-nums">
|
2026-02-16 09:20:02 +01:00
|
|
|
{juror.completed}/{juror.assigned} ({pct}%)
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
2026-02-16 12:38:28 +01:00
|
|
|
<div className="h-2 bg-muted rounded-full overflow-hidden">
|
2026-02-16 09:20:02 +01:00
|
|
|
<div
|
2026-02-16 12:38:28 +01:00
|
|
|
className={cn('h-full rounded-full transition-all duration-500', barGradient)}
|
2026-02-16 09:20:02 +01:00
|
|
|
style={{ width: `${pct}%` }}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Score Distribution ───────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function ScoreDistribution({ roundId }: { roundId: string }) {
|
2026-02-16 09:30:19 +01:00
|
|
|
const { data: dist, isLoading } = trpc.analytics.getRoundScoreDistribution.useQuery(
|
|
|
|
|
{ roundId },
|
|
|
|
|
{ refetchInterval: 15_000 },
|
|
|
|
|
)
|
2026-02-16 09:20:02 +01:00
|
|
|
|
|
|
|
|
const maxCount = useMemo(() =>
|
|
|
|
|
dist ? Math.max(...dist.globalDistribution.map((b) => b.count), 1) : 1,
|
|
|
|
|
[dist])
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<CardTitle className="text-base">Score Distribution</CardTitle>
|
|
|
|
|
<CardDescription>
|
|
|
|
|
{dist ? `${dist.totalEvaluations} evaluations \u2014 avg ${dist.averageGlobalScore.toFixed(1)}` : 'Loading...'}
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
{isLoading ? (
|
|
|
|
|
<div className="flex items-end gap-1 h-32">
|
|
|
|
|
{Array.from({ length: 10 }).map((_, i) => <Skeleton key={i} className="flex-1 h-full" />)}
|
|
|
|
|
</div>
|
|
|
|
|
) : !dist || dist.totalEvaluations === 0 ? (
|
|
|
|
|
<p className="text-sm text-muted-foreground text-center py-6">
|
|
|
|
|
No evaluations submitted yet
|
|
|
|
|
</p>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="flex items-end gap-1 h-32">
|
|
|
|
|
{dist.globalDistribution.map((bucket) => {
|
|
|
|
|
const heightPct = (bucket.count / maxCount) * 100
|
|
|
|
|
return (
|
|
|
|
|
<div key={bucket.score} className="flex-1 flex flex-col items-center gap-1">
|
|
|
|
|
<span className="text-[9px] text-muted-foreground">{bucket.count || ''}</span>
|
|
|
|
|
<div className="w-full relative rounded-t" style={{ height: `${Math.max(heightPct, 2)}%` }}>
|
|
|
|
|
<div className={cn(
|
|
|
|
|
'absolute inset-0 rounded-t transition-all',
|
|
|
|
|
bucket.score <= 3 ? 'bg-red-400' :
|
|
|
|
|
bucket.score <= 6 ? 'bg-amber-400' :
|
|
|
|
|
'bg-emerald-400',
|
|
|
|
|
)} />
|
|
|
|
|
</div>
|
|
|
|
|
<span className="text-[10px] text-muted-foreground">{bucket.score}</span>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Send Reminders Button ────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function SendRemindersButton({ roundId }: { roundId: string }) {
|
|
|
|
|
const [open, setOpen] = useState(false)
|
|
|
|
|
const mutation = trpc.evaluation.triggerReminders.useMutation({
|
|
|
|
|
onSuccess: (data) => {
|
|
|
|
|
toast.success(`Sent ${data.sent} reminder(s)`)
|
|
|
|
|
setOpen(false)
|
|
|
|
|
},
|
|
|
|
|
onError: (err) => toast.error(err.message),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
|
|
|
|
<Button variant="outline" size="sm" onClick={() => setOpen(true)}>
|
|
|
|
|
<Send className="h-4 w-4 mr-1.5" />
|
|
|
|
|
Send Reminders
|
|
|
|
|
</Button>
|
|
|
|
|
<AlertDialog open={open} onOpenChange={setOpen}>
|
|
|
|
|
<AlertDialogContent>
|
|
|
|
|
<AlertDialogHeader>
|
|
|
|
|
<AlertDialogTitle>Send evaluation reminders?</AlertDialogTitle>
|
|
|
|
|
<AlertDialogDescription>
|
|
|
|
|
This will send reminder emails to all jurors who have incomplete evaluations for this round.
|
|
|
|
|
</AlertDialogDescription>
|
|
|
|
|
</AlertDialogHeader>
|
|
|
|
|
<AlertDialogFooter>
|
|
|
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
|
|
|
<AlertDialogAction
|
|
|
|
|
onClick={() => mutation.mutate({ roundId })}
|
|
|
|
|
disabled={mutation.isPending}
|
|
|
|
|
>
|
|
|
|
|
{mutation.isPending && <Loader2 className="h-4 w-4 mr-1.5 animate-spin" />}
|
|
|
|
|
Send Reminders
|
|
|
|
|
</AlertDialogAction>
|
|
|
|
|
</AlertDialogFooter>
|
|
|
|
|
</AlertDialogContent>
|
|
|
|
|
</AlertDialog>
|
|
|
|
|
</>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Export Evaluations Dialog ─────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function ExportEvaluationsDialog({
|
|
|
|
|
roundId,
|
|
|
|
|
open,
|
|
|
|
|
onOpenChange,
|
|
|
|
|
}: {
|
|
|
|
|
roundId: string
|
|
|
|
|
open: boolean
|
|
|
|
|
onOpenChange: (open: boolean) => void
|
|
|
|
|
}) {
|
|
|
|
|
const [exportData, setExportData] = useState<any>(undefined)
|
|
|
|
|
const [isLoadingExport, setIsLoadingExport] = useState(false)
|
|
|
|
|
const utils = trpc.useUtils()
|
|
|
|
|
|
|
|
|
|
const handleRequestData = async () => {
|
|
|
|
|
setIsLoadingExport(true)
|
|
|
|
|
try {
|
|
|
|
|
const data = await utils.export.evaluations.fetch({ roundId, includeDetails: true })
|
|
|
|
|
setExportData(data)
|
|
|
|
|
return data
|
|
|
|
|
} finally {
|
|
|
|
|
setIsLoadingExport(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<CsvExportDialog
|
|
|
|
|
open={open}
|
|
|
|
|
onOpenChange={onOpenChange}
|
|
|
|
|
exportData={exportData}
|
|
|
|
|
isLoading={isLoadingExport}
|
|
|
|
|
filename={`evaluations-${roundId}`}
|
|
|
|
|
onRequestData={handleRequestData}
|
|
|
|
|
/>
|
|
|
|
|
)
|
|
|
|
|
}
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
|
2026-02-16 09:20:02 +01:00
|
|
|
// ── Individual Assignments Table ─────────────────────────────────────────
|
|
|
|
|
|
Platform-wide UX fixes: assignment dialog, invalidation, settings, dashboard
1. Assignment dialog overhaul: replace raw UUID inputs with searchable
juror Combobox (shows name, email, capacity) and multi-select project
checklist with bulk assignment support
2. Query invalidation sweep: fix missing invalidations in
assignment-preview-sheet (roundAssignment.execute) and
filtering-dashboard (filtering.finalizeResults) so data refreshes
without page reload
3. Rename Submissions tab to Document Windows with descriptive
header explaining upload window configuration
4. Connect 6 disconnected settings: storage_provider, local_storage_path,
avatar_max_size_mb, allowed_image_types, whatsapp_enabled,
whatsapp_provider - all now accessible in Settings UI
5. Admin dashboard redesign: branded Editorial Command Center with
Dark Blue gradient header, colored border-l-4 stat cards, staggered
animations, 2-column layout, action-required panel, activity timeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:05:25 +01:00
|
|
|
function IndividualAssignmentsTable({
|
|
|
|
|
roundId,
|
|
|
|
|
projectStates,
|
|
|
|
|
}: {
|
|
|
|
|
roundId: string
|
|
|
|
|
projectStates: any[] | undefined
|
|
|
|
|
}) {
|
2026-02-16 09:20:02 +01:00
|
|
|
const [addDialogOpen, setAddDialogOpen] = useState(false)
|
Platform-wide UX fixes: assignment dialog, invalidation, settings, dashboard
1. Assignment dialog overhaul: replace raw UUID inputs with searchable
juror Combobox (shows name, email, capacity) and multi-select project
checklist with bulk assignment support
2. Query invalidation sweep: fix missing invalidations in
assignment-preview-sheet (roundAssignment.execute) and
filtering-dashboard (filtering.finalizeResults) so data refreshes
without page reload
3. Rename Submissions tab to Document Windows with descriptive
header explaining upload window configuration
4. Connect 6 disconnected settings: storage_provider, local_storage_path,
avatar_max_size_mb, allowed_image_types, whatsapp_enabled,
whatsapp_provider - all now accessible in Settings UI
5. Admin dashboard redesign: branded Editorial Command Center with
Dark Blue gradient header, colored border-l-4 stat cards, staggered
animations, 2-column layout, action-required panel, activity timeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:05:25 +01:00
|
|
|
const [selectedJurorId, setSelectedJurorId] = useState('')
|
|
|
|
|
const [selectedProjectIds, setSelectedProjectIds] = useState<Set<string>>(new Set())
|
|
|
|
|
const [jurorPopoverOpen, setJurorPopoverOpen] = useState(false)
|
|
|
|
|
const [projectSearch, setProjectSearch] = useState('')
|
2026-02-16 09:20:02 +01:00
|
|
|
|
|
|
|
|
const utils = trpc.useUtils()
|
2026-02-16 09:30:19 +01:00
|
|
|
const { data: assignments, isLoading } = trpc.assignment.listByStage.useQuery(
|
|
|
|
|
{ roundId },
|
|
|
|
|
{ refetchInterval: 15_000 },
|
|
|
|
|
)
|
2026-02-16 09:20:02 +01:00
|
|
|
|
Platform-wide UX fixes: assignment dialog, invalidation, settings, dashboard
1. Assignment dialog overhaul: replace raw UUID inputs with searchable
juror Combobox (shows name, email, capacity) and multi-select project
checklist with bulk assignment support
2. Query invalidation sweep: fix missing invalidations in
assignment-preview-sheet (roundAssignment.execute) and
filtering-dashboard (filtering.finalizeResults) so data refreshes
without page reload
3. Rename Submissions tab to Document Windows with descriptive
header explaining upload window configuration
4. Connect 6 disconnected settings: storage_provider, local_storage_path,
avatar_max_size_mb, allowed_image_types, whatsapp_enabled,
whatsapp_provider - all now accessible in Settings UI
5. Admin dashboard redesign: branded Editorial Command Center with
Dark Blue gradient header, colored border-l-4 stat cards, staggered
animations, 2-column layout, action-required panel, activity timeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:05:25 +01:00
|
|
|
const { data: juryMembers } = trpc.user.getJuryMembers.useQuery(
|
|
|
|
|
{ roundId },
|
|
|
|
|
{ enabled: addDialogOpen },
|
|
|
|
|
)
|
|
|
|
|
|
2026-02-16 09:20:02 +01:00
|
|
|
const deleteMutation = trpc.assignment.delete.useMutation({
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
utils.assignment.listByStage.invalidate({ roundId })
|
Platform-wide UX fixes: assignment dialog, invalidation, settings, dashboard
1. Assignment dialog overhaul: replace raw UUID inputs with searchable
juror Combobox (shows name, email, capacity) and multi-select project
checklist with bulk assignment support
2. Query invalidation sweep: fix missing invalidations in
assignment-preview-sheet (roundAssignment.execute) and
filtering-dashboard (filtering.finalizeResults) so data refreshes
without page reload
3. Rename Submissions tab to Document Windows with descriptive
header explaining upload window configuration
4. Connect 6 disconnected settings: storage_provider, local_storage_path,
avatar_max_size_mb, allowed_image_types, whatsapp_enabled,
whatsapp_provider - all now accessible in Settings UI
5. Admin dashboard redesign: branded Editorial Command Center with
Dark Blue gradient header, colored border-l-4 stat cards, staggered
animations, 2-column layout, action-required panel, activity timeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:05:25 +01:00
|
|
|
utils.roundEngine.getProjectStates.invalidate({ roundId })
|
2026-02-16 09:20:02 +01:00
|
|
|
toast.success('Assignment removed')
|
|
|
|
|
},
|
|
|
|
|
onError: (err) => toast.error(err.message),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const createMutation = trpc.assignment.create.useMutation({
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
utils.assignment.listByStage.invalidate({ roundId })
|
Platform-wide UX fixes: assignment dialog, invalidation, settings, dashboard
1. Assignment dialog overhaul: replace raw UUID inputs with searchable
juror Combobox (shows name, email, capacity) and multi-select project
checklist with bulk assignment support
2. Query invalidation sweep: fix missing invalidations in
assignment-preview-sheet (roundAssignment.execute) and
filtering-dashboard (filtering.finalizeResults) so data refreshes
without page reload
3. Rename Submissions tab to Document Windows with descriptive
header explaining upload window configuration
4. Connect 6 disconnected settings: storage_provider, local_storage_path,
avatar_max_size_mb, allowed_image_types, whatsapp_enabled,
whatsapp_provider - all now accessible in Settings UI
5. Admin dashboard redesign: branded Editorial Command Center with
Dark Blue gradient header, colored border-l-4 stat cards, staggered
animations, 2-column layout, action-required panel, activity timeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:05:25 +01:00
|
|
|
utils.roundEngine.getProjectStates.invalidate({ roundId })
|
|
|
|
|
utils.user.getJuryMembers.invalidate({ roundId })
|
2026-02-16 09:20:02 +01:00
|
|
|
toast.success('Assignment created')
|
Platform-wide UX fixes: assignment dialog, invalidation, settings, dashboard
1. Assignment dialog overhaul: replace raw UUID inputs with searchable
juror Combobox (shows name, email, capacity) and multi-select project
checklist with bulk assignment support
2. Query invalidation sweep: fix missing invalidations in
assignment-preview-sheet (roundAssignment.execute) and
filtering-dashboard (filtering.finalizeResults) so data refreshes
without page reload
3. Rename Submissions tab to Document Windows with descriptive
header explaining upload window configuration
4. Connect 6 disconnected settings: storage_provider, local_storage_path,
avatar_max_size_mb, allowed_image_types, whatsapp_enabled,
whatsapp_provider - all now accessible in Settings UI
5. Admin dashboard redesign: branded Editorial Command Center with
Dark Blue gradient header, colored border-l-4 stat cards, staggered
animations, 2-column layout, action-required panel, activity timeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:05:25 +01:00
|
|
|
resetDialog()
|
|
|
|
|
},
|
|
|
|
|
onError: (err) => toast.error(err.message),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const bulkCreateMutation = trpc.assignment.bulkCreate.useMutation({
|
|
|
|
|
onSuccess: (result) => {
|
|
|
|
|
utils.assignment.listByStage.invalidate({ roundId })
|
|
|
|
|
utils.roundEngine.getProjectStates.invalidate({ roundId })
|
|
|
|
|
utils.user.getJuryMembers.invalidate({ roundId })
|
|
|
|
|
toast.success(`${result.created} assignment(s) created`)
|
|
|
|
|
resetDialog()
|
2026-02-16 09:20:02 +01:00
|
|
|
},
|
|
|
|
|
onError: (err) => toast.error(err.message),
|
|
|
|
|
})
|
|
|
|
|
|
Platform-wide UX fixes: assignment dialog, invalidation, settings, dashboard
1. Assignment dialog overhaul: replace raw UUID inputs with searchable
juror Combobox (shows name, email, capacity) and multi-select project
checklist with bulk assignment support
2. Query invalidation sweep: fix missing invalidations in
assignment-preview-sheet (roundAssignment.execute) and
filtering-dashboard (filtering.finalizeResults) so data refreshes
without page reload
3. Rename Submissions tab to Document Windows with descriptive
header explaining upload window configuration
4. Connect 6 disconnected settings: storage_provider, local_storage_path,
avatar_max_size_mb, allowed_image_types, whatsapp_enabled,
whatsapp_provider - all now accessible in Settings UI
5. Admin dashboard redesign: branded Editorial Command Center with
Dark Blue gradient header, colored border-l-4 stat cards, staggered
animations, 2-column layout, action-required panel, activity timeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:05:25 +01:00
|
|
|
const resetDialog = useCallback(() => {
|
|
|
|
|
setAddDialogOpen(false)
|
|
|
|
|
setSelectedJurorId('')
|
|
|
|
|
setSelectedProjectIds(new Set())
|
|
|
|
|
setProjectSearch('')
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
const selectedJuror = useMemo(
|
|
|
|
|
() => juryMembers?.find((j: any) => j.id === selectedJurorId),
|
|
|
|
|
[juryMembers, selectedJurorId],
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Filter projects by search term
|
|
|
|
|
const filteredProjects = useMemo(() => {
|
|
|
|
|
const items = projectStates ?? []
|
|
|
|
|
if (!projectSearch) return items
|
|
|
|
|
const q = projectSearch.toLowerCase()
|
|
|
|
|
return items.filter((ps: any) =>
|
|
|
|
|
ps.project?.title?.toLowerCase().includes(q) ||
|
|
|
|
|
ps.project?.teamName?.toLowerCase().includes(q) ||
|
|
|
|
|
ps.project?.competitionCategory?.toLowerCase().includes(q)
|
|
|
|
|
)
|
|
|
|
|
}, [projectStates, projectSearch])
|
|
|
|
|
|
|
|
|
|
// Existing assignments for the selected juror (to grey out already-assigned projects)
|
|
|
|
|
const jurorExistingProjectIds = useMemo(() => {
|
|
|
|
|
if (!selectedJurorId || !assignments) return new Set<string>()
|
|
|
|
|
return new Set(
|
|
|
|
|
assignments
|
|
|
|
|
.filter((a: any) => a.userId === selectedJurorId)
|
|
|
|
|
.map((a: any) => a.projectId)
|
|
|
|
|
)
|
|
|
|
|
}, [selectedJurorId, assignments])
|
|
|
|
|
|
|
|
|
|
const toggleProject = useCallback((projectId: string) => {
|
|
|
|
|
setSelectedProjectIds(prev => {
|
|
|
|
|
const next = new Set(prev)
|
|
|
|
|
if (next.has(projectId)) {
|
|
|
|
|
next.delete(projectId)
|
|
|
|
|
} else {
|
|
|
|
|
next.add(projectId)
|
|
|
|
|
}
|
|
|
|
|
return next
|
|
|
|
|
})
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
const selectAllUnassigned = useCallback(() => {
|
|
|
|
|
const unassigned = filteredProjects
|
|
|
|
|
.filter((ps: any) => !jurorExistingProjectIds.has(ps.project?.id))
|
|
|
|
|
.map((ps: any) => ps.project?.id)
|
|
|
|
|
.filter(Boolean)
|
|
|
|
|
setSelectedProjectIds(new Set(unassigned))
|
|
|
|
|
}, [filteredProjects, jurorExistingProjectIds])
|
|
|
|
|
|
|
|
|
|
const handleCreate = useCallback(() => {
|
|
|
|
|
if (!selectedJurorId || selectedProjectIds.size === 0) return
|
|
|
|
|
|
|
|
|
|
const projectIds = Array.from(selectedProjectIds)
|
|
|
|
|
if (projectIds.length === 1) {
|
|
|
|
|
createMutation.mutate({
|
|
|
|
|
userId: selectedJurorId,
|
|
|
|
|
projectId: projectIds[0],
|
|
|
|
|
roundId,
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
bulkCreateMutation.mutate({
|
|
|
|
|
roundId,
|
|
|
|
|
assignments: projectIds.map(projectId => ({
|
|
|
|
|
userId: selectedJurorId,
|
|
|
|
|
projectId,
|
|
|
|
|
})),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}, [selectedJurorId, selectedProjectIds, roundId, createMutation, bulkCreateMutation])
|
|
|
|
|
|
|
|
|
|
const isMutating = createMutation.isPending || bulkCreateMutation.isPending
|
|
|
|
|
|
2026-02-16 09:20:02 +01:00
|
|
|
return (
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<CardTitle className="text-base">All Assignments</CardTitle>
|
|
|
|
|
<CardDescription>
|
|
|
|
|
{assignments?.length ?? 0} individual jury-project assignments
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</div>
|
|
|
|
|
<Button size="sm" variant="outline" onClick={() => setAddDialogOpen(true)}>
|
|
|
|
|
<Plus className="h-4 w-4 mr-1.5" />
|
|
|
|
|
Add
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
{isLoading ? (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
{[1, 2, 3, 4, 5].map((i) => <Skeleton key={i} className="h-12 w-full" />)}
|
|
|
|
|
</div>
|
|
|
|
|
) : !assignments || assignments.length === 0 ? (
|
|
|
|
|
<p className="text-sm text-muted-foreground text-center py-6">
|
|
|
|
|
No assignments yet. Generate assignments or add one manually.
|
|
|
|
|
</p>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="space-y-1 max-h-[500px] overflow-y-auto">
|
|
|
|
|
<div className="grid grid-cols-[1fr_1fr_100px_60px] gap-2 text-xs text-muted-foreground font-medium px-3 py-2 sticky top-0 bg-background border-b">
|
|
|
|
|
<span>Juror</span>
|
|
|
|
|
<span>Project</span>
|
|
|
|
|
<span>Status</span>
|
|
|
|
|
<span />
|
|
|
|
|
</div>
|
2026-02-16 12:38:28 +01:00
|
|
|
{assignments.map((a: any, idx: number) => (
|
2026-02-16 09:20:02 +01:00
|
|
|
<div
|
|
|
|
|
key={a.id}
|
2026-02-16 12:38:28 +01:00
|
|
|
className={cn(
|
|
|
|
|
'grid grid-cols-[1fr_1fr_100px_60px] gap-2 items-center px-3 py-2 rounded-md text-sm transition-colors',
|
|
|
|
|
idx % 2 === 1 ? 'bg-muted/20' : 'hover:bg-muted/20',
|
|
|
|
|
)}
|
2026-02-16 09:20:02 +01:00
|
|
|
>
|
|
|
|
|
<span className="truncate">{a.user?.name || a.user?.email || 'Unknown'}</span>
|
|
|
|
|
<span className="truncate text-muted-foreground">{a.project?.title || 'Unknown'}</span>
|
|
|
|
|
<Badge
|
|
|
|
|
variant="outline"
|
|
|
|
|
className={cn(
|
|
|
|
|
'text-[10px] justify-center',
|
|
|
|
|
a.evaluation?.status === 'SUBMITTED'
|
|
|
|
|
? 'bg-emerald-50 text-emerald-700 border-emerald-200'
|
|
|
|
|
: a.evaluation?.status === 'IN_PROGRESS'
|
|
|
|
|
? 'bg-blue-50 text-blue-700 border-blue-200'
|
|
|
|
|
: 'bg-gray-50 text-gray-600 border-gray-200',
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
{a.evaluation?.status || 'PENDING'}
|
|
|
|
|
</Badge>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
className="h-7 w-7"
|
|
|
|
|
onClick={() => deleteMutation.mutate({ id: a.id })}
|
|
|
|
|
disabled={deleteMutation.isPending}
|
|
|
|
|
>
|
|
|
|
|
<Trash2 className="h-3.5 w-3.5 text-muted-foreground hover:text-red-500" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
|
|
|
|
|
{/* Add Assignment Dialog */}
|
Platform-wide UX fixes: assignment dialog, invalidation, settings, dashboard
1. Assignment dialog overhaul: replace raw UUID inputs with searchable
juror Combobox (shows name, email, capacity) and multi-select project
checklist with bulk assignment support
2. Query invalidation sweep: fix missing invalidations in
assignment-preview-sheet (roundAssignment.execute) and
filtering-dashboard (filtering.finalizeResults) so data refreshes
without page reload
3. Rename Submissions tab to Document Windows with descriptive
header explaining upload window configuration
4. Connect 6 disconnected settings: storage_provider, local_storage_path,
avatar_max_size_mb, allowed_image_types, whatsapp_enabled,
whatsapp_provider - all now accessible in Settings UI
5. Admin dashboard redesign: branded Editorial Command Center with
Dark Blue gradient header, colored border-l-4 stat cards, staggered
animations, 2-column layout, action-required panel, activity timeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:05:25 +01:00
|
|
|
<Dialog open={addDialogOpen} onOpenChange={(open) => {
|
|
|
|
|
if (!open) resetDialog()
|
|
|
|
|
else setAddDialogOpen(true)
|
|
|
|
|
}}>
|
|
|
|
|
<DialogContent className="sm:max-w-[540px]">
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
<DialogHeader>
|
2026-02-16 09:20:02 +01:00
|
|
|
<DialogTitle>Add Assignment</DialogTitle>
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
<DialogDescription>
|
Platform-wide UX fixes: assignment dialog, invalidation, settings, dashboard
1. Assignment dialog overhaul: replace raw UUID inputs with searchable
juror Combobox (shows name, email, capacity) and multi-select project
checklist with bulk assignment support
2. Query invalidation sweep: fix missing invalidations in
assignment-preview-sheet (roundAssignment.execute) and
filtering-dashboard (filtering.finalizeResults) so data refreshes
without page reload
3. Rename Submissions tab to Document Windows with descriptive
header explaining upload window configuration
4. Connect 6 disconnected settings: storage_provider, local_storage_path,
avatar_max_size_mb, allowed_image_types, whatsapp_enabled,
whatsapp_provider - all now accessible in Settings UI
5. Admin dashboard redesign: branded Editorial Command Center with
Dark Blue gradient header, colored border-l-4 stat cards, staggered
animations, 2-column layout, action-required panel, activity timeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:05:25 +01:00
|
|
|
Select a juror and one or more projects to assign
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
</DialogDescription>
|
|
|
|
|
</DialogHeader>
|
Platform-wide UX fixes: assignment dialog, invalidation, settings, dashboard
1. Assignment dialog overhaul: replace raw UUID inputs with searchable
juror Combobox (shows name, email, capacity) and multi-select project
checklist with bulk assignment support
2. Query invalidation sweep: fix missing invalidations in
assignment-preview-sheet (roundAssignment.execute) and
filtering-dashboard (filtering.finalizeResults) so data refreshes
without page reload
3. Rename Submissions tab to Document Windows with descriptive
header explaining upload window configuration
4. Connect 6 disconnected settings: storage_provider, local_storage_path,
avatar_max_size_mb, allowed_image_types, whatsapp_enabled,
whatsapp_provider - all now accessible in Settings UI
5. Admin dashboard redesign: branded Editorial Command Center with
Dark Blue gradient header, colored border-l-4 stat cards, staggered
animations, 2-column layout, action-required panel, activity timeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:05:25 +01:00
|
|
|
|
2026-02-16 09:20:02 +01:00
|
|
|
<div className="space-y-4">
|
Platform-wide UX fixes: assignment dialog, invalidation, settings, dashboard
1. Assignment dialog overhaul: replace raw UUID inputs with searchable
juror Combobox (shows name, email, capacity) and multi-select project
checklist with bulk assignment support
2. Query invalidation sweep: fix missing invalidations in
assignment-preview-sheet (roundAssignment.execute) and
filtering-dashboard (filtering.finalizeResults) so data refreshes
without page reload
3. Rename Submissions tab to Document Windows with descriptive
header explaining upload window configuration
4. Connect 6 disconnected settings: storage_provider, local_storage_path,
avatar_max_size_mb, allowed_image_types, whatsapp_enabled,
whatsapp_provider - all now accessible in Settings UI
5. Admin dashboard redesign: branded Editorial Command Center with
Dark Blue gradient header, colored border-l-4 stat cards, staggered
animations, 2-column layout, action-required panel, activity timeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:05:25 +01:00
|
|
|
{/* Juror Selector */}
|
2026-02-16 09:20:02 +01:00
|
|
|
<div className="space-y-2">
|
Platform-wide UX fixes: assignment dialog, invalidation, settings, dashboard
1. Assignment dialog overhaul: replace raw UUID inputs with searchable
juror Combobox (shows name, email, capacity) and multi-select project
checklist with bulk assignment support
2. Query invalidation sweep: fix missing invalidations in
assignment-preview-sheet (roundAssignment.execute) and
filtering-dashboard (filtering.finalizeResults) so data refreshes
without page reload
3. Rename Submissions tab to Document Windows with descriptive
header explaining upload window configuration
4. Connect 6 disconnected settings: storage_provider, local_storage_path,
avatar_max_size_mb, allowed_image_types, whatsapp_enabled,
whatsapp_provider - all now accessible in Settings UI
5. Admin dashboard redesign: branded Editorial Command Center with
Dark Blue gradient header, colored border-l-4 stat cards, staggered
animations, 2-column layout, action-required panel, activity timeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:05:25 +01:00
|
|
|
<Label className="text-sm font-medium">Juror</Label>
|
|
|
|
|
<Popover open={jurorPopoverOpen} onOpenChange={setJurorPopoverOpen}>
|
|
|
|
|
<PopoverTrigger asChild>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
role="combobox"
|
|
|
|
|
aria-expanded={jurorPopoverOpen}
|
|
|
|
|
className="w-full justify-between font-normal"
|
|
|
|
|
>
|
|
|
|
|
{selectedJuror
|
|
|
|
|
? (
|
|
|
|
|
<span className="flex items-center gap-2 truncate">
|
|
|
|
|
<span className="truncate">{selectedJuror.name || selectedJuror.email}</span>
|
|
|
|
|
<Badge variant="secondary" className="text-[10px] shrink-0">
|
|
|
|
|
{selectedJuror.currentAssignments}/{selectedJuror.maxAssignments ?? '\u221E'}
|
|
|
|
|
</Badge>
|
|
|
|
|
</span>
|
|
|
|
|
)
|
|
|
|
|
: <span className="text-muted-foreground">Select a jury member...</span>
|
|
|
|
|
}
|
|
|
|
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
|
|
|
</Button>
|
|
|
|
|
</PopoverTrigger>
|
|
|
|
|
<PopoverContent className="w-[480px] p-0" align="start">
|
|
|
|
|
<Command>
|
|
|
|
|
<CommandInput placeholder="Search by name or email..." />
|
|
|
|
|
<CommandList>
|
|
|
|
|
<CommandEmpty>No jury members found.</CommandEmpty>
|
|
|
|
|
<CommandGroup>
|
|
|
|
|
{juryMembers?.map((juror: any) => {
|
|
|
|
|
const atCapacity = juror.maxAssignments !== null && juror.availableSlots === 0
|
|
|
|
|
return (
|
|
|
|
|
<CommandItem
|
|
|
|
|
key={juror.id}
|
|
|
|
|
value={`${juror.name ?? ''} ${juror.email}`}
|
|
|
|
|
disabled={atCapacity}
|
|
|
|
|
onSelect={() => {
|
|
|
|
|
setSelectedJurorId(juror.id === selectedJurorId ? '' : juror.id)
|
|
|
|
|
setSelectedProjectIds(new Set())
|
|
|
|
|
setJurorPopoverOpen(false)
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Check
|
|
|
|
|
className={cn(
|
|
|
|
|
'mr-2 h-4 w-4',
|
|
|
|
|
selectedJurorId === juror.id ? 'opacity-100' : 'opacity-0',
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
<div className="flex flex-1 items-center justify-between min-w-0">
|
|
|
|
|
<div className="min-w-0">
|
|
|
|
|
<p className="text-sm font-medium truncate">
|
|
|
|
|
{juror.name || 'Unnamed'}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground truncate">
|
|
|
|
|
{juror.email}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Badge
|
|
|
|
|
variant={atCapacity ? 'destructive' : 'secondary'}
|
|
|
|
|
className="text-[10px] ml-2 shrink-0"
|
|
|
|
|
>
|
|
|
|
|
{juror.currentAssignments}/{juror.maxAssignments ?? '\u221E'}
|
|
|
|
|
{atCapacity ? ' full' : ''}
|
|
|
|
|
</Badge>
|
|
|
|
|
</div>
|
|
|
|
|
</CommandItem>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
</CommandGroup>
|
|
|
|
|
</CommandList>
|
|
|
|
|
</Command>
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
</Popover>
|
2026-02-16 09:20:02 +01:00
|
|
|
</div>
|
Platform-wide UX fixes: assignment dialog, invalidation, settings, dashboard
1. Assignment dialog overhaul: replace raw UUID inputs with searchable
juror Combobox (shows name, email, capacity) and multi-select project
checklist with bulk assignment support
2. Query invalidation sweep: fix missing invalidations in
assignment-preview-sheet (roundAssignment.execute) and
filtering-dashboard (filtering.finalizeResults) so data refreshes
without page reload
3. Rename Submissions tab to Document Windows with descriptive
header explaining upload window configuration
4. Connect 6 disconnected settings: storage_provider, local_storage_path,
avatar_max_size_mb, allowed_image_types, whatsapp_enabled,
whatsapp_provider - all now accessible in Settings UI
5. Admin dashboard redesign: branded Editorial Command Center with
Dark Blue gradient header, colored border-l-4 stat cards, staggered
animations, 2-column layout, action-required panel, activity timeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:05:25 +01:00
|
|
|
|
|
|
|
|
{/* Project Multi-Select */}
|
2026-02-16 09:20:02 +01:00
|
|
|
<div className="space-y-2">
|
Platform-wide UX fixes: assignment dialog, invalidation, settings, dashboard
1. Assignment dialog overhaul: replace raw UUID inputs with searchable
juror Combobox (shows name, email, capacity) and multi-select project
checklist with bulk assignment support
2. Query invalidation sweep: fix missing invalidations in
assignment-preview-sheet (roundAssignment.execute) and
filtering-dashboard (filtering.finalizeResults) so data refreshes
without page reload
3. Rename Submissions tab to Document Windows with descriptive
header explaining upload window configuration
4. Connect 6 disconnected settings: storage_provider, local_storage_path,
avatar_max_size_mb, allowed_image_types, whatsapp_enabled,
whatsapp_provider - all now accessible in Settings UI
5. Admin dashboard redesign: branded Editorial Command Center with
Dark Blue gradient header, colored border-l-4 stat cards, staggered
animations, 2-column layout, action-required panel, activity timeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:05:25 +01:00
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<Label className="text-sm font-medium">
|
|
|
|
|
Projects
|
|
|
|
|
{selectedProjectIds.size > 0 && (
|
|
|
|
|
<span className="ml-1.5 text-muted-foreground font-normal">
|
|
|
|
|
({selectedProjectIds.size} selected)
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</Label>
|
|
|
|
|
{selectedJurorId && (
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="h-7 text-xs"
|
|
|
|
|
onClick={selectAllUnassigned}
|
|
|
|
|
>
|
|
|
|
|
Select all
|
|
|
|
|
</Button>
|
|
|
|
|
{selectedProjectIds.size > 0 && (
|
|
|
|
|
<Button
|
|
|
|
|
type="button"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="sm"
|
|
|
|
|
className="h-7 text-xs"
|
|
|
|
|
onClick={() => setSelectedProjectIds(new Set())}
|
|
|
|
|
>
|
|
|
|
|
Clear
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Search input */}
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
|
|
|
|
<Input
|
|
|
|
|
placeholder="Filter projects..."
|
|
|
|
|
value={projectSearch}
|
|
|
|
|
onChange={(e) => setProjectSearch(e.target.value)}
|
|
|
|
|
className="pl-9 h-9"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Project checklist */}
|
|
|
|
|
<ScrollArea className="h-[240px] rounded-md border">
|
|
|
|
|
<div className="p-2 space-y-0.5">
|
|
|
|
|
{!selectedJurorId ? (
|
|
|
|
|
<p className="text-sm text-muted-foreground text-center py-8">
|
|
|
|
|
Select a juror first
|
|
|
|
|
</p>
|
|
|
|
|
) : filteredProjects.length === 0 ? (
|
|
|
|
|
<p className="text-sm text-muted-foreground text-center py-8">
|
|
|
|
|
No projects found
|
|
|
|
|
</p>
|
|
|
|
|
) : (
|
|
|
|
|
filteredProjects.map((ps: any) => {
|
|
|
|
|
const project = ps.project
|
|
|
|
|
if (!project) return null
|
|
|
|
|
const alreadyAssigned = jurorExistingProjectIds.has(project.id)
|
|
|
|
|
const isSelected = selectedProjectIds.has(project.id)
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<label
|
|
|
|
|
key={project.id}
|
|
|
|
|
className={cn(
|
|
|
|
|
'flex items-center gap-3 rounded-md px-2.5 py-2 text-sm cursor-pointer transition-colors',
|
|
|
|
|
alreadyAssigned
|
|
|
|
|
? 'opacity-50 cursor-not-allowed'
|
|
|
|
|
: isSelected
|
|
|
|
|
? 'bg-accent'
|
|
|
|
|
: 'hover:bg-muted/50',
|
|
|
|
|
)}
|
|
|
|
|
>
|
|
|
|
|
<Checkbox
|
|
|
|
|
checked={isSelected}
|
|
|
|
|
disabled={alreadyAssigned}
|
|
|
|
|
onCheckedChange={() => toggleProject(project.id)}
|
|
|
|
|
/>
|
|
|
|
|
<div className="flex flex-1 items-center justify-between min-w-0">
|
|
|
|
|
<span className="truncate">{project.title}</span>
|
|
|
|
|
<div className="flex items-center gap-1.5 shrink-0 ml-2">
|
|
|
|
|
{project.competitionCategory && (
|
|
|
|
|
<Badge variant="outline" className="text-[10px]">
|
|
|
|
|
{project.competitionCategory === 'STARTUP'
|
|
|
|
|
? 'Startup'
|
|
|
|
|
: project.competitionCategory === 'BUSINESS_CONCEPT'
|
|
|
|
|
? 'Concept'
|
|
|
|
|
: project.competitionCategory}
|
|
|
|
|
</Badge>
|
|
|
|
|
)}
|
|
|
|
|
{alreadyAssigned && (
|
|
|
|
|
<Badge variant="secondary" className="text-[10px]">
|
|
|
|
|
Assigned
|
|
|
|
|
</Badge>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</label>
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</ScrollArea>
|
2026-02-16 09:20:02 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
Platform-wide UX fixes: assignment dialog, invalidation, settings, dashboard
1. Assignment dialog overhaul: replace raw UUID inputs with searchable
juror Combobox (shows name, email, capacity) and multi-select project
checklist with bulk assignment support
2. Query invalidation sweep: fix missing invalidations in
assignment-preview-sheet (roundAssignment.execute) and
filtering-dashboard (filtering.finalizeResults) so data refreshes
without page reload
3. Rename Submissions tab to Document Windows with descriptive
header explaining upload window configuration
4. Connect 6 disconnected settings: storage_provider, local_storage_path,
avatar_max_size_mb, allowed_image_types, whatsapp_enabled,
whatsapp_provider - all now accessible in Settings UI
5. Admin dashboard redesign: branded Editorial Command Center with
Dark Blue gradient header, colored border-l-4 stat cards, staggered
animations, 2-column layout, action-required panel, activity timeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:05:25 +01:00
|
|
|
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
<DialogFooter>
|
Platform-wide UX fixes: assignment dialog, invalidation, settings, dashboard
1. Assignment dialog overhaul: replace raw UUID inputs with searchable
juror Combobox (shows name, email, capacity) and multi-select project
checklist with bulk assignment support
2. Query invalidation sweep: fix missing invalidations in
assignment-preview-sheet (roundAssignment.execute) and
filtering-dashboard (filtering.finalizeResults) so data refreshes
without page reload
3. Rename Submissions tab to Document Windows with descriptive
header explaining upload window configuration
4. Connect 6 disconnected settings: storage_provider, local_storage_path,
avatar_max_size_mb, allowed_image_types, whatsapp_enabled,
whatsapp_provider - all now accessible in Settings UI
5. Admin dashboard redesign: branded Editorial Command Center with
Dark Blue gradient header, colored border-l-4 stat cards, staggered
animations, 2-column layout, action-required panel, activity timeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:05:25 +01:00
|
|
|
<Button variant="outline" onClick={resetDialog}>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
2026-02-16 09:20:02 +01:00
|
|
|
<Button
|
Platform-wide UX fixes: assignment dialog, invalidation, settings, dashboard
1. Assignment dialog overhaul: replace raw UUID inputs with searchable
juror Combobox (shows name, email, capacity) and multi-select project
checklist with bulk assignment support
2. Query invalidation sweep: fix missing invalidations in
assignment-preview-sheet (roundAssignment.execute) and
filtering-dashboard (filtering.finalizeResults) so data refreshes
without page reload
3. Rename Submissions tab to Document Windows with descriptive
header explaining upload window configuration
4. Connect 6 disconnected settings: storage_provider, local_storage_path,
avatar_max_size_mb, allowed_image_types, whatsapp_enabled,
whatsapp_provider - all now accessible in Settings UI
5. Admin dashboard redesign: branded Editorial Command Center with
Dark Blue gradient header, colored border-l-4 stat cards, staggered
animations, 2-column layout, action-required panel, activity timeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:05:25 +01:00
|
|
|
onClick={handleCreate}
|
|
|
|
|
disabled={!selectedJurorId || selectedProjectIds.size === 0 || isMutating}
|
2026-02-16 09:20:02 +01:00
|
|
|
>
|
Platform-wide UX fixes: assignment dialog, invalidation, settings, dashboard
1. Assignment dialog overhaul: replace raw UUID inputs with searchable
juror Combobox (shows name, email, capacity) and multi-select project
checklist with bulk assignment support
2. Query invalidation sweep: fix missing invalidations in
assignment-preview-sheet (roundAssignment.execute) and
filtering-dashboard (filtering.finalizeResults) so data refreshes
without page reload
3. Rename Submissions tab to Document Windows with descriptive
header explaining upload window configuration
4. Connect 6 disconnected settings: storage_provider, local_storage_path,
avatar_max_size_mb, allowed_image_types, whatsapp_enabled,
whatsapp_provider - all now accessible in Settings UI
5. Admin dashboard redesign: branded Editorial Command Center with
Dark Blue gradient header, colored border-l-4 stat cards, staggered
animations, 2-column layout, action-required panel, activity timeline
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:05:25 +01:00
|
|
|
{isMutating && <Loader2 className="h-4 w-4 mr-1.5 animate-spin" />}
|
|
|
|
|
{selectedProjectIds.size <= 1
|
|
|
|
|
? 'Create Assignment'
|
|
|
|
|
: `Create ${selectedProjectIds.size} Assignments`
|
|
|
|
|
}
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
</Button>
|
|
|
|
|
</DialogFooter>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
2026-02-16 09:20:02 +01:00
|
|
|
</Card>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Evaluation Criteria Editor ───────────────────────────────────────────
|
|
|
|
|
|
AI category-aware evaluation: per-round config, file parsing, shortlist, advance flow
- Per-juror cap mode (HARD/SOFT/NONE) in add-member dialog and members table
- Jury invite flow: create user + add to group + send invitation from dialog
- Per-round config: notifyOnAdvance, aiParseFiles, startupAdvanceCount, conceptAdvanceCount
- Moved notify-on-advance from competition-level to per-round setting
- AI filtering: round-tagged files with newest-first sorting, optional file content extraction
- File content extractor service (pdf-parse for PDF, utf-8 for text files)
- AI shortlist runs independently per category (STARTUP / BUSINESS_CONCEPT)
- generateAIRecommendations tRPC endpoint with per-round config integration
- AI recommendations UI: trigger button, confirmation dialog, per-category results display
- Category-aware advance dialog: select/deselect projects by category with target caps
- STAGE_ACTIVE bug fix in assignment router
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:09:52 +01:00
|
|
|
// ── Advance Projects Dialog ─────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function AdvanceProjectsDialog({
|
|
|
|
|
open,
|
|
|
|
|
onOpenChange,
|
|
|
|
|
roundId,
|
|
|
|
|
projectStates,
|
|
|
|
|
config,
|
|
|
|
|
advanceMutation,
|
2026-02-16 16:43:23 +01:00
|
|
|
competitionRounds,
|
|
|
|
|
currentSortOrder,
|
AI category-aware evaluation: per-round config, file parsing, shortlist, advance flow
- Per-juror cap mode (HARD/SOFT/NONE) in add-member dialog and members table
- Jury invite flow: create user + add to group + send invitation from dialog
- Per-round config: notifyOnAdvance, aiParseFiles, startupAdvanceCount, conceptAdvanceCount
- Moved notify-on-advance from competition-level to per-round setting
- AI filtering: round-tagged files with newest-first sorting, optional file content extraction
- File content extractor service (pdf-parse for PDF, utf-8 for text files)
- AI shortlist runs independently per category (STARTUP / BUSINESS_CONCEPT)
- generateAIRecommendations tRPC endpoint with per-round config integration
- AI recommendations UI: trigger button, confirmation dialog, per-category results display
- Category-aware advance dialog: select/deselect projects by category with target caps
- STAGE_ACTIVE bug fix in assignment router
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:09:52 +01:00
|
|
|
}: {
|
|
|
|
|
open: boolean
|
|
|
|
|
onOpenChange: (open: boolean) => void
|
|
|
|
|
roundId: string
|
|
|
|
|
projectStates: any[] | undefined
|
|
|
|
|
config: Record<string, unknown>
|
2026-02-16 16:43:23 +01:00
|
|
|
advanceMutation: { mutate: (input: { roundId: string; projectIds?: string[]; targetRoundId?: string }) => void; isPending: boolean }
|
|
|
|
|
competitionRounds?: Array<{ id: string; name: string; sortOrder: number; roundType: string }>
|
|
|
|
|
currentSortOrder?: number
|
AI category-aware evaluation: per-round config, file parsing, shortlist, advance flow
- Per-juror cap mode (HARD/SOFT/NONE) in add-member dialog and members table
- Jury invite flow: create user + add to group + send invitation from dialog
- Per-round config: notifyOnAdvance, aiParseFiles, startupAdvanceCount, conceptAdvanceCount
- Moved notify-on-advance from competition-level to per-round setting
- AI filtering: round-tagged files with newest-first sorting, optional file content extraction
- File content extractor service (pdf-parse for PDF, utf-8 for text files)
- AI shortlist runs independently per category (STARTUP / BUSINESS_CONCEPT)
- generateAIRecommendations tRPC endpoint with per-round config integration
- AI recommendations UI: trigger button, confirmation dialog, per-category results display
- Category-aware advance dialog: select/deselect projects by category with target caps
- STAGE_ACTIVE bug fix in assignment router
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:09:52 +01:00
|
|
|
}) {
|
2026-02-16 16:43:23 +01:00
|
|
|
// Target round selector
|
|
|
|
|
const availableTargets = useMemo(() =>
|
|
|
|
|
(competitionRounds ?? [])
|
|
|
|
|
.filter((r) => r.sortOrder > (currentSortOrder ?? -1) && r.id !== roundId)
|
|
|
|
|
.sort((a, b) => a.sortOrder - b.sortOrder),
|
|
|
|
|
[competitionRounds, currentSortOrder, roundId])
|
|
|
|
|
|
|
|
|
|
const [targetRoundId, setTargetRoundId] = useState<string>('')
|
|
|
|
|
|
|
|
|
|
// Default to first available target when dialog opens
|
|
|
|
|
if (open && !targetRoundId && availableTargets.length > 0) {
|
|
|
|
|
setTargetRoundId(availableTargets[0].id)
|
|
|
|
|
}
|
AI category-aware evaluation: per-round config, file parsing, shortlist, advance flow
- Per-juror cap mode (HARD/SOFT/NONE) in add-member dialog and members table
- Jury invite flow: create user + add to group + send invitation from dialog
- Per-round config: notifyOnAdvance, aiParseFiles, startupAdvanceCount, conceptAdvanceCount
- Moved notify-on-advance from competition-level to per-round setting
- AI filtering: round-tagged files with newest-first sorting, optional file content extraction
- File content extractor service (pdf-parse for PDF, utf-8 for text files)
- AI shortlist runs independently per category (STARTUP / BUSINESS_CONCEPT)
- generateAIRecommendations tRPC endpoint with per-round config integration
- AI recommendations UI: trigger button, confirmation dialog, per-category results display
- Category-aware advance dialog: select/deselect projects by category with target caps
- STAGE_ACTIVE bug fix in assignment router
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:09:52 +01:00
|
|
|
const passedProjects = useMemo(() =>
|
|
|
|
|
(projectStates ?? []).filter((ps: any) => ps.state === 'PASSED'),
|
|
|
|
|
[projectStates])
|
|
|
|
|
|
|
|
|
|
const startups = useMemo(() =>
|
|
|
|
|
passedProjects.filter((ps: any) => ps.project?.competitionCategory === 'STARTUP'),
|
|
|
|
|
[passedProjects])
|
|
|
|
|
|
|
|
|
|
const concepts = useMemo(() =>
|
|
|
|
|
passedProjects.filter((ps: any) => ps.project?.competitionCategory === 'BUSINESS_CONCEPT'),
|
|
|
|
|
[passedProjects])
|
|
|
|
|
|
|
|
|
|
const other = useMemo(() =>
|
|
|
|
|
passedProjects.filter((ps: any) =>
|
|
|
|
|
ps.project?.competitionCategory !== 'STARTUP' && ps.project?.competitionCategory !== 'BUSINESS_CONCEPT',
|
|
|
|
|
),
|
|
|
|
|
[passedProjects])
|
|
|
|
|
|
|
|
|
|
const startupCap = (config.startupAdvanceCount as number) || 0
|
|
|
|
|
const conceptCap = (config.conceptAdvanceCount as number) || 0
|
|
|
|
|
|
|
|
|
|
const [selected, setSelected] = useState<Set<string>>(new Set())
|
|
|
|
|
|
|
|
|
|
// Reset selection when dialog opens
|
|
|
|
|
if (open && selected.size === 0 && passedProjects.length > 0) {
|
|
|
|
|
const initial = new Set<string>()
|
|
|
|
|
// Auto-select all (or up to cap if configured)
|
|
|
|
|
const startupSlice = startupCap > 0 ? startups.slice(0, startupCap) : startups
|
|
|
|
|
const conceptSlice = conceptCap > 0 ? concepts.slice(0, conceptCap) : concepts
|
|
|
|
|
for (const ps of startupSlice) initial.add(ps.project?.id)
|
|
|
|
|
for (const ps of conceptSlice) initial.add(ps.project?.id)
|
|
|
|
|
for (const ps of other) initial.add(ps.project?.id)
|
|
|
|
|
setSelected(initial)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const toggleProject = (projectId: string) => {
|
|
|
|
|
setSelected((prev) => {
|
|
|
|
|
const next = new Set(prev)
|
|
|
|
|
if (next.has(projectId)) next.delete(projectId)
|
|
|
|
|
else next.add(projectId)
|
|
|
|
|
return next
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const toggleAll = (projects: any[], on: boolean) => {
|
|
|
|
|
setSelected((prev) => {
|
|
|
|
|
const next = new Set(prev)
|
|
|
|
|
for (const ps of projects) {
|
|
|
|
|
if (on) next.add(ps.project?.id)
|
|
|
|
|
else next.delete(ps.project?.id)
|
|
|
|
|
}
|
|
|
|
|
return next
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleAdvance = () => {
|
|
|
|
|
const ids = Array.from(selected)
|
|
|
|
|
if (ids.length === 0) return
|
2026-02-16 16:43:23 +01:00
|
|
|
advanceMutation.mutate({
|
|
|
|
|
roundId,
|
|
|
|
|
projectIds: ids,
|
|
|
|
|
...(targetRoundId ? { targetRoundId } : {}),
|
|
|
|
|
})
|
AI category-aware evaluation: per-round config, file parsing, shortlist, advance flow
- Per-juror cap mode (HARD/SOFT/NONE) in add-member dialog and members table
- Jury invite flow: create user + add to group + send invitation from dialog
- Per-round config: notifyOnAdvance, aiParseFiles, startupAdvanceCount, conceptAdvanceCount
- Moved notify-on-advance from competition-level to per-round setting
- AI filtering: round-tagged files with newest-first sorting, optional file content extraction
- File content extractor service (pdf-parse for PDF, utf-8 for text files)
- AI shortlist runs independently per category (STARTUP / BUSINESS_CONCEPT)
- generateAIRecommendations tRPC endpoint with per-round config integration
- AI recommendations UI: trigger button, confirmation dialog, per-category results display
- Category-aware advance dialog: select/deselect projects by category with target caps
- STAGE_ACTIVE bug fix in assignment router
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:09:52 +01:00
|
|
|
onOpenChange(false)
|
|
|
|
|
setSelected(new Set())
|
2026-02-16 16:43:23 +01:00
|
|
|
setTargetRoundId('')
|
AI category-aware evaluation: per-round config, file parsing, shortlist, advance flow
- Per-juror cap mode (HARD/SOFT/NONE) in add-member dialog and members table
- Jury invite flow: create user + add to group + send invitation from dialog
- Per-round config: notifyOnAdvance, aiParseFiles, startupAdvanceCount, conceptAdvanceCount
- Moved notify-on-advance from competition-level to per-round setting
- AI filtering: round-tagged files with newest-first sorting, optional file content extraction
- File content extractor service (pdf-parse for PDF, utf-8 for text files)
- AI shortlist runs independently per category (STARTUP / BUSINESS_CONCEPT)
- generateAIRecommendations tRPC endpoint with per-round config integration
- AI recommendations UI: trigger button, confirmation dialog, per-category results display
- Category-aware advance dialog: select/deselect projects by category with target caps
- STAGE_ACTIVE bug fix in assignment router
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:09:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleClose = () => {
|
|
|
|
|
onOpenChange(false)
|
|
|
|
|
setSelected(new Set())
|
2026-02-16 16:43:23 +01:00
|
|
|
setTargetRoundId('')
|
AI category-aware evaluation: per-round config, file parsing, shortlist, advance flow
- Per-juror cap mode (HARD/SOFT/NONE) in add-member dialog and members table
- Jury invite flow: create user + add to group + send invitation from dialog
- Per-round config: notifyOnAdvance, aiParseFiles, startupAdvanceCount, conceptAdvanceCount
- Moved notify-on-advance from competition-level to per-round setting
- AI filtering: round-tagged files with newest-first sorting, optional file content extraction
- File content extractor service (pdf-parse for PDF, utf-8 for text files)
- AI shortlist runs independently per category (STARTUP / BUSINESS_CONCEPT)
- generateAIRecommendations tRPC endpoint with per-round config integration
- AI recommendations UI: trigger button, confirmation dialog, per-category results display
- Category-aware advance dialog: select/deselect projects by category with target caps
- STAGE_ACTIVE bug fix in assignment router
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:09:52 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const renderCategorySection = (
|
|
|
|
|
label: string,
|
|
|
|
|
projects: any[],
|
|
|
|
|
cap: number,
|
|
|
|
|
badgeColor: string,
|
|
|
|
|
) => {
|
|
|
|
|
const selectedInCategory = projects.filter((ps: any) => selected.has(ps.project?.id)).length
|
|
|
|
|
const overCap = cap > 0 && selectedInCategory > cap
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Checkbox
|
|
|
|
|
checked={projects.length > 0 && projects.every((ps: any) => selected.has(ps.project?.id))}
|
|
|
|
|
onCheckedChange={(checked) => toggleAll(projects, !!checked)}
|
|
|
|
|
/>
|
|
|
|
|
<span className="text-sm font-medium">{label}</span>
|
|
|
|
|
<Badge variant="secondary" className={cn('text-[10px]', badgeColor)}>
|
|
|
|
|
{selectedInCategory}/{projects.length}
|
|
|
|
|
</Badge>
|
|
|
|
|
{cap > 0 && (
|
|
|
|
|
<span className={cn('text-[10px]', overCap ? 'text-red-500 font-medium' : 'text-muted-foreground')}>
|
|
|
|
|
(target: {cap})
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{projects.length === 0 ? (
|
|
|
|
|
<p className="text-xs text-muted-foreground pl-7">No passed projects in this category</p>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="space-y-1 pl-7">
|
|
|
|
|
{projects.map((ps: any) => (
|
|
|
|
|
<label
|
|
|
|
|
key={ps.project?.id}
|
|
|
|
|
className="flex items-center gap-2 p-2 rounded hover:bg-muted/30 cursor-pointer"
|
|
|
|
|
>
|
|
|
|
|
<Checkbox
|
|
|
|
|
checked={selected.has(ps.project?.id)}
|
|
|
|
|
onCheckedChange={() => toggleProject(ps.project?.id)}
|
|
|
|
|
/>
|
|
|
|
|
<span className="text-sm truncate flex-1">{ps.project?.title || 'Untitled'}</span>
|
|
|
|
|
{ps.project?.teamName && (
|
|
|
|
|
<span className="text-xs text-muted-foreground shrink-0">{ps.project.teamName}</span>
|
|
|
|
|
)}
|
|
|
|
|
</label>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Dialog open={open} onOpenChange={handleClose}>
|
|
|
|
|
<DialogContent className="max-w-lg max-h-[85vh] flex flex-col">
|
|
|
|
|
<DialogHeader>
|
|
|
|
|
<DialogTitle>Advance Projects</DialogTitle>
|
|
|
|
|
<DialogDescription>
|
2026-02-16 16:43:23 +01:00
|
|
|
Select which passed projects to advance.
|
AI category-aware evaluation: per-round config, file parsing, shortlist, advance flow
- Per-juror cap mode (HARD/SOFT/NONE) in add-member dialog and members table
- Jury invite flow: create user + add to group + send invitation from dialog
- Per-round config: notifyOnAdvance, aiParseFiles, startupAdvanceCount, conceptAdvanceCount
- Moved notify-on-advance from competition-level to per-round setting
- AI filtering: round-tagged files with newest-first sorting, optional file content extraction
- File content extractor service (pdf-parse for PDF, utf-8 for text files)
- AI shortlist runs independently per category (STARTUP / BUSINESS_CONCEPT)
- generateAIRecommendations tRPC endpoint with per-round config integration
- AI recommendations UI: trigger button, confirmation dialog, per-category results display
- Category-aware advance dialog: select/deselect projects by category with target caps
- STAGE_ACTIVE bug fix in assignment router
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:09:52 +01:00
|
|
|
{selected.size} of {passedProjects.length} selected.
|
|
|
|
|
</DialogDescription>
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
|
2026-02-16 16:43:23 +01:00
|
|
|
{/* Target round selector */}
|
|
|
|
|
{availableTargets.length > 0 && (
|
|
|
|
|
<div className="space-y-2 pb-2 border-b">
|
|
|
|
|
<Label className="text-sm">Advance to</Label>
|
|
|
|
|
<Select value={targetRoundId} onValueChange={setTargetRoundId}>
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
<SelectValue placeholder="Select target round" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{availableTargets.map((r) => (
|
|
|
|
|
<SelectItem key={r.id} value={r.id}>
|
|
|
|
|
{r.name} ({r.roundType.replace('_', ' ').toLowerCase()})
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{availableTargets.length === 0 && (
|
|
|
|
|
<div className="text-sm text-amber-600 bg-amber-50 rounded-md p-3">
|
|
|
|
|
No subsequent rounds found. Projects will advance to the next round by sort order.
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
AI category-aware evaluation: per-round config, file parsing, shortlist, advance flow
- Per-juror cap mode (HARD/SOFT/NONE) in add-member dialog and members table
- Jury invite flow: create user + add to group + send invitation from dialog
- Per-round config: notifyOnAdvance, aiParseFiles, startupAdvanceCount, conceptAdvanceCount
- Moved notify-on-advance from competition-level to per-round setting
- AI filtering: round-tagged files with newest-first sorting, optional file content extraction
- File content extractor service (pdf-parse for PDF, utf-8 for text files)
- AI shortlist runs independently per category (STARTUP / BUSINESS_CONCEPT)
- generateAIRecommendations tRPC endpoint with per-round config integration
- AI recommendations UI: trigger button, confirmation dialog, per-category results display
- Category-aware advance dialog: select/deselect projects by category with target caps
- STAGE_ACTIVE bug fix in assignment router
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:09:52 +01:00
|
|
|
<div className="flex-1 overflow-y-auto space-y-4 py-2">
|
|
|
|
|
{renderCategorySection('Startup', startups, startupCap, 'bg-blue-100 text-blue-700')}
|
|
|
|
|
{renderCategorySection('Business Concept', concepts, conceptCap, 'bg-purple-100 text-purple-700')}
|
|
|
|
|
{other.length > 0 && renderCategorySection('Other / Uncategorized', other, 0, 'bg-gray-100 text-gray-700')}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<DialogFooter>
|
|
|
|
|
<Button variant="outline" onClick={handleClose}>Cancel</Button>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={handleAdvance}
|
|
|
|
|
disabled={selected.size === 0 || advanceMutation.isPending}
|
|
|
|
|
>
|
|
|
|
|
{advanceMutation.isPending && <Loader2 className="h-4 w-4 mr-1.5 animate-spin" />}
|
|
|
|
|
Advance {selected.size} Project{selected.size !== 1 ? 's' : ''}
|
|
|
|
|
</Button>
|
|
|
|
|
</DialogFooter>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── AI Recommendations Display ──────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
type RecommendationItem = {
|
|
|
|
|
projectId: string
|
|
|
|
|
rank: number
|
|
|
|
|
score: number
|
|
|
|
|
category: string
|
|
|
|
|
strengths: string[]
|
|
|
|
|
concerns: string[]
|
|
|
|
|
recommendation: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function AIRecommendationsDisplay({
|
|
|
|
|
recommendations,
|
|
|
|
|
onClear,
|
|
|
|
|
}: {
|
|
|
|
|
recommendations: { STARTUP: RecommendationItem[]; BUSINESS_CONCEPT: RecommendationItem[] }
|
|
|
|
|
onClear: () => void
|
|
|
|
|
}) {
|
|
|
|
|
const [expandedId, setExpandedId] = useState<string | null>(null)
|
|
|
|
|
|
|
|
|
|
const renderCategory = (label: string, items: RecommendationItem[], colorClass: string) => {
|
|
|
|
|
if (items.length === 0) return (
|
|
|
|
|
<div className="text-center py-4 text-muted-foreground text-sm">
|
|
|
|
|
No {label.toLowerCase()} projects evaluated
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
{items.map((item) => {
|
|
|
|
|
const isExpanded = expandedId === `${item.category}-${item.projectId}`
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
key={item.projectId}
|
|
|
|
|
className="border rounded-lg overflow-hidden"
|
|
|
|
|
>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setExpandedId(isExpanded ? null : `${item.category}-${item.projectId}`)}
|
|
|
|
|
className="w-full flex items-center gap-3 p-3 text-left hover:bg-muted/30 transition-colors"
|
|
|
|
|
>
|
|
|
|
|
<span className={cn(
|
2026-02-16 12:38:28 +01:00
|
|
|
'h-7 w-7 rounded-full flex items-center justify-center text-xs font-bold text-white shrink-0 shadow-sm',
|
|
|
|
|
colorClass === 'bg-blue-500' ? 'bg-gradient-to-br from-blue-400 to-blue-600' : 'bg-gradient-to-br from-purple-400 to-purple-600',
|
AI category-aware evaluation: per-round config, file parsing, shortlist, advance flow
- Per-juror cap mode (HARD/SOFT/NONE) in add-member dialog and members table
- Jury invite flow: create user + add to group + send invitation from dialog
- Per-round config: notifyOnAdvance, aiParseFiles, startupAdvanceCount, conceptAdvanceCount
- Moved notify-on-advance from competition-level to per-round setting
- AI filtering: round-tagged files with newest-first sorting, optional file content extraction
- File content extractor service (pdf-parse for PDF, utf-8 for text files)
- AI shortlist runs independently per category (STARTUP / BUSINESS_CONCEPT)
- generateAIRecommendations tRPC endpoint with per-round config integration
- AI recommendations UI: trigger button, confirmation dialog, per-category results display
- Category-aware advance dialog: select/deselect projects by category with target caps
- STAGE_ACTIVE bug fix in assignment router
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:09:52 +01:00
|
|
|
)}>
|
|
|
|
|
{item.rank}
|
|
|
|
|
</span>
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
<p className="text-sm font-medium truncate">{item.projectId}</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground truncate">{item.recommendation}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Badge variant="outline" className="shrink-0 text-xs font-mono">
|
|
|
|
|
{item.score}/100
|
|
|
|
|
</Badge>
|
|
|
|
|
<ChevronDown className={cn(
|
|
|
|
|
'h-4 w-4 text-muted-foreground transition-transform shrink-0',
|
|
|
|
|
isExpanded && 'rotate-180',
|
|
|
|
|
)} />
|
|
|
|
|
</button>
|
|
|
|
|
{isExpanded && (
|
|
|
|
|
<div className="px-3 pb-3 pt-0 space-y-2 border-t bg-muted/10">
|
|
|
|
|
<div className="pt-2">
|
|
|
|
|
<p className="text-xs font-medium text-emerald-700 mb-1">Strengths</p>
|
|
|
|
|
<ul className="text-xs text-muted-foreground space-y-0.5 pl-4 list-disc">
|
|
|
|
|
{item.strengths.map((s, i) => <li key={i}>{s}</li>)}
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
{item.concerns.length > 0 && (
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-xs font-medium text-amber-700 mb-1">Concerns</p>
|
|
|
|
|
<ul className="text-xs text-muted-foreground space-y-0.5 pl-4 list-disc">
|
|
|
|
|
{item.concerns.map((c, i) => <li key={i}>{c}</li>)}
|
|
|
|
|
</ul>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
<div>
|
|
|
|
|
<p className="text-xs font-medium text-blue-700 mb-1">Recommendation</p>
|
|
|
|
|
<p className="text-xs text-muted-foreground">{item.recommendation}</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<CardTitle className="text-base">AI Shortlist Recommendations</CardTitle>
|
|
|
|
|
<CardDescription>
|
|
|
|
|
Ranked independently per category — {recommendations.STARTUP.length} startups, {recommendations.BUSINESS_CONCEPT.length} concepts
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</div>
|
|
|
|
|
<Button variant="ghost" size="sm" onClick={onClear}>
|
|
|
|
|
<X className="h-4 w-4 mr-1" />
|
|
|
|
|
Dismiss
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
<div className="grid gap-6 lg:grid-cols-2">
|
|
|
|
|
<div>
|
|
|
|
|
<h4 className="text-sm font-semibold mb-3 flex items-center gap-2">
|
|
|
|
|
<div className="h-2 w-2 rounded-full bg-blue-500" />
|
|
|
|
|
Startup ({recommendations.STARTUP.length})
|
|
|
|
|
</h4>
|
|
|
|
|
{renderCategory('Startup', recommendations.STARTUP, 'bg-blue-500')}
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<h4 className="text-sm font-semibold mb-3 flex items-center gap-2">
|
|
|
|
|
<div className="h-2 w-2 rounded-full bg-purple-500" />
|
|
|
|
|
Business Concept ({recommendations.BUSINESS_CONCEPT.length})
|
|
|
|
|
</h4>
|
|
|
|
|
{renderCategory('Business Concept', recommendations.BUSINESS_CONCEPT, 'bg-purple-500')}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Evaluation Criteria Editor ───────────────────────────────────────────
|
|
|
|
|
|
2026-02-16 09:20:02 +01:00
|
|
|
function EvaluationCriteriaEditor({ roundId }: { roundId: string }) {
|
|
|
|
|
const [editing, setEditing] = useState(false)
|
|
|
|
|
const [criteria, setCriteria] = useState<Array<{
|
|
|
|
|
id: string; label: string; description?: string; weight?: number; minScore?: number; maxScore?: number
|
|
|
|
|
}>>([])
|
|
|
|
|
|
|
|
|
|
const utils = trpc.useUtils()
|
2026-02-16 09:30:19 +01:00
|
|
|
const { data: form, isLoading } = trpc.evaluation.getForm.useQuery(
|
|
|
|
|
{ roundId },
|
|
|
|
|
{ refetchInterval: 30_000 },
|
|
|
|
|
)
|
2026-02-16 09:20:02 +01:00
|
|
|
|
|
|
|
|
const upsertMutation = trpc.evaluation.upsertForm.useMutation({
|
|
|
|
|
onSuccess: () => {
|
|
|
|
|
utils.evaluation.getForm.invalidate({ roundId })
|
|
|
|
|
toast.success('Evaluation criteria saved')
|
|
|
|
|
setEditing(false)
|
|
|
|
|
},
|
|
|
|
|
onError: (err) => toast.error(err.message),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Sync from server
|
|
|
|
|
if (form && !editing) {
|
|
|
|
|
const serverCriteria = form.criteriaJson ?? []
|
|
|
|
|
if (JSON.stringify(serverCriteria) !== JSON.stringify(criteria)) {
|
|
|
|
|
setCriteria(serverCriteria)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleAdd = () => {
|
|
|
|
|
setCriteria([...criteria, {
|
|
|
|
|
id: `c-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
|
|
|
|
label: '',
|
|
|
|
|
description: '',
|
|
|
|
|
weight: 1,
|
|
|
|
|
minScore: 0,
|
|
|
|
|
maxScore: 10,
|
|
|
|
|
}])
|
|
|
|
|
setEditing(true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleRemove = (id: string) => {
|
|
|
|
|
setCriteria(criteria.filter((c) => c.id !== id))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleChange = (id: string, field: string, value: string | number) => {
|
|
|
|
|
setCriteria(criteria.map((c) =>
|
|
|
|
|
c.id === id ? { ...c, [field]: value } : c,
|
|
|
|
|
))
|
|
|
|
|
setEditing(true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleSave = () => {
|
|
|
|
|
const validCriteria = criteria.filter((c) => c.label.trim())
|
|
|
|
|
if (validCriteria.length === 0) {
|
|
|
|
|
toast.error('Add at least one criterion')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
upsertMutation.mutate({ roundId, criteria: validCriteria })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Card>
|
|
|
|
|
<CardHeader>
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<CardTitle className="text-base">Evaluation Criteria</CardTitle>
|
|
|
|
|
<CardDescription>
|
|
|
|
|
{form ? `Version ${form.version} \u2014 ${form.criteriaJson.length} criteria` : 'No criteria defined yet'}
|
|
|
|
|
</CardDescription>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
{editing && (
|
|
|
|
|
<Button size="sm" variant="outline" onClick={() => {
|
|
|
|
|
setEditing(false)
|
|
|
|
|
if (form) setCriteria(form.criteriaJson)
|
|
|
|
|
}}>
|
|
|
|
|
Cancel
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
{editing ? (
|
|
|
|
|
<Button size="sm" onClick={handleSave} disabled={upsertMutation.isPending}>
|
|
|
|
|
{upsertMutation.isPending && <Loader2 className="h-4 w-4 mr-1.5 animate-spin" />}
|
|
|
|
|
Save Criteria
|
|
|
|
|
</Button>
|
|
|
|
|
) : (
|
|
|
|
|
<Button size="sm" variant="outline" onClick={handleAdd}>
|
|
|
|
|
<Plus className="h-4 w-4 mr-1.5" />
|
|
|
|
|
Add Criterion
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent>
|
|
|
|
|
{isLoading ? (
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
{[1, 2, 3].map((i) => <Skeleton key={i} className="h-16 w-full" />)}
|
|
|
|
|
</div>
|
|
|
|
|
) : criteria.length === 0 ? (
|
|
|
|
|
<div className="text-center py-6 text-muted-foreground">
|
|
|
|
|
<FileText className="h-8 w-8 mx-auto mb-2 opacity-40" />
|
|
|
|
|
<p className="text-sm">No evaluation criteria defined</p>
|
|
|
|
|
<p className="text-xs mt-1">Add criteria that jurors will use to score projects</p>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
{criteria.map((c, idx) => (
|
|
|
|
|
<div key={c.id} className="flex gap-3 items-start p-3 border rounded-lg bg-muted/20">
|
|
|
|
|
<span className="text-xs text-muted-foreground font-mono mt-2.5 shrink-0 w-5 text-center">
|
|
|
|
|
{idx + 1}
|
|
|
|
|
</span>
|
|
|
|
|
<div className="flex-1 space-y-2">
|
|
|
|
|
<Input
|
|
|
|
|
placeholder="Criterion label (e.g., Innovation)"
|
|
|
|
|
value={c.label}
|
|
|
|
|
onChange={(e) => handleChange(c.id, 'label', e.target.value)}
|
|
|
|
|
className="h-8 text-sm"
|
|
|
|
|
/>
|
|
|
|
|
<Input
|
|
|
|
|
placeholder="Description (optional)"
|
|
|
|
|
value={c.description || ''}
|
|
|
|
|
onChange={(e) => handleChange(c.id, 'description', e.target.value)}
|
|
|
|
|
className="h-7 text-xs"
|
|
|
|
|
/>
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
<Label className="text-[10px] text-muted-foreground">Weight</Label>
|
|
|
|
|
<Input
|
|
|
|
|
type="number"
|
|
|
|
|
min={0}
|
|
|
|
|
max={100}
|
|
|
|
|
value={c.weight ?? 1}
|
|
|
|
|
onChange={(e) => handleChange(c.id, 'weight', Number(e.target.value))}
|
|
|
|
|
className="h-7 text-xs"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
<Label className="text-[10px] text-muted-foreground">Min Score</Label>
|
|
|
|
|
<Input
|
|
|
|
|
type="number"
|
|
|
|
|
min={0}
|
|
|
|
|
value={c.minScore ?? 0}
|
|
|
|
|
onChange={(e) => handleChange(c.id, 'minScore', Number(e.target.value))}
|
|
|
|
|
className="h-7 text-xs"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
<Label className="text-[10px] text-muted-foreground">Max Score</Label>
|
|
|
|
|
<Input
|
|
|
|
|
type="number"
|
|
|
|
|
min={1}
|
|
|
|
|
value={c.maxScore ?? 10}
|
|
|
|
|
onChange={(e) => handleChange(c.id, 'maxScore', Number(e.target.value))}
|
|
|
|
|
className="h-7 text-xs"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon"
|
|
|
|
|
className="h-7 w-7 mt-1 shrink-0"
|
|
|
|
|
onClick={() => { handleRemove(c.id); setEditing(true) }}
|
|
|
|
|
>
|
|
|
|
|
<X className="h-3.5 w-3.5 text-muted-foreground hover:text-red-500" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
{!editing && (
|
|
|
|
|
<Button variant="outline" size="sm" className="w-full" onClick={handleAdd}>
|
|
|
|
|
<Plus className="h-4 w-4 mr-1.5" />
|
|
|
|
|
Add Criterion
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
Admin system overhaul: full round config UI, flattened navigation, juries, awards integration, evaluation rewrite
- 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>
2026-02-16 01:16:55 +01:00
|
|
|
)
|
|
|
|
|
}
|