- F1: Set seed jury/mentors/observers to NONE status (not invited), remove passwords - F2: Add bulk invite UI with checkbox selection and floating toolbar - F3: Add getProjectRequirements backend query + requirement slots on project detail - F4: Redesign filtering section: AI criteria textarea, "What AI sees" card, field-aware eligibility rules with human-readable previews - F5: Auto-redirect to pipeline detail when only one pipeline exists - F6: Make project names clickable in pipeline intake panel - F7: Fix pipeline creation error: edition context fallback + .min(1) validation - Pipeline wizard sections: add isActive locking, info tooltips, UX improvements Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
258 lines
9.6 KiB
TypeScript
258 lines
9.6 KiB
TypeScript
'use client'
|
|
|
|
import { Input } from '@/components/ui/input'
|
|
import { Label } from '@/components/ui/label'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select'
|
|
import { Textarea } from '@/components/ui/textarea'
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
AlertDialogTrigger,
|
|
} from '@/components/ui/alert-dialog'
|
|
import { Plus, Trash2, Trophy } from 'lucide-react'
|
|
import { InfoTooltip } from '@/components/ui/info-tooltip'
|
|
import { defaultAwardTrack } from '@/lib/pipeline-defaults'
|
|
import type { WizardTrackConfig } from '@/types/pipeline-wizard'
|
|
import type { RoutingMode, DecisionMode, AwardScoringMode } from '@prisma/client'
|
|
|
|
type AwardsSectionProps = {
|
|
tracks: WizardTrackConfig[]
|
|
onChange: (tracks: WizardTrackConfig[]) => void
|
|
isActive?: boolean
|
|
}
|
|
|
|
function slugify(name: string): string {
|
|
return name
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, '-')
|
|
.replace(/^-|-$/g, '')
|
|
}
|
|
|
|
export function AwardsSection({ tracks, onChange, isActive }: AwardsSectionProps) {
|
|
const awardTracks = tracks.filter((t) => t.kind === 'AWARD')
|
|
const nonAwardTracks = tracks.filter((t) => t.kind !== 'AWARD')
|
|
|
|
const addAward = () => {
|
|
const newTrack = defaultAwardTrack(awardTracks.length)
|
|
newTrack.sortOrder = tracks.length
|
|
onChange([...tracks, newTrack])
|
|
}
|
|
|
|
const updateAward = (index: number, updates: Partial<WizardTrackConfig>) => {
|
|
const updated = [...tracks]
|
|
const awardIndex = tracks.findIndex(
|
|
(t) => t.kind === 'AWARD' && awardTracks.indexOf(t) === index
|
|
)
|
|
if (awardIndex >= 0) {
|
|
updated[awardIndex] = { ...updated[awardIndex], ...updates }
|
|
onChange(updated)
|
|
}
|
|
}
|
|
|
|
const removeAward = (index: number) => {
|
|
const toRemove = awardTracks[index]
|
|
onChange(tracks.filter((t) => t !== toRemove))
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<p className="text-sm text-muted-foreground">
|
|
Configure special award tracks that run alongside the main competition.
|
|
</p>
|
|
<Button type="button" variant="outline" size="sm" onClick={addAward} disabled={isActive}>
|
|
<Plus className="h-3.5 w-3.5 mr-1" />
|
|
Add Award Track
|
|
</Button>
|
|
</div>
|
|
|
|
{awardTracks.length === 0 && (
|
|
<div className="text-center py-8 text-sm text-muted-foreground">
|
|
<Trophy className="h-8 w-8 mx-auto mb-2 text-muted-foreground/50" />
|
|
No award tracks configured. Awards are optional.
|
|
</div>
|
|
)}
|
|
|
|
{awardTracks.map((track, index) => (
|
|
<Card key={index}>
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-center justify-between">
|
|
<CardTitle className="text-sm flex items-center gap-2">
|
|
<Trophy className="h-4 w-4 text-amber-500" />
|
|
Award Track {index + 1}
|
|
</CardTitle>
|
|
<AlertDialog>
|
|
<AlertDialogTrigger asChild>
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-7 w-7 text-muted-foreground hover:text-destructive"
|
|
disabled={isActive}
|
|
>
|
|
<Trash2 className="h-3.5 w-3.5" />
|
|
</Button>
|
|
</AlertDialogTrigger>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Remove Award Track?</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
This will remove the "{track.name}" award track and all
|
|
its stages. This action cannot be undone.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction onClick={() => removeAward(index)}>
|
|
Remove
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<div className="space-y-2">
|
|
<Label className="text-xs">Award Name</Label>
|
|
<Input
|
|
placeholder="e.g., Innovation Award"
|
|
value={track.awardConfig?.name ?? track.name}
|
|
disabled={isActive}
|
|
onChange={(e) => {
|
|
const name = e.target.value
|
|
updateAward(index, {
|
|
name,
|
|
slug: slugify(name),
|
|
awardConfig: {
|
|
...track.awardConfig,
|
|
name,
|
|
},
|
|
})
|
|
}}
|
|
/>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-1.5">
|
|
<Label className="text-xs">Routing Mode</Label>
|
|
<InfoTooltip content="Parallel: projects compete for all awards simultaneously. Exclusive: each project can only win one award. Post-main: awards are decided after the main track completes." />
|
|
</div>
|
|
<Select
|
|
value={track.routingModeDefault ?? 'PARALLEL'}
|
|
onValueChange={(value) =>
|
|
updateAward(index, {
|
|
routingModeDefault: value as RoutingMode,
|
|
})
|
|
}
|
|
disabled={isActive}
|
|
>
|
|
<SelectTrigger className="text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="PARALLEL">
|
|
Parallel — Runs alongside main track
|
|
</SelectItem>
|
|
<SelectItem value="EXCLUSIVE">
|
|
Exclusive — Projects enter only this track
|
|
</SelectItem>
|
|
<SelectItem value="POST_MAIN">
|
|
Post-Main — After main track completes
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-1.5">
|
|
<Label className="text-xs">Decision Mode</Label>
|
|
<InfoTooltip content="How the winner is determined for this award track." />
|
|
</div>
|
|
<Select
|
|
value={track.decisionMode ?? 'JURY_VOTE'}
|
|
onValueChange={(value) =>
|
|
updateAward(index, { decisionMode: value as DecisionMode })
|
|
}
|
|
disabled={isActive}
|
|
>
|
|
<SelectTrigger className="text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="JURY_VOTE">Jury Vote</SelectItem>
|
|
<SelectItem value="AWARD_MASTER_DECISION">
|
|
Award Master Decision
|
|
</SelectItem>
|
|
<SelectItem value="ADMIN_DECISION">Admin Decision</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-1.5">
|
|
<Label className="text-xs">Scoring Mode</Label>
|
|
<InfoTooltip content="The method used to aggregate scores for this award." />
|
|
</div>
|
|
<Select
|
|
value={track.awardConfig?.scoringMode ?? 'PICK_WINNER'}
|
|
onValueChange={(value) =>
|
|
updateAward(index, {
|
|
awardConfig: {
|
|
...track.awardConfig!,
|
|
scoringMode: value as AwardScoringMode,
|
|
},
|
|
})
|
|
}
|
|
disabled={isActive}
|
|
>
|
|
<SelectTrigger className="text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="PICK_WINNER">Pick Winner</SelectItem>
|
|
<SelectItem value="RANKED">Ranked</SelectItem>
|
|
<SelectItem value="SCORED">Scored</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="text-xs">Description (optional)</Label>
|
|
<Textarea
|
|
placeholder="Brief description of this award..."
|
|
value={track.awardConfig?.description ?? ''}
|
|
rows={2}
|
|
className="text-sm"
|
|
onChange={(e) =>
|
|
updateAward(index, {
|
|
awardConfig: {
|
|
...track.awardConfig!,
|
|
description: e.target.value,
|
|
},
|
|
})
|
|
}
|
|
/>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|