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'
|
|
|
|
|
import { validateRoundConfig, defaultRoundConfig } from '@/types/competition-configs'
|
AI category-aware evaluation: per-round config, file parsing, shortlist, advance flow
- Per-juror cap mode (HARD/SOFT/NONE) in add-member dialog and members table
- Jury invite flow: create user + add to group + send invitation from dialog
- Per-round config: notifyOnAdvance, aiParseFiles, startupAdvanceCount, conceptAdvanceCount
- Moved notify-on-advance from competition-level to per-round setting
- AI filtering: round-tagged files with newest-first sorting, optional file content extraction
- File content extractor service (pdf-parse for PDF, utf-8 for text files)
- AI shortlist runs independently per category (STARTUP / BUSINESS_CONCEPT)
- generateAIRecommendations tRPC endpoint with per-round config integration
- AI recommendations UI: trigger button, confirmation dialog, per-category results display
- Category-aware advance dialog: select/deselect projects by category with target caps
- STAGE_ACTIVE bug fix in assignment router
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:09:52 +01:00
|
|
|
import { generateShortlist } from '../services/ai-shortlist'
|
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 {
|
|
|
|
|
openWindow,
|
|
|
|
|
closeWindow,
|
|
|
|
|
lockWindow,
|
|
|
|
|
checkDeadlinePolicy,
|
|
|
|
|
validateSubmission,
|
|
|
|
|
getVisibleWindows,
|
|
|
|
|
} from '../services/submission-manager'
|
|
|
|
|
|
|
|
|
|
const roundTypeEnum = z.enum([
|
|
|
|
|
'INTAKE',
|
|
|
|
|
'FILTERING',
|
|
|
|
|
'EVALUATION',
|
|
|
|
|
'SUBMISSION',
|
|
|
|
|
'MENTORING',
|
|
|
|
|
'LIVE_FINAL',
|
|
|
|
|
'DELIBERATION',
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
export const roundRouter = router({
|
|
|
|
|
/**
|
|
|
|
|
* Create a new round within 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-]+$/),
|
|
|
|
|
roundType: roundTypeEnum,
|
|
|
|
|
sortOrder: z.number().int().nonnegative(),
|
|
|
|
|
configJson: z.record(z.unknown()).optional(),
|
|
|
|
|
windowOpenAt: z.date().nullable().optional(),
|
|
|
|
|
windowCloseAt: z.date().nullable().optional(),
|
|
|
|
|
juryGroupId: z.string().nullable().optional(),
|
|
|
|
|
submissionWindowId: z.string().nullable().optional(),
|
|
|
|
|
purposeKey: z.string().nullable().optional(),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
// Verify competition exists
|
|
|
|
|
await ctx.prisma.competition.findUniqueOrThrow({
|
|
|
|
|
where: { id: input.competitionId },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Validate configJson against the Zod schema for this roundType
|
|
|
|
|
const config = input.configJson
|
|
|
|
|
? validateRoundConfig(input.roundType, input.configJson)
|
|
|
|
|
: defaultRoundConfig(input.roundType)
|
|
|
|
|
|
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 round = await ctx.prisma.round.create({
|
|
|
|
|
data: {
|
|
|
|
|
competitionId: input.competitionId,
|
|
|
|
|
name: input.name,
|
|
|
|
|
slug: input.slug,
|
|
|
|
|
roundType: input.roundType,
|
|
|
|
|
sortOrder: input.sortOrder,
|
|
|
|
|
configJson: config as unknown as Prisma.InputJsonValue,
|
|
|
|
|
windowOpenAt: input.windowOpenAt ?? undefined,
|
|
|
|
|
windowCloseAt: input.windowCloseAt ?? undefined,
|
|
|
|
|
juryGroupId: input.juryGroupId ?? undefined,
|
|
|
|
|
submissionWindowId: input.submissionWindowId ?? undefined,
|
|
|
|
|
purposeKey: input.purposeKey ?? 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
|
|
|
await logAudit({
|
|
|
|
|
prisma: ctx.prisma,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'CREATE',
|
|
|
|
|
entityType: 'Round',
|
|
|
|
|
entityId: round.id,
|
|
|
|
|
detailsJson: {
|
|
|
|
|
name: input.name,
|
|
|
|
|
roundType: input.roundType,
|
|
|
|
|
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 round
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get round by ID with all relations
|
|
|
|
|
*/
|
|
|
|
|
getById: protectedProcedure
|
|
|
|
|
.input(z.object({ id: z.string() }))
|
|
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
const round = await ctx.prisma.round.findUnique({
|
|
|
|
|
where: { id: input.id },
|
|
|
|
|
include: {
|
|
|
|
|
juryGroup: {
|
|
|
|
|
include: { members: true },
|
|
|
|
|
},
|
|
|
|
|
submissionWindow: {
|
|
|
|
|
include: { fileRequirements: true },
|
|
|
|
|
},
|
|
|
|
|
advancementRules: { orderBy: { sortOrder: 'asc' } },
|
|
|
|
|
visibleSubmissionWindows: {
|
|
|
|
|
include: { submissionWindow: true },
|
|
|
|
|
},
|
|
|
|
|
_count: {
|
|
|
|
|
select: { projectRoundStates: true },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (!round) {
|
|
|
|
|
throw new TRPCError({ code: 'NOT_FOUND', message: 'Round not found' })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return round
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update round settings/config
|
|
|
|
|
*/
|
|
|
|
|
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(),
|
|
|
|
|
status: z.enum(['ROUND_DRAFT', 'ROUND_ACTIVE', 'ROUND_CLOSED', 'ROUND_ARCHIVED']).optional(),
|
|
|
|
|
configJson: z.record(z.unknown()).optional(),
|
|
|
|
|
windowOpenAt: z.date().nullable().optional(),
|
|
|
|
|
windowCloseAt: z.date().nullable().optional(),
|
|
|
|
|
juryGroupId: z.string().nullable().optional(),
|
|
|
|
|
submissionWindowId: z.string().nullable().optional(),
|
|
|
|
|
purposeKey: z.string().nullable().optional(),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
const { id, configJson, ...data } = 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.round.findUniqueOrThrow({ where: { 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
|
|
|
// If configJson provided, validate it against the round type
|
|
|
|
|
let validatedConfig: Prisma.InputJsonValue | undefined
|
|
|
|
|
if (configJson) {
|
|
|
|
|
const parsed = validateRoundConfig(existing.roundType, configJson)
|
|
|
|
|
validatedConfig = parsed as unknown as Prisma.InputJsonValue
|
|
|
|
|
}
|
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
|
|
|
const round = await ctx.prisma.round.update({
|
|
|
|
|
where: { id },
|
|
|
|
|
data: {
|
|
|
|
|
...data,
|
|
|
|
|
...(validatedConfig !== undefined ? { configJson: validatedConfig } : {}),
|
|
|
|
|
},
|
|
|
|
|
})
|
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 logAudit({
|
|
|
|
|
prisma: ctx.prisma,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'UPDATE',
|
|
|
|
|
entityType: 'Round',
|
|
|
|
|
entityId: id,
|
|
|
|
|
detailsJson: {
|
|
|
|
|
changes: input,
|
|
|
|
|
previous: {
|
|
|
|
|
name: existing.name,
|
|
|
|
|
status: existing.status,
|
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
|
|
|
},
|
|
|
|
|
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 round
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Reorder rounds within a competition
|
|
|
|
|
*/
|
|
|
|
|
updateOrder: adminProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
competitionId: z.string(),
|
|
|
|
|
roundIds: z.array(z.string()),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
return ctx.prisma.$transaction(
|
|
|
|
|
input.roundIds.map((roundId, index) =>
|
|
|
|
|
ctx.prisma.round.update({
|
|
|
|
|
where: { id: roundId },
|
|
|
|
|
data: { sortOrder: index },
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Delete a round
|
|
|
|
|
*/
|
|
|
|
|
delete: 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.round.findUniqueOrThrow({ where: { id: input.id } })
|
|
|
|
|
|
|
|
|
|
await ctx.prisma.round.delete({ where: { id: input.id } })
|
|
|
|
|
|
|
|
|
|
await logAudit({
|
|
|
|
|
prisma: ctx.prisma,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'DELETE',
|
|
|
|
|
entityType: 'Round',
|
|
|
|
|
entityId: input.id,
|
|
|
|
|
detailsJson: {
|
|
|
|
|
name: existing.name,
|
|
|
|
|
roundType: existing.roundType,
|
|
|
|
|
competitionId: existing.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
|
|
|
})
|
|
|
|
|
|
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 09:20:02 +01:00
|
|
|
// =========================================================================
|
|
|
|
|
// Project Advancement (Manual Only)
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Advance PASSED projects from one round to the next.
|
|
|
|
|
* This is ALWAYS manual — no auto-advancement after AI filtering.
|
|
|
|
|
* Admin must explicitly trigger this after reviewing results.
|
|
|
|
|
*/
|
|
|
|
|
advanceProjects: adminProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
roundId: z.string(),
|
|
|
|
|
targetRoundId: z.string().optional(),
|
|
|
|
|
projectIds: z.array(z.string()).optional(),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
const { roundId, targetRoundId, projectIds } = input
|
|
|
|
|
|
|
|
|
|
// Get current round with competition context
|
|
|
|
|
const currentRound = await ctx.prisma.round.findUniqueOrThrow({
|
|
|
|
|
where: { id: roundId },
|
|
|
|
|
select: { id: true, name: true, competitionId: true, sortOrder: true },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Determine target round
|
|
|
|
|
let targetRound: { id: string; name: string }
|
|
|
|
|
if (targetRoundId) {
|
|
|
|
|
targetRound = await ctx.prisma.round.findUniqueOrThrow({
|
|
|
|
|
where: { id: targetRoundId },
|
|
|
|
|
select: { id: true, name: true },
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
// Find next round in same competition by sortOrder
|
|
|
|
|
const nextRound = await ctx.prisma.round.findFirst({
|
|
|
|
|
where: {
|
|
|
|
|
competitionId: currentRound.competitionId,
|
|
|
|
|
sortOrder: { gt: currentRound.sortOrder },
|
|
|
|
|
},
|
|
|
|
|
orderBy: { sortOrder: 'asc' },
|
|
|
|
|
select: { id: true, name: true },
|
|
|
|
|
})
|
|
|
|
|
if (!nextRound) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'BAD_REQUEST',
|
|
|
|
|
message: 'No subsequent round exists in this competition. Create the next round first.',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
targetRound = nextRound
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Determine which projects to advance
|
|
|
|
|
let idsToAdvance: string[]
|
|
|
|
|
if (projectIds && projectIds.length > 0) {
|
|
|
|
|
idsToAdvance = projectIds
|
|
|
|
|
} else {
|
|
|
|
|
// Default: all PASSED projects in current round
|
|
|
|
|
const passedStates = await ctx.prisma.projectRoundState.findMany({
|
|
|
|
|
where: { roundId, state: 'PASSED' },
|
|
|
|
|
select: { projectId: true },
|
|
|
|
|
})
|
|
|
|
|
idsToAdvance = passedStates.map((s) => s.projectId)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (idsToAdvance.length === 0) {
|
|
|
|
|
return { advancedCount: 0, targetRoundId: targetRound.id, targetRoundName: targetRound.name }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Transaction: create entries in target round + mark current as COMPLETED
|
|
|
|
|
await ctx.prisma.$transaction(async (tx) => {
|
|
|
|
|
// Create ProjectRoundState in target round
|
|
|
|
|
await tx.projectRoundState.createMany({
|
|
|
|
|
data: idsToAdvance.map((projectId) => ({
|
|
|
|
|
projectId,
|
|
|
|
|
roundId: targetRound.id,
|
|
|
|
|
})),
|
|
|
|
|
skipDuplicates: true,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Mark current round states as COMPLETED
|
|
|
|
|
await tx.projectRoundState.updateMany({
|
|
|
|
|
where: {
|
|
|
|
|
roundId,
|
|
|
|
|
projectId: { in: idsToAdvance },
|
|
|
|
|
state: 'PASSED',
|
|
|
|
|
},
|
|
|
|
|
data: { state: 'COMPLETED' },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Update project status to ASSIGNED
|
|
|
|
|
await tx.project.updateMany({
|
|
|
|
|
where: { id: { in: idsToAdvance } },
|
|
|
|
|
data: { status: 'ASSIGNED' },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Status history
|
|
|
|
|
await tx.projectStatusHistory.createMany({
|
|
|
|
|
data: idsToAdvance.map((projectId) => ({
|
|
|
|
|
projectId,
|
|
|
|
|
status: 'ASSIGNED',
|
|
|
|
|
changedBy: ctx.user?.id,
|
|
|
|
|
})),
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Audit
|
|
|
|
|
await logAudit({
|
|
|
|
|
prisma: ctx.prisma,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'ADVANCE_PROJECTS',
|
|
|
|
|
entityType: 'Round',
|
|
|
|
|
entityId: roundId,
|
|
|
|
|
detailsJson: {
|
|
|
|
|
fromRound: currentRound.name,
|
|
|
|
|
toRound: targetRound.name,
|
|
|
|
|
targetRoundId: targetRound.id,
|
|
|
|
|
projectCount: idsToAdvance.length,
|
|
|
|
|
projectIds: idsToAdvance,
|
|
|
|
|
},
|
|
|
|
|
ipAddress: ctx.ip,
|
|
|
|
|
userAgent: ctx.userAgent,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
advancedCount: idsToAdvance.length,
|
|
|
|
|
targetRoundId: targetRound.id,
|
|
|
|
|
targetRoundName: targetRound.name,
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
|
AI category-aware evaluation: per-round config, file parsing, shortlist, advance flow
- Per-juror cap mode (HARD/SOFT/NONE) in add-member dialog and members table
- Jury invite flow: create user + add to group + send invitation from dialog
- Per-round config: notifyOnAdvance, aiParseFiles, startupAdvanceCount, conceptAdvanceCount
- Moved notify-on-advance from competition-level to per-round setting
- AI filtering: round-tagged files with newest-first sorting, optional file content extraction
- File content extractor service (pdf-parse for PDF, utf-8 for text files)
- AI shortlist runs independently per category (STARTUP / BUSINESS_CONCEPT)
- generateAIRecommendations tRPC endpoint with per-round config integration
- AI recommendations UI: trigger button, confirmation dialog, per-category results display
- Category-aware advance dialog: select/deselect projects by category with target caps
- STAGE_ACTIVE bug fix in assignment router
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 10:09:52 +01:00
|
|
|
// =========================================================================
|
|
|
|
|
// AI Shortlist Recommendations
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Generate AI-powered shortlist recommendations for a round.
|
|
|
|
|
* Runs independently for STARTUP and BUSINESS_CONCEPT categories.
|
|
|
|
|
* Uses per-round config for advancement targets and file parsing.
|
|
|
|
|
*/
|
|
|
|
|
generateAIRecommendations: adminProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
roundId: z.string(),
|
|
|
|
|
rubric: z.string().optional(),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
const round = await ctx.prisma.round.findUniqueOrThrow({
|
|
|
|
|
where: { id: input.roundId },
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
name: true,
|
|
|
|
|
competitionId: true,
|
|
|
|
|
configJson: true,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const config = (round.configJson as Record<string, unknown>) ?? {}
|
|
|
|
|
const startupTopN = (config.startupAdvanceCount as number) || 10
|
|
|
|
|
const conceptTopN = (config.conceptAdvanceCount as number) || 10
|
|
|
|
|
const aiParseFiles = !!config.aiParseFiles
|
|
|
|
|
|
|
|
|
|
const result = await generateShortlist(
|
|
|
|
|
{
|
|
|
|
|
roundId: input.roundId,
|
|
|
|
|
competitionId: round.competitionId,
|
|
|
|
|
startupTopN,
|
|
|
|
|
conceptTopN,
|
|
|
|
|
rubric: input.rubric,
|
|
|
|
|
aiParseFiles,
|
|
|
|
|
},
|
|
|
|
|
ctx.prisma,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await logAudit({
|
|
|
|
|
prisma: ctx.prisma,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'AI_SHORTLIST',
|
|
|
|
|
entityType: 'Round',
|
|
|
|
|
entityId: input.roundId,
|
|
|
|
|
detailsJson: {
|
|
|
|
|
roundName: round.name,
|
|
|
|
|
startupTopN,
|
|
|
|
|
conceptTopN,
|
|
|
|
|
aiParseFiles,
|
|
|
|
|
success: result.success,
|
|
|
|
|
startupCount: result.recommendations.STARTUP.length,
|
|
|
|
|
conceptCount: result.recommendations.BUSINESS_CONCEPT.length,
|
|
|
|
|
tokensUsed: result.tokensUsed,
|
|
|
|
|
errors: result.errors,
|
|
|
|
|
},
|
|
|
|
|
ipAddress: ctx.ip,
|
|
|
|
|
userAgent: ctx.userAgent,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
}),
|
|
|
|
|
|
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
|
|
|
// =========================================================================
|
|
|
|
|
// Submission Window Management
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create a submission window for a round
|
|
|
|
|
*/
|
|
|
|
|
createSubmissionWindow: 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-]+$/),
|
|
|
|
|
roundNumber: z.number().int().min(1),
|
|
|
|
|
windowOpenAt: z.date().optional(),
|
|
|
|
|
windowCloseAt: z.date().optional(),
|
|
|
|
|
deadlinePolicy: z.enum(['HARD_DEADLINE', 'FLAG', 'GRACE']).default('HARD_DEADLINE'),
|
|
|
|
|
graceHours: z.number().int().min(0).optional(),
|
|
|
|
|
lockOnClose: z.boolean().default(true),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.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 window = await ctx.prisma.submissionWindow.create({
|
|
|
|
|
data: {
|
|
|
|
|
competitionId: input.competitionId,
|
|
|
|
|
name: input.name,
|
|
|
|
|
slug: input.slug,
|
|
|
|
|
roundNumber: input.roundNumber,
|
|
|
|
|
windowOpenAt: input.windowOpenAt,
|
|
|
|
|
windowCloseAt: input.windowCloseAt,
|
|
|
|
|
deadlinePolicy: input.deadlinePolicy,
|
|
|
|
|
graceHours: input.graceHours,
|
|
|
|
|
lockOnClose: input.lockOnClose,
|
|
|
|
|
},
|
|
|
|
|
})
|
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 logAudit({
|
|
|
|
|
prisma: ctx.prisma,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'CREATE',
|
|
|
|
|
entityType: 'SubmissionWindow',
|
|
|
|
|
entityId: window.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 window
|
|
|
|
|
}),
|
|
|
|
|
|
Rounds overhaul: full CRUD submission windows, scheduling UI, analytics, design refresh
- Fix special award FK crash: replace 4x raw auditLog.create with logAudit() helper
- Add updateSubmissionWindow + deleteSubmissionWindow mutations to round router
- Add per-round analytics (_count, juryGroup) to competition.getById
- Remove redundant acceptedCategories from intake config
- Rewrite submission window manager with full CRUD, all fields, date pickers
- Add round scheduling card (open/close dates) to round detail page
- Add project count, assignment count, jury group to round list cards
- Visual redesign: pipeline view, brand colors, progress bars, enhanced cards
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:07:09 +01:00
|
|
|
/**
|
|
|
|
|
* Update an existing submission window
|
|
|
|
|
*/
|
|
|
|
|
updateSubmissionWindow: 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(),
|
|
|
|
|
roundNumber: z.number().int().min(1).optional(),
|
|
|
|
|
windowOpenAt: z.date().nullable().optional(),
|
|
|
|
|
windowCloseAt: z.date().nullable().optional(),
|
|
|
|
|
deadlinePolicy: z.enum(['HARD_DEADLINE', 'FLAG', 'GRACE']).optional(),
|
|
|
|
|
graceHours: z.number().int().min(0).nullable().optional(),
|
|
|
|
|
lockOnClose: z.boolean().optional(),
|
|
|
|
|
sortOrder: z.number().int().optional(),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
const { id, ...data } = 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 window = await ctx.prisma.submissionWindow.update({
|
|
|
|
|
where: { id },
|
|
|
|
|
data,
|
|
|
|
|
})
|
|
|
|
|
await logAudit({
|
|
|
|
|
prisma: ctx.prisma,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'UPDATE',
|
|
|
|
|
entityType: 'SubmissionWindow',
|
|
|
|
|
entityId: id,
|
|
|
|
|
detailsJson: data,
|
|
|
|
|
ipAddress: ctx.ip,
|
|
|
|
|
userAgent: ctx.userAgent,
|
Rounds overhaul: full CRUD submission windows, scheduling UI, analytics, design refresh
- Fix special award FK crash: replace 4x raw auditLog.create with logAudit() helper
- Add updateSubmissionWindow + deleteSubmissionWindow mutations to round router
- Add per-round analytics (_count, juryGroup) to competition.getById
- Remove redundant acceptedCategories from intake config
- Rewrite submission window manager with full CRUD, all fields, date pickers
- Add round scheduling card (open/close dates) to round detail page
- Add project count, assignment count, jury group to round list cards
- Visual redesign: pipeline view, brand colors, progress bars, enhanced cards
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:07:09 +01:00
|
|
|
})
|
|
|
|
|
return window
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Delete a submission window (only if no files uploaded)
|
|
|
|
|
*/
|
|
|
|
|
deleteSubmissionWindow: adminProcedure
|
|
|
|
|
.input(z.object({ id: z.string() }))
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
// Check if window has uploaded files
|
|
|
|
|
const window = await ctx.prisma.submissionWindow.findUniqueOrThrow({
|
|
|
|
|
where: { id: input.id },
|
|
|
|
|
select: { id: true, name: true, _count: { select: { projectFiles: true } } },
|
|
|
|
|
})
|
|
|
|
|
if (window._count.projectFiles > 0) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'BAD_REQUEST',
|
|
|
|
|
message: `Cannot delete window "${window.name}" — it has ${window._count.projectFiles} uploaded files. Remove files first.`,
|
|
|
|
|
})
|
|
|
|
|
}
|
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.submissionWindow.delete({ where: { id: input.id } })
|
|
|
|
|
await logAudit({
|
|
|
|
|
prisma: ctx.prisma,
|
|
|
|
|
userId: ctx.user.id,
|
|
|
|
|
action: 'DELETE',
|
|
|
|
|
entityType: 'SubmissionWindow',
|
|
|
|
|
entityId: input.id,
|
|
|
|
|
detailsJson: { name: window.name },
|
|
|
|
|
ipAddress: ctx.ip,
|
|
|
|
|
userAgent: ctx.userAgent,
|
Rounds overhaul: full CRUD submission windows, scheduling UI, analytics, design refresh
- Fix special award FK crash: replace 4x raw auditLog.create with logAudit() helper
- Add updateSubmissionWindow + deleteSubmissionWindow mutations to round router
- Add per-round analytics (_count, juryGroup) to competition.getById
- Remove redundant acceptedCategories from intake config
- Rewrite submission window manager with full CRUD, all fields, date pickers
- Add round scheduling card (open/close dates) to round detail page
- Add project count, assignment count, jury group to round list cards
- Visual redesign: pipeline view, brand colors, progress bars, enhanced cards
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 07:07:09 +01:00
|
|
|
})
|
|
|
|
|
return { success: 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
|
|
|
/**
|
|
|
|
|
* Open a submission window
|
|
|
|
|
*/
|
|
|
|
|
openSubmissionWindow: adminProcedure
|
|
|
|
|
.input(z.object({ windowId: z.string() }))
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
const result = await openWindow(input.windowId, ctx.user.id, ctx.prisma)
|
|
|
|
|
if (!result.success) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'BAD_REQUEST',
|
|
|
|
|
message: result.errors?.join('; ') ?? 'Failed to open window',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Close a submission window
|
|
|
|
|
*/
|
|
|
|
|
closeSubmissionWindow: adminProcedure
|
|
|
|
|
.input(z.object({ windowId: z.string() }))
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
const result = await closeWindow(input.windowId, ctx.user.id, ctx.prisma)
|
|
|
|
|
if (!result.success) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'BAD_REQUEST',
|
|
|
|
|
message: result.errors?.join('; ') ?? 'Failed to close window',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Lock a submission window
|
|
|
|
|
*/
|
|
|
|
|
lockSubmissionWindow: adminProcedure
|
|
|
|
|
.input(z.object({ windowId: z.string() }))
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
const result = await lockWindow(input.windowId, ctx.user.id, ctx.prisma)
|
|
|
|
|
if (!result.success) {
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'BAD_REQUEST',
|
|
|
|
|
message: result.errors?.join('; ') ?? 'Failed to lock window',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check deadline status of a window
|
|
|
|
|
*/
|
|
|
|
|
checkDeadline: protectedProcedure
|
|
|
|
|
.input(z.object({ windowId: z.string() }))
|
|
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
return checkDeadlinePolicy(input.windowId, ctx.prisma)
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Validate files against window requirements
|
|
|
|
|
*/
|
|
|
|
|
validateSubmission: protectedProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
projectId: z.string(),
|
|
|
|
|
windowId: z.string(),
|
|
|
|
|
files: z.array(
|
|
|
|
|
z.object({
|
|
|
|
|
mimeType: z.string(),
|
|
|
|
|
size: z.number(),
|
|
|
|
|
requirementId: z.string().optional(),
|
|
|
|
|
})
|
|
|
|
|
),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
return validateSubmission(input.projectId, input.windowId, input.files, ctx.prisma)
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get visible submission windows for a round
|
|
|
|
|
*/
|
|
|
|
|
getVisibleWindows: protectedProcedure
|
|
|
|
|
.input(z.object({ roundId: z.string() }))
|
|
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
return getVisibleWindows(input.roundId, ctx.prisma)
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
// =========================================================================
|
|
|
|
|
// File Requirements Management
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Create a file requirement for a submission window
|
|
|
|
|
*/
|
|
|
|
|
createFileRequirement: adminProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
submissionWindowId: z.string(),
|
|
|
|
|
slug: z.string().min(1).max(100).regex(/^[a-z0-9-]+$/),
|
|
|
|
|
label: z.string().min(1).max(255),
|
|
|
|
|
description: z.string().max(2000).optional(),
|
|
|
|
|
mimeTypes: z.array(z.string()).default([]),
|
|
|
|
|
maxSizeMb: z.number().int().min(0).optional(),
|
|
|
|
|
required: z.boolean().default(false),
|
|
|
|
|
sortOrder: z.number().int().default(0),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
return ctx.prisma.submissionFileRequirement.create({
|
|
|
|
|
data: input,
|
|
|
|
|
})
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Update a file requirement
|
|
|
|
|
*/
|
|
|
|
|
updateFileRequirement: adminProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
id: z.string(),
|
|
|
|
|
label: z.string().min(1).max(255).optional(),
|
|
|
|
|
description: z.string().max(2000).optional().nullable(),
|
|
|
|
|
mimeTypes: z.array(z.string()).optional(),
|
|
|
|
|
maxSizeMb: z.number().min(0).optional().nullable(),
|
|
|
|
|
required: z.boolean().optional(),
|
|
|
|
|
sortOrder: z.number().int().optional(),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
const { id, ...data } = input
|
|
|
|
|
return ctx.prisma.submissionFileRequirement.update({
|
|
|
|
|
where: { id },
|
|
|
|
|
data,
|
|
|
|
|
})
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Delete a file requirement
|
|
|
|
|
*/
|
|
|
|
|
deleteFileRequirement: adminProcedure
|
|
|
|
|
.input(z.object({ id: z.string() }))
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
return ctx.prisma.submissionFileRequirement.delete({
|
|
|
|
|
where: { id: input.id },
|
|
|
|
|
})
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get submission windows for applicants in a competition
|
|
|
|
|
*/
|
|
|
|
|
getApplicantWindows: protectedProcedure
|
|
|
|
|
.input(z.object({ competitionId: z.string() }))
|
|
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
return ctx.prisma.submissionWindow.findMany({
|
|
|
|
|
where: { competitionId: input.competitionId },
|
|
|
|
|
include: {
|
|
|
|
|
fileRequirements: { orderBy: { sortOrder: 'asc' } },
|
|
|
|
|
},
|
|
|
|
|
orderBy: { sortOrder: 'asc' },
|
|
|
|
|
})
|
|
|
|
|
}),
|
|
|
|
|
})
|