feat: clickable status badges, observer status alignment, CSV export all
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:
2026-03-05 17:19:12 +01:00
parent ffe12a9e85
commit 0d94ee1fe8
7 changed files with 114 additions and 42 deletions

View File

@@ -74,6 +74,9 @@ export const projectRouter = router({
wantsMentorship: z.boolean().optional(),
hasFiles: 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),
perPage: z.number().int().min(1).max(200).default(20),
sortBy: z.enum(['title', 'category', 'program', 'assignments', 'status', 'createdAt']).optional(),
@@ -84,7 +87,7 @@ export const projectRouter = router({
const {
programId, roundId, excludeInRoundId, status, statuses, unassignedOnly, search, tags,
competitionCategory, oceanIssue, country,
wantsMentorship, hasFiles, hasAssignments,
wantsMentorship, hasFiles, hasAssignments, roundStates,
page, perPage, sortBy, sortDir,
} = input
const skip = (page - 1) * perPage
@@ -143,6 +146,27 @@ export const projectRouter = router({
if (hasAssignments === true) where.assignments = { some: {} }
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) {
where.OR = [
{ title: { contains: search, mode: 'insensitive' } },