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:
@@ -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
|
||||
// =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user