Performance optimization, applicant portal, and missing DB migration
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>
This commit is contained in:
@@ -1164,4 +1164,121 @@ export const projectRouter = router({
|
||||
|
||||
return { projects, total, page, perPage, totalPages: Math.ceil(total / perPage) }
|
||||
}),
|
||||
|
||||
/**
|
||||
* Get full project detail with assignments and evaluation stats in one call.
|
||||
* Reduces client-side waterfall by combining project.get + assignment.listByProject + evaluation.getProjectStats.
|
||||
*/
|
||||
getFullDetail: adminProcedure
|
||||
.input(z.object({ id: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const [projectRaw, projectTags, assignments, submittedEvaluations] = await Promise.all([
|
||||
ctx.prisma.project.findUniqueOrThrow({
|
||||
where: { id: input.id },
|
||||
include: {
|
||||
files: true,
|
||||
round: true,
|
||||
teamMembers: {
|
||||
include: {
|
||||
user: {
|
||||
select: { id: true, name: true, email: true, profileImageKey: true, profileImageProvider: true },
|
||||
},
|
||||
},
|
||||
orderBy: { joinedAt: 'asc' },
|
||||
},
|
||||
mentorAssignment: {
|
||||
include: {
|
||||
mentor: {
|
||||
select: { id: true, name: true, email: true, expertiseTags: true, profileImageKey: true, profileImageProvider: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
ctx.prisma.projectTag.findMany({
|
||||
where: { projectId: input.id },
|
||||
include: { tag: { select: { id: true, name: true, category: true, color: true } } },
|
||||
orderBy: { confidence: 'desc' },
|
||||
}).catch(() => [] as { id: string; projectId: string; tagId: string; confidence: number; tag: { id: string; name: string; category: string | null; color: string | null } }[]),
|
||||
ctx.prisma.assignment.findMany({
|
||||
where: { projectId: input.id },
|
||||
include: {
|
||||
user: { select: { id: true, name: true, email: true, expertiseTags: true, profileImageKey: true, profileImageProvider: true } },
|
||||
evaluation: { select: { status: true, submittedAt: true, globalScore: true, binaryDecision: true } },
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
}),
|
||||
ctx.prisma.evaluation.findMany({
|
||||
where: {
|
||||
status: 'SUBMITTED',
|
||||
assignment: { projectId: input.id },
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
// Compute evaluation stats
|
||||
let stats = null
|
||||
if (submittedEvaluations.length > 0) {
|
||||
const globalScores = submittedEvaluations
|
||||
.map((e) => e.globalScore)
|
||||
.filter((s): s is number => s !== null)
|
||||
const yesVotes = submittedEvaluations.filter((e) => e.binaryDecision === true).length
|
||||
stats = {
|
||||
totalEvaluations: submittedEvaluations.length,
|
||||
averageGlobalScore: globalScores.length > 0
|
||||
? globalScores.reduce((a, b) => a + b, 0) / globalScores.length
|
||||
: null,
|
||||
minScore: globalScores.length > 0 ? Math.min(...globalScores) : null,
|
||||
maxScore: globalScores.length > 0 ? Math.max(...globalScores) : null,
|
||||
yesVotes,
|
||||
noVotes: submittedEvaluations.length - yesVotes,
|
||||
yesPercentage: (yesVotes / submittedEvaluations.length) * 100,
|
||||
}
|
||||
}
|
||||
|
||||
// Attach avatar URLs in parallel
|
||||
const [teamMembersWithAvatars, assignmentsWithAvatars, mentorWithAvatar] = await Promise.all([
|
||||
Promise.all(
|
||||
projectRaw.teamMembers.map(async (member) => ({
|
||||
...member,
|
||||
user: {
|
||||
...member.user,
|
||||
avatarUrl: await getUserAvatarUrl(member.user.profileImageKey, member.user.profileImageProvider),
|
||||
},
|
||||
}))
|
||||
),
|
||||
Promise.all(
|
||||
assignments.map(async (a) => ({
|
||||
...a,
|
||||
user: {
|
||||
...a.user,
|
||||
avatarUrl: await getUserAvatarUrl(a.user.profileImageKey, a.user.profileImageProvider),
|
||||
},
|
||||
}))
|
||||
),
|
||||
projectRaw.mentorAssignment
|
||||
? (async () => ({
|
||||
...projectRaw.mentorAssignment!,
|
||||
mentor: {
|
||||
...projectRaw.mentorAssignment!.mentor,
|
||||
avatarUrl: await getUserAvatarUrl(
|
||||
projectRaw.mentorAssignment!.mentor.profileImageKey,
|
||||
projectRaw.mentorAssignment!.mentor.profileImageProvider
|
||||
),
|
||||
},
|
||||
}))()
|
||||
: Promise.resolve(null),
|
||||
])
|
||||
|
||||
return {
|
||||
project: {
|
||||
...projectRaw,
|
||||
projectTags,
|
||||
teamMembers: teamMembersWithAvatars,
|
||||
mentorAssignment: mentorWithAvatar,
|
||||
},
|
||||
assignments: assignmentsWithAvatars,
|
||||
stats,
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user