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:
@@ -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),
|
||||
}
|
||||
}),
|
||||
|
||||
|
||||
@@ -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' } },
|
||||
|
||||
Reference in New Issue
Block a user