import { z } from 'zod' import { router, adminProcedure } from '../trpc' export const exportRouter = router({ /** * Export evaluations as CSV data */ evaluations: adminProcedure .input( z.object({ roundId: z.string(), includeDetails: z.boolean().default(true), }) ) .query(async ({ ctx, input }) => { const evaluations = await ctx.prisma.evaluation.findMany({ where: { status: 'SUBMITTED', assignment: { roundId: input.roundId }, }, include: { assignment: { include: { user: { select: { name: true, email: true } }, project: { select: { title: true, teamName: true, tags: true } }, }, }, form: { select: { criteriaJson: true } }, }, orderBy: [ { assignment: { project: { title: 'asc' } } }, { submittedAt: 'asc' }, ], }) // Get criteria labels from form const criteriaLabels: Record = {} if (evaluations.length > 0) { const criteria = evaluations[0].form.criteriaJson as Array<{ id: string label: string }> criteria.forEach((c) => { criteriaLabels[c.id] = c.label }) } // Build export data const data = evaluations.map((e) => { const scores = e.criterionScoresJson as Record | null const criteriaScores: Record = {} Object.keys(criteriaLabels).forEach((id) => { criteriaScores[criteriaLabels[id]] = scores?.[id] ?? null }) return { projectTitle: e.assignment.project.title, teamName: e.assignment.project.teamName, tags: e.assignment.project.tags.join(', '), jurorName: e.assignment.user.name, jurorEmail: e.assignment.user.email, ...criteriaScores, globalScore: e.globalScore, decision: e.binaryDecision ? 'Yes' : 'No', feedback: input.includeDetails ? e.feedbackText : null, submittedAt: e.submittedAt?.toISOString(), } }) // Audit log await ctx.prisma.auditLog.create({ data: { userId: ctx.user.id, action: 'EXPORT', entityType: 'Evaluation', detailsJson: { roundId: input.roundId, count: data.length }, ipAddress: ctx.ip, userAgent: ctx.userAgent, }, }) return { data, columns: [ 'projectTitle', 'teamName', 'tags', 'jurorName', 'jurorEmail', ...Object.values(criteriaLabels), 'globalScore', 'decision', ...(input.includeDetails ? ['feedback'] : []), 'submittedAt', ], } }), /** * Export project scores summary */ projectScores: adminProcedure .input(z.object({ roundId: z.string() })) .query(async ({ ctx, input }) => { const projects = await ctx.prisma.project.findMany({ where: { roundId: input.roundId }, include: { assignments: { include: { evaluation: { where: { status: 'SUBMITTED' }, }, }, }, }, orderBy: { title: 'asc' }, }) const data = projects.map((p) => { const evaluations = p.assignments .map((a) => a.evaluation) .filter((e) => e !== null) const globalScores = evaluations .map((e) => e?.globalScore) .filter((s): s is number => s !== null) const yesVotes = evaluations.filter( (e) => e?.binaryDecision === true ).length return { title: p.title, teamName: p.teamName, status: p.status, tags: p.tags.join(', '), totalEvaluations: evaluations.length, averageScore: globalScores.length > 0 ? ( globalScores.reduce((a, b) => a + b, 0) / globalScores.length ).toFixed(2) : null, minScore: globalScores.length > 0 ? Math.min(...globalScores) : null, maxScore: globalScores.length > 0 ? Math.max(...globalScores) : null, yesVotes, noVotes: evaluations.length - yesVotes, yesPercentage: evaluations.length > 0 ? ((yesVotes / evaluations.length) * 100).toFixed(1) : null, } }) // Audit log await ctx.prisma.auditLog.create({ data: { userId: ctx.user.id, action: 'EXPORT', entityType: 'ProjectScores', detailsJson: { roundId: input.roundId, count: data.length }, ipAddress: ctx.ip, userAgent: ctx.userAgent, }, }) return { data, columns: [ 'title', 'teamName', 'status', 'tags', 'totalEvaluations', 'averageScore', 'minScore', 'maxScore', 'yesVotes', 'noVotes', 'yesPercentage', ], } }), /** * Export assignments */ assignments: adminProcedure .input(z.object({ roundId: z.string() })) .query(async ({ ctx, input }) => { const assignments = await ctx.prisma.assignment.findMany({ where: { roundId: input.roundId }, include: { user: { select: { name: true, email: true } }, project: { select: { title: true, teamName: true } }, evaluation: { select: { status: true, submittedAt: true } }, }, orderBy: [{ project: { title: 'asc' } }, { user: { name: 'asc' } }], }) const data = assignments.map((a) => ({ projectTitle: a.project.title, teamName: a.project.teamName, jurorName: a.user.name, jurorEmail: a.user.email, method: a.method, isRequired: a.isRequired ? 'Yes' : 'No', isCompleted: a.isCompleted ? 'Yes' : 'No', evaluationStatus: a.evaluation?.status ?? 'NOT_STARTED', submittedAt: a.evaluation?.submittedAt?.toISOString() ?? null, assignedAt: a.createdAt.toISOString(), })) return { data, columns: [ 'projectTitle', 'teamName', 'jurorName', 'jurorEmail', 'method', 'isRequired', 'isCompleted', 'evaluationStatus', 'submittedAt', 'assignedAt', ], } }), /** * Export audit logs as CSV data */ auditLogs: adminProcedure .input( z.object({ userId: z.string().optional(), action: z.string().optional(), entityType: z.string().optional(), startDate: z.date().optional(), endDate: z.date().optional(), }) ) .query(async ({ ctx, input }) => { const { userId, action, entityType, startDate, endDate } = input const where: Record = {} if (userId) where.userId = userId if (action) where.action = { contains: action, mode: 'insensitive' } if (entityType) where.entityType = entityType if (startDate || endDate) { where.timestamp = {} if (startDate) (where.timestamp as Record).gte = startDate if (endDate) (where.timestamp as Record).lte = endDate } const logs = await ctx.prisma.auditLog.findMany({ where, orderBy: { timestamp: 'desc' }, include: { user: { select: { name: true, email: true } }, }, take: 10000, // Limit export to 10k records }) const data = logs.map((log) => ({ timestamp: log.timestamp.toISOString(), userName: log.user?.name ?? 'System', userEmail: log.user?.email ?? 'N/A', action: log.action, entityType: log.entityType, entityId: log.entityId ?? '', ipAddress: log.ipAddress ?? '', userAgent: log.userAgent ?? '', details: log.detailsJson ? JSON.stringify(log.detailsJson) : '', })) return { data, columns: [ 'timestamp', 'userName', 'userEmail', 'action', 'entityType', 'entityId', 'ipAddress', 'userAgent', 'details', ], } }), })