Implement Prototype 1 improvements: unified members, project filters, audit expansion, filtering rounds, special awards
- Unified Member Management: merge /admin/users and /admin/mentors into /admin/members with role tabs, search, pagination - Project List Filters: add search, multi-status filter, round/category/country selects, boolean toggles, URL persistence - Audit Log Expansion: track logins, round state changes, evaluation submissions, file access, role changes via shared logAudit utility - Founding Date Field: add foundedAt to Project model with CSV import support - Filtering Round System: configurable rules (field-based, document check, AI screening), execution engine, results review with override/reinstate - Special Awards System: named awards with eligibility criteria, dedicated jury, PICK_WINNER/RANKED/SCORED voting modes, AI eligibility - Dashboard resilience: wrap heavy queries in try-catch to prevent error boundary on transient DB failures - Reusable pagination component extracted to src/components/shared/pagination.tsx - Old /admin/users and /admin/mentors routes redirect to /admin/members - Prisma migration for all schema additions (additive, no data loss) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@ export const projectRouter = router({
|
||||
list: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
roundId: z.string(),
|
||||
roundId: z.string().optional(),
|
||||
status: z
|
||||
.enum([
|
||||
'SUBMITTED',
|
||||
@@ -23,23 +23,63 @@ export const projectRouter = router({
|
||||
'REJECTED',
|
||||
])
|
||||
.optional(),
|
||||
statuses: z.array(
|
||||
z.enum([
|
||||
'SUBMITTED',
|
||||
'ELIGIBLE',
|
||||
'ASSIGNED',
|
||||
'SEMIFINALIST',
|
||||
'FINALIST',
|
||||
'REJECTED',
|
||||
])
|
||||
).optional(),
|
||||
search: z.string().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
competitionCategory: z.enum(['STARTUP', 'BUSINESS_CONCEPT']).optional(),
|
||||
oceanIssue: z.enum([
|
||||
'POLLUTION_REDUCTION', 'CLIMATE_MITIGATION', 'TECHNOLOGY_INNOVATION',
|
||||
'SUSTAINABLE_SHIPPING', 'BLUE_CARBON', 'HABITAT_RESTORATION',
|
||||
'COMMUNITY_CAPACITY', 'SUSTAINABLE_FISHING', 'CONSUMER_AWARENESS',
|
||||
'OCEAN_ACIDIFICATION', 'OTHER',
|
||||
]).optional(),
|
||||
country: z.string().optional(),
|
||||
wantsMentorship: z.boolean().optional(),
|
||||
hasFiles: z.boolean().optional(),
|
||||
hasAssignments: z.boolean().optional(),
|
||||
page: z.number().int().min(1).default(1),
|
||||
perPage: z.number().int().min(1).max(100).default(20),
|
||||
})
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { roundId, status, search, tags, page, perPage } = input
|
||||
const {
|
||||
roundId, status, statuses, search, tags,
|
||||
competitionCategory, oceanIssue, country,
|
||||
wantsMentorship, hasFiles, hasAssignments,
|
||||
page, perPage,
|
||||
} = input
|
||||
const skip = (page - 1) * perPage
|
||||
|
||||
// Build where clause
|
||||
const where: Record<string, unknown> = { roundId }
|
||||
const where: Record<string, unknown> = {}
|
||||
|
||||
if (status) where.status = status
|
||||
if (roundId) where.roundId = roundId
|
||||
if (statuses && statuses.length > 0) {
|
||||
where.status = { in: statuses }
|
||||
} else if (status) {
|
||||
where.status = status
|
||||
}
|
||||
if (tags && tags.length > 0) {
|
||||
where.tags = { hasSome: tags }
|
||||
}
|
||||
if (competitionCategory) where.competitionCategory = competitionCategory
|
||||
if (oceanIssue) where.oceanIssue = oceanIssue
|
||||
if (country) where.country = country
|
||||
if (wantsMentorship !== undefined) where.wantsMentorship = wantsMentorship
|
||||
if (hasFiles === true) where.files = { some: {} }
|
||||
if (hasFiles === false) where.files = { none: {} }
|
||||
if (hasAssignments === true) where.assignments = { some: {} }
|
||||
if (hasAssignments === false) where.assignments = { none: {} }
|
||||
|
||||
if (search) {
|
||||
where.OR = [
|
||||
{ title: { contains: search, mode: 'insensitive' } },
|
||||
@@ -50,7 +90,9 @@ export const projectRouter = router({
|
||||
|
||||
// Jury members can only see assigned projects
|
||||
if (ctx.user.role === 'JURY_MEMBER') {
|
||||
// If hasAssignments filter is already set, combine with jury filter
|
||||
where.assignments = {
|
||||
...((where.assignments as Record<string, unknown>) || {}),
|
||||
some: { userId: ctx.user.id },
|
||||
}
|
||||
}
|
||||
@@ -63,6 +105,9 @@ export const projectRouter = router({
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
files: true,
|
||||
round: {
|
||||
select: { id: true, name: true, program: { select: { name: true } } },
|
||||
},
|
||||
_count: { select: { assignments: true } },
|
||||
},
|
||||
}),
|
||||
@@ -78,6 +123,48 @@ export const projectRouter = router({
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Get filter options for the project list (distinct values)
|
||||
*/
|
||||
getFilterOptions: protectedProcedure
|
||||
.query(async ({ ctx }) => {
|
||||
const [rounds, countries, categories, issues] = await Promise.all([
|
||||
ctx.prisma.round.findMany({
|
||||
select: { id: true, name: true, program: { select: { name: true } } },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
}),
|
||||
ctx.prisma.project.findMany({
|
||||
where: { country: { not: null } },
|
||||
select: { country: true },
|
||||
distinct: ['country'],
|
||||
orderBy: { country: 'asc' },
|
||||
}),
|
||||
ctx.prisma.project.groupBy({
|
||||
by: ['competitionCategory'],
|
||||
where: { competitionCategory: { not: null } },
|
||||
_count: true,
|
||||
}),
|
||||
ctx.prisma.project.groupBy({
|
||||
by: ['oceanIssue'],
|
||||
where: { oceanIssue: { not: null } },
|
||||
_count: true,
|
||||
}),
|
||||
])
|
||||
|
||||
return {
|
||||
rounds,
|
||||
countries: countries.map((c) => c.country).filter(Boolean) as string[],
|
||||
categories: categories.map((c) => ({
|
||||
value: c.competitionCategory!,
|
||||
count: c._count,
|
||||
})),
|
||||
issues: issues.map((i) => ({
|
||||
value: i.oceanIssue!,
|
||||
count: i._count,
|
||||
})),
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Get a single project with details
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user