Display filtering controls inline for FILTERING round type

For rounds with roundType=FILTERING, the filtering controls (run button,
stats, finalize) are now shown directly on the round detail page instead
of requiring navigation to a separate /filtering page. Rules configuration
and results review still link to their dedicated pages for detailed work.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-03 10:33:34 +01:00
parent db728830d4
commit c063f5bba3
4 changed files with 198 additions and 7 deletions

View File

@@ -47,6 +47,10 @@ import {
Plus,
ArrowRightCircle,
Minus,
XCircle,
AlertTriangle,
ListChecks,
ClipboardCheck,
} from 'lucide-react'
import { toast } from 'sonner'
import { AssignProjectsDialog } from '@/components/admin/assign-projects-dialog'
@@ -64,9 +68,23 @@ function RoundDetailContent({ roundId }: { roundId: string }) {
const [advanceOpen, setAdvanceOpen] = useState(false)
const [removeOpen, setRemoveOpen] = useState(false)
const { data: round, isLoading } = trpc.round.get.useQuery({ id: roundId })
const { data: round, isLoading, refetch: refetchRound } = trpc.round.get.useQuery({ id: roundId })
const { data: progress } = trpc.round.getProgress.useQuery({ id: roundId })
// Filtering queries (only fetch for FILTERING rounds)
const roundType = (round?.settingsJson as { roundType?: string } | null)?.roundType
const isFilteringRound = roundType === 'FILTERING'
const { data: filteringStats, refetch: refetchFilteringStats } =
trpc.filtering.getResultStats.useQuery(
{ roundId },
{ enabled: isFilteringRound }
)
const { data: filteringRules } = trpc.filtering.getRules.useQuery(
{ roundId },
{ enabled: isFilteringRound }
)
const utils = trpc.useUtils()
const updateStatus = trpc.round.updateStatus.useMutation({
onSuccess: () => {
@@ -85,6 +103,40 @@ function RoundDetailContent({ roundId }: { roundId: string }) {
},
})
// Filtering mutations
const executeRules = trpc.filtering.executeRules.useMutation()
const finalizeResults = trpc.filtering.finalizeResults.useMutation()
const handleExecuteFiltering = async () => {
try {
const result = await executeRules.mutateAsync({ roundId })
toast.success(
`Filtering complete: ${result.passed} passed, ${result.filteredOut} filtered out, ${result.flagged} flagged`
)
refetchFilteringStats()
} catch (error) {
toast.error(
error instanceof Error ? error.message : 'Failed to execute filtering'
)
}
}
const handleFinalizeFiltering = async () => {
try {
const result = await finalizeResults.mutateAsync({ roundId })
toast.success(
`Finalized: ${result.passed} passed, ${result.filteredOut} filtered out`
)
refetchFilteringStats()
refetchRound()
utils.project.list.invalidate()
} catch (error) {
toast.error(
error instanceof Error ? error.message : 'Failed to finalize'
)
}
}
if (isLoading) {
return <RoundDetailSkeleton />
}
@@ -403,6 +455,128 @@ function RoundDetailContent({ roundId }: { roundId: string }) {
</CardContent>
</Card>
{/* Filtering Section (for FILTERING rounds) */}
{isFilteringRound && (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-lg flex items-center gap-2">
<Filter className="h-5 w-5" />
Project Filtering
</CardTitle>
<CardDescription>
Run automated screening rules on projects in this round
</CardDescription>
</div>
<div className="flex gap-2">
<Button
onClick={handleExecuteFiltering}
disabled={executeRules.isPending || !filteringRules || filteringRules.length === 0}
>
{executeRules.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<Play className="mr-2 h-4 w-4" />
)}
Run Filtering
</Button>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* Stats */}
{filteringStats && filteringStats.total > 0 ? (
<div className="grid gap-4 sm:grid-cols-4">
<div className="flex items-center gap-3 p-3 rounded-lg bg-muted">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-background">
<Filter className="h-5 w-5" />
</div>
<div>
<p className="text-2xl font-bold">{filteringStats.total}</p>
<p className="text-sm text-muted-foreground">Total</p>
</div>
</div>
<div className="flex items-center gap-3 p-3 rounded-lg bg-green-500/10">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-green-500/20">
<CheckCircle2 className="h-5 w-5 text-green-600" />
</div>
<div>
<p className="text-2xl font-bold text-green-600">
{filteringStats.passed}
</p>
<p className="text-sm text-muted-foreground">Passed</p>
</div>
</div>
<div className="flex items-center gap-3 p-3 rounded-lg bg-red-500/10">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-red-500/20">
<XCircle className="h-5 w-5 text-red-600" />
</div>
<div>
<p className="text-2xl font-bold text-red-600">
{filteringStats.filteredOut}
</p>
<p className="text-sm text-muted-foreground">Filtered Out</p>
</div>
</div>
<div className="flex items-center gap-3 p-3 rounded-lg bg-amber-500/10">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-amber-500/20">
<AlertTriangle className="h-5 w-5 text-amber-600" />
</div>
<div>
<p className="text-2xl font-bold text-amber-600">
{filteringStats.flagged}
</p>
<p className="text-sm text-muted-foreground">Flagged</p>
</div>
</div>
</div>
) : (
<div className="flex flex-col items-center justify-center py-8 text-center">
<Filter className="h-12 w-12 text-muted-foreground/50" />
<p className="mt-2 font-medium">No filtering results yet</p>
<p className="text-sm text-muted-foreground">
Configure rules and run filtering to screen projects
</p>
</div>
)}
{/* Quick links */}
<div className="flex flex-wrap gap-3 pt-2 border-t">
<Button variant="outline" asChild>
<Link href={`/admin/rounds/${round.id}/filtering/rules`}>
<ListChecks className="mr-2 h-4 w-4" />
Configure Rules
<Badge variant="secondary" className="ml-2">
{filteringRules?.length || 0}
</Badge>
</Link>
</Button>
<Button variant="outline" asChild>
<Link href={`/admin/rounds/${round.id}/filtering/results`}>
<ClipboardCheck className="mr-2 h-4 w-4" />
Review Results
</Link>
</Button>
{filteringStats && filteringStats.total > 0 && (
<Button
onClick={handleFinalizeFiltering}
disabled={finalizeResults.isPending}
variant="default"
>
{finalizeResults.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<CheckCircle2 className="mr-2 h-4 w-4" />
)}
Finalize Results
</Button>
)}
</div>
</CardContent>
</Card>
)}
{/* Quick Actions */}
<Card>
<CardHeader>
@@ -416,12 +590,14 @@ function RoundDetailContent({ roundId }: { roundId: string }) {
Import Projects
</Link>
</Button>
<Button variant="outline" asChild>
<Link href={`/admin/rounds/${round.id}/filtering`}>
<Filter className="mr-2 h-4 w-4" />
Manage Filtering
</Link>
</Button>
{!isFilteringRound && (
<Button variant="outline" asChild>
<Link href={`/admin/rounds/${round.id}/filtering`}>
<Filter className="mr-2 h-4 w-4" />
Manage Filtering
</Link>
</Button>
)}
<Button variant="outline" asChild>
<Link href={`/admin/rounds/${round.id}/assignments`}>
<Users className="mr-2 h-4 w-4" />