Files
MOPC-Portal/src/server/routers/assignmentPolicy.ts

114 lines
4.0 KiB
TypeScript
Raw Normal View History

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
}),
})