import { z } from 'zod' import { router, adminProcedure } from '../trpc' import { resolveMemberContext } from '@/server/services/competition-context' import { evaluateAssignmentPolicy } from '@/server/services/assignment-policy' export const assignmentPolicyRouter = router({ /** * Get the fully-resolved assignment policy for a specific member in a round. * Returns cap, cap mode, buffer, category bias — all with provenance. */ getMemberPolicy: adminProcedure .input(z.object({ roundId: z.string(), userId: z.string() })) .query(async ({ input }) => { const ctx = await resolveMemberContext(input.roundId, input.userId) return evaluateAssignmentPolicy(ctx) }), /** * Get policy summary for all members in a jury group for a given round. * Useful for admin dashboards showing cap compliance across the group. */ getGroupPolicySummary: adminProcedure .input(z.object({ juryGroupId: z.string(), roundId: z.string() })) .query(async ({ ctx, input }) => { const members = await ctx.prisma.juryGroupMember.findMany({ where: { juryGroupId: input.juryGroupId }, include: { user: { select: { id: true, name: true, email: true } }, }, }) const results = await Promise.all( members.map(async (member) => { try { const memberCtx = await resolveMemberContext(input.roundId, member.userId) const policy = evaluateAssignmentPolicy(memberCtx) return { userId: member.userId, userName: member.user.name, userEmail: member.user.email, role: member.role, policy, } } catch (err) { console.error('[AssignmentPolicy] Failed to resolve member context:', err) return null } }), ) return results.filter(Boolean) }), /** * Get cap compliance report for a round. * Groups members into overCap, atCap, belowCap, and noCap buckets. */ getCapComplianceReport: adminProcedure .input(z.object({ roundId: z.string() })) .query(async ({ ctx, input }) => { const round = await ctx.prisma.round.findUniqueOrThrow({ where: { id: input.roundId }, select: { juryGroupId: true }, }) if (!round.juryGroupId) { return { overCap: [], atCap: [], belowCap: [], noCap: [] } } const members = await ctx.prisma.juryGroupMember.findMany({ where: { juryGroupId: round.juryGroupId }, include: { user: { select: { id: true, name: true, email: true } }, }, }) const report: { overCap: Array<{ userId: string; userName: string | null; overCapBy: number }> atCap: Array<{ userId: string; userName: string | null }> belowCap: Array<{ userId: string; userName: string | null; remaining: number }> noCap: Array<{ userId: string; userName: string | null }> } = { overCap: [], atCap: [], belowCap: [], noCap: [] } for (const member of members) { try { const memberCtx = await resolveMemberContext(input.roundId, member.userId) const policy = evaluateAssignmentPolicy(memberCtx) if (policy.effectiveCapMode.value === 'NONE') { report.noCap.push({ userId: member.userId, userName: member.user.name }) } else if (policy.isOverCap) { report.overCap.push({ userId: member.userId, userName: member.user.name, overCapBy: policy.overCapBy, }) } else if (policy.remainingCapacity === 0) { report.atCap.push({ userId: member.userId, userName: member.user.name }) } else { report.belowCap.push({ userId: member.userId, userName: member.user.name, remaining: policy.remainingCapacity, }) } } catch (err) { console.error('[AssignmentPolicy] Failed to evaluate policy for member:', err) } } return report }), })