Performance: - Convert admin dashboard from SSR to client-side tRPC (fixes 503/ChunkLoadError) - New dashboard.getStats tRPC endpoint batches 16 queries into single response - Parallelize jury dashboard queries (assignments + gracePeriods via Promise.all) - Add project.getFullDetail combined endpoint (project + assignments + stats) - Configure Prisma connection pool (connection_limit=20, pool_timeout=10) - Add optimizePackageImports for lucide-react tree-shaking - Increase React Query staleTime from 1min to 5min Applicant portal: - Add applicant layout, nav, dashboard, documents, team, and mentor pages - Add applicant router with document and team management endpoints - Add chunk error recovery utility - Update role nav and auth redirect for applicant role Database: - Add migration for missing schema elements (SpecialAward job tracking columns, WizardTemplate table, missing indexes) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
185 lines
5.0 KiB
TypeScript
185 lines
5.0 KiB
TypeScript
import { z } from 'zod'
|
|
import { router, adminProcedure } from '../trpc'
|
|
|
|
export const dashboardRouter = router({
|
|
/**
|
|
* Get all dashboard stats in a single query batch.
|
|
* Replaces the 16 parallel Prisma queries that were previously
|
|
* run during SSR, which blocked the event loop and caused 503s.
|
|
*/
|
|
getStats: adminProcedure
|
|
.input(z.object({ editionId: z.string() }))
|
|
.query(async ({ ctx, input }) => {
|
|
const { editionId } = input
|
|
|
|
const edition = await ctx.prisma.program.findUnique({
|
|
where: { id: editionId },
|
|
select: { name: true, year: true },
|
|
})
|
|
|
|
if (!edition) return null
|
|
|
|
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
|
|
|
|
const [
|
|
activeRoundCount,
|
|
totalRoundCount,
|
|
projectCount,
|
|
newProjectsThisWeek,
|
|
totalJurors,
|
|
activeJurors,
|
|
evaluationStats,
|
|
totalAssignments,
|
|
recentRounds,
|
|
latestProjects,
|
|
categoryBreakdown,
|
|
oceanIssueBreakdown,
|
|
recentActivity,
|
|
pendingCOIs,
|
|
draftRounds,
|
|
unassignedProjects,
|
|
] = await Promise.all([
|
|
ctx.prisma.round.count({
|
|
where: { programId: editionId, status: 'ACTIVE' },
|
|
}),
|
|
ctx.prisma.round.count({
|
|
where: { programId: editionId },
|
|
}),
|
|
ctx.prisma.project.count({
|
|
where: { programId: editionId },
|
|
}),
|
|
ctx.prisma.project.count({
|
|
where: {
|
|
programId: editionId,
|
|
createdAt: { gte: sevenDaysAgo },
|
|
},
|
|
}),
|
|
ctx.prisma.user.count({
|
|
where: {
|
|
role: 'JURY_MEMBER',
|
|
status: { in: ['ACTIVE', 'INVITED', 'NONE'] },
|
|
assignments: { some: { round: { programId: editionId } } },
|
|
},
|
|
}),
|
|
ctx.prisma.user.count({
|
|
where: {
|
|
role: 'JURY_MEMBER',
|
|
status: 'ACTIVE',
|
|
assignments: { some: { round: { programId: editionId } } },
|
|
},
|
|
}),
|
|
ctx.prisma.evaluation.groupBy({
|
|
by: ['status'],
|
|
where: { assignment: { round: { programId: editionId } } },
|
|
_count: true,
|
|
}),
|
|
ctx.prisma.assignment.count({
|
|
where: { round: { programId: editionId } },
|
|
}),
|
|
ctx.prisma.round.findMany({
|
|
where: { programId: editionId },
|
|
orderBy: { createdAt: 'desc' },
|
|
take: 5,
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
status: true,
|
|
votingStartAt: true,
|
|
votingEndAt: true,
|
|
submissionEndDate: true,
|
|
_count: {
|
|
select: {
|
|
projects: true,
|
|
assignments: true,
|
|
},
|
|
},
|
|
assignments: {
|
|
select: {
|
|
evaluation: { select: { status: true } },
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
ctx.prisma.project.findMany({
|
|
where: { programId: editionId },
|
|
orderBy: { createdAt: 'desc' },
|
|
take: 8,
|
|
select: {
|
|
id: true,
|
|
title: true,
|
|
teamName: true,
|
|
country: true,
|
|
competitionCategory: true,
|
|
oceanIssue: true,
|
|
logoKey: true,
|
|
createdAt: true,
|
|
submittedAt: true,
|
|
status: true,
|
|
round: { select: { name: true } },
|
|
},
|
|
}),
|
|
ctx.prisma.project.groupBy({
|
|
by: ['competitionCategory'],
|
|
where: { programId: editionId },
|
|
_count: true,
|
|
}),
|
|
ctx.prisma.project.groupBy({
|
|
by: ['oceanIssue'],
|
|
where: { programId: editionId },
|
|
_count: true,
|
|
}),
|
|
ctx.prisma.auditLog.findMany({
|
|
where: {
|
|
timestamp: { gte: sevenDaysAgo },
|
|
},
|
|
orderBy: { timestamp: 'desc' },
|
|
take: 8,
|
|
select: {
|
|
id: true,
|
|
action: true,
|
|
entityType: true,
|
|
timestamp: true,
|
|
user: { select: { name: true } },
|
|
},
|
|
}),
|
|
ctx.prisma.conflictOfInterest.count({
|
|
where: {
|
|
hasConflict: true,
|
|
reviewedAt: null,
|
|
assignment: { round: { programId: editionId } },
|
|
},
|
|
}),
|
|
ctx.prisma.round.count({
|
|
where: { programId: editionId, status: 'DRAFT' },
|
|
}),
|
|
ctx.prisma.project.count({
|
|
where: {
|
|
programId: editionId,
|
|
round: { status: 'ACTIVE' },
|
|
assignments: { none: {} },
|
|
},
|
|
}),
|
|
])
|
|
|
|
return {
|
|
edition,
|
|
activeRoundCount,
|
|
totalRoundCount,
|
|
projectCount,
|
|
newProjectsThisWeek,
|
|
totalJurors,
|
|
activeJurors,
|
|
evaluationStats,
|
|
totalAssignments,
|
|
recentRounds,
|
|
latestProjects,
|
|
categoryBreakdown,
|
|
oceanIssueBreakdown,
|
|
recentActivity,
|
|
pendingCOIs,
|
|
draftRounds,
|
|
unassignedProjects,
|
|
}
|
|
}),
|
|
})
|