feat: clickable status badges, observer status alignment, CSV export all
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m36s
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m36s
- Admin projects: status summary badges are clickable to filter by round state with ring highlight, opacity fade, and clear button - Add roundStates filter param to project.list backend query (filters by latest round state per project, consistent with counts) - Observer status dropdown now uses ProjectRoundState values (Pending/In Progress/Completed/Passed/Rejected/Withdrawn) - Observer status derived from latest ProjectRoundState instead of stale Project.status - Observer CSV export fetches all matching projects, not just current page - Add PENDING and PASSED styles to StatusBadge component Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import Link from 'next/link'
|
|||||||
import { useSearchParams, usePathname } from 'next/navigation'
|
import { useSearchParams, usePathname } from 'next/navigation'
|
||||||
import { trpc } from '@/lib/trpc/client'
|
import { trpc } from '@/lib/trpc/client'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -145,6 +146,9 @@ function parseFiltersFromParams(
|
|||||||
statuses: searchParams.get('status')
|
statuses: searchParams.get('status')
|
||||||
? searchParams.get('status')!.split(',')
|
? searchParams.get('status')!.split(',')
|
||||||
: [],
|
: [],
|
||||||
|
roundStates: searchParams.get('roundState')
|
||||||
|
? searchParams.get('roundState')!.split(',')
|
||||||
|
: [],
|
||||||
roundId: searchParams.get('round') || '',
|
roundId: searchParams.get('round') || '',
|
||||||
competitionCategory: searchParams.get('category') || '',
|
competitionCategory: searchParams.get('category') || '',
|
||||||
oceanIssue: searchParams.get('issue') || '',
|
oceanIssue: searchParams.get('issue') || '',
|
||||||
@@ -179,6 +183,8 @@ function filtersToParams(
|
|||||||
if (filters.search) params.set('q', filters.search)
|
if (filters.search) params.set('q', filters.search)
|
||||||
if (filters.statuses.length > 0)
|
if (filters.statuses.length > 0)
|
||||||
params.set('status', filters.statuses.join(','))
|
params.set('status', filters.statuses.join(','))
|
||||||
|
if (filters.roundStates.length > 0)
|
||||||
|
params.set('roundState', filters.roundStates.join(','))
|
||||||
if (filters.roundId) params.set('round', filters.roundId)
|
if (filters.roundId) params.set('round', filters.roundId)
|
||||||
if (filters.competitionCategory)
|
if (filters.competitionCategory)
|
||||||
params.set('category', filters.competitionCategory)
|
params.set('category', filters.competitionCategory)
|
||||||
@@ -204,6 +210,7 @@ export default function ProjectsPage() {
|
|||||||
const [filters, setFilters] = useState<ProjectFilters>({
|
const [filters, setFilters] = useState<ProjectFilters>({
|
||||||
search: parsed.search,
|
search: parsed.search,
|
||||||
statuses: parsed.statuses,
|
statuses: parsed.statuses,
|
||||||
|
roundStates: parsed.roundStates,
|
||||||
roundId: parsed.roundId,
|
roundId: parsed.roundId,
|
||||||
competitionCategory: parsed.competitionCategory,
|
competitionCategory: parsed.competitionCategory,
|
||||||
oceanIssue: parsed.oceanIssue,
|
oceanIssue: parsed.oceanIssue,
|
||||||
@@ -309,6 +316,12 @@ export default function ProjectsPage() {
|
|||||||
wantsMentorship: filters.wantsMentorship,
|
wantsMentorship: filters.wantsMentorship,
|
||||||
hasFiles: filters.hasFiles,
|
hasFiles: filters.hasFiles,
|
||||||
hasAssignments: filters.hasAssignments,
|
hasAssignments: filters.hasAssignments,
|
||||||
|
roundStates:
|
||||||
|
filters.roundStates.length > 0
|
||||||
|
? (filters.roundStates as Array<
|
||||||
|
'PENDING' | 'IN_PROGRESS' | 'COMPLETED' | 'PASSED' | 'REJECTED' | 'WITHDRAWN'
|
||||||
|
>)
|
||||||
|
: undefined,
|
||||||
page,
|
page,
|
||||||
perPage,
|
perPage,
|
||||||
sortBy: sortBy as 'title' | 'category' | 'program' | 'assignments' | 'status' | 'createdAt' | undefined,
|
sortBy: sortBy as 'title' | 'category' | 'program' | 'assignments' | 'status' | 'createdAt' | undefined,
|
||||||
@@ -752,7 +765,7 @@ export default function ProjectsPage() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Stats Summary + View Toggle */}
|
{/* Stats Summary + View Toggle */}
|
||||||
{data && data.projects.length > 0 && (
|
{data && (Object.keys(data.statusCounts ?? {}).length > 0 || data.projects.length > 0) && (
|
||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<div className="flex flex-wrap items-center gap-2 text-sm">
|
<div className="flex flex-wrap items-center gap-2 text-sm">
|
||||||
{Object.entries(data.statusCounts ?? {})
|
{Object.entries(data.statusCounts ?? {})
|
||||||
@@ -760,15 +773,43 @@ export default function ProjectsPage() {
|
|||||||
const order = ['PENDING', 'IN_PROGRESS', 'COMPLETED', 'PASSED', 'REJECTED', 'WITHDRAWN']
|
const order = ['PENDING', 'IN_PROGRESS', 'COMPLETED', 'PASSED', 'REJECTED', 'WITHDRAWN']
|
||||||
return order.indexOf(a) - order.indexOf(b)
|
return order.indexOf(a) - order.indexOf(b)
|
||||||
})
|
})
|
||||||
.map(([status, count]) => (
|
.map(([status, count]) => {
|
||||||
<Badge
|
const isActive = filters.roundStates.includes(status)
|
||||||
key={status}
|
return (
|
||||||
variant={statusColors[status] || 'secondary'}
|
<button
|
||||||
className="text-xs font-normal"
|
key={status}
|
||||||
>
|
type="button"
|
||||||
{count} {status.charAt(0) + status.slice(1).toLowerCase().replace('_', ' ')}
|
onClick={() => {
|
||||||
</Badge>
|
const next = isActive
|
||||||
))}
|
? filters.roundStates.filter((s) => s !== status)
|
||||||
|
: [...filters.roundStates, status]
|
||||||
|
handleFiltersChange({ ...filters, roundStates: next })
|
||||||
|
}}
|
||||||
|
className="inline-flex items-center"
|
||||||
|
>
|
||||||
|
<Badge
|
||||||
|
variant={statusColors[status] || 'secondary'}
|
||||||
|
className={cn(
|
||||||
|
'text-xs font-normal cursor-pointer transition-all',
|
||||||
|
isActive && 'ring-2 ring-offset-1 ring-primary',
|
||||||
|
!isActive && filters.roundStates.length > 0 && 'opacity-50'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{count} {status.charAt(0) + status.slice(1).toLowerCase().replace('_', ' ')}
|
||||||
|
</Badge>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{filters.roundStates.length > 0 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleFiltersChange({ ...filters, roundStates: [] })}
|
||||||
|
className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors ml-1"
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
Clear
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{data.total > data.projects.length && (
|
{data.total > data.projects.length && (
|
||||||
<span className="text-xs text-muted-foreground ml-1">
|
<span className="text-xs text-muted-foreground ml-1">
|
||||||
(page {data.page} of {data.totalPages})
|
(page {data.page} of {data.totalPages})
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ const ISSUE_LABELS: Record<string, string> = {
|
|||||||
export interface ProjectFilters {
|
export interface ProjectFilters {
|
||||||
search: string
|
search: string
|
||||||
statuses: string[]
|
statuses: string[]
|
||||||
|
roundStates: string[]
|
||||||
roundId: string
|
roundId: string
|
||||||
competitionCategory: string
|
competitionCategory: string
|
||||||
oceanIssue: string
|
oceanIssue: string
|
||||||
@@ -94,6 +95,7 @@ export function ProjectFiltersBar({
|
|||||||
|
|
||||||
const activeFilterCount = [
|
const activeFilterCount = [
|
||||||
filters.statuses.length > 0,
|
filters.statuses.length > 0,
|
||||||
|
filters.roundStates.length > 0,
|
||||||
filters.roundId !== '',
|
filters.roundId !== '',
|
||||||
filters.competitionCategory !== '',
|
filters.competitionCategory !== '',
|
||||||
filters.oceanIssue !== '',
|
filters.oceanIssue !== '',
|
||||||
@@ -114,6 +116,7 @@ export function ProjectFiltersBar({
|
|||||||
onChange({
|
onChange({
|
||||||
search: filters.search,
|
search: filters.search,
|
||||||
statuses: [],
|
statuses: [],
|
||||||
|
roundStates: [],
|
||||||
roundId: '',
|
roundId: '',
|
||||||
competitionCategory: '',
|
competitionCategory: '',
|
||||||
oceanIssue: '',
|
oceanIssue: '',
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ export function EvaluationPanel({ roundId, programId }: { roundId: string; progr
|
|||||||
{projects
|
{projects
|
||||||
.filter((p) => {
|
.filter((p) => {
|
||||||
const s = p.observerStatus ?? p.status
|
const s = p.observerStatus ?? p.status
|
||||||
return s !== 'NOT_REVIEWED' && s !== 'SUBMITTED'
|
return s !== 'PENDING'
|
||||||
})
|
})
|
||||||
.slice(0, 6)
|
.slice(0, 6)
|
||||||
.map((p) => (
|
.map((p) => (
|
||||||
|
|||||||
@@ -141,11 +141,19 @@ export function ObserverProjectsContent() {
|
|||||||
{ refetchInterval: 30_000 },
|
{ refetchInterval: 30_000 },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const utils = trpc.useUtils()
|
||||||
const handleRequestCsvData = useCallback(async () => {
|
const handleRequestCsvData = useCallback(async () => {
|
||||||
setCsvLoading(true)
|
setCsvLoading(true)
|
||||||
try {
|
try {
|
||||||
const allData = await new Promise<typeof projectsData>((resolve) => {
|
const allData = await utils.analytics.getAllProjects.fetch({
|
||||||
resolve(projectsData)
|
roundId: roundFilter !== 'all' ? roundFilter : undefined,
|
||||||
|
search: debouncedSearch || undefined,
|
||||||
|
status: statusFilter !== 'all' ? statusFilter : undefined,
|
||||||
|
sortBy,
|
||||||
|
sortDir,
|
||||||
|
page: 1,
|
||||||
|
perPage: 100,
|
||||||
|
exportAll: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!allData?.projects) {
|
if (!allData?.projects) {
|
||||||
@@ -158,7 +166,7 @@ export function ObserverProjectsContent() {
|
|||||||
teamName: p.teamName ?? '',
|
teamName: p.teamName ?? '',
|
||||||
country: p.country ?? '',
|
country: p.country ?? '',
|
||||||
roundName: p.roundName ?? '',
|
roundName: p.roundName ?? '',
|
||||||
status: p.status,
|
status: p.observerStatus ?? p.status,
|
||||||
averageScore: p.averageScore !== null ? p.averageScore.toFixed(2) : '',
|
averageScore: p.averageScore !== null ? p.averageScore.toFixed(2) : '',
|
||||||
evaluationCount: p.evaluationCount,
|
evaluationCount: p.evaluationCount,
|
||||||
}))
|
}))
|
||||||
@@ -174,7 +182,7 @@ export function ObserverProjectsContent() {
|
|||||||
setCsvLoading(false)
|
setCsvLoading(false)
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
}, [projectsData])
|
}, [utils, roundFilter, debouncedSearch, statusFilter, sortBy, sortDir])
|
||||||
|
|
||||||
const SortIcon = ({ column }: { column: 'title' | 'score' | 'evaluations' }) => {
|
const SortIcon = ({ column }: { column: 'title' | 'score' | 'evaluations' }) => {
|
||||||
if (sortBy !== column)
|
if (sortBy !== column)
|
||||||
@@ -251,13 +259,12 @@ export function ObserverProjectsContent() {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="all">All Statuses</SelectItem>
|
<SelectItem value="all">All Statuses</SelectItem>
|
||||||
<SelectItem value="SUBMITTED">Submitted</SelectItem>
|
<SelectItem value="PENDING">Pending</SelectItem>
|
||||||
<SelectItem value="NOT_REVIEWED">Not Reviewed</SelectItem>
|
<SelectItem value="IN_PROGRESS">In Progress</SelectItem>
|
||||||
<SelectItem value="UNDER_REVIEW">Under Review</SelectItem>
|
<SelectItem value="COMPLETED">Completed</SelectItem>
|
||||||
<SelectItem value="REVIEWED">Reviewed</SelectItem>
|
<SelectItem value="PASSED">Passed</SelectItem>
|
||||||
<SelectItem value="SEMIFINALIST">Semi-finalist</SelectItem>
|
|
||||||
<SelectItem value="FINALIST">Finalist</SelectItem>
|
|
||||||
<SelectItem value="REJECTED">Rejected</SelectItem>
|
<SelectItem value="REJECTED">Rejected</SelectItem>
|
||||||
|
<SelectItem value="WITHDRAWN">Withdrawn</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,9 +28,11 @@ const STATUS_STYLES: Record<string, { variant: BadgeProps['variant']; className?
|
|||||||
NOT_REVIEWED: { variant: 'secondary', className: 'bg-slate-500/10 text-slate-600 border-slate-200 dark:text-slate-400' },
|
NOT_REVIEWED: { variant: 'secondary', className: 'bg-slate-500/10 text-slate-600 border-slate-200 dark:text-slate-400' },
|
||||||
REVIEWED: { variant: 'default', className: 'bg-emerald-500/10 text-emerald-700 border-emerald-200 dark:text-emerald-400' },
|
REVIEWED: { variant: 'default', className: 'bg-emerald-500/10 text-emerald-700 border-emerald-200 dark:text-emerald-400' },
|
||||||
|
|
||||||
// Evaluation statuses
|
// Round state statuses
|
||||||
|
PENDING: { variant: 'secondary', className: 'bg-slate-500/10 text-slate-600 border-slate-200 dark:text-slate-400' },
|
||||||
IN_PROGRESS: { variant: 'default', className: 'bg-blue-500/10 text-blue-700 border-blue-200 dark:text-blue-400' },
|
IN_PROGRESS: { variant: 'default', className: 'bg-blue-500/10 text-blue-700 border-blue-200 dark:text-blue-400' },
|
||||||
COMPLETED: { variant: 'default', className: 'bg-emerald-500/10 text-emerald-700 border-emerald-200 dark:text-emerald-400' },
|
COMPLETED: { variant: 'default', className: 'bg-emerald-500/10 text-emerald-700 border-emerald-200 dark:text-emerald-400' },
|
||||||
|
PASSED: { variant: 'default', className: 'bg-green-500/10 text-green-700 border-green-200 dark:text-green-400' },
|
||||||
|
|
||||||
// User statuses
|
// User statuses
|
||||||
NONE: { variant: 'secondary', className: 'bg-slate-500/10 text-slate-500 border-slate-200 dark:text-slate-400' },
|
NONE: { variant: 'secondary', className: 'bg-slate-500/10 text-slate-500 border-slate-200 dark:text-slate-400' },
|
||||||
|
|||||||
@@ -988,17 +988,19 @@ export const analyticsRouter = router({
|
|||||||
sortDir: z.enum(['asc', 'desc']).default('asc'),
|
sortDir: z.enum(['asc', 'desc']).default('asc'),
|
||||||
page: z.number().min(1).default(1),
|
page: z.number().min(1).default(1),
|
||||||
perPage: z.number().min(1).max(100).default(20),
|
perPage: z.number().min(1).max(100).default(20),
|
||||||
|
exportAll: z.boolean().optional(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
|
const effectivePerPage = input.exportAll ? 10000 : input.perPage
|
||||||
const where: Record<string, unknown> = {}
|
const where: Record<string, unknown> = {}
|
||||||
|
|
||||||
if (input.roundId) {
|
if (input.roundId) {
|
||||||
where.projectRoundStates = { some: { roundId: input.roundId } }
|
where.projectRoundStates = { some: { roundId: input.roundId } }
|
||||||
}
|
}
|
||||||
|
|
||||||
const OBSERVER_DERIVED_STATUSES = ['NOT_REVIEWED', 'UNDER_REVIEW', 'REVIEWED']
|
const ROUND_STATE_STATUSES = ['PENDING', 'IN_PROGRESS', 'COMPLETED', 'PASSED', 'REJECTED', 'WITHDRAWN']
|
||||||
if (input.status && !OBSERVER_DERIVED_STATUSES.includes(input.status)) {
|
if (input.status && !ROUND_STATE_STATUSES.includes(input.status)) {
|
||||||
where.status = input.status
|
where.status = input.status
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1047,9 +1049,9 @@ export const analyticsRouter = router({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
orderBy: prismaOrderBy,
|
orderBy: prismaOrderBy,
|
||||||
// When sorting by computed fields or filtering by observer-derived status, fetch all then slice in JS
|
// When sorting by computed fields or filtering by round-state status, fetch all then slice in JS
|
||||||
...(input.sortBy === 'title' && !OBSERVER_DERIVED_STATUSES.includes(input.status ?? '')
|
...(input.sortBy === 'title' && !ROUND_STATE_STATUSES.includes(input.status ?? '') && !input.exportAll
|
||||||
? { skip: (input.page - 1) * input.perPage, take: input.perPage }
|
? { skip: (input.page - 1) * effectivePerPage, take: effectivePerPage }
|
||||||
: {}),
|
: {}),
|
||||||
}),
|
}),
|
||||||
ctx.prisma.project.count({ where }),
|
ctx.prisma.project.count({ where }),
|
||||||
@@ -1077,15 +1079,8 @@ export const analyticsRouter = router({
|
|||||||
? p.assignments.find((a) => a.roundId === input.roundId)
|
? p.assignments.find((a) => a.roundId === input.roundId)
|
||||||
: p.assignments[0]
|
: p.assignments[0]
|
||||||
|
|
||||||
// Derive observer-friendly status
|
// Derive observer-friendly status from latest round state
|
||||||
let observerStatus: string
|
const observerStatus = furthestRoundState?.state ?? 'PENDING'
|
||||||
if (p.status === 'REJECTED') observerStatus = 'REJECTED'
|
|
||||||
else if (p.status === 'SEMIFINALIST') observerStatus = 'SEMIFINALIST'
|
|
||||||
else if (p.status === 'FINALIST') observerStatus = 'FINALIST'
|
|
||||||
else if (p.status === 'SUBMITTED') observerStatus = 'SUBMITTED'
|
|
||||||
else if (submitted.length > 0) observerStatus = 'REVIEWED'
|
|
||||||
else if (drafts.length > 0) observerStatus = 'UNDER_REVIEW'
|
|
||||||
else observerStatus = 'NOT_REVIEWED'
|
|
||||||
|
|
||||||
const logoUrl = await getProjectLogoUrl(p.logoKey, p.logoProvider)
|
const logoUrl = await getProjectLogoUrl(p.logoKey, p.logoProvider)
|
||||||
|
|
||||||
@@ -1104,8 +1099,8 @@ export const analyticsRouter = router({
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Filter by observer-derived status in JS
|
// Filter by round-state status in JS
|
||||||
const observerStatusFilter = input.status && OBSERVER_DERIVED_STATUSES.includes(input.status)
|
const observerStatusFilter = input.status && ROUND_STATE_STATUSES.includes(input.status)
|
||||||
? input.status
|
? input.status
|
||||||
: null
|
: null
|
||||||
const filtered = observerStatusFilter
|
const filtered = observerStatusFilter
|
||||||
@@ -1132,15 +1127,15 @@ export const analyticsRouter = router({
|
|||||||
// Paginate in JS for computed-field sorts or observer status filter
|
// Paginate in JS for computed-field sorts or observer status filter
|
||||||
const needsJsPagination = input.sortBy !== 'title' || observerStatusFilter
|
const needsJsPagination = input.sortBy !== 'title' || observerStatusFilter
|
||||||
const paginated = needsJsPagination
|
const paginated = needsJsPagination
|
||||||
? sorted.slice((input.page - 1) * input.perPage, input.page * input.perPage)
|
? sorted.slice((input.page - 1) * effectivePerPage, input.page * effectivePerPage)
|
||||||
: sorted
|
: sorted
|
||||||
|
|
||||||
return {
|
return {
|
||||||
projects: paginated,
|
projects: paginated,
|
||||||
total: filteredTotal,
|
total: filteredTotal,
|
||||||
page: input.page,
|
page: input.page,
|
||||||
perPage: input.perPage,
|
perPage: effectivePerPage,
|
||||||
totalPages: Math.ceil(filteredTotal / input.perPage),
|
totalPages: Math.ceil(filteredTotal / effectivePerPage),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@@ -74,6 +74,9 @@ export const projectRouter = router({
|
|||||||
wantsMentorship: z.boolean().optional(),
|
wantsMentorship: z.boolean().optional(),
|
||||||
hasFiles: z.boolean().optional(),
|
hasFiles: z.boolean().optional(),
|
||||||
hasAssignments: z.boolean().optional(),
|
hasAssignments: z.boolean().optional(),
|
||||||
|
roundStates: z.array(z.enum([
|
||||||
|
'PENDING', 'IN_PROGRESS', 'COMPLETED', 'PASSED', 'REJECTED', 'WITHDRAWN',
|
||||||
|
])).optional(),
|
||||||
page: z.number().int().min(1).default(1),
|
page: z.number().int().min(1).default(1),
|
||||||
perPage: z.number().int().min(1).max(200).default(20),
|
perPage: z.number().int().min(1).max(200).default(20),
|
||||||
sortBy: z.enum(['title', 'category', 'program', 'assignments', 'status', 'createdAt']).optional(),
|
sortBy: z.enum(['title', 'category', 'program', 'assignments', 'status', 'createdAt']).optional(),
|
||||||
@@ -84,7 +87,7 @@ export const projectRouter = router({
|
|||||||
const {
|
const {
|
||||||
programId, roundId, excludeInRoundId, status, statuses, unassignedOnly, search, tags,
|
programId, roundId, excludeInRoundId, status, statuses, unassignedOnly, search, tags,
|
||||||
competitionCategory, oceanIssue, country,
|
competitionCategory, oceanIssue, country,
|
||||||
wantsMentorship, hasFiles, hasAssignments,
|
wantsMentorship, hasFiles, hasAssignments, roundStates,
|
||||||
page, perPage, sortBy, sortDir,
|
page, perPage, sortBy, sortDir,
|
||||||
} = input
|
} = input
|
||||||
const skip = (page - 1) * perPage
|
const skip = (page - 1) * perPage
|
||||||
@@ -143,6 +146,27 @@ export const projectRouter = router({
|
|||||||
if (hasAssignments === true) where.assignments = { some: {} }
|
if (hasAssignments === true) where.assignments = { some: {} }
|
||||||
if (hasAssignments === false) where.assignments = { none: {} }
|
if (hasAssignments === false) where.assignments = { none: {} }
|
||||||
|
|
||||||
|
// Filter by latest round state (matches the statusCounts logic)
|
||||||
|
if (roundStates?.length) {
|
||||||
|
const stateFilter: Record<string, unknown> = {}
|
||||||
|
if (programId) stateFilter.project = { programId }
|
||||||
|
const allStates = await ctx.prisma.projectRoundState.findMany({
|
||||||
|
where: stateFilter,
|
||||||
|
select: { projectId: true, state: true, round: { select: { sortOrder: true } } },
|
||||||
|
orderBy: { round: { sortOrder: 'desc' } },
|
||||||
|
})
|
||||||
|
const latestByProject = new Map<string, string>()
|
||||||
|
for (const s of allStates) {
|
||||||
|
if (!latestByProject.has(s.projectId)) {
|
||||||
|
latestByProject.set(s.projectId, s.state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const matchingIds = [...latestByProject.entries()]
|
||||||
|
.filter(([, state]) => roundStates.includes(state as typeof roundStates[number]))
|
||||||
|
.map(([id]) => id)
|
||||||
|
where.id = { in: matchingIds }
|
||||||
|
}
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
where.OR = [
|
where.OR = [
|
||||||
{ title: { contains: search, mode: 'insensitive' } },
|
{ title: { contains: search, mode: 'insensitive' } },
|
||||||
|
|||||||
Reference in New Issue
Block a user