merge: PR10 — applicant nationality stats card

This commit is contained in:
Matt
2026-05-22 18:42:51 +02:00
2 changed files with 216 additions and 0 deletions

View File

@@ -2403,4 +2403,67 @@ export const analyticsRouter = router({
prisma: ctx.prisma,
})
}),
/**
* Nationality breakdown for the applicants (team members) of projects in
* the selected scope. Counts UNIQUE users so a single applicant on
* multiple teams isn't double-counted.
*
* Scope:
* - roundId set → projects with a ProjectRoundState in that round
* - programId set → projects in that program
* - neither → all team members across all projects (global)
*/
getApplicantNationalities: adminProcedure
.input(
z
.object({
roundId: z.string().optional(),
programId: z.string().optional(),
})
.optional()
)
.query(async ({ ctx, input }) => {
const roundId = input?.roundId
const programId = input?.programId
const projectFilter = roundId
? { projectRoundStates: { some: { roundId } } }
: programId
? { programId }
: {}
// Pull all distinct team-member userIds + their nationality in one query.
// `distinct: ['userId']` collapses a user appearing on multiple teams in
// the same scope to a single row.
const teamMembers = await ctx.prisma.teamMember.findMany({
where: { project: projectFilter },
select: { userId: true, user: { select: { nationality: true } } },
distinct: ['userId'],
})
const total = teamMembers.length
const declaredEntries = teamMembers.filter(
(tm) => tm.user?.nationality && tm.user.nationality.trim().length > 0
)
const declared = declaredEntries.length
const notDeclared = total - declared
const counts = new Map<string, number>()
for (const tm of declaredEntries) {
const code = (tm.user!.nationality as string).trim()
counts.set(code, (counts.get(code) ?? 0) + 1)
}
const byCountry = Array.from(counts.entries())
.map(([country, count]) => ({ country, count }))
.sort((a, b) => b.count - a.count || a.country.localeCompare(b.country))
return {
total,
declared,
notDeclared,
byCountry,
}
}),
})