Fix first-login error, awards performance, filter animation, cache invalidation, and query fixes

- Guard onboarding tRPC queries with session hydration check (fixes UNAUTHORIZED on first login)
- Defer expensive queries on awards page until UI elements are opened (dialog/tab)
- Fix perPage: 500 exceeding backend Zod max of 100 on awards eligibility query
- Add smooth open/close animation to project filters collapsible bar
- Fix seeded user status from ACTIVE to INVITED in seed-candidatures.ts
- Add router.refresh() cache invalidation across ~22 admin forms
- Fix geographic analytics query to use programId instead of round.programId
- Fix dashboard queries to scope by programId correctly
- Fix project.listPool and round queries for projects outside round context
- Add rounds page useEffect for state sync after mutations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-10 21:21:54 +01:00
parent 573785e440
commit 5cae78fe0c
26 changed files with 830 additions and 341 deletions

View File

@@ -353,11 +353,11 @@ export const analyticsRouter = router({
.query(async ({ ctx, input }) => {
const where = input.roundId
? { roundId: input.roundId }
: { round: { programId: input.programId } }
: { programId: input.programId }
const distribution = await ctx.prisma.project.groupBy({
by: ['country'],
where,
where: { ...where, country: { not: null } },
_count: { id: true },
})

View File

@@ -385,7 +385,7 @@ async function resolveRecipients(
if (!programId) return []
// Get all applicants with projects in rounds of this program
const projects = await prisma.project.findMany({
where: { round: { programId } },
where: { programId },
select: { submittedByUserId: true },
})
const ids = new Set(projects.map((p) => p.submittedByUserId).filter(Boolean) as string[])

View File

@@ -597,6 +597,51 @@ export const projectRouter = router({
return project
}),
/**
* Bulk delete projects (admin only)
*/
bulkDelete: adminProcedure
.input(
z.object({
ids: z.array(z.string()).min(1).max(200),
})
)
.mutation(async ({ ctx, input }) => {
const projects = await ctx.prisma.project.findMany({
where: { id: { in: input.ids } },
select: { id: true, title: true },
})
if (projects.length === 0) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'No projects found to delete',
})
}
const result = await ctx.prisma.$transaction(async (tx) => {
await logAudit({
prisma: tx,
userId: ctx.user.id,
action: 'BULK_DELETE',
entityType: 'Project',
detailsJson: {
count: projects.length,
titles: projects.map((p) => p.title),
ids: projects.map((p) => p.id),
},
ipAddress: ctx.ip,
userAgent: ctx.userAgent,
})
return tx.project.deleteMany({
where: { id: { in: projects.map((p) => p.id) } },
})
})
return { deleted: result.count }
}),
/**
* Import projects from CSV data (admin only)
* Projects belong to a program. Optionally assign to a round.
@@ -887,7 +932,7 @@ export const projectRouter = router({
const skip = (page - 1) * perPage
const where: Record<string, unknown> = {
round: { programId },
programId,
roundId: null,
}

View File

@@ -170,7 +170,7 @@ export const roundRouter = router({
if (input.roundType === 'FILTERING') {
await tx.project.updateMany({
where: {
round: { programId: input.programId },
programId: input.programId,
roundId: { not: created.id },
},
data: {
@@ -664,7 +664,7 @@ export const roundRouter = router({
const updated = await ctx.prisma.project.updateMany({
where: {
id: { in: input.projectIds },
round: { programId: round.programId },
programId: round.programId,
},
data: {
roundId: input.roundId,