2026-02-14 15:26:42 +01:00
|
|
|
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 [
|
|
|
|
|
activeStageCount,
|
|
|
|
|
totalStageCount,
|
|
|
|
|
projectCount,
|
|
|
|
|
newProjectsThisWeek,
|
|
|
|
|
totalJurors,
|
|
|
|
|
activeJurors,
|
|
|
|
|
evaluationStats,
|
|
|
|
|
totalAssignments,
|
|
|
|
|
recentStages,
|
|
|
|
|
latestProjects,
|
|
|
|
|
categoryBreakdown,
|
|
|
|
|
oceanIssueBreakdown,
|
|
|
|
|
recentActivity,
|
|
|
|
|
pendingCOIs,
|
|
|
|
|
draftStages,
|
|
|
|
|
unassignedProjects,
|
|
|
|
|
] = await Promise.all([
|
|
|
|
|
ctx.prisma.stage.count({
|
|
|
|
|
where: { track: { pipeline: { programId: editionId } }, status: 'STAGE_ACTIVE' },
|
|
|
|
|
}),
|
|
|
|
|
ctx.prisma.stage.count({
|
|
|
|
|
where: { track: { pipeline: { 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: { stage: { track: { pipeline: { programId: editionId } } } } },
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
ctx.prisma.user.count({
|
|
|
|
|
where: {
|
|
|
|
|
role: 'JURY_MEMBER',
|
|
|
|
|
status: 'ACTIVE',
|
|
|
|
|
assignments: { some: { stage: { track: { pipeline: { programId: editionId } } } } },
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
ctx.prisma.evaluation.groupBy({
|
|
|
|
|
by: ['status'],
|
|
|
|
|
where: { assignment: { stage: { track: { pipeline: { programId: editionId } } } } },
|
|
|
|
|
_count: true,
|
|
|
|
|
}),
|
|
|
|
|
ctx.prisma.assignment.count({
|
|
|
|
|
where: { stage: { track: { pipeline: { programId: editionId } } } },
|
|
|
|
|
}),
|
|
|
|
|
ctx.prisma.stage.findMany({
|
|
|
|
|
where: { track: { pipeline: { programId: editionId } } },
|
|
|
|
|
orderBy: { createdAt: 'desc' },
|
|
|
|
|
take: 5,
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
name: true,
|
|
|
|
|
status: true,
|
|
|
|
|
stageType: true,
|
|
|
|
|
windowOpenAt: true,
|
|
|
|
|
windowCloseAt: true,
|
|
|
|
|
_count: {
|
|
|
|
|
select: {
|
|
|
|
|
projectStageStates: 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,
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
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: { stage: { track: { pipeline: { programId: editionId } } } },
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
ctx.prisma.stage.count({
|
|
|
|
|
where: { track: { pipeline: { programId: editionId } }, status: 'STAGE_DRAFT' },
|
|
|
|
|
}),
|
|
|
|
|
ctx.prisma.project.count({
|
|
|
|
|
where: {
|
|
|
|
|
programId: editionId,
|
|
|
|
|
projectStageStates: {
|
|
|
|
|
some: {
|
|
|
|
|
stage: { status: 'STAGE_ACTIVE' },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
assignments: { none: {} },
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
edition,
|
|
|
|
|
activeStageCount,
|
|
|
|
|
totalStageCount,
|
|
|
|
|
projectCount,
|
|
|
|
|
newProjectsThisWeek,
|
|
|
|
|
totalJurors,
|
|
|
|
|
activeJurors,
|
|
|
|
|
evaluationStats,
|
|
|
|
|
totalAssignments,
|
|
|
|
|
recentStages,
|
|
|
|
|
latestProjects,
|
|
|
|
|
categoryBreakdown,
|
|
|
|
|
oceanIssueBreakdown,
|
|
|
|
|
recentActivity,
|
|
|
|
|
pendingCOIs,
|
|
|
|
|
draftStages,
|
|
|
|
|
unassignedProjects,
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
})
|