feat: enhance project search to include all criteria, add AI tag generation button
- ProjectStatesTable local search now covers country, institution, competitionCategory, geographicZone - project-pool.ts DB search extended to institution, country, geographicZone, team member names - AwardShortlist eligibility table gains a search input filtering by title, team, country, institution, category - IndividualAssignmentsTable project filter extended to include country and institution - Add "Generate AI Tags" dropdown item per row in ProjectStatesTable using tag.tagProject mutation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -171,7 +171,9 @@ export function IndividualAssignmentsTable({
|
|||||||
return items.filter((ps: any) =>
|
return items.filter((ps: any) =>
|
||||||
ps.project?.title?.toLowerCase().includes(q) ||
|
ps.project?.title?.toLowerCase().includes(q) ||
|
||||||
ps.project?.teamName?.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])
|
}, [projectStates, projectSearch])
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState, useMemo } from 'react'
|
||||||
import { trpc } from '@/lib/trpc/client'
|
import { trpc } from '@/lib/trpc/client'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import { Progress } from '@/components/ui/progress'
|
import { Progress } from '@/components/ui/progress'
|
||||||
@@ -32,6 +33,7 @@ import {
|
|||||||
Play,
|
Play,
|
||||||
Trophy,
|
Trophy,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
|
Search,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
|
||||||
type AwardShortlistProps = {
|
type AwardShortlistProps = {
|
||||||
@@ -58,6 +60,7 @@ export function AwardShortlist({
|
|||||||
jobDone,
|
jobDone,
|
||||||
}: AwardShortlistProps) {
|
}: AwardShortlistProps) {
|
||||||
const [expanded, setExpanded] = useState(false)
|
const [expanded, setExpanded] = useState(false)
|
||||||
|
const [eligibilitySearch, setEligibilitySearch] = useState('')
|
||||||
const utils = trpc.useUtils()
|
const utils = trpc.useUtils()
|
||||||
|
|
||||||
const isRunning = jobStatus === 'PENDING' || jobStatus === 'PROCESSING'
|
const isRunning = jobStatus === 'PENDING' || jobStatus === 'PROCESSING'
|
||||||
@@ -124,6 +127,19 @@ export function AwardShortlist({
|
|||||||
? Math.round(((currentJobDone ?? 0) / currentJobTotal) * 100)
|
? Math.round(((currentJobDone ?? 0) / currentJobTotal) * 100)
|
||||||
: 0
|
: 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 shortlistedCount = shortlist?.eligibilities?.filter((e) => e.shortlisted).length ?? 0
|
||||||
const allShortlisted = shortlist && shortlist.eligibilities.length > 0 && shortlist.eligibilities.every((e) => e.shortlisted)
|
const allShortlisted = shortlist && shortlist.eligibilities.length > 0 && shortlist.eligibilities.every((e) => e.shortlisted)
|
||||||
const someShortlisted = shortlistedCount > 0 && !allShortlisted
|
const someShortlisted = shortlistedCount > 0 && !allShortlisted
|
||||||
@@ -266,6 +282,18 @@ export function AwardShortlist({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-3">
|
||||||
|
<div className="relative">
|
||||||
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="Search projects..."
|
||||||
|
value={eligibilitySearch}
|
||||||
|
onChange={(e) => setEligibilitySearch(e.target.value)}
|
||||||
|
className="pl-9 h-9 max-w-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="border rounded-md overflow-hidden">
|
<div className="border rounded-md overflow-hidden">
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead className="bg-muted/50">
|
<thead className="bg-muted/50">
|
||||||
@@ -288,7 +316,7 @@ export function AwardShortlist({
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{shortlist.eligibilities.map((e, i) => {
|
{filteredEligibilities.map((e, i) => {
|
||||||
const reasoning = (e.aiReasoningJson as Record<string, unknown>)?.reasoning as string | undefined
|
const reasoning = (e.aiReasoningJson as Record<string, unknown>)?.reasoning as string | undefined
|
||||||
const isTop5 = i < shortlistSize
|
const isTop5 = i < shortlistSize
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ import {
|
|||||||
Plus,
|
Plus,
|
||||||
Search,
|
Search,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
|
Sparkles,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import type { Route } from 'next'
|
import type { Route } from 'next'
|
||||||
@@ -136,6 +137,14 @@ export function ProjectStatesTable({ competitionId, roundId }: ProjectStatesTabl
|
|||||||
onError: (err) => toast.error(err.message),
|
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) => {
|
const handleTransition = (projectId: string, newState: ProjectState) => {
|
||||||
transitionMutation.mutate({ projectId, roundId, newState })
|
transitionMutation.mutate({ projectId, roundId, newState })
|
||||||
}
|
}
|
||||||
@@ -165,10 +174,17 @@ export function ProjectStatesTable({ competitionId, roundId }: ProjectStatesTabl
|
|||||||
}
|
}
|
||||||
if (searchQuery.trim()) {
|
if (searchQuery.trim()) {
|
||||||
const q = searchQuery.toLowerCase()
|
const q = searchQuery.toLowerCase()
|
||||||
result = result.filter((ps: any) =>
|
result = result.filter((ps: any) => {
|
||||||
(ps.project?.title || '').toLowerCase().includes(q) ||
|
const p = ps.project
|
||||||
(ps.project?.teamName || '').toLowerCase().includes(q)
|
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
|
return result
|
||||||
}, [projectStates, stateFilter, searchQuery])
|
}, [projectStates, stateFilter, searchQuery])
|
||||||
@@ -411,6 +427,16 @@ export function ProjectStatesTable({ competitionId, roundId }: ProjectStatesTabl
|
|||||||
View Project
|
View Project
|
||||||
</a>
|
</a>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
tagProject.mutate({ projectId: ps.projectId })
|
||||||
|
}}
|
||||||
|
disabled={tagProject.isPending}
|
||||||
|
>
|
||||||
|
<Sparkles className="mr-2 h-4 w-4" />
|
||||||
|
Generate AI Tags
|
||||||
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
{PROJECT_STATES.filter((s) => s !== ps.state).map((state) => {
|
{PROJECT_STATES.filter((s) => s !== ps.state).map((state) => {
|
||||||
const sCfg = stateConfig[state]
|
const sCfg = stateConfig[state]
|
||||||
|
|||||||
@@ -123,12 +123,16 @@ export const projectPoolRouter = router({
|
|||||||
where.competitionCategory = competitionCategory
|
where.competitionCategory = competitionCategory
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search in title, teamName, description
|
// Search in title, teamName, description, institution, country, geographicZone, team member names
|
||||||
if (search) {
|
if (search) {
|
||||||
where.OR = [
|
where.OR = [
|
||||||
{ title: { contains: search, mode: 'insensitive' } },
|
{ title: { contains: search, mode: 'insensitive' } },
|
||||||
{ teamName: { contains: search, mode: 'insensitive' } },
|
{ teamName: { contains: search, mode: 'insensitive' } },
|
||||||
{ description: { 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' } } } } },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user