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