2026-02-14 15:26:42 +01:00
|
|
|
import { z } from 'zod'
|
|
|
|
|
import { TRPCError } from '@trpc/server'
|
|
|
|
|
import { router, adminProcedure } from '../trpc'
|
|
|
|
|
import { logAudit } from '../utils/audit'
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Project Pool Router
|
|
|
|
|
*
|
|
|
|
|
* Manages the pool of unassigned projects (projects not yet assigned to any stage).
|
|
|
|
|
* Provides procedures for listing unassigned projects and bulk assigning them to stages.
|
|
|
|
|
*/
|
|
|
|
|
export const projectPoolRouter = router({
|
|
|
|
|
/**
|
|
|
|
|
* List unassigned projects with filtering and pagination
|
|
|
|
|
* Projects not assigned to any stage
|
|
|
|
|
*/
|
|
|
|
|
listUnassigned: adminProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
programId: z.string(), // Required - must specify which program
|
|
|
|
|
competitionCategory: z
|
|
|
|
|
.enum(['STARTUP', 'BUSINESS_CONCEPT'])
|
|
|
|
|
.optional(),
|
|
|
|
|
search: z.string().optional(), // Search in title, teamName, description
|
|
|
|
|
page: z.number().int().min(1).default(1),
|
|
|
|
|
perPage: z.number().int().min(1).max(200).default(20),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.query(async ({ ctx, input }) => {
|
|
|
|
|
const { programId, competitionCategory, search, page, perPage } = input
|
|
|
|
|
const skip = (page - 1) * perPage
|
|
|
|
|
|
|
|
|
|
// Build where clause
|
|
|
|
|
const where: Record<string, unknown> = {
|
|
|
|
|
programId,
|
|
|
|
|
stageStates: { none: {} }, // Only unassigned projects (not in any stage)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Filter by competition category
|
|
|
|
|
if (competitionCategory) {
|
|
|
|
|
where.competitionCategory = competitionCategory
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Search in title, teamName, description
|
|
|
|
|
if (search) {
|
|
|
|
|
where.OR = [
|
|
|
|
|
{ title: { contains: search, mode: 'insensitive' } },
|
|
|
|
|
{ teamName: { contains: search, mode: 'insensitive' } },
|
|
|
|
|
{ description: { contains: search, mode: 'insensitive' } },
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Execute queries in parallel
|
|
|
|
|
const [projects, total] = await Promise.all([
|
|
|
|
|
ctx.prisma.project.findMany({
|
|
|
|
|
where,
|
|
|
|
|
skip,
|
|
|
|
|
take: perPage,
|
|
|
|
|
orderBy: { createdAt: 'desc' },
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
title: true,
|
|
|
|
|
teamName: true,
|
|
|
|
|
description: true,
|
|
|
|
|
competitionCategory: true,
|
|
|
|
|
oceanIssue: true,
|
|
|
|
|
country: true,
|
|
|
|
|
status: true,
|
|
|
|
|
submittedAt: true,
|
|
|
|
|
createdAt: true,
|
|
|
|
|
tags: true,
|
|
|
|
|
wantsMentorship: true,
|
|
|
|
|
programId: true,
|
|
|
|
|
_count: {
|
|
|
|
|
select: {
|
|
|
|
|
files: true,
|
|
|
|
|
teamMembers: true,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
ctx.prisma.project.count({ where }),
|
|
|
|
|
])
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
projects,
|
|
|
|
|
total,
|
|
|
|
|
page,
|
|
|
|
|
perPage,
|
|
|
|
|
totalPages: Math.ceil(total / perPage),
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Bulk assign projects to a stage
|
|
|
|
|
*
|
|
|
|
|
* Validates that:
|
|
|
|
|
* - All projects exist
|
|
|
|
|
* - Stage exists
|
|
|
|
|
*
|
|
|
|
|
* Creates:
|
|
|
|
|
* - ProjectStageState entries for each project
|
|
|
|
|
* - Project.status updated to 'ASSIGNED'
|
|
|
|
|
* - ProjectStatusHistory records for each project
|
|
|
|
|
* - Audit log
|
|
|
|
|
*/
|
|
|
|
|
assignToStage: adminProcedure
|
|
|
|
|
.input(
|
|
|
|
|
z.object({
|
|
|
|
|
projectIds: z.array(z.string()).min(1).max(200), // Max 200 projects at once
|
|
|
|
|
stageId: z.string(),
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
.mutation(async ({ ctx, input }) => {
|
|
|
|
|
const { projectIds, stageId } = input
|
|
|
|
|
|
|
|
|
|
// Step 1: Fetch all projects to validate
|
|
|
|
|
const projects = await ctx.prisma.project.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
id: { in: projectIds },
|
|
|
|
|
},
|
|
|
|
|
select: {
|
|
|
|
|
id: true,
|
|
|
|
|
title: true,
|
|
|
|
|
programId: true,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Validate all projects were found
|
|
|
|
|
if (projects.length !== projectIds.length) {
|
|
|
|
|
const foundIds = new Set(projects.map((p) => p.id))
|
|
|
|
|
const missingIds = projectIds.filter((id) => !foundIds.has(id))
|
|
|
|
|
throw new TRPCError({
|
|
|
|
|
code: 'BAD_REQUEST',
|
|
|
|
|
message: `Some projects were not found: ${missingIds.join(', ')}`,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verify stage exists and get its trackId
|
|
|
|
|
const stage = await ctx.prisma.stage.findUniqueOrThrow({
|
|
|
|
|
where: { id: stageId },
|
|
|
|
|
select: { id: true, trackId: true },
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Step 2: Perform bulk assignment in a transaction
|
|
|
|
|
const result = await ctx.prisma.$transaction(async (tx) => {
|
|
|
|
|
// Create ProjectStageState entries for each project (skip existing)
|
|
|
|
|
const stageStateData = projectIds.map((projectId) => ({
|
|
|
|
|
projectId,
|
|
|
|
|
stageId,
|
|
|
|
|
trackId: stage.trackId,
|
|
|
|
|
state: 'PENDING' as const,
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
await tx.projectStageState.createMany({
|
|
|
|
|
data: stageStateData,
|
|
|
|
|
skipDuplicates: true,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Update project statuses
|
|
|
|
|
const updatedProjects = await tx.project.updateMany({
|
|
|
|
|
where: {
|
|
|
|
|
id: { in: projectIds },
|
|
|
|
|
},
|
|
|
|
|
data: {
|
|
|
|
|
status: 'ASSIGNED',
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Create status history records for each project
|
|
|
|
|
await tx.projectStatusHistory.createMany({
|
|
|
|
|
data: projectIds.map((projectId) => ({
|
|
|
|
|
projectId,
|
|
|
|
|
status: 'ASSIGNED',
|
|
|
|
|
changedBy: ctx.user?.id,
|
|
|
|
|
})),
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Create audit log
|
|
|
|
|
await logAudit({
|
|
|
|
|
prisma: tx,
|
|
|
|
|
userId: ctx.user?.id,
|
|
|
|
|
action: 'BULK_ASSIGN_TO_STAGE',
|
|
|
|
|
entityType: 'Project',
|
|
|
|
|
detailsJson: {
|
|
|
|
|
stageId,
|
|
|
|
|
projectCount: projectIds.length,
|
|
|
|
|
projectIds,
|
|
|
|
|
},
|
|
|
|
|
ipAddress: ctx.ip,
|
|
|
|
|
userAgent: ctx.userAgent,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return updatedProjects
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
assignedCount: result.count,
|
|
|
|
|
stageId,
|
|
|
|
|
}
|
|
|
|
|
}),
|
|
|
|
|
})
|