diff --git a/src/components/admin/assignment/individual-assignments-table.tsx b/src/components/admin/assignment/individual-assignments-table.tsx index 4b86977..68cc24a 100644 --- a/src/components/admin/assignment/individual-assignments-table.tsx +++ b/src/components/admin/assignment/individual-assignments-table.tsx @@ -171,7 +171,9 @@ export function IndividualAssignmentsTable({ return items.filter((ps: any) => ps.project?.title?.toLowerCase().includes(q) || ps.project?.teamName?.toLowerCase().includes(q) || - ps.project?.competitionCategory?.toLowerCase().includes(q) + ps.project?.competitionCategory?.toLowerCase().includes(q) || + (ps.project?.country || '').toLowerCase().includes(q) || + (ps.project?.institution || '').toLowerCase().includes(q) ) }, [projectStates, projectSearch]) diff --git a/src/components/admin/round/award-shortlist.tsx b/src/components/admin/round/award-shortlist.tsx index ab573fc..d39c16a 100644 --- a/src/components/admin/round/award-shortlist.tsx +++ b/src/components/admin/round/award-shortlist.tsx @@ -1,9 +1,10 @@ 'use client' -import { useState } from 'react' +import { useState, useMemo } from 'react' import { trpc } from '@/lib/trpc/client' import { toast } from 'sonner' import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' import { Badge } from '@/components/ui/badge' import { Checkbox } from '@/components/ui/checkbox' import { Progress } from '@/components/ui/progress' @@ -32,6 +33,7 @@ import { Play, Trophy, AlertTriangle, + Search, } from 'lucide-react' type AwardShortlistProps = { @@ -58,6 +60,7 @@ export function AwardShortlist({ jobDone, }: AwardShortlistProps) { const [expanded, setExpanded] = useState(false) + const [eligibilitySearch, setEligibilitySearch] = useState('') const utils = trpc.useUtils() const isRunning = jobStatus === 'PENDING' || jobStatus === 'PROCESSING' @@ -124,6 +127,19 @@ export function AwardShortlist({ ? Math.round(((currentJobDone ?? 0) / currentJobTotal) * 100) : 0 + const filteredEligibilities = useMemo(() => { + if (!shortlist) return [] + if (!eligibilitySearch.trim()) return shortlist.eligibilities + const q = eligibilitySearch.toLowerCase() + return shortlist.eligibilities.filter((e: any) => + (e.project?.title || '').toLowerCase().includes(q) || + (e.project?.teamName || '').toLowerCase().includes(q) || + (e.project?.country || '').toLowerCase().includes(q) || + (e.project?.institution || '').toLowerCase().includes(q) || + (e.project?.competitionCategory || '').toLowerCase().includes(q) + ) + }, [shortlist, eligibilitySearch]) + const shortlistedCount = shortlist?.eligibilities?.filter((e) => e.shortlisted).length ?? 0 const allShortlisted = shortlist && shortlist.eligibilities.length > 0 && shortlist.eligibilities.every((e) => e.shortlisted) const someShortlisted = shortlistedCount > 0 && !allShortlisted @@ -266,6 +282,18 @@ export function AwardShortlist({ )} +
+
+ + setEligibilitySearch(e.target.value)} + className="pl-9 h-9 max-w-sm" + /> +
+
+
@@ -288,7 +316,7 @@ export function AwardShortlist({ - {shortlist.eligibilities.map((e, i) => { + {filteredEligibilities.map((e, i) => { const reasoning = (e.aiReasoningJson as Record)?.reasoning as string | undefined const isTop5 = i < shortlistSize return ( diff --git a/src/components/admin/round/project-states-table.tsx b/src/components/admin/round/project-states-table.tsx index 3d6b818..d450357 100644 --- a/src/components/admin/round/project-states-table.tsx +++ b/src/components/admin/round/project-states-table.tsx @@ -58,6 +58,7 @@ import { Plus, Search, ExternalLink, + Sparkles, } from 'lucide-react' import Link from 'next/link' import type { Route } from 'next' @@ -136,6 +137,14 @@ export function ProjectStatesTable({ competitionId, roundId }: ProjectStatesTabl onError: (err) => toast.error(err.message), }) + const tagProject = trpc.tag.tagProject.useMutation({ + onSuccess: () => { + toast.success('AI tags generated') + utils.roundEngine.getProjectStates.invalidate({ roundId }) + }, + onError: (err: any) => toast.error(`Tag generation failed: ${err.message}`), + }) + const handleTransition = (projectId: string, newState: ProjectState) => { transitionMutation.mutate({ projectId, roundId, newState }) } @@ -165,10 +174,17 @@ export function ProjectStatesTable({ competitionId, roundId }: ProjectStatesTabl } if (searchQuery.trim()) { const q = searchQuery.toLowerCase() - result = result.filter((ps: any) => - (ps.project?.title || '').toLowerCase().includes(q) || - (ps.project?.teamName || '').toLowerCase().includes(q) - ) + result = result.filter((ps: any) => { + const p = ps.project + return ( + (p?.title || '').toLowerCase().includes(q) || + (p?.teamName || '').toLowerCase().includes(q) || + (p?.country || '').toLowerCase().includes(q) || + (p?.institution || '').toLowerCase().includes(q) || + (p?.competitionCategory || '').toLowerCase().includes(q) || + (p?.geographicZone || '').toLowerCase().includes(q) + ) + }) } return result }, [projectStates, stateFilter, searchQuery]) @@ -411,6 +427,16 @@ export function ProjectStatesTable({ competitionId, roundId }: ProjectStatesTabl View Project + { + e.stopPropagation() + tagProject.mutate({ projectId: ps.projectId }) + }} + disabled={tagProject.isPending} + > + + Generate AI Tags + {PROJECT_STATES.filter((s) => s !== ps.state).map((state) => { const sCfg = stateConfig[state] diff --git a/src/server/routers/project-pool.ts b/src/server/routers/project-pool.ts index 112f8f4..dd4412e 100644 --- a/src/server/routers/project-pool.ts +++ b/src/server/routers/project-pool.ts @@ -123,12 +123,16 @@ export const projectPoolRouter = router({ where.competitionCategory = competitionCategory } - // Search in title, teamName, description + // Search in title, teamName, description, institution, country, geographicZone, team member names if (search) { where.OR = [ { title: { contains: search, mode: 'insensitive' } }, { teamName: { contains: search, mode: 'insensitive' } }, { description: { contains: search, mode: 'insensitive' } }, + { institution: { contains: search, mode: 'insensitive' } }, + { country: { contains: search, mode: 'insensitive' } }, + { geographicZone: { contains: search, mode: 'insensitive' } }, + { teamMembers: { some: { user: { name: { contains: search, mode: 'insensitive' } } } } }, ] }