Remove dynamic form builder and complete RoundProject→roundId migration

Major cleanup and schema migration:
- Remove unused dynamic form builder system (ApplicationForm, ApplicationFormField, etc.)
- Complete migration from RoundProject junction table to direct Project.roundId
- Add sortOrder and entryNotificationType fields to Round model
- Add country field to User model for mentor matching
- Enhance onboarding with profile photo and country selection steps
- Fix all TypeScript errors related to roundProjects references
- Remove unused libraries (@radix-ui/react-toast, embla-carousel-react, vaul)

Files removed:
- admin/forms/* pages and related components
- admin/onboarding/* pages
- applicationForm.ts and onboarding.ts routers
- Dynamic form builder Prisma models and enums

Schema changes:
- Removed ApplicationForm, ApplicationFormField, OnboardingStep, ApplicationFormSubmission, SubmissionFile models
- Removed FormFieldType and SpecialFieldType enums
- Added Round.sortOrder, Round.entryNotificationType
- Added User.country

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-04 14:15:06 +01:00
parent 7bcd2ce6ca
commit 29827268b2
71 changed files with 2139 additions and 6609 deletions

View File

@@ -29,6 +29,7 @@ export const userRouter = router({
expertiseTags: true,
metadataJson: true,
phoneNumber: true,
country: true,
notificationPreference: true,
profileImageKey: true,
createdAt: true,
@@ -415,6 +416,7 @@ export const userRouter = router({
/**
* Bulk import users (admin only)
* Optionally pre-assign projects to jury members during invitation
*/
bulkCreate: adminProcedure
.input(
@@ -425,6 +427,15 @@ export const userRouter = router({
name: z.string().optional(),
role: z.enum(['JURY_MEMBER', 'MENTOR', 'OBSERVER']).default('JURY_MEMBER'),
expertiseTags: z.array(z.string()).optional(),
// Optional pre-assignments for jury members
assignments: z
.array(
z.object({
projectId: z.string(),
roundId: z.string(),
})
)
.optional(),
})
),
})
@@ -456,10 +467,20 @@ export const userRouter = router({
return { created: 0, skipped }
}
// Build map of email -> assignments before createMany (since createMany removes extra fields)
const emailToAssignments = new Map<string, Array<{ projectId: string; roundId: string }>>()
for (const u of newUsers) {
if (u.assignments && u.assignments.length > 0) {
emailToAssignments.set(u.email.toLowerCase(), u.assignments)
}
}
const created = await ctx.prisma.user.createMany({
data: newUsers.map((u) => ({
...u,
email: u.email.toLowerCase(),
name: u.name,
role: u.role,
expertiseTags: u.expertiseTags,
status: 'INVITED',
})),
})
@@ -483,6 +504,44 @@ export const userRouter = router({
select: { id: true, email: true, name: true, role: true },
})
// Create pre-assignments for users who have them
let assignmentsCreated = 0
for (const user of createdUsers) {
const assignments = emailToAssignments.get(user.email.toLowerCase())
if (assignments && assignments.length > 0) {
for (const assignment of assignments) {
try {
await ctx.prisma.assignment.create({
data: {
userId: user.id,
projectId: assignment.projectId,
roundId: assignment.roundId,
method: 'MANUAL',
createdBy: ctx.user.id,
},
})
assignmentsCreated++
} catch {
// Skip if assignment already exists (shouldn't happen for new users)
}
}
}
}
// Audit log for assignments if any were created
if (assignmentsCreated > 0) {
await ctx.prisma.auditLog.create({
data: {
userId: ctx.user.id,
action: 'BULK_ASSIGN',
entityType: 'Assignment',
detailsJson: { count: assignmentsCreated, context: 'invitation_pre_assignment' },
ipAddress: ctx.ip,
userAgent: ctx.userAgent,
},
})
}
let emailsSent = 0
const emailErrors: string[] = []
@@ -525,7 +584,7 @@ export const userRouter = router({
}
}
return { created: created.count, skipped, emailsSent, emailErrors }
return { created: created.count, skipped, emailsSent, emailErrors, assignmentsCreated }
}),
/**
@@ -729,6 +788,7 @@ export const userRouter = router({
z.object({
name: z.string().min(1).max(255),
phoneNumber: z.string().optional(),
country: z.string().optional(),
expertiseTags: z.array(z.string()).optional(),
notificationPreference: z.enum(['EMAIL', 'WHATSAPP', 'BOTH', 'NONE']).optional(),
})
@@ -750,6 +810,7 @@ export const userRouter = router({
data: {
name: input.name,
phoneNumber: input.phoneNumber,
country: input.country,
expertiseTags: mergedTags,
notificationPreference: input.notificationPreference || 'EMAIL',
onboardingCompletedAt: new Date(),
@@ -782,8 +843,8 @@ export const userRouter = router({
select: { onboardingCompletedAt: true, role: true },
})
// Jury members and mentors need onboarding
const rolesRequiringOnboarding = ['JURY_MEMBER', 'MENTOR']
// Jury members, mentors, and admins need onboarding
const rolesRequiringOnboarding = ['JURY_MEMBER', 'MENTOR', 'PROGRAM_ADMIN', 'SUPER_ADMIN']
if (!rolesRequiringOnboarding.includes(user.role)) {
return false
}