feat: semi-finalist tracker dashboard, account reminders, search + UX fixes
- Add getSemiFinalistStats query with per-category/per-award breakdown - Add sendAccountReminders mutation with invite token generation and dedup - Add SemiFinalistTracker dashboard widget with progress bars and remind buttons - Add ACCOUNT_REMINDER email template - Extend project search to match team member name/email (7 locations) - Fix Passed count deduplication: count distinct projects, not round-state rows - Fix role switcher: visible pills above user section, auto-refresh session on mount Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -133,6 +133,8 @@ export const projectRouter = router({
|
||||
{ title: { contains: search, mode: 'insensitive' } },
|
||||
{ teamName: { contains: search, mode: 'insensitive' } },
|
||||
{ description: { contains: search, mode: 'insensitive' } },
|
||||
{ teamMembers: { some: { user: { name: { contains: search, mode: 'insensitive' } } } } },
|
||||
{ teamMembers: { some: { user: { email: { contains: search, mode: 'insensitive' } } } } },
|
||||
]
|
||||
}
|
||||
|
||||
@@ -163,17 +165,28 @@ export const projectRouter = router({
|
||||
},
|
||||
}),
|
||||
ctx.prisma.project.count({ where }),
|
||||
ctx.prisma.projectRoundState.groupBy({
|
||||
by: ['state'],
|
||||
where: where.programId ? { project: { programId: where.programId as string } } : {},
|
||||
_count: true,
|
||||
}),
|
||||
// Count distinct projects per state (avoids double-counting projects that passed multiple rounds)
|
||||
(async () => {
|
||||
const stateFilter = where.programId ? { project: { programId: where.programId as string } } : {}
|
||||
const states = ['PENDING', 'IN_PROGRESS', 'PASSED', 'REJECTED', 'COMPLETED', 'WITHDRAWN'] as const
|
||||
const counts = await Promise.all(
|
||||
states.map(async (state) => ({
|
||||
state,
|
||||
count: await ctx.prisma.projectRoundState.findMany({
|
||||
where: { ...stateFilter, state },
|
||||
select: { projectId: true },
|
||||
distinct: ['projectId'],
|
||||
}).then((rows) => rows.length),
|
||||
}))
|
||||
)
|
||||
return counts
|
||||
})(),
|
||||
])
|
||||
|
||||
// Build round-state counts
|
||||
// Build round-state counts (distinct projects per state)
|
||||
const statusCounts: Record<string, number> = {}
|
||||
for (const g of roundStateCounts) {
|
||||
statusCounts[g.state] = g._count
|
||||
statusCounts[g.state] = g.count
|
||||
}
|
||||
|
||||
const projectsWithLogos = await attachProjectLogoUrls(projects)
|
||||
@@ -259,6 +272,8 @@ export const projectRouter = router({
|
||||
{ title: { contains: search, mode: 'insensitive' } },
|
||||
{ teamName: { contains: search, mode: 'insensitive' } },
|
||||
{ description: { contains: search, mode: 'insensitive' } },
|
||||
{ teamMembers: { some: { user: { name: { contains: search, mode: 'insensitive' } } } } },
|
||||
{ teamMembers: { some: { user: { email: { contains: search, mode: 'insensitive' } } } } },
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1151,6 +1166,8 @@ export const projectRouter = router({
|
||||
where.OR = [
|
||||
{ title: { contains: search, mode: 'insensitive' } },
|
||||
{ teamName: { contains: search, mode: 'insensitive' } },
|
||||
{ teamMembers: { some: { user: { name: { contains: search, mode: 'insensitive' } } } } },
|
||||
{ teamMembers: { some: { user: { email: { contains: search, mode: 'insensitive' } } } } },
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user