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:
2026-03-04 15:41:03 +01:00
parent af03c12ae5
commit 43e21c6c6e
10 changed files with 622 additions and 14 deletions

View File

@@ -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' } } } } },
]
}