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>
|
||||
|
||||
Reference in New Issue
Block a user