Special awards: Rounds tab UI, auto-filter threshold, remove auto-tag rules
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m23s
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m23s
- Add Rounds tab to award detail page with create/list/delete functionality - Add "Entry point" badge on first award round (confirmShortlist routes here) - Fix round detail back-link to navigate to parent award when specialAwardId set - Filter award rounds out of competition round list - Add specialAwardId to competition getById round select - Warn on confirmShortlist when no award rounds exist (SEPARATE_POOL mode) - Remove auto-tag rules from award config, edit page, router, and AI service - Fix competitionId not passed when creating awards from competition context - Add AUTO_FILTER quality threshold to AI filtering dashboard Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -25,15 +25,7 @@ import {
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { toast } from 'sonner'
|
||||
import { ArrowLeft, Save, Loader2, Plus, X, Info } from 'lucide-react'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
||||
type AutoTagRule = {
|
||||
id: string
|
||||
field: 'competitionCategory' | 'country' | 'geographicZone' | 'tags' | 'oceanIssue'
|
||||
operator: 'equals' | 'contains' | 'in'
|
||||
value: string
|
||||
}
|
||||
import { ArrowLeft, Save, Loader2 } from 'lucide-react'
|
||||
|
||||
export default function EditAwardPage({
|
||||
params,
|
||||
@@ -70,7 +62,6 @@ export default function EditAwardPage({
|
||||
const [votingEndAt, setVotingEndAt] = useState('')
|
||||
const [evaluationRoundId, setEvaluationRoundId] = useState('')
|
||||
const [eligibilityMode, setEligibilityMode] = useState<'STAY_IN_MAIN' | 'SEPARATE_POOL'>('STAY_IN_MAIN')
|
||||
const [autoTagRules, setAutoTagRules] = useState<AutoTagRule[]>([])
|
||||
|
||||
// Helper to format date for datetime-local input
|
||||
const formatDateForInput = (date: Date | string | null | undefined): string => {
|
||||
@@ -93,14 +84,6 @@ export default function EditAwardPage({
|
||||
setVotingEndAt(formatDateForInput(award.votingEndAt))
|
||||
setEvaluationRoundId(award.evaluationRoundId || '')
|
||||
setEligibilityMode(award.eligibilityMode as 'STAY_IN_MAIN' | 'SEPARATE_POOL')
|
||||
|
||||
// Parse autoTagRulesJson
|
||||
if (award.autoTagRulesJson && typeof award.autoTagRulesJson === 'object') {
|
||||
const rules = award.autoTagRulesJson as { rules?: AutoTagRule[] }
|
||||
setAutoTagRules(rules.rules || [])
|
||||
} else {
|
||||
setAutoTagRules([])
|
||||
}
|
||||
}
|
||||
}, [award])
|
||||
|
||||
@@ -119,7 +102,6 @@ export default function EditAwardPage({
|
||||
votingEndAt: votingEndAt ? new Date(votingEndAt) : undefined,
|
||||
evaluationRoundId: evaluationRoundId || undefined,
|
||||
eligibilityMode,
|
||||
autoTagRulesJson: autoTagRules.length > 0 ? { rules: autoTagRules } : undefined,
|
||||
})
|
||||
toast.success('Award updated')
|
||||
router.push(`/admin/awards/${awardId}`)
|
||||
@@ -130,28 +112,6 @@ export default function EditAwardPage({
|
||||
}
|
||||
}
|
||||
|
||||
const addRule = () => {
|
||||
setAutoTagRules([
|
||||
...autoTagRules,
|
||||
{
|
||||
id: `rule-${Date.now()}`,
|
||||
field: 'competitionCategory',
|
||||
operator: 'equals',
|
||||
value: '',
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
const removeRule = (id: string) => {
|
||||
setAutoTagRules(autoTagRules.filter((r) => r.id !== id))
|
||||
}
|
||||
|
||||
const updateRule = (id: string, updates: Partial<AutoTagRule>) => {
|
||||
setAutoTagRules(
|
||||
autoTagRules.map((r) => (r.id === id ? { ...r, ...updates } : r))
|
||||
)
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
@@ -348,135 +308,6 @@ export default function EditAwardPage({
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Auto-Tag Rules */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<CardTitle>Auto-Tag Rules</CardTitle>
|
||||
<CardDescription>
|
||||
Deterministic eligibility rules based on project metadata
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={addRule}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Add Rule
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{autoTagRules.length === 0 ? (
|
||||
<div className="flex items-start gap-2 rounded-lg border border-dashed p-4 text-sm text-muted-foreground">
|
||||
<Info className="h-4 w-4 mt-0.5 shrink-0" />
|
||||
<p>
|
||||
No rules defined. Add rules to automatically filter projects based on category, location, tags, or ocean issues.
|
||||
Rules work together with the source round setting.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{autoTagRules.map((rule, index) => (
|
||||
<div
|
||||
key={rule.id}
|
||||
className="flex items-start gap-3 rounded-lg border p-3"
|
||||
>
|
||||
<div className="flex-1 grid gap-3 sm:grid-cols-3">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">Field</Label>
|
||||
<Select
|
||||
value={rule.field}
|
||||
onValueChange={(v) =>
|
||||
updateRule(rule.id, {
|
||||
field: v as AutoTagRule['field'],
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-9">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="competitionCategory">
|
||||
Competition Category
|
||||
</SelectItem>
|
||||
<SelectItem value="country">Country</SelectItem>
|
||||
<SelectItem value="geographicZone">
|
||||
Geographic Zone
|
||||
</SelectItem>
|
||||
<SelectItem value="tags">Tags</SelectItem>
|
||||
<SelectItem value="oceanIssue">Ocean Issue</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">Operator</Label>
|
||||
<Select
|
||||
value={rule.operator}
|
||||
onValueChange={(v) =>
|
||||
updateRule(rule.id, {
|
||||
operator: v as AutoTagRule['operator'],
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-9">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="equals">Equals</SelectItem>
|
||||
<SelectItem value="contains">Contains</SelectItem>
|
||||
<SelectItem value="in">In (comma-separated)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs">Value</Label>
|
||||
<Input
|
||||
className="h-9"
|
||||
value={rule.value}
|
||||
onChange={(e) =>
|
||||
updateRule(rule.id, { value: e.target.value })
|
||||
}
|
||||
placeholder={
|
||||
rule.operator === 'in'
|
||||
? 'value1,value2,value3'
|
||||
: 'Enter value...'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-9 w-9 shrink-0"
|
||||
onClick={() => removeRule(rule.id)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{autoTagRules.length > 0 && (
|
||||
<div className="flex items-start gap-2 rounded-lg bg-muted p-3 text-xs text-muted-foreground">
|
||||
<Info className="h-3 w-3 mt-0.5 shrink-0" />
|
||||
<p>
|
||||
<strong>How it works:</strong> Filter from{' '}
|
||||
<Badge variant="outline" className="mx-1">
|
||||
{evaluationRoundId
|
||||
? competition?.rounds?.find((r) => r.id === evaluationRoundId)
|
||||
?.name || 'Selected Round'
|
||||
: 'All Projects'}
|
||||
</Badge>
|
||||
, where ALL rules match (AND logic). Projects matching these deterministic rules will be marked eligible.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Voting Window Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
@@ -89,6 +89,8 @@ import {
|
||||
Vote,
|
||||
ChevronDown,
|
||||
AlertCircle,
|
||||
Layers,
|
||||
Info,
|
||||
} from 'lucide-react'
|
||||
|
||||
const STATUS_COLORS: Record<string, 'default' | 'secondary' | 'destructive' | 'outline'> = {
|
||||
@@ -151,6 +153,8 @@ export default function AwardDetailPage({
|
||||
const [projectSearchQuery, setProjectSearchQuery] = useState('')
|
||||
const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set())
|
||||
const [activeTab, setActiveTab] = useState('eligibility')
|
||||
const [addRoundOpen, setAddRoundOpen] = useState(false)
|
||||
const [roundForm, setRoundForm] = useState({ name: '', roundType: 'EVALUATION' as string })
|
||||
|
||||
// Pagination for eligibility list
|
||||
const [eligibilityPage, setEligibilityPage] = useState(1)
|
||||
@@ -175,6 +179,10 @@ export default function AwardDetailPage({
|
||||
trpc.specialAward.getVoteResults.useQuery({ awardId }, {
|
||||
enabled: activeTab === 'results',
|
||||
})
|
||||
const { data: awardRounds, refetch: refetchRounds } =
|
||||
trpc.specialAward.listRounds.useQuery({ awardId }, {
|
||||
enabled: activeTab === 'rounds',
|
||||
})
|
||||
|
||||
// Deferred queries - only load when needed
|
||||
const { data: allUsers } = trpc.user.list.useQuery(
|
||||
@@ -258,6 +266,22 @@ export default function AwardDetailPage({
|
||||
const deleteAward = trpc.specialAward.delete.useMutation({
|
||||
onSuccess: () => utils.specialAward.list.invalidate(),
|
||||
})
|
||||
const createRound = trpc.specialAward.createRound.useMutation({
|
||||
onSuccess: () => {
|
||||
refetchRounds()
|
||||
setAddRoundOpen(false)
|
||||
setRoundForm({ name: '', roundType: 'EVALUATION' })
|
||||
toast.success('Round created')
|
||||
},
|
||||
onError: (err) => toast.error(err.message),
|
||||
})
|
||||
const deleteRound = trpc.specialAward.deleteRound.useMutation({
|
||||
onSuccess: () => {
|
||||
refetchRounds()
|
||||
toast.success('Round deleted')
|
||||
},
|
||||
onError: (err) => toast.error(err.message),
|
||||
})
|
||||
|
||||
const handleStatusChange = async (
|
||||
status: 'DRAFT' | 'NOMINATIONS_OPEN' | 'VOTING_OPEN' | 'CLOSED' | 'ARCHIVED'
|
||||
@@ -619,6 +643,10 @@ export default function AwardDetailPage({
|
||||
<Users className="mr-2 h-4 w-4" />
|
||||
Jurors ({award._count.jurors})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="rounds">
|
||||
<Layers className="mr-2 h-4 w-4" />
|
||||
Rounds {awardRounds ? `(${awardRounds.length})` : ''}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="results">
|
||||
<BarChart3 className="mr-2 h-4 w-4" />
|
||||
Results
|
||||
@@ -1083,6 +1111,199 @@ export default function AwardDetailPage({
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* Rounds Tab */}
|
||||
<TabsContent value="rounds" className="space-y-4">
|
||||
{award.eligibilityMode !== 'SEPARATE_POOL' && (
|
||||
<div className="flex items-start gap-2 rounded-md border border-blue-200 bg-blue-50 p-3 text-blue-800 dark:border-blue-800 dark:bg-blue-950/30 dark:text-blue-300">
|
||||
<Info className="h-4 w-4 mt-0.5 shrink-0" />
|
||||
<p className="text-sm">
|
||||
Rounds are used in <strong>Separate Pool</strong> mode to create a dedicated evaluation track for shortlisted projects.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{!award.competitionId && (
|
||||
<div className="flex items-start gap-2 rounded-md border border-amber-200 bg-amber-50 p-3 text-amber-800 dark:border-amber-800 dark:bg-amber-950/30 dark:text-amber-300">
|
||||
<AlertCircle className="h-4 w-4 mt-0.5 shrink-0" />
|
||||
<p className="text-sm">
|
||||
Link this award to a competition first before creating rounds.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h2 className="text-lg font-semibold">Award Rounds ({awardRounds?.length ?? 0})</h2>
|
||||
<Dialog open={addRoundOpen} onOpenChange={setAddRoundOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="sm" variant="outline" disabled={!award.competitionId}>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Add Round
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Award Round</DialogTitle>
|
||||
<DialogDescription>
|
||||
Add a new round to the "{award.name}" award evaluation track.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="round-name">Round Name</Label>
|
||||
<Input
|
||||
id="round-name"
|
||||
placeholder="e.g. Award Evaluation"
|
||||
value={roundForm.name}
|
||||
onChange={(e) => setRoundForm({ ...roundForm, name: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="round-type">Round Type</Label>
|
||||
<Select
|
||||
value={roundForm.roundType}
|
||||
onValueChange={(v) => setRoundForm({ ...roundForm, roundType: v })}
|
||||
>
|
||||
<SelectTrigger id="round-type">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="EVALUATION">Evaluation</SelectItem>
|
||||
<SelectItem value="FILTERING">Filtering</SelectItem>
|
||||
<SelectItem value="SUBMISSION">Submission</SelectItem>
|
||||
<SelectItem value="MENTORING">Mentoring</SelectItem>
|
||||
<SelectItem value="LIVE_FINAL">Live Final</SelectItem>
|
||||
<SelectItem value="DELIBERATION">Deliberation</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setAddRoundOpen(false)}>Cancel</Button>
|
||||
<Button
|
||||
onClick={() => createRound.mutate({
|
||||
awardId,
|
||||
name: roundForm.name.trim(),
|
||||
roundType: roundForm.roundType as any,
|
||||
})}
|
||||
disabled={!roundForm.name.trim() || createRound.isPending}
|
||||
>
|
||||
{createRound.isPending ? (
|
||||
<><Loader2 className="mr-2 h-4 w-4 animate-spin" />Creating...</>
|
||||
) : 'Create Round'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{!awardRounds ? (
|
||||
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<Skeleton key={i} className="h-32 rounded-lg" />
|
||||
))}
|
||||
</div>
|
||||
) : awardRounds.length === 0 ? (
|
||||
<Card className="border-dashed">
|
||||
<CardContent className="py-8 text-center text-sm text-muted-foreground">
|
||||
No rounds yet. Create your first award round to build an evaluation track.
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
|
||||
{awardRounds.map((round: any, index: number) => {
|
||||
const projectCount = round._count?.projectRoundStates ?? 0
|
||||
const assignmentCount = round._count?.assignments ?? 0
|
||||
const statusLabel = round.status.replace('ROUND_', '')
|
||||
const statusColors: Record<string, string> = {
|
||||
DRAFT: 'bg-gray-100 text-gray-600',
|
||||
ACTIVE: 'bg-emerald-100 text-emerald-700',
|
||||
CLOSED: 'bg-blue-100 text-blue-700',
|
||||
ARCHIVED: 'bg-muted text-muted-foreground',
|
||||
}
|
||||
const roundTypeColors: Record<string, string> = {
|
||||
EVALUATION: 'bg-violet-100 text-violet-700',
|
||||
FILTERING: 'bg-amber-100 text-amber-700',
|
||||
SUBMISSION: 'bg-blue-100 text-blue-700',
|
||||
MENTORING: 'bg-teal-100 text-teal-700',
|
||||
LIVE_FINAL: 'bg-rose-100 text-rose-700',
|
||||
DELIBERATION: 'bg-indigo-100 text-indigo-700',
|
||||
}
|
||||
return (
|
||||
<Card key={round.id} className="hover:shadow-md transition-shadow h-full">
|
||||
<CardContent className="pt-4 pb-3 space-y-3">
|
||||
<div className="flex items-start gap-2.5">
|
||||
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-muted text-xs font-bold shrink-0 mt-0.5">
|
||||
{index + 1}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<Link href={`/admin/rounds/${round.id}` as any} className="text-sm font-semibold truncate hover:underline">
|
||||
{round.name}
|
||||
</Link>
|
||||
<div className="flex flex-wrap gap-1.5 mt-1">
|
||||
<Badge variant="secondary" className={`text-[10px] ${roundTypeColors[round.roundType] ?? 'bg-gray-100 text-gray-700'}`}>
|
||||
{round.roundType.replace('_', ' ')}
|
||||
</Badge>
|
||||
<Badge variant="outline" className={`text-[10px] ${statusColors[statusLabel]}`}>
|
||||
{statusLabel}
|
||||
</Badge>
|
||||
{index === 0 && (
|
||||
<Badge variant="outline" className="text-[10px] border-amber-300 bg-amber-50 text-amber-700">
|
||||
Entry point
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 text-xs">
|
||||
<div className="flex items-center gap-1.5 text-muted-foreground">
|
||||
<Layers className="h-3.5 w-3.5" />
|
||||
<span>{projectCount} project{projectCount !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
{assignmentCount > 0 && (
|
||||
<div className="flex items-center gap-1.5 text-muted-foreground">
|
||||
<ListChecks className="h-3.5 w-3.5" />
|
||||
<span>{assignmentCount} assignment{assignmentCount !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{round.status === 'ROUND_DRAFT' && (
|
||||
<div className="flex justify-end pt-1">
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="text-destructive hover:text-destructive">
|
||||
<Trash2 className="h-3.5 w-3.5 mr-1" />
|
||||
Delete
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Delete Round</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will permanently delete "{round.name}". This cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={() => deleteRound.mutate({ roundId: round.id })}
|
||||
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
||||
>
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* Results Tab */}
|
||||
<TabsContent value="results" className="space-y-4">
|
||||
{voteResults && voteResults.results.length > 0 ? (() => {
|
||||
|
||||
@@ -60,6 +60,7 @@ export default function NewAwardPage({ params: paramsPromise }: { params: Promis
|
||||
|
||||
createMutation.mutate({
|
||||
programId: competition.programId,
|
||||
competitionId: params.competitionId,
|
||||
name: formData.name.trim(),
|
||||
description: formData.description.trim() || undefined,
|
||||
scoringMode: formData.scoringMode,
|
||||
|
||||
@@ -338,14 +338,14 @@ export default function CompetitionDetailPage() {
|
||||
{/* Rounds Tab */}
|
||||
<TabsContent value="rounds" className="space-y-4">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<h2 className="text-lg font-semibold">Rounds ({competition.rounds.length})</h2>
|
||||
<h2 className="text-lg font-semibold">Rounds ({competition.rounds.filter((r: any) => !r.specialAwardId).length})</h2>
|
||||
<Button size="sm" variant="outline" className="w-full sm:w-auto" onClick={() => setAddRoundOpen(true)}>
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
Add Round
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{competition.rounds.length === 0 ? (
|
||||
{competition.rounds.filter((r: any) => !r.specialAwardId).length === 0 ? (
|
||||
<Card className="border-dashed">
|
||||
<CardContent className="py-8 text-center text-sm text-muted-foreground">
|
||||
No rounds configured. Add rounds to define the competition flow.
|
||||
@@ -353,7 +353,7 @@ export default function CompetitionDetailPage() {
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
|
||||
{competition.rounds.map((round: any, index: number) => {
|
||||
{competition.rounds.filter((r: any) => !r.specialAwardId).map((round: any, index: number) => {
|
||||
const projectCount = round._count?.projectRoundStates ?? 0
|
||||
const assignmentCount = round._count?.assignments ?? 0
|
||||
const statusLabel = round.status.replace('ROUND_', '')
|
||||
|
||||
@@ -548,8 +548,8 @@ export default function RoundDetailPage() {
|
||||
>
|
||||
<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">
|
||||
<Link href={(round.specialAwardId ? `/admin/awards/${round.specialAwardId}` : '/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={round.specialAwardId ? 'Back to Award' : 'Back to rounds'}>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
@@ -1950,8 +1950,6 @@ export default function RoundDetailPage() {
|
||||
<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
|
||||
@@ -1983,10 +1981,6 @@ export default function RoundDetailPage() {
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground shrink-0">
|
||||
<div className="text-right">
|
||||
<div className="font-medium text-foreground">{ruleCount}</div>
|
||||
<div className="text-xs">{ruleCount === 1 ? 'rule' : 'rules'}</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="font-medium text-foreground">{eligibleCount}</div>
|
||||
<div className="text-xs">eligible</div>
|
||||
|
||||
Reference in New Issue
Block a user