Platform polish: bulk invite, file requirements, filtering redesign, UX fixes

- F1: Set seed jury/mentors/observers to NONE status (not invited), remove passwords
- F2: Add bulk invite UI with checkbox selection and floating toolbar
- F3: Add getProjectRequirements backend query + requirement slots on project detail
- F4: Redesign filtering section: AI criteria textarea, "What AI sees" card,
  field-aware eligibility rules with human-readable previews
- F5: Auto-redirect to pipeline detail when only one pipeline exists
- F6: Make project names clickable in pipeline intake panel
- F7: Fix pipeline creation error: edition context fallback + .min(1) validation
- Pipeline wizard sections: add isActive locking, info tooltips, UX improvements

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 23:45:21 +01:00
parent 451b483880
commit 70cfad7d46
28 changed files with 1312 additions and 200 deletions

View File

@@ -696,6 +696,132 @@ export const fileRouter = router({
return results
}),
/**
* Get file requirements for a project from its pipeline's intake stage.
* Returns both configJson-based requirements and actual FileRequirement records,
* along with which ones are already fulfilled by uploaded files.
*/
getProjectRequirements: adminProcedure
.input(z.object({ projectId: z.string() }))
.query(async ({ ctx, input }) => {
// 1. Get the project and its program
const project = await ctx.prisma.project.findUniqueOrThrow({
where: { id: input.projectId },
select: { programId: true },
})
// 2. Find the pipeline for this program
const pipeline = await ctx.prisma.pipeline.findFirst({
where: { programId: project.programId },
include: {
tracks: {
where: { kind: 'MAIN' },
include: {
stages: {
where: { stageType: 'INTAKE' },
take: 1,
},
},
},
},
})
if (!pipeline) return null
const mainTrack = pipeline.tracks[0]
if (!mainTrack) return null
const intakeStage = mainTrack.stages[0]
if (!intakeStage) return null
// 3. Check for actual FileRequirement records first
const dbRequirements = await ctx.prisma.fileRequirement.findMany({
where: { stageId: intakeStage.id },
orderBy: { sortOrder: 'asc' },
include: {
files: {
where: { projectId: input.projectId },
select: {
id: true,
fileName: true,
fileType: true,
mimeType: true,
size: true,
createdAt: true,
},
},
},
})
// 4. If we have DB requirements, return those (they're the canonical source)
if (dbRequirements.length > 0) {
return {
stageId: intakeStage.id,
requirements: dbRequirements.map((req) => ({
id: req.id,
name: req.name,
description: req.description,
acceptedMimeTypes: req.acceptedMimeTypes,
maxSizeMB: req.maxSizeMB,
isRequired: req.isRequired,
fulfilled: req.files.length > 0,
fulfilledFile: req.files[0] ?? null,
})),
}
}
// 5. Fall back to configJson requirements
const configJson = intakeStage.configJson as Record<string, unknown> | null
const fileRequirements = (configJson?.fileRequirements as Array<{
name: string
description?: string
acceptedMimeTypes?: string[]
maxSizeMB?: number
isRequired?: boolean
type?: string
required?: boolean
}>) ?? []
if (fileRequirements.length === 0) return null
// 6. Get project files to check fulfillment
const projectFiles = await ctx.prisma.projectFile.findMany({
where: { projectId: input.projectId },
select: {
id: true,
fileName: true,
fileType: true,
mimeType: true,
size: true,
createdAt: true,
},
})
return {
stageId: intakeStage.id,
requirements: fileRequirements.map((req) => {
const reqName = req.name.toLowerCase()
// Match by checking if any uploaded file's fileName contains the requirement name
const matchingFile = projectFiles.find((f) =>
f.fileName.toLowerCase().includes(reqName) ||
reqName.includes(f.fileName.toLowerCase().replace(/\.[^.]+$/, ''))
)
return {
id: null as string | null,
name: req.name,
description: req.description ?? null,
acceptedMimeTypes: req.acceptedMimeTypes ?? [],
maxSizeMB: req.maxSizeMB ?? null,
// Handle both formats: isRequired (wizard type) and required (seed data)
isRequired: req.isRequired ?? req.required ?? false,
fulfilled: !!matchingFile,
fulfilledFile: matchingFile ?? null,
}
}),
}
}),
// =========================================================================
// FILE REQUIREMENTS
// =========================================================================