Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
import { z } from 'zod'
|
|
|
|
|
import { TRPCError } from '@trpc/server'
|
|
|
|
|
import { Prisma } from '@prisma/client'
|
|
|
|
|
import { router, adminProcedure, protectedProcedure } from '../trpc'
|
|
|
|
|
import { logAudit } from '@/server/utils/audit'
|
|
|
|
|
|
|
|
|
|
const capModeEnum = z.enum(['HARD', 'SOFT', 'NONE'])
|
|
|
|
|
|
|
|
|
|
export const juryGroupRouter = router({
|
|
|
|
|
/**
|
|
|
|
|
* Create a new jury group for a competition
|
|
|
|
|
*/
|
|
|
|
|
create: adminProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
competitionId: z.string(),
|
|
|
|
|
name: z.string().min(1).max(255),
|
|
|
|
|
slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/),
|
|
|
|
|
description: z.string().optional(),
|
|
|
|
|
sortOrder: z.number().int().nonnegative().default(0),
|
|
|
|
|
defaultMaxAssignments: z.number().int().positive().default(20),
|
|
|
|
|
defaultCapMode: capModeEnum.default('SOFT'),
|
|
|
|
|
softCapBuffer: z.number().int().nonnegative().default(2),
|
|
|
|
|
categoryQuotasEnabled: z.boolean().default(false),
|
|
|
|
|
defaultCategoryQuotas: z
|
|
|
|
|
.record(z.object({ min: z.number().int().nonnegative(), max: z.number().int().positive() }))
|
|
|
|
|
.optional(),
|
|
|
|
|
allowJurorCapAdjustment: z.boolean().default(false),
|
|
|
|
|
allowJurorRatioAdjustment: z.boolean().default(false),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
await ctx.prisma.competition.findUniqueOrThrow({
|
|
|
|
|
where: { id: input.competitionId },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const { defaultCategoryQuotas, ...rest } = input
|
|
|
|
|
|
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
|
|
|
const juryGroup = await ctx.prisma.juryGroup.create({
|
|
|
|
|
data: {
|
|
|
|
|
...rest,
|
|
|
|
|
defaultCategoryQuotas: defaultCategoryQuotas ?? undefined,
|
|
|
|
|
},
|
|
|
|
|
})
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
|
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
|
|
|
// Audit outside transaction so failures don't roll back the create
|
|
|
|
|
await logAudit({
|
|
|
|
|
prisma: ctx.prisma,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'CREATE',
|
|
|
|
|
entityType: 'JuryGroup',
|
|
|
|
|
entityId: juryGroup.id,
|
|
|
|
|
detailsJson: { name: input.name, competitionId: input.competitionId },
|
|
|
|
|
ipAddress: ctx.ip,
|
|
|
|
|
userAgent: ctx.userAgent,
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return juryGroup
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get jury group by ID with members
|
|
|
|
|
*/
|
|
|
|
|
getById: protectedProcedure
|
|
|
|
|
.input(z.object({ id: z.string() }))
|
|
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
const group = await ctx.prisma.juryGroup.findUnique({
|
|
|
|
|
where: { id: input.id },
|
|
|
|
|
include: {
|
|
|
|
|
members: {
|
|
|
|
|
include: {
|
|
|
|
|
user: {
|
|
|
|
|
select: { id: true, name: true, email: true, role: true },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
orderBy: { joinedAt: 'asc' },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (!group) {
|
|
|
|
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'Jury group not found' })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return group
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* List jury groups for a competition
|
|
|
|
|
*/
|
|
|
|
|
list: protectedProcedure
|
|
|
|
|
.input(z.object({ competitionId: z.string() }))
|
|
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
return ctx.prisma.juryGroup.findMany({
|
|
|
|
|
where: { competitionId: input.competitionId },
|
|
|
|
|
orderBy: { sortOrder: 'asc' },
|
|
|
|
|
include: {
|
|
|
|
|
_count: { select: { members: true, assignments: true } },
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update jury group settings
|
|
|
|
|
*/
|
|
|
|
|
update: adminProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
id: z.string(),
|
|
|
|
|
name: z.string().min(1).max(255).optional(),
|
|
|
|
|
slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/).optional(),
|
|
|
|
|
description: z.string().optional(),
|
|
|
|
|
sortOrder: z.number().int().nonnegative().optional(),
|
|
|
|
|
defaultMaxAssignments: z.number().int().positive().optional(),
|
|
|
|
|
defaultCapMode: capModeEnum.optional(),
|
|
|
|
|
softCapBuffer: z.number().int().nonnegative().optional(),
|
|
|
|
|
categoryQuotasEnabled: z.boolean().optional(),
|
|
|
|
|
defaultCategoryQuotas: z
|
|
|
|
|
.record(z.object({ min: z.number().int().nonnegative(), max: z.number().int().positive() }))
|
|
|
|
|
.nullable()
|
|
|
|
|
.optional(),
|
|
|
|
|
allowJurorCapAdjustment: z.boolean().optional(),
|
|
|
|
|
allowJurorRatioAdjustment: z.boolean().optional(),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
const { id, defaultCategoryQuotas, ...rest } = input
|
|
|
|
|
|
|
|
|
|
return ctx.prisma.juryGroup.update({
|
|
|
|
|
where: { id },
|
|
|
|
|
data: {
|
|
|
|
|
...rest,
|
|
|
|
|
...(defaultCategoryQuotas !== undefined
|
|
|
|
|
? {
|
|
|
|
|
defaultCategoryQuotas:
|
|
|
|
|
defaultCategoryQuotas === null
|
|
|
|
|
? Prisma.JsonNull
|
|
|
|
|
: (defaultCategoryQuotas as Prisma.InputJsonValue),
|
|
|
|
|
}
|
|
|
|
|
: {}),
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Add a member to a jury group
|
|
|
|
|
*/
|
|
|
|
|
addMember: adminProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
juryGroupId: z.string(),
|
|
|
|
|
userId: z.string(),
|
|
|
|
|
role: z.enum(['CHAIR', 'MEMBER', 'OBSERVER']).default('MEMBER'),
|
|
|
|
|
maxAssignmentsOverride: z.number().int().positive().nullable().optional(),
|
|
|
|
|
capModeOverride: capModeEnum.nullable().optional(),
|
|
|
|
|
categoryQuotasOverride: z
|
|
|
|
|
.record(z.object({ min: z.number().int().nonnegative(), max: z.number().int().positive() }))
|
|
|
|
|
.nullable()
|
|
|
|
|
.optional(),
|
|
|
|
|
preferredStartupRatio: z.number().min(0).max(1).nullable().optional(),
|
|
|
|
|
availabilityNotes: z.string().nullable().optional(),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
// Verify the user exists
|
|
|
|
|
await ctx.prisma.user.findUniqueOrThrow({
|
|
|
|
|
where: { id: input.userId },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Check if already a member
|
|
|
|
|
const existing = await ctx.prisma.juryGroupMember.findUnique({
|
|
|
|
|
where: {
|
|
|
|
|
juryGroupId_userId: {
|
|
|
|
|
juryGroupId: input.juryGroupId,
|
|
|
|
|
userId: input.userId,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (existing) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'CONFLICT',
|
|
|
|
|
message: 'User is already a member of this jury group',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
|
|
|
const member = await ctx.prisma.juryGroupMember.create({
|
|
|
|
|
data: {
|
|
|
|
|
juryGroupId: input.juryGroupId,
|
|
|
|
|
userId: input.userId,
|
|
|
|
|
role: input.role,
|
|
|
|
|
maxAssignmentsOverride: input.maxAssignmentsOverride ?? undefined,
|
|
|
|
|
capModeOverride: input.capModeOverride ?? undefined,
|
|
|
|
|
categoryQuotasOverride: input.categoryQuotasOverride ?? undefined,
|
|
|
|
|
preferredStartupRatio: input.preferredStartupRatio ?? undefined,
|
|
|
|
|
availabilityNotes: input.availabilityNotes ?? undefined,
|
|
|
|
|
},
|
|
|
|
|
include: {
|
|
|
|
|
user: { select: { id: true, name: true, email: true, role: true } },
|
|
|
|
|
},
|
|
|
|
|
})
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
|
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
|
|
|
// Audit outside transaction so failures don't roll back the member add
|
|
|
|
|
await logAudit({
|
|
|
|
|
prisma: ctx.prisma,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'CREATE',
|
|
|
|
|
entityType: 'JuryGroupMember',
|
|
|
|
|
entityId: member.id,
|
|
|
|
|
detailsJson: {
|
|
|
|
|
juryGroupId: input.juryGroupId,
|
|
|
|
|
addedUserId: input.userId,
|
|
|
|
|
role: input.role,
|
|
|
|
|
},
|
|
|
|
|
ipAddress: ctx.ip,
|
|
|
|
|
userAgent: ctx.userAgent,
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return member
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Remove a member from a jury group
|
|
|
|
|
*/
|
|
|
|
|
removeMember: adminProcedure
|
|
|
|
|
.input(z.object({ id: z.string() }))
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
|
|
|
const existing = await ctx.prisma.juryGroupMember.findUniqueOrThrow({
|
|
|
|
|
where: { id: input.id },
|
|
|
|
|
})
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
|
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
|
|
|
await ctx.prisma.juryGroupMember.delete({ where: { id: input.id } })
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
|
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
|
|
|
// Audit outside transaction so failures don't roll back the member removal
|
|
|
|
|
await logAudit({
|
|
|
|
|
prisma: ctx.prisma,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'DELETE',
|
|
|
|
|
entityType: 'JuryGroupMember',
|
|
|
|
|
entityId: input.id,
|
|
|
|
|
detailsJson: {
|
|
|
|
|
juryGroupId: existing.juryGroupId,
|
|
|
|
|
removedUserId: existing.userId,
|
|
|
|
|
},
|
|
|
|
|
ipAddress: ctx.ip,
|
|
|
|
|
userAgent: ctx.userAgent,
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
})
|
|
|
|
|
|
Round detail overhaul, file requirements, project management, audit log fix
- Redesign round detail page with 6 tabs (overview, projects, filtering, assignments, config, documents)
- Add jury group assignment selector in round stats bar
- Add FileRequirementsEditor component replacing SubmissionWindowManager
- Add FilteringDashboard component for AI-powered project screening
- Add project removal from rounds (single + bulk) with cascading to subsequent rounds
- Add project add/remove UI in ProjectStatesTable with confirmation dialogs
- Fix logAudit inside $transaction pattern across all 12 router files
(PostgreSQL aborted-transaction state caused silent operation failures)
- Fix special awards creation, deletion, status update, and winner assignment
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:49:39 +01:00
|
|
|
return existing
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
}),
|
|
|
|
|
|
2026-02-16 12:46:01 +01:00
|
|
|
/**
|
|
|
|
|
* Delete a jury group entirely
|
|
|
|
|
*/
|
|
|
|
|
delete: adminProcedure
|
|
|
|
|
.input(z.object({ id: z.string() }))
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
const group = await ctx.prisma.juryGroup.findUniqueOrThrow({
|
|
|
|
|
where: { id: input.id },
|
|
|
|
|
include: {
|
|
|
|
|
_count: { select: { assignments: true, rounds: true } },
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Unlink any rounds that reference this jury group
|
|
|
|
|
await ctx.prisma.round.updateMany({
|
|
|
|
|
where: { juryGroupId: input.id },
|
|
|
|
|
data: { juryGroupId: null },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Delete all members first (cascade should handle this, but be explicit)
|
|
|
|
|
await ctx.prisma.juryGroupMember.deleteMany({
|
|
|
|
|
where: { juryGroupId: input.id },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
await ctx.prisma.juryGroup.delete({ where: { id: input.id } })
|
|
|
|
|
|
|
|
|
|
await logAudit({
|
|
|
|
|
prisma: ctx.prisma,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'DELETE',
|
|
|
|
|
entityType: 'JuryGroup',
|
|
|
|
|
entityId: input.id,
|
|
|
|
|
detailsJson: {
|
|
|
|
|
name: group.name,
|
|
|
|
|
competitionId: group.competitionId,
|
|
|
|
|
memberCount: group._count.assignments,
|
|
|
|
|
},
|
|
|
|
|
ipAddress: ctx.ip,
|
|
|
|
|
userAgent: ctx.userAgent,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return { success: true, name: group.name }
|
|
|
|
|
}),
|
|
|
|
|
|
Competition/Round architecture: full platform rewrite (Phases 1-9)
Replace Pipeline/Stage system with Competition/Round architecture.
New schema: Competition, Round (7 types), JuryGroup, AssignmentPolicy,
ProjectRoundState, DeliberationSession, ResultLock, SubmissionWindow.
New services: round-engine, round-assignment, deliberation, result-lock,
submission-manager, competition-context, ai-prompt-guard.
Full admin/jury/applicant/mentor UI rewrite. AI prompt hardening with
structured prompts, retry logic, and injection detection. All legacy
pipeline/stage code removed. 4 new migrations + seed aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-15 23:04:15 +01:00
|
|
|
/**
|
|
|
|
|
* Update a jury group member's role/overrides
|
|
|
|
|
*/
|
|
|
|
|
updateMember: adminProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
id: z.string(),
|
|
|
|
|
role: z.enum(['CHAIR', 'MEMBER', 'OBSERVER']).optional(),
|
|
|
|
|
maxAssignmentsOverride: z.number().int().positive().nullable().optional(),
|
|
|
|
|
capModeOverride: capModeEnum.nullable().optional(),
|
|
|
|
|
categoryQuotasOverride: z
|
|
|
|
|
.record(z.object({ min: z.number().int().nonnegative(), max: z.number().int().positive() }))
|
|
|
|
|
.nullable()
|
|
|
|
|
.optional(),
|
|
|
|
|
preferredStartupRatio: z.number().min(0).max(1).nullable().optional(),
|
|
|
|
|
availabilityNotes: z.string().nullable().optional(),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
const { id, categoryQuotasOverride, ...rest } = input
|
|
|
|
|
|
|
|
|
|
return ctx.prisma.juryGroupMember.update({
|
|
|
|
|
where: { id },
|
|
|
|
|
data: {
|
|
|
|
|
...rest,
|
|
|
|
|
...(categoryQuotasOverride !== undefined
|
|
|
|
|
? {
|
|
|
|
|
categoryQuotasOverride:
|
|
|
|
|
categoryQuotasOverride === null
|
|
|
|
|
? Prisma.JsonNull
|
|
|
|
|
: (categoryQuotasOverride as Prisma.InputJsonValue),
|
|
|
|
|
}
|
|
|
|
|
: {}),
|
|
|
|
|
},
|
|
|
|
|
include: {
|
|
|
|
|
user: { select: { id: true, name: true, email: true, role: true } },
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Review self-service values set by jurors during onboarding.
|
|
|
|
|
* Returns members who have self-service cap or ratio adjustments.
|
|
|
|
|
*/
|
|
|
|
|
reviewSelfServiceValues: adminProcedure
|
|
|
|
|
.input(z.object({ juryGroupId: z.string() }))
|
|
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
const group = await ctx.prisma.juryGroup.findUniqueOrThrow({
|
|
|
|
|
where: { id: input.juryGroupId },
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
name: true,
|
|
|
|
|
defaultMaxAssignments: true,
|
|
|
|
|
allowJurorCapAdjustment: true,
|
|
|
|
|
allowJurorRatioAdjustment: true,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const members = await ctx.prisma.juryGroupMember.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
juryGroupId: input.juryGroupId,
|
|
|
|
|
OR: [
|
|
|
|
|
{ selfServiceCap: { not: null } },
|
|
|
|
|
{ selfServiceRatio: { not: null } },
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
include: {
|
|
|
|
|
user: { select: { id: true, name: true, email: true } },
|
|
|
|
|
},
|
|
|
|
|
orderBy: { joinedAt: 'asc' },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
group,
|
|
|
|
|
members: members.map((m) => ({
|
|
|
|
|
id: m.id,
|
|
|
|
|
userId: m.userId,
|
|
|
|
|
userName: m.user.name,
|
|
|
|
|
userEmail: m.user.email,
|
|
|
|
|
role: m.role,
|
|
|
|
|
adminCap: m.maxAssignmentsOverride ?? group.defaultMaxAssignments,
|
|
|
|
|
selfServiceCap: m.selfServiceCap,
|
|
|
|
|
selfServiceRatio: m.selfServiceRatio,
|
|
|
|
|
preferredStartupRatio: m.preferredStartupRatio,
|
|
|
|
|
})),
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
})
|