From 29827268b2808f977e775558a761f6e343744024 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 4 Feb 2026 14:15:06 +0100 Subject: [PATCH] =?UTF-8?q?Remove=20dynamic=20form=20builder=20and=20compl?= =?UTF-8?q?ete=20RoundProject=E2=86=92roundId=20migration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- package-lock.json | 78 -- package.json | 3 - prisma/check-data.ts | 14 +- prisma/cleanup-all-dummy.ts | 16 +- prisma/cleanup-dummy.ts | 12 +- prisma/schema.prisma | 234 +--- prisma/seed-candidatures.ts | 15 +- prisma/seed-jury-demo.ts | 9 +- prisma/seed-mopc-onboarding.mjs | 270 ----- prisma/seed-mopc-onboarding.ts | 456 ------- .../(admin)/admin/forms/[id]/form-editor.tsx | 241 ---- src/app/(admin)/admin/forms/[id]/page.tsx | 83 -- .../[id]/submissions/[submissionId]/page.tsx | 130 -- .../admin/forms/[id]/submissions/page.tsx | 135 --- src/app/(admin)/admin/forms/new/page.tsx | 131 -- src/app/(admin)/admin/forms/page.tsx | 152 --- src/app/(admin)/admin/members/[id]/page.tsx | 2 +- src/app/(admin)/admin/members/invite/page.tsx | 179 ++- .../(admin)/admin/onboarding/[id]/page.tsx | 686 ----------- src/app/(admin)/admin/onboarding/new/page.tsx | 171 --- src/app/(admin)/admin/onboarding/page.tsx | 153 --- src/app/(admin)/admin/page.tsx | 24 +- src/app/(admin)/admin/programs/[id]/page.tsx | 2 +- .../(admin)/admin/projects/[id]/edit/page.tsx | 6 +- .../admin/projects/[id]/mentor/page.tsx | 2 +- src/app/(admin)/admin/projects/[id]/page.tsx | 25 +- src/app/(admin)/admin/projects/new/page.tsx | 1 - src/app/(admin)/admin/projects/page.tsx | 20 +- .../admin/projects/project-filters.tsx | 2 +- .../(admin)/admin/rounds/[id]/edit/page.tsx | 37 +- .../admin/rounds/[id]/live-voting/page.tsx | 4 +- src/app/(admin)/admin/rounds/[id]/page.tsx | 2 +- src/app/(admin)/admin/rounds/page.tsx | 8 +- src/app/(auth)/onboarding/page.tsx | 114 +- src/app/(mentor)/mentor/page.tsx | 12 +- .../(mentor)/mentor/projects/[id]/page.tsx | 12 +- src/app/(mentor)/mentor/projects/page.tsx | 12 +- src/app/(observer)/observer/page.tsx | 4 +- src/app/(observer)/observer/reports/page.tsx | 8 +- src/app/(public)/apply-wizard/[slug]/page.tsx | 423 ------- src/app/(public)/apply/[slug]/page.tsx | 723 ++++++----- src/app/(public)/apply/[slug]/wizard/page.tsx | 676 ----------- src/app/(public)/apply/page.tsx | 160 --- .../[id]/submission-detail-client.tsx | 2 +- .../my-submission/my-submission-client.tsx | 7 +- .../admin/advance-projects-dialog.tsx | 2 +- .../admin/remove-projects-dialog.tsx | 2 +- src/components/layouts/admin-sidebar.tsx | 6 - src/server/routers/_app.ts | 4 - src/server/routers/analytics.ts | 33 +- src/server/routers/applicant.ts | 50 +- src/server/routers/application.ts | 15 +- src/server/routers/applicationForm.ts | 1068 ----------------- src/server/routers/assignment.ts | 54 +- src/server/routers/export.ts | 19 +- src/server/routers/filtering.ts | 67 +- src/server/routers/learningResource.ts | 36 +- src/server/routers/live-voting.ts | 16 +- src/server/routers/mentor.ts | 31 +- src/server/routers/notion-import.ts | 14 +- src/server/routers/onboarding.ts | 433 ------- src/server/routers/program.ts | 10 +- src/server/routers/project.ts | 142 +-- src/server/routers/round.ts | 99 +- src/server/routers/specialAward.ts | 27 +- src/server/routers/tag.ts | 158 +++ src/server/routers/typeform-import.ts | 14 +- src/server/routers/user.ts | 69 +- src/server/services/ai-tagging.ts | 541 +++++++++ src/server/services/smart-assignment.ts | 381 ++++++ src/server/utils/ai-usage.ts | 1 + 71 files changed, 2139 insertions(+), 6609 deletions(-) delete mode 100644 prisma/seed-mopc-onboarding.mjs delete mode 100644 prisma/seed-mopc-onboarding.ts delete mode 100644 src/app/(admin)/admin/forms/[id]/form-editor.tsx delete mode 100644 src/app/(admin)/admin/forms/[id]/page.tsx delete mode 100644 src/app/(admin)/admin/forms/[id]/submissions/[submissionId]/page.tsx delete mode 100644 src/app/(admin)/admin/forms/[id]/submissions/page.tsx delete mode 100644 src/app/(admin)/admin/forms/new/page.tsx delete mode 100644 src/app/(admin)/admin/forms/page.tsx delete mode 100644 src/app/(admin)/admin/onboarding/[id]/page.tsx delete mode 100644 src/app/(admin)/admin/onboarding/new/page.tsx delete mode 100644 src/app/(admin)/admin/onboarding/page.tsx delete mode 100644 src/app/(public)/apply-wizard/[slug]/page.tsx delete mode 100644 src/app/(public)/apply/[slug]/wizard/page.tsx delete mode 100644 src/app/(public)/apply/page.tsx delete mode 100644 src/server/routers/applicationForm.ts delete mode 100644 src/server/routers/onboarding.ts create mode 100644 src/server/services/ai-tagging.ts create mode 100644 src/server/services/smart-assignment.ts diff --git a/package-lock.json b/package-lock.json index 67eecb2..89a9cee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,7 +37,6 @@ "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2", - "@radix-ui/react-toast": "^1.2.4", "@radix-ui/react-tooltip": "^1.1.6", "@tailwindcss/postcss": "^4.1.18", "@tanstack/react-query": "^5.62.0", @@ -50,7 +49,6 @@ "clsx": "^2.1.1", "cmdk": "^1.0.4", "date-fns": "^4.1.0", - "embla-carousel-react": "^8.5.1", "leaflet": "^1.9.4", "lucide-react": "^0.563.0", "minio": "^8.0.2", @@ -73,7 +71,6 @@ "tailwind-merge": "^3.4.0", "twilio": "^5.4.0", "use-debounce": "^10.0.4", - "vaul": "^1.1.2", "zod": "^3.24.1" }, "devDependencies": { @@ -3072,40 +3069,6 @@ } } }, - "node_modules/@radix-ui/react-toast": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.15.tgz", - "integrity": "sha512-3OSz3TacUWy4WtOXV38DggwxoqJK4+eDkNMl5Z/MJZaoUPaP4/9lf81xXMe1I2ReTAptverZUpbPY4wWwWyL5g==", - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-tooltip": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", @@ -6586,34 +6549,6 @@ "dev": true, "license": "ISC" }, - "node_modules/embla-carousel": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz", - "integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==", - "license": "MIT" - }, - "node_modules/embla-carousel-react": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/embla-carousel-react/-/embla-carousel-react-8.6.0.tgz", - "integrity": "sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==", - "license": "MIT", - "dependencies": { - "embla-carousel": "8.6.0", - "embla-carousel-reactive-utils": "8.6.0" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - } - }, - "node_modules/embla-carousel-reactive-utils": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/embla-carousel-reactive-utils/-/embla-carousel-reactive-utils-8.6.0.tgz", - "integrity": "sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==", - "license": "MIT", - "peerDependencies": { - "embla-carousel": "8.6.0" - } - }, "node_modules/emoji-mart": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/emoji-mart/-/emoji-mart-5.6.0.tgz", @@ -13601,19 +13536,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/vaul": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz", - "integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==", - "license": "MIT", - "dependencies": { - "@radix-ui/react-dialog": "^1.1.1" - }, - "peerDependencies": { - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" - } - }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", diff --git a/package.json b/package.json index d7c7674..507a2c4 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,6 @@ "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2", - "@radix-ui/react-toast": "^1.2.4", "@radix-ui/react-tooltip": "^1.1.6", "@tailwindcss/postcss": "^4.1.18", "@tanstack/react-query": "^5.62.0", @@ -63,7 +62,6 @@ "clsx": "^2.1.1", "cmdk": "^1.0.4", "date-fns": "^4.1.0", - "embla-carousel-react": "^8.5.1", "leaflet": "^1.9.4", "lucide-react": "^0.563.0", "minio": "^8.0.2", @@ -86,7 +84,6 @@ "tailwind-merge": "^3.4.0", "twilio": "^5.4.0", "use-debounce": "^10.0.4", - "vaul": "^1.1.2", "zod": "^3.24.1" }, "devDependencies": { diff --git a/prisma/check-data.ts b/prisma/check-data.ts index 2e2984d..f60529e 100644 --- a/prisma/check-data.ts +++ b/prisma/check-data.ts @@ -3,28 +3,28 @@ import { PrismaClient } from '@prisma/client' const prisma = new PrismaClient() async function check() { - const projects = await prisma.project.count() - console.log('Total projects:', projects) + const projectCount = await prisma.project.count() + console.log('Total projects:', projectCount) const rounds = await prisma.round.findMany({ include: { - _count: { select: { roundProjects: true } } + _count: { select: { projects: true } } } }) for (const r of rounds) { console.log(`Round: ${r.name} (id: ${r.id})`) - console.log(` Projects: ${r._count.roundProjects}`) + console.log(` Projects: ${r._count.projects}`) } - // Check if projects have programId set + // Check sample projects with their round const sampleProjects = await prisma.project.findMany({ - select: { id: true, title: true, programId: true }, + select: { id: true, title: true, roundId: true }, take: 5 }) console.log('\nSample projects:') for (const p of sampleProjects) { - console.log(` ${p.title}: programId=${p.programId}`) + console.log(` ${p.title}: roundId=${p.roundId}`) } } diff --git a/prisma/cleanup-all-dummy.ts b/prisma/cleanup-all-dummy.ts index 8763564..7689c05 100644 --- a/prisma/cleanup-all-dummy.ts +++ b/prisma/cleanup-all-dummy.ts @@ -10,18 +10,18 @@ async function cleanup() { id: true, name: true, slug: true, - roundProjects: { select: { id: true, projectId: true, project: { select: { id: true, title: true } } } }, - _count: { select: { roundProjects: true } } + projects: { select: { id: true, title: true } }, + _count: { select: { projects: true } } } }) console.log(`Found ${rounds.length} rounds:`) for (const round of rounds) { - console.log(`- ${round.name} (slug: ${round.slug}): ${round._count.roundProjects} projects`) + console.log(`- ${round.name} (slug: ${round.slug}): ${round._count.projects} projects`) } // Find rounds with 9 or fewer projects (dummy data) - const dummyRounds = rounds.filter(r => r._count.roundProjects <= 9) + const dummyRounds = rounds.filter(r => r._count.projects <= 9) if (dummyRounds.length > 0) { console.log(`\nDeleting ${dummyRounds.length} dummy round(s)...`) @@ -29,15 +29,9 @@ async function cleanup() { for (const round of dummyRounds) { console.log(`\nProcessing: ${round.name}`) - const projectIds = round.roundProjects.map(rp => rp.projectId) + const projectIds = round.projects.map(p => p.id) if (projectIds.length > 0) { - // Delete round-project associations first - const rpDeleted = await prisma.roundProject.deleteMany({ - where: { roundId: round.id } - }) - console.log(` Deleted ${rpDeleted.count} round-project associations`) - // Delete team members const teamDeleted = await prisma.teamMember.deleteMany({ where: { projectId: { in: projectIds } } diff --git a/prisma/cleanup-dummy.ts b/prisma/cleanup-dummy.ts index e6d049b..34dd699 100644 --- a/prisma/cleanup-dummy.ts +++ b/prisma/cleanup-dummy.ts @@ -8,15 +8,15 @@ async function cleanup() { // Find and delete the dummy round const dummyRound = await prisma.round.findFirst({ where: { slug: 'round-1-2026' }, - include: { roundProjects: { include: { project: true } } } + include: { projects: true } }) if (dummyRound) { console.log(`Found dummy round: ${dummyRound.name}`) - console.log(`Projects in round: ${dummyRound.roundProjects.length}`) + console.log(`Projects in round: ${dummyRound.projects.length}`) // Get project IDs to delete - const projectIds = dummyRound.roundProjects.map(rp => rp.projectId) + const projectIds = dummyRound.projects.map(p => p.id) // Delete team members for these projects if (projectIds.length > 0) { @@ -25,12 +25,6 @@ async function cleanup() { }) console.log(`Deleted ${teamDeleted.count} team members`) - // Delete round-project associations - await prisma.roundProject.deleteMany({ - where: { roundId: dummyRound.id } - }) - console.log(`Deleted round-project associations`) - // Delete the projects const projDeleted = await prisma.project.deleteMany({ where: { id: { in: projectIds } } diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3b6c152..edf8229 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -147,35 +147,6 @@ enum PartnerType { OTHER } -enum FormFieldType { - TEXT - TEXTAREA - NUMBER - EMAIL - PHONE - URL - DATE - DATETIME - SELECT - MULTI_SELECT - RADIO - CHECKBOX - CHECKBOX_GROUP - FILE - FILE_MULTIPLE - SECTION - INSTRUCTIONS -} - -enum SpecialFieldType { - TEAM_MEMBERS // Team member repeater - COMPETITION_CATEGORY // Business Concept vs Startup - OCEAN_ISSUE // Ocean issue dropdown - FILE_UPLOAD // File upload - GDPR_CONSENT // GDPR consent checkbox - COUNTRY_SELECT // Country dropdown -} - // ============================================================================= // APPLICANT SYSTEM ENUMS // ============================================================================= @@ -225,6 +196,7 @@ model User { status UserStatus @default(INVITED) expertiseTags String[] @default([]) maxAssignments Int? // Per-round limit + country String? // User's home country (for mentor matching) metadataJson Json? @db.JsonB // Profile image @@ -348,10 +320,8 @@ model Program { // Relations rounds Round[] - projects Project[] learningResources LearningResource[] partners Partner[] - applicationForms ApplicationForm[] specialAwards SpecialAward[] @@unique([name, year]) @@ -365,7 +335,10 @@ model Round { slug String? @unique // URL-friendly identifier for public submissions status RoundStatus @default(DRAFT) roundType RoundType @default(EVALUATION) - sortOrder Int @default(0) // Progression order within program + sortOrder Int @default(0) // Display order within program + + // Entry notification settings + entryNotificationType String? // Type of notification to send when project enters round // Submission window (for applicant portal) submissionDeadline DateTime? // Deadline for project submissions @@ -385,15 +358,12 @@ model Round { requiredReviews Int @default(3) // Min evaluations per project settingsJson Json? @db.JsonB // Grace periods, visibility rules, etc. - // Notification sent to project team when they enter this round - entryNotificationType String? // e.g., "ADVANCED_SEMIFINAL", "ADVANCED_FINAL" - createdAt DateTime @default(now()) updatedAt DateTime @updatedAt // Relations program Program @relation(fields: [programId], references: [id], onDelete: Cascade) - roundProjects RoundProject[] + projects Project[] assignments Assignment[] evaluationForms EvaluationForm[] gracePeriods GracePeriod[] @@ -401,7 +371,6 @@ model Round { filteringRules FilteringRule[] filteringResults FilteringResult[] filteringJobs FilteringJob[] - applicationForm ApplicationForm? @@index([programId]) @@index([status]) @@ -439,7 +408,8 @@ model EvaluationForm { model Project { id String @id @default(cuid()) - programId String + roundId String + status ProjectStatus @default(SUBMITTED) // Core fields title String @@ -493,8 +463,7 @@ model Project { updatedAt DateTime @updatedAt // Relations - program Program @relation(fields: [programId], references: [id], onDelete: Cascade) - roundProjects RoundProject[] + round Round @relation(fields: [roundId], references: [id], onDelete: Cascade) files ProjectFile[] assignments Assignment[] submittedBy User? @relation("ProjectSubmittedBy", fields: [submittedByUserId], references: [id], onDelete: SetNull) @@ -504,8 +473,10 @@ model Project { awardEligibilities AwardEligibility[] awardVotes AwardVote[] wonAwards SpecialAward[] @relation("AwardWinner") + projectTags ProjectTag[] - @@index([programId]) + @@index([roundId]) + @@index([status]) @@index([tags]) @@index([submissionSource]) @@index([submittedByUserId]) @@ -514,23 +485,6 @@ model Project { @@index([country]) } -model RoundProject { - id String @id @default(cuid()) - roundId String - projectId String - status ProjectStatus @default(SUBMITTED) - addedAt DateTime @default(now()) - - // Relations - round Round @relation(fields: [roundId], references: [id], onDelete: Cascade) - project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) - - @@unique([roundId, projectId]) - @@index([roundId]) - @@index([projectId]) - @@index([status]) -} - model ProjectFile { id String @id @default(cuid()) projectId String @@ -906,149 +860,6 @@ model Partner { @@index([sortOrder]) } -// ============================================================================= -// APPLICATION FORMS (Phase 2) -// ============================================================================= - -model ApplicationForm { - id String @id @default(cuid()) - programId String? // null = global form - name String - description String? @db.Text - status String @default("DRAFT") // DRAFT, PUBLISHED, CLOSED - - isPublic Boolean @default(false) - publicSlug String? @unique // /apply/ocean-challenge-2026 - submissionLimit Int? - opensAt DateTime? - closesAt DateTime? - - confirmationMessage String? @db.Text - - // Round linking (for onboarding forms that create projects) - roundId String? @unique - - // Email settings - sendConfirmationEmail Boolean @default(true) - sendTeamInviteEmails Boolean @default(true) - confirmationEmailSubject String? - confirmationEmailBody String? @db.Text - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - // Relations - program Program? @relation(fields: [programId], references: [id], onDelete: SetNull) - round Round? @relation(fields: [roundId], references: [id], onDelete: SetNull) - fields ApplicationFormField[] - steps OnboardingStep[] - submissions ApplicationFormSubmission[] - - @@index([programId]) - @@index([status]) - @@index([isPublic]) - @@index([roundId]) -} - -model ApplicationFormField { - id String @id @default(cuid()) - formId String - stepId String? // Which step this field belongs to (for onboarding) - fieldType FormFieldType - name String // Internal name (e.g., "project_title") - label String // Display label (e.g., "Project Title") - description String? @db.Text - placeholder String? - - required Boolean @default(false) - minLength Int? - maxLength Int? - minValue Float? // For NUMBER type - maxValue Float? // For NUMBER type - - optionsJson Json? @db.JsonB // For select/radio: [{ value, label }] - conditionJson Json? @db.JsonB // Conditional logic: { fieldId, operator, value } - - // Onboarding-specific fields - projectMapping String? // Maps to Project column: "title", "description", etc. - specialType SpecialFieldType? // Special handling for complex fields - - sortOrder Int @default(0) - width String @default("full") // full, half - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - // Relations - form ApplicationForm @relation(fields: [formId], references: [id], onDelete: Cascade) - step OnboardingStep? @relation(fields: [stepId], references: [id], onDelete: SetNull) - - @@index([formId]) - @@index([stepId]) - @@index([sortOrder]) -} - -model OnboardingStep { - id String @id @default(cuid()) - formId String - name String // Internal identifier (e.g., "category", "contact") - title String // Display title (e.g., "Category", "Contact Information") - description String? @db.Text - sortOrder Int @default(0) - isOptional Boolean @default(false) - conditionJson Json? @db.JsonB // Conditional visibility: { fieldId, operator, value } - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - // Relations - form ApplicationForm @relation(fields: [formId], references: [id], onDelete: Cascade) - fields ApplicationFormField[] - - @@index([formId]) - @@index([sortOrder]) -} - -model ApplicationFormSubmission { - id String @id @default(cuid()) - formId String - email String? - name String? - dataJson Json @db.JsonB // Field values: { fieldName: value, ... } - status String @default("SUBMITTED") // SUBMITTED, REVIEWED, APPROVED, REJECTED - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - // Relations - form ApplicationForm @relation(fields: [formId], references: [id], onDelete: Cascade) - files SubmissionFile[] - - @@index([formId]) - @@index([status]) - @@index([email]) - @@index([createdAt]) -} - -model SubmissionFile { - id String @id @default(cuid()) - submissionId String - fieldName String - fileName String - mimeType String? - size Int? - bucket String - objectKey String - - createdAt DateTime @default(now()) - - // Relations - submission ApplicationFormSubmission @relation(fields: [submissionId], references: [id], onDelete: Cascade) - - @@index([submissionId]) - @@unique([bucket, objectKey]) -} - // ============================================================================= // EXPERTISE TAGS (Phase 2B) // ============================================================================= @@ -1065,11 +876,32 @@ model ExpertiseTag { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt + // Relations + projectTags ProjectTag[] + @@index([category]) @@index([isActive]) @@index([sortOrder]) } +// Project-Tag relationship for AI tagging +model ProjectTag { + id String @id @default(cuid()) + projectId String + tagId String + confidence Float @default(1.0) // AI confidence score 0-1 + source String @default("AI") // "AI" or "MANUAL" + createdAt DateTime @default(now()) + + // Relations + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + tag ExpertiseTag @relation(fields: [tagId], references: [id], onDelete: Cascade) + + @@unique([projectId, tagId]) + @@index([projectId]) + @@index([tagId]) +} + // ============================================================================= // LIVE VOTING (Phase 2B) // ============================================================================= diff --git a/prisma/seed-candidatures.ts b/prisma/seed-candidatures.ts index 2397481..9c4662a 100644 --- a/prisma/seed-candidatures.ts +++ b/prisma/seed-candidatures.ts @@ -321,7 +321,7 @@ async function main() { // Check if project already exists const existingProject = await prisma.project.findFirst({ where: { - programId: program.id, + roundId: round.id, OR: [ { title: projectName }, { submittedByEmail: email }, @@ -365,7 +365,7 @@ async function main() { // Create project const project = await prisma.project.create({ data: { - programId: program.id, + roundId: round.id, title: projectName, description: row['Comment ']?.trim() || null, competitionCategory: mapCategory(row['Category']), @@ -391,15 +391,6 @@ async function main() { }, }) - // Create round-project association - await prisma.roundProject.create({ - data: { - roundId: round.id, - projectId: project.id, - status: 'SUBMITTED', - }, - }) - // Create team lead membership await prisma.teamMember.create({ data: { @@ -474,7 +465,7 @@ async function main() { console.log('\nBackfilling missing country codes...\n') let backfilled = 0 const nullCountryProjects = await prisma.project.findMany({ - where: { programId: program.id, country: null }, + where: { roundId: round.id, country: null }, select: { id: true, submittedByEmail: true, title: true }, }) diff --git a/prisma/seed-jury-demo.ts b/prisma/seed-jury-demo.ts index b3fe7fc..4a297db 100644 --- a/prisma/seed-jury-demo.ts +++ b/prisma/seed-jury-demo.ts @@ -64,14 +64,13 @@ async function main() { console.log(`Voting window: ${votingStart.toISOString()} → ${votingEnd.toISOString()}\n`) - // Get some projects to assign (via RoundProject) - const roundProjects = await prisma.roundProject.findMany({ + // Get some projects to assign + const projects = await prisma.project.findMany({ where: { roundId: round.id }, take: 8, - orderBy: { addedAt: 'desc' }, - select: { project: { select: { id: true, title: true } } }, + orderBy: { createdAt: 'desc' }, + select: { id: true, title: true }, }) - const projects = roundProjects.map(rp => rp.project) if (projects.length === 0) { console.error('No projects found! Run seed-candidatures first.') diff --git a/prisma/seed-mopc-onboarding.mjs b/prisma/seed-mopc-onboarding.mjs deleted file mode 100644 index 4d79cc3..0000000 --- a/prisma/seed-mopc-onboarding.mjs +++ /dev/null @@ -1,270 +0,0 @@ -/** - * Seed script for MOPC Onboarding Form (ESM version for production) - * Run with: node prisma/seed-mopc-onboarding.mjs - */ - -import { PrismaClient } from '@prisma/client' - -const prisma = new PrismaClient() - -const MOPC_FORM_CONFIG = { - name: 'MOPC Application 2026', - description: 'Monaco Ocean Protection Challenge application form', - publicSlug: 'mopc-2026', - status: 'PUBLISHED', - isPublic: true, - sendConfirmationEmail: true, - sendTeamInviteEmails: true, - confirmationEmailSubject: 'Application Received - Monaco Ocean Protection Challenge', - confirmationEmailBody: `Thank you for applying to the Monaco Ocean Protection Challenge 2026! - -We have received your application and our team will review it carefully. - -If you have any questions, please don't hesitate to reach out. - -Good luck! -The MOPC Team`, - confirmationMessage: 'Thank you for your application! We have sent a confirmation email to the address you provided. Our team will review your submission and get back to you soon.', -} - -const STEPS = [ - { - name: 'category', - title: 'Competition Category', - description: 'Select your competition track', - sortOrder: 0, - isOptional: false, - fields: [ - { - name: 'competitionCategory', - label: 'Which category best describes your project?', - fieldType: 'RADIO', - specialType: 'COMPETITION_CATEGORY', - required: true, - sortOrder: 0, - width: 'full', - projectMapping: 'competitionCategory', - description: 'Choose the category that best fits your stage of development', - optionsJson: [ - { value: 'STARTUP', label: 'Startup', description: 'You have an existing company or registered business entity' }, - { value: 'BUSINESS_CONCEPT', label: 'Business Concept', description: 'You are a student, graduate, or have an idea not yet incorporated' }, - ], - }, - ], - }, - { - name: 'contact', - title: 'Contact Information', - description: 'Tell us how to reach you', - sortOrder: 1, - isOptional: false, - fields: [ - { name: 'contactName', label: 'Full Name', fieldType: 'TEXT', required: true, sortOrder: 0, width: 'half', placeholder: 'Enter your full name' }, - { name: 'contactEmail', label: 'Email Address', fieldType: 'EMAIL', required: true, sortOrder: 1, width: 'half', placeholder: 'your.email@example.com', description: 'We will use this email for all communications' }, - { name: 'contactPhone', label: 'Phone Number', fieldType: 'PHONE', required: true, sortOrder: 2, width: 'half', placeholder: '+1 (555) 123-4567' }, - { name: 'country', label: 'Country', fieldType: 'SELECT', specialType: 'COUNTRY_SELECT', required: true, sortOrder: 3, width: 'half', projectMapping: 'country' }, - { name: 'city', label: 'City', fieldType: 'TEXT', required: false, sortOrder: 4, width: 'half', placeholder: 'City name' }, - ], - }, - { - name: 'project', - title: 'Project Details', - description: 'Tell us about your ocean protection project', - sortOrder: 2, - isOptional: false, - fields: [ - { name: 'projectName', label: 'Project Name', fieldType: 'TEXT', required: true, sortOrder: 0, width: 'full', projectMapping: 'title', maxLength: 200, placeholder: 'Give your project a memorable name' }, - { name: 'teamName', label: 'Team / Company Name', fieldType: 'TEXT', required: false, sortOrder: 1, width: 'half', projectMapping: 'teamName', placeholder: 'Your team or company name' }, - { name: 'oceanIssue', label: 'Primary Ocean Issue', fieldType: 'SELECT', specialType: 'OCEAN_ISSUE', required: true, sortOrder: 2, width: 'half', projectMapping: 'oceanIssue', description: 'Select the primary ocean issue your project addresses' }, - { name: 'description', label: 'Project Description', fieldType: 'TEXTAREA', required: true, sortOrder: 3, width: 'full', projectMapping: 'description', minLength: 50, maxLength: 2000, placeholder: 'Describe your project, its goals, and how it will help protect the ocean...', description: 'Provide a clear description of your project (50-2000 characters)' }, - { name: 'websiteUrl', label: 'Website URL', fieldType: 'URL', required: false, sortOrder: 4, width: 'half', projectMapping: 'websiteUrl', placeholder: 'https://yourproject.com' }, - ], - }, - { - name: 'team', - title: 'Team Members', - description: 'Add your team members (they will receive email invitations)', - sortOrder: 3, - isOptional: true, - fields: [ - { name: 'teamMembers', label: 'Team Members', fieldType: 'TEXT', specialType: 'TEAM_MEMBERS', required: false, sortOrder: 0, width: 'full', description: 'Add up to 5 team members. They will receive an invitation email to join your application.' }, - ], - }, - { - name: 'additional', - title: 'Additional Details', - description: 'A few more questions about your project', - sortOrder: 4, - isOptional: false, - fields: [ - { name: 'institution', label: 'University / School', fieldType: 'TEXT', required: false, sortOrder: 0, width: 'half', projectMapping: 'institution', placeholder: 'Name of your institution', conditionJson: { field: 'competitionCategory', operator: 'equals', value: 'BUSINESS_CONCEPT' } }, - { name: 'startupCreatedDate', label: 'Startup Founded Date', fieldType: 'DATE', required: false, sortOrder: 1, width: 'half', description: 'When was your company founded?', conditionJson: { field: 'competitionCategory', operator: 'equals', value: 'STARTUP' } }, - { name: 'wantsMentorship', label: 'I am interested in receiving mentorship', fieldType: 'CHECKBOX', required: false, sortOrder: 2, width: 'full', projectMapping: 'wantsMentorship', description: 'Check this box if you would like to be paired with an expert mentor' }, - { name: 'referralSource', label: 'How did you hear about MOPC?', fieldType: 'SELECT', required: false, sortOrder: 3, width: 'half', optionsJson: [ - { value: 'social_media', label: 'Social Media' }, - { value: 'search_engine', label: 'Search Engine' }, - { value: 'word_of_mouth', label: 'Word of Mouth' }, - { value: 'university', label: 'University / School' }, - { value: 'partner', label: 'Partner Organization' }, - { value: 'media', label: 'News / Media' }, - { value: 'event', label: 'Event / Conference' }, - { value: 'other', label: 'Other' }, - ]}, - ], - }, - { - name: 'review', - title: 'Review & Submit', - description: 'Review your application and accept the terms', - sortOrder: 5, - isOptional: false, - fields: [ - { name: 'instructions', label: 'Review Instructions', fieldType: 'INSTRUCTIONS', required: false, sortOrder: 0, width: 'full', description: 'Please review all the information you have provided. Once submitted, you will not be able to make changes.' }, - { name: 'gdprConsent', label: 'I consent to the processing of my personal data in accordance with the GDPR and the MOPC Privacy Policy', fieldType: 'CHECKBOX', specialType: 'GDPR_CONSENT', required: true, sortOrder: 1, width: 'full' }, - { name: 'termsAccepted', label: 'I have read and accept the Terms and Conditions of the Monaco Ocean Protection Challenge', fieldType: 'CHECKBOX', required: true, sortOrder: 2, width: 'full' }, - ], - }, -] - -async function main() { - console.log('Seeding MOPC onboarding form...') - - // Check if form already exists - const existingForm = await prisma.applicationForm.findUnique({ - where: { publicSlug: MOPC_FORM_CONFIG.publicSlug }, - }) - - if (existingForm) { - console.log('Form with slug "mopc-2026" already exists. Updating...') - - // Delete existing steps and fields to recreate them - await prisma.applicationFormField.deleteMany({ where: { formId: existingForm.id } }) - await prisma.onboardingStep.deleteMany({ where: { formId: existingForm.id } }) - - // Update the form - await prisma.applicationForm.update({ - where: { id: existingForm.id }, - data: { - name: MOPC_FORM_CONFIG.name, - description: MOPC_FORM_CONFIG.description, - status: MOPC_FORM_CONFIG.status, - isPublic: MOPC_FORM_CONFIG.isPublic, - sendConfirmationEmail: MOPC_FORM_CONFIG.sendConfirmationEmail, - sendTeamInviteEmails: MOPC_FORM_CONFIG.sendTeamInviteEmails, - confirmationEmailSubject: MOPC_FORM_CONFIG.confirmationEmailSubject, - confirmationEmailBody: MOPC_FORM_CONFIG.confirmationEmailBody, - confirmationMessage: MOPC_FORM_CONFIG.confirmationMessage, - }, - }) - - // Create steps and fields - for (const stepData of STEPS) { - const step = await prisma.onboardingStep.create({ - data: { - formId: existingForm.id, - name: stepData.name, - title: stepData.title, - description: stepData.description, - sortOrder: stepData.sortOrder, - isOptional: stepData.isOptional, - }, - }) - - for (const field of stepData.fields) { - await prisma.applicationFormField.create({ - data: { - formId: existingForm.id, - stepId: step.id, - name: field.name, - label: field.label, - fieldType: field.fieldType, - specialType: field.specialType || null, - required: field.required, - sortOrder: field.sortOrder, - width: field.width, - description: field.description || null, - placeholder: field.placeholder || null, - projectMapping: field.projectMapping || null, - minLength: field.minLength || null, - maxLength: field.maxLength || null, - optionsJson: field.optionsJson || undefined, - conditionJson: field.conditionJson || undefined, - }, - }) - } - console.log(` - Created step: ${stepData.title} (${stepData.fields.length} fields)`) - } - - console.log(`\nForm updated: ${existingForm.id}`) - return - } - - // Create new form - const form = await prisma.applicationForm.create({ - data: { - name: MOPC_FORM_CONFIG.name, - description: MOPC_FORM_CONFIG.description, - publicSlug: MOPC_FORM_CONFIG.publicSlug, - status: MOPC_FORM_CONFIG.status, - isPublic: MOPC_FORM_CONFIG.isPublic, - sendConfirmationEmail: MOPC_FORM_CONFIG.sendConfirmationEmail, - sendTeamInviteEmails: MOPC_FORM_CONFIG.sendTeamInviteEmails, - confirmationEmailSubject: MOPC_FORM_CONFIG.confirmationEmailSubject, - confirmationEmailBody: MOPC_FORM_CONFIG.confirmationEmailBody, - confirmationMessage: MOPC_FORM_CONFIG.confirmationMessage, - }, - }) - - console.log(`Created form: ${form.id}`) - - // Create steps and fields - for (const stepData of STEPS) { - const step = await prisma.onboardingStep.create({ - data: { - formId: form.id, - name: stepData.name, - title: stepData.title, - description: stepData.description, - sortOrder: stepData.sortOrder, - isOptional: stepData.isOptional, - }, - }) - - for (const field of stepData.fields) { - await prisma.applicationFormField.create({ - data: { - formId: form.id, - stepId: step.id, - name: field.name, - label: field.label, - fieldType: field.fieldType, - specialType: field.specialType || null, - required: field.required, - sortOrder: field.sortOrder, - width: field.width, - description: field.description || null, - placeholder: field.placeholder || null, - projectMapping: field.projectMapping || null, - minLength: field.minLength || null, - maxLength: field.maxLength || null, - optionsJson: field.optionsJson || undefined, - conditionJson: field.conditionJson || undefined, - }, - }) - } - console.log(` - Created step: ${stepData.title} (${stepData.fields.length} fields)`) - } - - console.log(`\nMOPC form seeded successfully!`) - console.log(`Form ID: ${form.id}`) - console.log(`Public URL: /apply/${form.publicSlug}`) -} - -main() - .catch((e) => { - console.error(e) - process.exit(1) - }) - .finally(async () => { - await prisma.$disconnect() - }) diff --git a/prisma/seed-mopc-onboarding.ts b/prisma/seed-mopc-onboarding.ts deleted file mode 100644 index 5fe45b8..0000000 --- a/prisma/seed-mopc-onboarding.ts +++ /dev/null @@ -1,456 +0,0 @@ -/** - * Seed script for MOPC Onboarding Form - * - * This creates the application form configuration for the Monaco Ocean Protection Challenge. - * The form is accessible at /apply/mopc-2026 - * - * Run with: npx tsx prisma/seed-mopc-onboarding.ts - */ - -import { PrismaClient, FormFieldType, SpecialFieldType } from '@prisma/client' - -const prisma = new PrismaClient() - -const MOPC_FORM_CONFIG = { - name: 'MOPC Application 2026', - description: 'Monaco Ocean Protection Challenge application form', - publicSlug: 'mopc-2026', - status: 'PUBLISHED', - isPublic: true, - sendConfirmationEmail: true, - sendTeamInviteEmails: true, - confirmationEmailSubject: 'Application Received - Monaco Ocean Protection Challenge', - confirmationEmailBody: `Thank you for applying to the Monaco Ocean Protection Challenge 2026! - -We have received your application and our team will review it carefully. - -If you have any questions, please don't hesitate to reach out. - -Good luck! -The MOPC Team`, - confirmationMessage: 'Thank you for your application! We have sent a confirmation email to the address you provided. Our team will review your submission and get back to you soon.', -} - -const STEPS = [ - { - name: 'category', - title: 'Competition Category', - description: 'Select your competition track', - sortOrder: 0, - isOptional: false, - fields: [ - { - name: 'competitionCategory', - label: 'Which category best describes your project?', - fieldType: FormFieldType.RADIO, - specialType: SpecialFieldType.COMPETITION_CATEGORY, - required: true, - sortOrder: 0, - width: 'full', - projectMapping: 'competitionCategory', - description: 'Choose the category that best fits your stage of development', - optionsJson: [ - { - value: 'STARTUP', - label: 'Startup', - description: 'You have an existing company or registered business entity', - }, - { - value: 'BUSINESS_CONCEPT', - label: 'Business Concept', - description: 'You are a student, graduate, or have an idea not yet incorporated', - }, - ], - }, - ], - }, - { - name: 'contact', - title: 'Contact Information', - description: 'Tell us how to reach you', - sortOrder: 1, - isOptional: false, - fields: [ - { - name: 'contactName', - label: 'Full Name', - fieldType: FormFieldType.TEXT, - required: true, - sortOrder: 0, - width: 'half', - placeholder: 'Enter your full name', - }, - { - name: 'contactEmail', - label: 'Email Address', - fieldType: FormFieldType.EMAIL, - required: true, - sortOrder: 1, - width: 'half', - placeholder: 'your.email@example.com', - description: 'We will use this email for all communications', - }, - { - name: 'contactPhone', - label: 'Phone Number', - fieldType: FormFieldType.PHONE, - required: true, - sortOrder: 2, - width: 'half', - placeholder: '+1 (555) 123-4567', - }, - { - name: 'country', - label: 'Country', - fieldType: FormFieldType.SELECT, - specialType: SpecialFieldType.COUNTRY_SELECT, - required: true, - sortOrder: 3, - width: 'half', - projectMapping: 'country', - }, - { - name: 'city', - label: 'City', - fieldType: FormFieldType.TEXT, - required: false, - sortOrder: 4, - width: 'half', - placeholder: 'City name', - }, - ], - }, - { - name: 'project', - title: 'Project Details', - description: 'Tell us about your ocean protection project', - sortOrder: 2, - isOptional: false, - fields: [ - { - name: 'projectName', - label: 'Project Name', - fieldType: FormFieldType.TEXT, - required: true, - sortOrder: 0, - width: 'full', - projectMapping: 'title', - maxLength: 200, - placeholder: 'Give your project a memorable name', - }, - { - name: 'teamName', - label: 'Team / Company Name', - fieldType: FormFieldType.TEXT, - required: false, - sortOrder: 1, - width: 'half', - projectMapping: 'teamName', - placeholder: 'Your team or company name', - }, - { - name: 'oceanIssue', - label: 'Primary Ocean Issue', - fieldType: FormFieldType.SELECT, - specialType: SpecialFieldType.OCEAN_ISSUE, - required: true, - sortOrder: 2, - width: 'half', - projectMapping: 'oceanIssue', - description: 'Select the primary ocean issue your project addresses', - }, - { - name: 'description', - label: 'Project Description', - fieldType: FormFieldType.TEXTAREA, - required: true, - sortOrder: 3, - width: 'full', - projectMapping: 'description', - minLength: 50, - maxLength: 2000, - placeholder: 'Describe your project, its goals, and how it will help protect the ocean...', - description: 'Provide a clear description of your project (50-2000 characters)', - }, - { - name: 'websiteUrl', - label: 'Website URL', - fieldType: FormFieldType.URL, - required: false, - sortOrder: 4, - width: 'half', - projectMapping: 'websiteUrl', - placeholder: 'https://yourproject.com', - }, - ], - }, - { - name: 'team', - title: 'Team Members', - description: 'Add your team members (they will receive email invitations)', - sortOrder: 3, - isOptional: true, - fields: [ - { - name: 'teamMembers', - label: 'Team Members', - fieldType: FormFieldType.TEXT, // Will use specialType for rendering - specialType: SpecialFieldType.TEAM_MEMBERS, - required: false, - sortOrder: 0, - width: 'full', - description: 'Add up to 5 team members. They will receive an invitation email to join your application.', - }, - ], - }, - { - name: 'additional', - title: 'Additional Details', - description: 'A few more questions about your project', - sortOrder: 4, - isOptional: false, - fields: [ - { - name: 'institution', - label: 'University / School', - fieldType: FormFieldType.TEXT, - required: false, - sortOrder: 0, - width: 'half', - projectMapping: 'institution', - placeholder: 'Name of your institution', - conditionJson: { - field: 'competitionCategory', - operator: 'equals', - value: 'BUSINESS_CONCEPT', - }, - }, - { - name: 'startupCreatedDate', - label: 'Startup Founded Date', - fieldType: FormFieldType.DATE, - required: false, - sortOrder: 1, - width: 'half', - description: 'When was your company founded?', - conditionJson: { - field: 'competitionCategory', - operator: 'equals', - value: 'STARTUP', - }, - }, - { - name: 'wantsMentorship', - label: 'I am interested in receiving mentorship', - fieldType: FormFieldType.CHECKBOX, - required: false, - sortOrder: 2, - width: 'full', - projectMapping: 'wantsMentorship', - description: 'Check this box if you would like to be paired with an expert mentor', - }, - { - name: 'referralSource', - label: 'How did you hear about MOPC?', - fieldType: FormFieldType.SELECT, - required: false, - sortOrder: 3, - width: 'half', - optionsJson: [ - { value: 'social_media', label: 'Social Media' }, - { value: 'search_engine', label: 'Search Engine' }, - { value: 'word_of_mouth', label: 'Word of Mouth' }, - { value: 'university', label: 'University / School' }, - { value: 'partner', label: 'Partner Organization' }, - { value: 'media', label: 'News / Media' }, - { value: 'event', label: 'Event / Conference' }, - { value: 'other', label: 'Other' }, - ], - }, - ], - }, - { - name: 'review', - title: 'Review & Submit', - description: 'Review your application and accept the terms', - sortOrder: 5, - isOptional: false, - fields: [ - { - name: 'instructions', - label: 'Review Instructions', - fieldType: FormFieldType.INSTRUCTIONS, - required: false, - sortOrder: 0, - width: 'full', - description: 'Please review all the information you have provided. Once submitted, you will not be able to make changes.', - }, - { - name: 'gdprConsent', - label: 'I consent to the processing of my personal data in accordance with the GDPR and the MOPC Privacy Policy', - fieldType: FormFieldType.CHECKBOX, - specialType: SpecialFieldType.GDPR_CONSENT, - required: true, - sortOrder: 1, - width: 'full', - }, - { - name: 'termsAccepted', - label: 'I have read and accept the Terms and Conditions of the Monaco Ocean Protection Challenge', - fieldType: FormFieldType.CHECKBOX, - required: true, - sortOrder: 2, - width: 'full', - }, - ], - }, -] - -async function main() { - console.log('Seeding MOPC onboarding form...') - - // Check if form already exists - const existingForm = await prisma.applicationForm.findUnique({ - where: { publicSlug: MOPC_FORM_CONFIG.publicSlug }, - }) - - if (existingForm) { - console.log('Form with slug "mopc-2026" already exists. Updating...') - - // Delete existing steps and fields to recreate them - await prisma.applicationFormField.deleteMany({ - where: { formId: existingForm.id }, - }) - await prisma.onboardingStep.deleteMany({ - where: { formId: existingForm.id }, - }) - - // Update the form - await prisma.applicationForm.update({ - where: { id: existingForm.id }, - data: { - name: MOPC_FORM_CONFIG.name, - description: MOPC_FORM_CONFIG.description, - status: MOPC_FORM_CONFIG.status, - isPublic: MOPC_FORM_CONFIG.isPublic, - sendConfirmationEmail: MOPC_FORM_CONFIG.sendConfirmationEmail, - sendTeamInviteEmails: MOPC_FORM_CONFIG.sendTeamInviteEmails, - confirmationEmailSubject: MOPC_FORM_CONFIG.confirmationEmailSubject, - confirmationEmailBody: MOPC_FORM_CONFIG.confirmationEmailBody, - confirmationMessage: MOPC_FORM_CONFIG.confirmationMessage, - }, - }) - - // Create steps and fields - for (const stepData of STEPS) { - const step = await prisma.onboardingStep.create({ - data: { - formId: existingForm.id, - name: stepData.name, - title: stepData.title, - description: stepData.description, - sortOrder: stepData.sortOrder, - isOptional: stepData.isOptional, - }, - }) - - for (const fieldData of stepData.fields) { - const field = fieldData as Record - await prisma.applicationFormField.create({ - data: { - formId: existingForm.id, - stepId: step.id, - name: field.name as string, - label: field.label as string, - fieldType: field.fieldType as FormFieldType, - specialType: (field.specialType as SpecialFieldType) || null, - required: field.required as boolean, - sortOrder: field.sortOrder as number, - width: field.width as string, - description: (field.description as string) || null, - placeholder: (field.placeholder as string) || null, - projectMapping: (field.projectMapping as string) || null, - minLength: (field.minLength as number) || null, - maxLength: (field.maxLength as number) || null, - optionsJson: field.optionsJson as object | undefined, - conditionJson: field.conditionJson as object | undefined, - }, - }) - } - console.log(` - Created step: ${stepData.title} (${stepData.fields.length} fields)`) - } - - console.log(`\nForm updated: ${existingForm.id}`) - return - } - - // Create new form - const form = await prisma.applicationForm.create({ - data: { - name: MOPC_FORM_CONFIG.name, - description: MOPC_FORM_CONFIG.description, - publicSlug: MOPC_FORM_CONFIG.publicSlug, - status: MOPC_FORM_CONFIG.status, - isPublic: MOPC_FORM_CONFIG.isPublic, - sendConfirmationEmail: MOPC_FORM_CONFIG.sendConfirmationEmail, - sendTeamInviteEmails: MOPC_FORM_CONFIG.sendTeamInviteEmails, - confirmationEmailSubject: MOPC_FORM_CONFIG.confirmationEmailSubject, - confirmationEmailBody: MOPC_FORM_CONFIG.confirmationEmailBody, - confirmationMessage: MOPC_FORM_CONFIG.confirmationMessage, - }, - }) - - console.log(`Created form: ${form.id}`) - - // Create steps and fields - for (const stepData of STEPS) { - const step = await prisma.onboardingStep.create({ - data: { - formId: form.id, - name: stepData.name, - title: stepData.title, - description: stepData.description, - sortOrder: stepData.sortOrder, - isOptional: stepData.isOptional, - }, - }) - - for (const fieldData of stepData.fields) { - const field = fieldData as Record - await prisma.applicationFormField.create({ - data: { - formId: form.id, - stepId: step.id, - name: field.name as string, - label: field.label as string, - fieldType: field.fieldType as FormFieldType, - specialType: (field.specialType as SpecialFieldType) || null, - required: field.required as boolean, - sortOrder: field.sortOrder as number, - width: field.width as string, - description: (field.description as string) || null, - placeholder: (field.placeholder as string) || null, - projectMapping: (field.projectMapping as string) || null, - minLength: (field.minLength as number) || null, - maxLength: (field.maxLength as number) || null, - optionsJson: field.optionsJson as object | undefined, - conditionJson: field.conditionJson as object | undefined, - }, - }) - } - console.log(` - Created step: ${stepData.title} (${stepData.fields.length} fields)`) - } - - console.log(`\nMOPC form seeded successfully!`) - console.log(`Form ID: ${form.id}`) - console.log(`Public URL: /apply/${form.publicSlug}`) -} - -main() - .catch((e) => { - console.error(e) - process.exit(1) - }) - .finally(async () => { - await prisma.$disconnect() - }) diff --git a/src/app/(admin)/admin/forms/[id]/form-editor.tsx b/src/app/(admin)/admin/forms/[id]/form-editor.tsx deleted file mode 100644 index 603226f..0000000 --- a/src/app/(admin)/admin/forms/[id]/form-editor.tsx +++ /dev/null @@ -1,241 +0,0 @@ -'use client' - -import { useState } from 'react' -import { useRouter } from 'next/navigation' -import { trpc } from '@/lib/trpc/client' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Label } from '@/components/ui/label' -import { Textarea } from '@/components/ui/textarea' -import { Switch } from '@/components/ui/switch' -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@/components/ui/card' -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select' -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' -import { Loader2, Plus, Trash2, GripVertical, Save } from 'lucide-react' -import { toast } from 'sonner' - -interface FormEditorProps { - form: { - id: string - name: string - description: string | null - status: string - isPublic: boolean - publicSlug: string | null - submissionLimit: number | null - opensAt: Date | null - closesAt: Date | null - confirmationMessage: string | null - fields: Array<{ - id: string - fieldType: string - name: string - label: string - description: string | null - placeholder: string | null - required: boolean - sortOrder: number - }> - } -} - -export function FormEditor({ form }: FormEditorProps) { - const router = useRouter() - const [isSubmitting, setIsSubmitting] = useState(false) - const [formData, setFormData] = useState({ - name: form.name, - description: form.description || '', - status: form.status, - isPublic: form.isPublic, - publicSlug: form.publicSlug || '', - confirmationMessage: form.confirmationMessage || '', - }) - - const updateForm = trpc.applicationForm.update.useMutation({ - onSuccess: () => { - toast.success('Form updated successfully') - router.refresh() - setIsSubmitting(false) - }, - onError: (error) => { - toast.error(error.message || 'Failed to update form') - setIsSubmitting(false) - }, - }) - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - setIsSubmitting(true) - updateForm.mutate({ - id: form.id, - name: formData.name, - status: formData.status as 'DRAFT' | 'PUBLISHED' | 'CLOSED', - isPublic: formData.isPublic, - description: formData.description || null, - publicSlug: formData.publicSlug || null, - confirmationMessage: formData.confirmationMessage || null, - }) - } - - return ( - - - Settings - Fields ({form.fields.length}) - - - - - - Form Settings - - Configure the basic settings for this form - - - -
-
-
- - setFormData({ ...formData, name: e.target.value })} - required - /> -
- -
- - -
-
- -
- -