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

@@ -988,17 +988,19 @@ export const analyticsRouter = router({
sortDir: z.enum(['asc', 'desc']).default('asc'),
page: z.number().min(1).default(1),
perPage: z.number().min(1).max(100).default(20),
exportAll: z.boolean().optional(),
})
)
.query(async ({ ctx, input }) => {
const effectivePerPage = input.exportAll ? 10000 : input.perPage
const where: Record<string, unknown> = {}
if (input.roundId) {
where.projectRoundStates = { some: { roundId: input.roundId } }
}
const OBSERVER_DERIVED_STATUSES = ['NOT_REVIEWED', 'UNDER_REVIEW', 'REVIEWED']
if (input.status && !OBSERVER_DERIVED_STATUSES.includes(input.status)) {
const ROUND_STATE_STATUSES = ['PENDING', 'IN_PROGRESS', 'COMPLETED', 'PASSED', 'REJECTED', 'WITHDRAWN']
if (input.status && !ROUND_STATE_STATUSES.includes(input.status)) {
where.status = input.status
}
@@ -1047,9 +1049,9 @@ export const analyticsRouter = router({
},
},
orderBy: prismaOrderBy,
// When sorting by computed fields or filtering by observer-derived status, fetch all then slice in JS
...(input.sortBy === 'title' && !OBSERVER_DERIVED_STATUSES.includes(input.status ?? '')
? { skip: (input.page - 1) * input.perPage, take: input.perPage }
// When sorting by computed fields or filtering by round-state status, fetch all then slice in JS
...(input.sortBy === 'title' && !ROUND_STATE_STATUSES.includes(input.status ?? '') && !input.exportAll
? { skip: (input.page - 1) * effectivePerPage, take: effectivePerPage }
: {}),
}),
ctx.prisma.project.count({ where }),
@@ -1077,15 +1079,8 @@ export const analyticsRouter = router({
? p.assignments.find((a) => a.roundId === input.roundId)
: p.assignments[0]
// Derive observer-friendly status
let observerStatus: string
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'
// Derive observer-friendly status from latest round state
const observerStatus = furthestRoundState?.state ?? 'PENDING'
const logoUrl = await getProjectLogoUrl(p.logoKey, p.logoProvider)
@@ -1104,8 +1099,8 @@ export const analyticsRouter = router({
}
}))
// Filter by observer-derived status in JS
const observerStatusFilter = input.status && OBSERVER_DERIVED_STATUSES.includes(input.status)
// Filter by round-state status in JS
const observerStatusFilter = input.status && ROUND_STATE_STATUSES.includes(input.status)
? input.status
: null
const filtered = observerStatusFilter
@@ -1132,15 +1127,15 @@ export const analyticsRouter = router({
// Paginate in JS for computed-field sorts or observer status filter
const needsJsPagination = input.sortBy !== 'title' || observerStatusFilter
const paginated = needsJsPagination
? sorted.slice((input.page - 1) * input.perPage, input.page * input.perPage)
? sorted.slice((input.page - 1) * effectivePerPage, input.page * effectivePerPage)
: sorted
return {
projects: paginated,
total: filteredTotal,
page: input.page,
perPage: input.perPage,
totalPages: Math.ceil(filteredTotal / input.perPage),
perPage: effectivePerPage,
totalPages: Math.ceil(filteredTotal / effectivePerPage),
}
}),