Fix round reopen bug + redesign round detail page UI
Build and Push Docker Image / build (push) Failing after 18s
Details
Build and Push Docker Image / build (push) Failing after 18s
Details
Round engine: moved logAudit() calls outside $transaction blocks to prevent FK violations from poisoning PostgreSQL transactions and rolling back status changes. Round detail page: redesigned with Editorial Command Center aesthetic - dark blue gradient header, colored accent stat cards, underline tab bar, SVG readiness ring, grouped quick actions, branded progress bars and animations. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
079468d2ca
commit
86fa542371
|
|
@ -87,6 +87,8 @@ import { FilteringDashboard } from '@/components/admin/round/filtering-dashboard
|
||||||
import { CoverageReport } from '@/components/admin/assignment/coverage-report'
|
import { CoverageReport } from '@/components/admin/assignment/coverage-report'
|
||||||
import { AssignmentPreviewSheet } from '@/components/admin/assignment/assignment-preview-sheet'
|
import { AssignmentPreviewSheet } from '@/components/admin/assignment/assignment-preview-sheet'
|
||||||
import { CsvExportDialog } from '@/components/shared/csv-export-dialog'
|
import { CsvExportDialog } from '@/components/shared/csv-export-dialog'
|
||||||
|
import { AnimatedCard } from '@/components/shared/animated-container'
|
||||||
|
import { motion } from 'motion/react'
|
||||||
|
|
||||||
// ── Status & type config maps ──────────────────────────────────────────────
|
// ── Status & type config maps ──────────────────────────────────────────────
|
||||||
const roundStatusConfig = {
|
const roundStatusConfig = {
|
||||||
|
|
@ -313,18 +315,21 @@ export default function RoundDetailPage() {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
{/* 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">
|
<div className="flex items-center gap-3">
|
||||||
<Skeleton className="h-8 w-8" />
|
<Skeleton className="h-8 w-8 rounded bg-white/20" />
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Skeleton className="h-7 w-64" />
|
<Skeleton className="h-7 w-64 bg-white/20" />
|
||||||
<Skeleton className="h-4 w-40" />
|
<Skeleton className="h-4 w-40 bg-white/20" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-3 grid-cols-2 sm:grid-cols-4">
|
<div className="grid gap-3 grid-cols-2 sm:grid-cols-4">
|
||||||
{[1, 2, 3, 4].map((i) => <Skeleton key={i} className="h-24" />)}
|
{[1, 2, 3, 4].map((i) => <Skeleton key={i} className="h-28 rounded-lg" />)}
|
||||||
</div>
|
</div>
|
||||||
<Skeleton className="h-10 w-full" />
|
<Skeleton className="h-10 w-full" />
|
||||||
<Skeleton className="h-96 w-full" />
|
<Skeleton className="h-96 w-full rounded-lg" />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -398,18 +403,24 @@ export default function RoundDetailPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* ===== HEADER ===== */}
|
{/* ===== 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 flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||||
<div className="flex items-start gap-3 min-w-0">
|
<div className="flex items-start gap-3 min-w-0">
|
||||||
<Link href={'/admin/rounds' as Route} className="mt-1 shrink-0">
|
<Link href={'/admin/rounds' as Route} className="mt-0.5 shrink-0">
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8" aria-label="Back to rounds">
|
<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" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2.5">
|
||||||
<h1 className="text-xl font-bold tracking-tight truncate">{round.name}</h1>
|
<h1 className="text-xl font-bold tracking-tight truncate">{round.name}</h1>
|
||||||
<Badge variant="secondary" className={cn('text-xs shrink-0', typeCfg.color)}>
|
<Badge variant="secondary" className="text-xs shrink-0 bg-white/15 text-white border-white/20 hover:bg-white/20">
|
||||||
{typeCfg.label}
|
{typeCfg.label}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|
||||||
|
|
@ -419,8 +430,7 @@ export default function RoundDetailPage() {
|
||||||
<button
|
<button
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center gap-1.5 text-[11px] font-medium px-2.5 py-1 rounded-full transition-colors shrink-0',
|
'inline-flex items-center gap-1.5 text-[11px] font-medium px-2.5 py-1 rounded-full transition-colors shrink-0',
|
||||||
statusCfg.bgClass,
|
'bg-white/15 text-white hover:bg-white/25',
|
||||||
'hover:opacity-80',
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className={cn('h-1.5 w-1.5 rounded-full', statusCfg.dotClass)} />
|
<span className={cn('h-1.5 w-1.5 rounded-full', statusCfg.dotClass)} />
|
||||||
|
|
@ -475,14 +485,14 @@ export default function RoundDetailPage() {
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground mt-0.5">{typeCfg.description}</p>
|
<p className="text-sm text-white/60 mt-1">{typeCfg.description}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action buttons */}
|
{/* Action buttons */}
|
||||||
<div className="flex items-center gap-2 shrink-0 flex-wrap">
|
<div className="flex items-center gap-2 shrink-0 flex-wrap">
|
||||||
{hasChanges && (
|
{hasChanges && (
|
||||||
<Button size="sm" onClick={handleSave} disabled={updateMutation.isPending}>
|
<Button size="sm" onClick={handleSave} disabled={updateMutation.isPending} className="bg-white text-[#053d57] hover:bg-white/90">
|
||||||
{updateMutation.isPending ? (
|
{updateMutation.isPending ? (
|
||||||
<Loader2 className="h-4 w-4 mr-1.5 animate-spin" />
|
<Loader2 className="h-4 w-4 mr-1.5 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -492,26 +502,28 @@ export default function RoundDetailPage() {
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<Link href={poolLink}>
|
<Link href={poolLink}>
|
||||||
<Button variant="outline" size="sm">
|
<Button variant="outline" size="sm" className="border-white/30 text-white hover:bg-white/10 hover:text-white">
|
||||||
<Layers className="h-4 w-4 mr-1.5" />
|
<Layers className="h-4 w-4 mr-1.5" />
|
||||||
Project Pool
|
Project Pool
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
{/* ===== STATS BAR ===== */}
|
{/* ===== STATS BAR — Accent-bordered cards ===== */}
|
||||||
<div className="grid gap-3 grid-cols-2 sm:grid-cols-4">
|
<div className="grid gap-3 grid-cols-2 sm:grid-cols-4">
|
||||||
{/* Projects */}
|
{/* Projects */}
|
||||||
<Card>
|
<AnimatedCard index={0}>
|
||||||
|
<Card className="border-l-4 border-l-[#557f8c] hover:shadow-md transition-shadow">
|
||||||
<CardContent className="pt-4 pb-3">
|
<CardContent className="pt-4 pb-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center gap-2.5">
|
||||||
<div className="flex items-center gap-2">
|
<div className="rounded-full bg-[#557f8c]/10 p-1.5">
|
||||||
<Layers className="h-4 w-4 text-blue-500" />
|
<Layers className="h-4 w-4 text-[#557f8c]" />
|
||||||
<span className="text-sm font-medium">Projects</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">Projects</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-2xl font-bold mt-1">{projectCount}</p>
|
<p className="text-3xl font-bold mt-2">{projectCount}</p>
|
||||||
<div className="flex flex-wrap gap-1.5 mt-1.5">
|
<div className="flex flex-wrap gap-1.5 mt-1.5">
|
||||||
{Object.entries(stateCounts).map(([state, count]) => (
|
{Object.entries(stateCounts).map(([state, count]) => (
|
||||||
<span key={state} className="text-[10px] text-muted-foreground">
|
<span key={state} className="text-[10px] text-muted-foreground">
|
||||||
|
|
@ -521,13 +533,17 @@ export default function RoundDetailPage() {
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</AnimatedCard>
|
||||||
|
|
||||||
{/* Jury (with inline group selector) */}
|
{/* Jury (with inline group selector) */}
|
||||||
<Card>
|
<AnimatedCard index={1}>
|
||||||
|
<Card className="border-l-4 border-l-purple-500 hover:shadow-md transition-shadow">
|
||||||
<CardContent className="pt-4 pb-3">
|
<CardContent className="pt-4 pb-3">
|
||||||
<div className="flex items-center gap-2 mb-1" data-jury-select>
|
<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" />
|
<Users className="h-4 w-4 text-purple-500" />
|
||||||
<span className="text-sm font-medium">Jury</span>
|
</div>
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">Jury</span>
|
||||||
</div>
|
</div>
|
||||||
{juryGroups && juryGroups.length > 0 ? (
|
{juryGroups && juryGroups.length > 0 ? (
|
||||||
<Select
|
<Select
|
||||||
|
|
@ -554,28 +570,32 @@ export default function RoundDetailPage() {
|
||||||
</Select>
|
</Select>
|
||||||
) : juryGroup ? (
|
) : juryGroup ? (
|
||||||
<>
|
<>
|
||||||
<p className="text-2xl font-bold mt-1">{juryMemberCount}</p>
|
<p className="text-3xl font-bold mt-2">{juryMemberCount}</p>
|
||||||
<p className="text-xs text-muted-foreground truncate">{juryGroup.name}</p>
|
<p className="text-xs text-muted-foreground truncate">{juryGroup.name}</p>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<p className="text-2xl font-bold mt-1 text-muted-foreground">—</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>
|
<p className="text-xs text-muted-foreground">No jury groups yet</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</AnimatedCard>
|
||||||
|
|
||||||
{/* Window */}
|
{/* Window */}
|
||||||
<Card>
|
<AnimatedCard index={2}>
|
||||||
|
<Card className="border-l-4 border-l-emerald-500 hover:shadow-md transition-shadow">
|
||||||
<CardContent className="pt-4 pb-3">
|
<CardContent className="pt-4 pb-3">
|
||||||
<div className="flex items-center gap-2">
|
<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" />
|
<CalendarDays className="h-4 w-4 text-emerald-500" />
|
||||||
<span className="text-sm font-medium">Window</span>
|
</div>
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">Window</span>
|
||||||
</div>
|
</div>
|
||||||
{round.windowOpenAt || round.windowCloseAt ? (
|
{round.windowOpenAt || round.windowCloseAt ? (
|
||||||
<>
|
<>
|
||||||
<p className="text-sm font-bold mt-1">
|
<p className="text-sm font-bold mt-2">
|
||||||
{round.windowOpenAt
|
{round.windowOpenAt
|
||||||
? new Date(round.windowOpenAt).toLocaleDateString()
|
? new Date(round.windowOpenAt).toLocaleDateString()
|
||||||
: 'No start'}
|
: 'No start'}
|
||||||
|
|
@ -588,91 +608,110 @@ export default function RoundDetailPage() {
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<p className="text-2xl font-bold mt-1 text-muted-foreground">—</p>
|
<p className="text-3xl font-bold mt-2 text-muted-foreground">—</p>
|
||||||
<p className="text-xs text-muted-foreground">No dates set</p>
|
<p className="text-xs text-muted-foreground">No dates set</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</AnimatedCard>
|
||||||
|
|
||||||
{/* Advancement */}
|
{/* Advancement */}
|
||||||
<Card>
|
<AnimatedCard index={3}>
|
||||||
|
<Card className="border-l-4 border-l-amber-500 hover:shadow-md transition-shadow">
|
||||||
<CardContent className="pt-4 pb-3">
|
<CardContent className="pt-4 pb-3">
|
||||||
<div className="flex items-center gap-2">
|
<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" />
|
<BarChart3 className="h-4 w-4 text-amber-500" />
|
||||||
<span className="text-sm font-medium">Advancement</span>
|
</div>
|
||||||
|
<span className="text-sm font-medium text-muted-foreground">Advancement</span>
|
||||||
</div>
|
</div>
|
||||||
{round.advancementRules && round.advancementRules.length > 0 ? (
|
{round.advancementRules && round.advancementRules.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<p className="text-2xl font-bold mt-1">{round.advancementRules.length}</p>
|
<p className="text-3xl font-bold mt-2">{round.advancementRules.length}</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{round.advancementRules.map((r: any) => r.ruleType.replace('_', ' ').toLowerCase()).join(', ')}
|
{round.advancementRules.map((r: any) => r.ruleType.replace('_', ' ').toLowerCase()).join(', ')}
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<p className="text-2xl font-bold mt-1 text-muted-foreground">—</p>
|
<p className="text-3xl font-bold mt-2 text-muted-foreground">—</p>
|
||||||
<p className="text-xs text-muted-foreground">Admin selection</p>
|
<p className="text-xs text-muted-foreground">Admin selection</p>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</AnimatedCard>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ===== TABS ===== */}
|
{/* ===== TABS — Underline style ===== */}
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
|
||||||
<TabsList className="w-full sm:w-auto overflow-x-auto">
|
<div className="border-b overflow-x-auto">
|
||||||
<TabsTrigger value="overview">
|
<TabsList className="bg-transparent h-auto p-0 gap-0 w-full sm:w-auto">
|
||||||
<Zap className="h-3.5 w-3.5 mr-1.5" />
|
{[
|
||||||
Overview
|
{ value: 'overview', label: 'Overview', icon: Zap },
|
||||||
</TabsTrigger>
|
{ value: 'projects', label: 'Projects', icon: Layers },
|
||||||
<TabsTrigger value="projects">
|
...(isFiltering ? [{ value: 'filtering', label: 'Filtering', icon: Shield }] : []),
|
||||||
<Layers className="h-3.5 w-3.5 mr-1.5" />
|
...(isEvaluation ? [{ value: 'assignments', label: 'Assignments', icon: ClipboardList }] : []),
|
||||||
Projects
|
{ value: 'config', label: 'Config', icon: Settings },
|
||||||
</TabsTrigger>
|
{ value: 'windows', label: 'Submissions', icon: Clock },
|
||||||
{isFiltering && (
|
{ value: 'awards', label: 'Awards', icon: Trophy },
|
||||||
<TabsTrigger value="filtering">
|
].map((tab) => (
|
||||||
<Shield className="h-3.5 w-3.5 mr-1.5" />
|
<TabsTrigger
|
||||||
Filtering
|
key={tab.value}
|
||||||
</TabsTrigger>
|
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',
|
||||||
)}
|
)}
|
||||||
{isEvaluation && (
|
>
|
||||||
<TabsTrigger value="assignments">
|
<tab.icon className={cn('h-3.5 w-3.5 mr-1.5', activeTab === tab.value ? 'text-[#557f8c]' : '')} />
|
||||||
<ClipboardList className="h-3.5 w-3.5 mr-1.5" />
|
{tab.label}
|
||||||
Assignments
|
{tab.value === 'awards' && roundAwards.length > 0 && (
|
||||||
</TabsTrigger>
|
|
||||||
)}
|
|
||||||
<TabsTrigger value="config">
|
|
||||||
<Settings className="h-3.5 w-3.5 mr-1.5" />
|
|
||||||
Config
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="windows">
|
|
||||||
<Clock className="h-3.5 w-3.5 mr-1.5" />
|
|
||||||
Submissions
|
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger value="awards">
|
|
||||||
<Trophy className="h-3.5 w-3.5 mr-1.5" />
|
|
||||||
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">
|
<Badge variant="secondary" className="ml-1.5 h-5 min-w-5 text-[10px] px-1.5 bg-[#de0f1e] text-white">
|
||||||
{roundAwards.length}
|
{roundAwards.length}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* ═══════════ OVERVIEW TAB ═══════════ */}
|
{/* ═══════════ OVERVIEW TAB ═══════════ */}
|
||||||
<TabsContent value="overview" className="space-y-6">
|
<TabsContent value="overview" className="space-y-6">
|
||||||
{/* Readiness Checklist */}
|
{/* Readiness Checklist with Progress Ring */}
|
||||||
|
<AnimatedCard index={0}>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center justify-between">
|
<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>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<CardTitle className="text-base">Readiness Checklist</CardTitle>
|
<CardTitle className="text-base">Launch Readiness</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
{readyCount}/{readinessItems.length} items ready
|
{readyCount === readinessItems.length
|
||||||
|
? 'All checks passed — ready to go'
|
||||||
|
: `${readinessItems.length - readyCount} item(s) remaining`}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<Badge
|
<Badge
|
||||||
variant={readyCount === readinessItems.length ? 'default' : 'secondary'}
|
variant={readyCount === readinessItems.length ? 'default' : 'secondary'}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -696,14 +735,14 @@ export default function RoundDetailPage() {
|
||||||
<AlertTriangle className="h-4 w-4 text-amber-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">
|
<div className="flex-1 min-w-0">
|
||||||
<p className={cn('text-sm font-medium', item.ready && 'text-muted-foreground')}>
|
<p className={cn('text-sm font-medium', item.ready && 'text-muted-foreground line-through opacity-60')}>
|
||||||
{item.label}
|
{item.label}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">{item.detail}</p>
|
<p className="text-xs text-muted-foreground">{item.detail}</p>
|
||||||
</div>
|
</div>
|
||||||
{item.action && (
|
{item.action && (
|
||||||
<Link href={item.action}>
|
<Link href={item.action}>
|
||||||
<Button variant="outline" size="sm" className="shrink-0 text-xs">
|
<Button size="sm" className="shrink-0 text-xs bg-[#de0f1e] hover:bg-[#c00d1a] text-white">
|
||||||
{item.actionLabel}
|
{item.actionLabel}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
@ -713,20 +752,25 @@ export default function RoundDetailPage() {
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</AnimatedCard>
|
||||||
|
|
||||||
{/* Quick Actions */}
|
{/* Quick Actions — Grouped & styled */}
|
||||||
|
<AnimatedCard index={1}>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-base">Quick Actions</CardTitle>
|
<CardTitle className="text-base">Quick Actions</CardTitle>
|
||||||
<CardDescription>Common operations for this round</CardDescription>
|
<CardDescription>Common operations for this round</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<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">
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{/* Status transitions */}
|
|
||||||
{status === 'ROUND_DRAFT' && (
|
{status === 'ROUND_DRAFT' && (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<button className="flex items-start gap-3 p-4 rounded-lg border hover:bg-muted/50 transition-colors text-left">
|
<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" />
|
<Play className="h-5 w-5 text-emerald-600 mt-0.5 shrink-0" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium">Activate Round</p>
|
<p className="text-sm font-medium">Activate Round</p>
|
||||||
|
|
@ -756,7 +800,7 @@ export default function RoundDetailPage() {
|
||||||
{status === 'ROUND_ACTIVE' && (
|
{status === 'ROUND_ACTIVE' && (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<button className="flex items-start gap-3 p-4 rounded-lg border hover:bg-muted/50 transition-colors text-left">
|
<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" />
|
<Square className="h-5 w-5 text-blue-600 mt-0.5 shrink-0" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium">Close Round</p>
|
<p className="text-sm font-medium">Close Round</p>
|
||||||
|
|
@ -791,7 +835,7 @@ export default function RoundDetailPage() {
|
||||||
{status === 'ROUND_CLOSED' && (
|
{status === 'ROUND_CLOSED' && (
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<button className="flex items-start gap-3 p-4 rounded-lg border hover:bg-muted/50 transition-colors text-left border-amber-200 bg-amber-50/50">
|
<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" />
|
<RotateCcw className="h-5 w-5 text-amber-600 mt-0.5 shrink-0" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium">Reopen Round</p>
|
<p className="text-sm font-medium">Reopen Round</p>
|
||||||
|
|
@ -817,11 +861,17 @@ export default function RoundDetailPage() {
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Assign projects */}
|
{/* 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}>
|
<Link href={poolLink}>
|
||||||
<button className="flex items-start gap-3 p-4 rounded-lg border hover:bg-muted/50 transition-colors text-left w-full">
|
<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-blue-600 mt-0.5 shrink-0" />
|
<Layers className="h-5 w-5 text-[#557f8c] mt-0.5 shrink-0" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium">Assign Projects</p>
|
<p className="text-sm font-medium">Assign Projects</p>
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
|
@ -831,20 +881,34 @@ export default function RoundDetailPage() {
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Filtering specific */}
|
|
||||||
{isFiltering && (
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('filtering')}
|
onClick={() => setActiveTab('projects')}
|
||||||
className="flex items-start gap-3 p-4 rounded-lg border hover:bg-muted/50 transition-colors text-left"
|
className="flex items-start gap-3 p-4 rounded-lg border hover:-translate-y-0.5 hover:shadow-md transition-all text-left"
|
||||||
>
|
>
|
||||||
<Shield className="h-5 w-5 text-amber-600 mt-0.5 shrink-0" />
|
<BarChart3 className="h-5 w-5 text-[#557f8c] mt-0.5 shrink-0" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium">Run AI Filtering</p>
|
<p className="text-sm font-medium">Manage Projects</p>
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
Screen projects with AI and manual review
|
View, filter, and transition project states
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Advance projects (shown when PASSED > 0) */}
|
||||||
|
{passedCount > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={() => setAdvanceDialogOpen(true)}
|
||||||
|
className="flex items-start gap-3 p-4 rounded-lg border border-l-4 border-l-emerald-500 bg-emerald-50/30 hover:-translate-y-0.5 hover:shadow-md transition-all text-left"
|
||||||
|
>
|
||||||
|
<ArrowRight className="h-5 w-5 text-emerald-600 mt-0.5 shrink-0" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium">Advance Projects</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
Move {passedCount} passed project(s) to the next round
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Badge className="ml-auto shrink-0 bg-emerald-100 text-emerald-700 text-[10px]">{passedCount}</Badge>
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Jury assignment for evaluation/filtering */}
|
{/* Jury assignment for evaluation/filtering */}
|
||||||
|
|
@ -854,7 +918,7 @@ export default function RoundDetailPage() {
|
||||||
const el = document.querySelector('[data-jury-select]')
|
const el = document.querySelector('[data-jury-select]')
|
||||||
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
if (el) el.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
||||||
}}
|
}}
|
||||||
className="flex items-start gap-3 p-4 rounded-lg border hover:bg-muted/50 transition-colors text-left border-amber-200 bg-amber-50/50"
|
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" />
|
<UserPlus className="h-5 w-5 text-amber-600 mt-0.5 shrink-0" />
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -870,9 +934,9 @@ export default function RoundDetailPage() {
|
||||||
{isEvaluation && (
|
{isEvaluation && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab('assignments')}
|
onClick={() => setActiveTab('assignments')}
|
||||||
className="flex items-start gap-3 p-4 rounded-lg border hover:bg-muted/50 transition-colors text-left"
|
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-blue-600 mt-0.5 shrink-0" />
|
<ClipboardList className="h-5 w-5 text-[#557f8c] mt-0.5 shrink-0" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium">Manage Assignments</p>
|
<p className="text-sm font-medium">Manage Assignments</p>
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
|
|
@ -881,29 +945,35 @@ export default function RoundDetailPage() {
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* View projects */}
|
{/* AI Tools Group */}
|
||||||
<button
|
{((isFiltering || isEvaluation) && projectCount > 0) && (
|
||||||
onClick={() => setActiveTab('projects')}
|
|
||||||
className="flex items-start gap-3 p-4 rounded-lg border hover:bg-muted/50 transition-colors text-left"
|
|
||||||
>
|
|
||||||
<BarChart3 className="h-5 w-5 text-purple-600 mt-0.5 shrink-0" />
|
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium">Manage Projects</p>
|
<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">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
View, filter, and transition project states
|
Screen projects with AI and manual review
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* AI Shortlist Recommendations */}
|
|
||||||
{(isEvaluation || isFiltering) && projectCount > 0 && (
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setShortlistDialogOpen(true)}
|
onClick={() => setShortlistDialogOpen(true)}
|
||||||
className="flex items-start gap-3 p-4 rounded-lg border hover:bg-muted/50 transition-colors text-left border-purple-200 bg-purple-50/50"
|
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}
|
disabled={shortlistMutation.isPending}
|
||||||
>
|
>
|
||||||
<BarChart3 className="h-5 w-5 text-purple-600 mt-0.5 shrink-0" />
|
<Zap className="h-5 w-5 text-purple-600 mt-0.5 shrink-0" />
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium">
|
<p className="text-sm font-medium">
|
||||||
{shortlistMutation.isPending ? 'Generating...' : 'AI Recommendations'}
|
{shortlistMutation.isPending ? 'Generating...' : 'AI Recommendations'}
|
||||||
|
|
@ -913,26 +983,12 @@ export default function RoundDetailPage() {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Advance projects (shown when PASSED > 0) */}
|
|
||||||
{passedCount > 0 && (
|
|
||||||
<button
|
|
||||||
onClick={() => setAdvanceDialogOpen(true)}
|
|
||||||
className="flex items-start gap-3 p-4 rounded-lg border hover:bg-muted/50 transition-colors text-left border-green-200 bg-green-50/50"
|
|
||||||
>
|
|
||||||
<ArrowRight className="h-5 w-5 text-green-600 mt-0.5 shrink-0" />
|
|
||||||
<div>
|
|
||||||
<p className="text-sm font-medium">Advance Projects</p>
|
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
|
||||||
Move {passedCount} passed project(s) to the next round
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</AnimatedCard>
|
||||||
|
|
||||||
{/* Advance Projects Dialog */}
|
{/* Advance Projects Dialog */}
|
||||||
<AdvanceProjectsDialog
|
<AdvanceProjectsDialog
|
||||||
|
|
@ -992,53 +1048,39 @@ export default function RoundDetailPage() {
|
||||||
|
|
||||||
{/* Round Info + Project Breakdown */}
|
{/* Round Info + Project Breakdown */}
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<AnimatedCard index={2}>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-sm">Round Details</CardTitle>
|
<CardTitle className="text-sm">Round Details</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3 text-sm">
|
<CardContent className="space-y-0 text-sm">
|
||||||
<div className="flex justify-between">
|
{[
|
||||||
<span className="text-muted-foreground">Type</span>
|
{ label: 'Type', value: <Badge variant="secondary" className={cn('text-xs', typeCfg.color)}>{typeCfg.label}</Badge> },
|
||||||
<Badge variant="secondary" className={cn('text-xs', typeCfg.color)}>{typeCfg.label}</Badge>
|
{ label: 'Status', value: <span className="font-medium">{statusCfg.label}</span> },
|
||||||
</div>
|
{ label: 'Sort Order', value: <span className="font-medium font-mono">{round.sortOrder}</span> },
|
||||||
<div className="flex justify-between">
|
...(round.purposeKey ? [{ label: 'Purpose', value: <span className="font-medium">{round.purposeKey}</span> }] : []),
|
||||||
<span className="text-muted-foreground">Status</span>
|
{ label: 'Jury Group', value: <span className="font-medium">{juryGroup ? juryGroup.name : '\u2014'}</span> },
|
||||||
<span className="font-medium">{statusCfg.label}</span>
|
{ label: 'Opens', value: <span className="font-medium">{round.windowOpenAt ? new Date(round.windowOpenAt).toLocaleString() : '\u2014'}</span> },
|
||||||
</div>
|
{ label: 'Closes', value: <span className="font-medium">{round.windowCloseAt ? new Date(round.windowCloseAt).toLocaleString() : '\u2014'}</span> },
|
||||||
<div className="flex justify-between">
|
].map((row, i) => (
|
||||||
<span className="text-muted-foreground">Sort Order</span>
|
<div key={row.label} className={cn('flex justify-between items-center py-2.5', i > 0 && 'border-t border-dotted border-muted')}>
|
||||||
<span className="font-medium font-mono">{round.sortOrder}</span>
|
<span className="text-muted-foreground">{row.label}</span>
|
||||||
</div>
|
{row.value}
|
||||||
{round.purposeKey && (
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-muted-foreground">Purpose</span>
|
|
||||||
<span className="font-medium">{round.purposeKey}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-muted-foreground">Jury Group</span>
|
|
||||||
<span className="font-medium">
|
|
||||||
{juryGroup ? juryGroup.name : '\u2014'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-muted-foreground">Opens</span>
|
|
||||||
<span className="font-medium">
|
|
||||||
{round.windowOpenAt ? new Date(round.windowOpenAt).toLocaleString() : '\u2014'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="text-muted-foreground">Closes</span>
|
|
||||||
<span className="font-medium">
|
|
||||||
{round.windowCloseAt ? new Date(round.windowCloseAt).toLocaleString() : '\u2014'}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</AnimatedCard>
|
||||||
|
|
||||||
|
<AnimatedCard index={3}>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
<CardTitle className="text-sm">Project Breakdown</CardTitle>
|
<CardTitle className="text-sm">Project Breakdown</CardTitle>
|
||||||
|
{projectCount > 0 && (
|
||||||
|
<span className="text-xs font-mono text-muted-foreground">{projectCount} total</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{projectCount === 0 ? (
|
{projectCount === 0 ? (
|
||||||
|
|
@ -1046,20 +1088,20 @@ export default function RoundDetailPage() {
|
||||||
No projects assigned yet
|
No projects assigned yet
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-3">
|
||||||
{['PENDING', 'IN_PROGRESS', 'PASSED', 'REJECTED', 'COMPLETED', 'WITHDRAWN'].map((state) => {
|
{['PENDING', 'IN_PROGRESS', 'PASSED', 'REJECTED', 'COMPLETED', 'WITHDRAWN'].map((state) => {
|
||||||
const count = stateCounts[state] || 0
|
const count = stateCounts[state] || 0
|
||||||
if (count === 0) return null
|
if (count === 0) return null
|
||||||
const pct = ((count / projectCount) * 100).toFixed(0)
|
const pct = ((count / projectCount) * 100).toFixed(0)
|
||||||
return (
|
return (
|
||||||
<div key={state}>
|
<div key={state}>
|
||||||
<div className="flex justify-between text-xs mb-1">
|
<div className="flex justify-between text-xs mb-1.5">
|
||||||
<span className="text-muted-foreground capitalize">{state.toLowerCase().replace('_', ' ')}</span>
|
<span className="text-muted-foreground capitalize font-medium">{state.toLowerCase().replace('_', ' ')}</span>
|
||||||
<span className="font-medium">{count} ({pct}%)</span>
|
<span className="font-bold tabular-nums">{count} <span className="font-normal text-muted-foreground">({pct}%)</span></span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-1.5 bg-muted rounded-full overflow-hidden">
|
<div className="h-2 bg-muted rounded-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className={cn('h-full rounded-full transition-all', stateColors[state])}
|
className={cn('h-full rounded-full transition-all duration-500', stateColors[state])}
|
||||||
style={{ width: `${pct}%` }}
|
style={{ width: `${pct}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1070,6 +1112,7 @@ export default function RoundDetailPage() {
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</AnimatedCard>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
|
@ -1170,12 +1213,12 @@ export default function RoundDetailPage() {
|
||||||
<TabsContent value="config" className="space-y-6">
|
<TabsContent value="config" className="space-y-6">
|
||||||
{/* General Round Settings */}
|
{/* General Round Settings */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader className="border-b">
|
||||||
<CardTitle className="text-base">General Settings</CardTitle>
|
<CardTitle className="text-base">General Settings</CardTitle>
|
||||||
<CardDescription>Settings that apply to this round regardless of type</CardDescription>
|
<CardDescription>Settings that apply to this round regardless of type</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-5">
|
<CardContent className="space-y-0 pt-0">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between p-4 rounded-md">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<Label htmlFor="notify-on-entry" className="text-sm font-medium">
|
<Label htmlFor="notify-on-entry" className="text-sm font-medium">
|
||||||
Notify on round entry
|
Notify on round entry
|
||||||
|
|
@ -1193,7 +1236,7 @@ export default function RoundDetailPage() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between p-4 rounded-md bg-muted/30">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<Label htmlFor="notify-on-advance" className="text-sm font-medium">
|
<Label htmlFor="notify-on-advance" className="text-sm font-medium">
|
||||||
Notify on advance
|
Notify on advance
|
||||||
|
|
@ -1211,7 +1254,7 @@ export default function RoundDetailPage() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between p-4 rounded-md">
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<Label htmlFor="ai-parse-files" className="text-sm font-medium">
|
<Label htmlFor="ai-parse-files" className="text-sm font-medium">
|
||||||
AI document parsing
|
AI document parsing
|
||||||
|
|
@ -1229,7 +1272,7 @@ export default function RoundDetailPage() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border-t pt-4">
|
<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">
|
||||||
<Label className="text-sm font-medium">Advancement Targets</Label>
|
<Label className="text-sm font-medium">Advancement Targets</Label>
|
||||||
<p className="text-xs text-muted-foreground mb-3">
|
<p className="text-xs text-muted-foreground mb-3">
|
||||||
Target number of projects per category to advance from this round
|
Target number of projects per category to advance from this round
|
||||||
|
|
@ -1316,10 +1359,12 @@ export default function RoundDetailPage() {
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
{roundAwards.length === 0 ? (
|
{roundAwards.length === 0 ? (
|
||||||
<div className="text-center py-8 text-muted-foreground">
|
<div className="text-center py-12 text-muted-foreground">
|
||||||
<Trophy className="h-8 w-8 mx-auto mb-2 opacity-40" />
|
<div className="rounded-full bg-[#de0f1e]/10 p-4 w-fit mx-auto mb-4">
|
||||||
<p className="text-sm">No awards linked to this round</p>
|
<Trophy className="h-8 w-8 text-[#de0f1e]/60" />
|
||||||
<p className="text-xs mt-1">
|
</div>
|
||||||
|
<p className="text-sm font-medium text-foreground">No Awards Linked</p>
|
||||||
|
<p className="text-xs mt-1 max-w-sm mx-auto">
|
||||||
Create an award and set this round as its evaluation round to see it here
|
Create an award and set this round as its evaluation round to see it here
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1336,7 +1381,7 @@ export default function RoundDetailPage() {
|
||||||
href={`/admin/awards/${award.id}` as Route}
|
href={`/admin/awards/${award.id}` as Route}
|
||||||
className="block"
|
className="block"
|
||||||
>
|
>
|
||||||
<div className="flex items-start justify-between gap-4 rounded-lg border p-4 transition-all hover:bg-muted/50 hover:shadow-sm">
|
<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">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<h3 className="font-medium truncate">{award.name}</h3>
|
<h3 className="font-medium truncate">{award.name}</h3>
|
||||||
|
|
@ -1411,7 +1456,10 @@ function RoundUnassignedQueue({ roundId }: { roundId: string }) {
|
||||||
{unassigned.map((project: any) => (
|
{unassigned.map((project: any) => (
|
||||||
<div
|
<div
|
||||||
key={project.id}
|
key={project.id}
|
||||||
className="flex justify-between items-center p-3 border rounded-md hover:bg-muted/30"
|
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',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="text-sm font-medium truncate">{project.title}</p>
|
<p className="text-sm font-medium truncate">{project.title}</p>
|
||||||
|
|
@ -1468,25 +1516,25 @@ function JuryProgressTable({ roundId }: { roundId: string }) {
|
||||||
<div className="space-y-3 max-h-[350px] overflow-y-auto">
|
<div className="space-y-3 max-h-[350px] overflow-y-auto">
|
||||||
{workload.map((juror) => {
|
{workload.map((juror) => {
|
||||||
const pct = juror.completionRate
|
const pct = juror.completionRate
|
||||||
const barColor = pct === 100
|
const barGradient = pct === 100
|
||||||
? 'bg-emerald-500'
|
? 'bg-gradient-to-r from-emerald-400 to-emerald-600'
|
||||||
: pct >= 50
|
: pct >= 50
|
||||||
? 'bg-blue-500'
|
? 'bg-gradient-to-r from-blue-400 to-blue-600'
|
||||||
: pct > 0
|
: pct > 0
|
||||||
? 'bg-amber-500'
|
? 'bg-gradient-to-r from-amber-400 to-amber-600'
|
||||||
: 'bg-gray-300'
|
: 'bg-gray-300'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={juror.id} className="space-y-1">
|
<div key={juror.id} className="space-y-1 hover:bg-muted/20 rounded px-1 py-0.5 -mx-1 transition-colors">
|
||||||
<div className="flex justify-between text-xs">
|
<div className="flex justify-between text-xs">
|
||||||
<span className="font-medium truncate max-w-[60%]">{juror.name}</span>
|
<span className="font-medium truncate max-w-[60%]">{juror.name}</span>
|
||||||
<span className="text-muted-foreground shrink-0">
|
<span className="text-muted-foreground shrink-0 tabular-nums">
|
||||||
{juror.completed}/{juror.assigned} ({pct}%)
|
{juror.completed}/{juror.assigned} ({pct}%)
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-1.5 bg-muted rounded-full overflow-hidden">
|
<div className="h-2 bg-muted rounded-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className={cn('h-full rounded-full transition-all', barColor)}
|
className={cn('h-full rounded-full transition-all duration-500', barGradient)}
|
||||||
style={{ width: `${pct}%` }}
|
style={{ width: `${pct}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1700,10 +1748,13 @@ function IndividualAssignmentsTable({ roundId }: { roundId: string }) {
|
||||||
<span>Status</span>
|
<span>Status</span>
|
||||||
<span />
|
<span />
|
||||||
</div>
|
</div>
|
||||||
{assignments.map((a: any) => (
|
{assignments.map((a: any, idx: number) => (
|
||||||
<div
|
<div
|
||||||
key={a.id}
|
key={a.id}
|
||||||
className="grid grid-cols-[1fr_1fr_100px_60px] gap-2 items-center px-3 py-2 rounded-md hover:bg-muted/30 text-sm"
|
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',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<span className="truncate">{a.user?.name || a.user?.email || 'Unknown'}</span>
|
<span className="truncate">{a.user?.name || a.user?.email || 'Unknown'}</span>
|
||||||
<span className="truncate text-muted-foreground">{a.project?.title || 'Unknown'}</span>
|
<span className="truncate text-muted-foreground">{a.project?.title || 'Unknown'}</span>
|
||||||
|
|
@ -1996,8 +2047,8 @@ function AIRecommendationsDisplay({
|
||||||
className="w-full flex items-center gap-3 p-3 text-left hover:bg-muted/30 transition-colors"
|
className="w-full flex items-center gap-3 p-3 text-left hover:bg-muted/30 transition-colors"
|
||||||
>
|
>
|
||||||
<span className={cn(
|
<span className={cn(
|
||||||
'h-7 w-7 rounded-full flex items-center justify-center text-xs font-bold text-white shrink-0',
|
'h-7 w-7 rounded-full flex items-center justify-center text-xs font-bold text-white shrink-0 shadow-sm',
|
||||||
colorClass,
|
colorClass === 'bg-blue-500' ? 'bg-gradient-to-br from-blue-400 to-blue-600' : 'bg-gradient-to-br from-purple-400 to-purple-600',
|
||||||
)}>
|
)}>
|
||||||
{item.rank}
|
{item.rank}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -112,7 +112,6 @@ export async function activateRound(
|
||||||
data: { status: 'ROUND_ACTIVE' },
|
data: { status: 'ROUND_ACTIVE' },
|
||||||
})
|
})
|
||||||
|
|
||||||
// Dual audit trail
|
|
||||||
await tx.decisionAuditLog.create({
|
await tx.decisionAuditLog.create({
|
||||||
data: {
|
data: {
|
||||||
eventType: 'round.activated',
|
eventType: 'round.activated',
|
||||||
|
|
@ -132,8 +131,11 @@ export async function activateRound(
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
// Audit log outside transaction to avoid FK violations poisoning the tx
|
||||||
await logAudit({
|
await logAudit({
|
||||||
prisma: tx,
|
|
||||||
userId: actorId,
|
userId: actorId,
|
||||||
action: 'ROUND_ACTIVATE',
|
action: 'ROUND_ACTIVATE',
|
||||||
entityType: 'Round',
|
entityType: 'Round',
|
||||||
|
|
@ -141,9 +143,6 @@ export async function activateRound(
|
||||||
detailsJson: { name: round.name, roundType: round.roundType },
|
detailsJson: { name: round.name, roundType: round.roundType },
|
||||||
})
|
})
|
||||||
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
round: { id: updated.id, status: updated.status },
|
round: { id: updated.id, status: updated.status },
|
||||||
|
|
@ -225,8 +224,11 @@ export async function closeRound(
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
// Audit log outside transaction to avoid FK violations poisoning the tx
|
||||||
await logAudit({
|
await logAudit({
|
||||||
prisma: tx,
|
|
||||||
userId: actorId,
|
userId: actorId,
|
||||||
action: 'ROUND_CLOSE',
|
action: 'ROUND_CLOSE',
|
||||||
entityType: 'Round',
|
entityType: 'Round',
|
||||||
|
|
@ -234,9 +236,6 @@ export async function closeRound(
|
||||||
detailsJson: { name: round.name, roundType: round.roundType },
|
detailsJson: { name: round.name, roundType: round.roundType },
|
||||||
})
|
})
|
||||||
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
round: { id: updated.id, status: updated.status },
|
round: { id: updated.id, status: updated.status },
|
||||||
|
|
@ -296,8 +295,11 @@ export async function archiveRound(
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
})
|
||||||
|
|
||||||
|
// Audit log outside transaction to avoid FK violations poisoning the tx
|
||||||
await logAudit({
|
await logAudit({
|
||||||
prisma: tx,
|
|
||||||
userId: actorId,
|
userId: actorId,
|
||||||
action: 'ROUND_ARCHIVE',
|
action: 'ROUND_ARCHIVE',
|
||||||
entityType: 'Round',
|
entityType: 'Round',
|
||||||
|
|
@ -305,9 +307,6 @@ export async function archiveRound(
|
||||||
detailsJson: { name: round.name },
|
detailsJson: { name: round.name },
|
||||||
})
|
})
|
||||||
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
round: { id: updated.id, status: updated.status },
|
round: { id: updated.id, status: updated.status },
|
||||||
|
|
@ -412,24 +411,24 @@ export async function reopenRound(
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
updated,
|
||||||
|
pausedRounds: subsequentActiveRounds.map((r: any) => r.name),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Audit log outside transaction to avoid FK violations poisoning the tx
|
||||||
await logAudit({
|
await logAudit({
|
||||||
prisma: tx,
|
|
||||||
userId: actorId,
|
userId: actorId,
|
||||||
action: 'ROUND_REOPEN',
|
action: 'ROUND_REOPEN',
|
||||||
entityType: 'Round',
|
entityType: 'Round',
|
||||||
entityId: roundId,
|
entityId: roundId,
|
||||||
detailsJson: {
|
detailsJson: {
|
||||||
name: round.name,
|
name: round.name,
|
||||||
pausedRounds: subsequentActiveRounds.map((r: any) => r.name),
|
pausedRounds: result.pausedRounds,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
|
||||||
updated,
|
|
||||||
pausedRounds: subsequentActiveRounds.map((r: any) => r.name),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
round: { id: result.updated.id, status: result.updated.status },
|
round: { id: result.updated.id, status: result.updated.status },
|
||||||
|
|
@ -527,25 +526,25 @@ export async function transitionProject(
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return { prs, previousState: existing?.state ?? null }
|
||||||
|
})
|
||||||
|
|
||||||
|
// Audit log outside transaction to avoid FK violations poisoning the tx
|
||||||
await logAudit({
|
await logAudit({
|
||||||
prisma: tx,
|
|
||||||
userId: actorId,
|
userId: actorId,
|
||||||
action: 'PROJECT_ROUND_TRANSITION',
|
action: 'PROJECT_ROUND_TRANSITION',
|
||||||
entityType: 'ProjectRoundState',
|
entityType: 'ProjectRoundState',
|
||||||
entityId: prs.id,
|
entityId: result.prs.id,
|
||||||
detailsJson: { projectId, roundId, newState, previousState: existing?.state ?? null },
|
detailsJson: { projectId, roundId, newState, previousState: result.previousState },
|
||||||
})
|
|
||||||
|
|
||||||
return prs
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
projectRoundState: {
|
projectRoundState: {
|
||||||
id: result.id,
|
id: result.prs.id,
|
||||||
projectId: result.projectId,
|
projectId: result.prs.projectId,
|
||||||
roundId: result.roundId,
|
roundId: result.prs.roundId,
|
||||||
state: result.state,
|
state: result.prs.state,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue