Apply full refactor updates plus pipeline/email UX confirmations
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m33s

This commit is contained in:
Matt
2026-02-14 15:26:42 +01:00
parent e56e143a40
commit b5425e705e
374 changed files with 116737 additions and 111969 deletions

View File

@@ -1,203 +1,203 @@
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,
}
}),
})
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,
}
}),
})