Add dynamic apply wizard customization with admin settings UI
- Create wizard config types, utilities, and defaults (wizard-config.ts) - Add admin apply settings page with drag-and-drop step ordering, dropdown option management, feature toggles, welcome message customization, and custom field builder with select/multiselect options editor - Build dynamic apply wizard component with animated step transitions, mobile-first responsive design, and config-driven form validation - Update step components to accept dynamic config (categories, ocean issues, field visibility, feature flags) - Replace hardcoded enum validation with string-based validation for admin-configurable dropdown values, with safe enum casting at storage layer - Add wizard template system (model, router, admin UI) with built-in MOPC Classic preset - Add program wizard config CRUD procedures to program router - Update application router getConfig to return wizardConfig, submit handler to store custom field data in metadataJson - Add edition-based apply page, project pool page, and supporting routers - Fix CSS (invalid sm:fixed-none), Enter key handler (skip textarea), safe area insets for notched phones, buildStepsArray field visibility Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -110,12 +110,6 @@ enum SettingCategory {
|
||||
SECURITY
|
||||
DEFAULTS
|
||||
WHATSAPP
|
||||
DIGEST
|
||||
ANALYTICS
|
||||
AUDIT_CONFIG
|
||||
INTEGRATIONS
|
||||
LOCALIZATION
|
||||
COMMUNICATION
|
||||
}
|
||||
|
||||
enum NotificationChannel {
|
||||
@@ -216,13 +210,6 @@ model User {
|
||||
notificationPreference NotificationChannel @default(EMAIL)
|
||||
whatsappOptIn Boolean @default(false)
|
||||
|
||||
// Digest preferences (F1)
|
||||
digestFrequency String @default("none") // none, daily, weekly
|
||||
|
||||
// Availability & workload (F2)
|
||||
availabilityJson Json? @db.JsonB // Array of { start, end } date ranges
|
||||
preferredWorkload Int? // Preferred number of assignments
|
||||
|
||||
// Onboarding (Phase 2B)
|
||||
onboardingCompletedAt DateTime?
|
||||
|
||||
@@ -282,23 +269,8 @@ model User {
|
||||
// Mentor messages
|
||||
mentorMessages MentorMessage[] @relation("MentorMessageSender")
|
||||
|
||||
// Digest logs (F1)
|
||||
digestLogs DigestLog[]
|
||||
|
||||
// Mentor notes & milestones (F8)
|
||||
mentorNotesMade MentorNote[] @relation("MentorNoteAuthor")
|
||||
milestoneCompletions MentorMilestoneCompletion[] @relation("MilestoneCompletedByUser")
|
||||
|
||||
// Messages (F9)
|
||||
sentMessages Message[] @relation("MessageSender")
|
||||
messageRecipients MessageRecipient[]
|
||||
|
||||
// Webhooks (F12)
|
||||
createdWebhooks Webhook[] @relation("WebhookCreatedBy")
|
||||
|
||||
// Discussion comments (F13)
|
||||
discussionComments DiscussionComment[]
|
||||
closedDiscussions EvaluationDiscussion[] @relation("DiscussionClosedBy")
|
||||
// Wizard templates
|
||||
wizardTemplates WizardTemplate[] @relation("WizardTemplateCreatedBy")
|
||||
|
||||
// NextAuth relations
|
||||
accounts Account[]
|
||||
@@ -355,6 +327,7 @@ model VerificationToken {
|
||||
model Program {
|
||||
id String @id @default(cuid())
|
||||
name String // e.g., "Monaco Ocean Protection Challenge"
|
||||
slug String? @unique // URL-friendly identifier for edition-wide applications
|
||||
year Int // e.g., 2026
|
||||
status ProgramStatus @default(DRAFT)
|
||||
description String?
|
||||
@@ -364,17 +337,35 @@ model Program {
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
projects Project[]
|
||||
rounds Round[]
|
||||
learningResources LearningResource[]
|
||||
partners Partner[]
|
||||
specialAwards SpecialAward[]
|
||||
taggingJobs TaggingJob[]
|
||||
mentorMilestones MentorMilestone[]
|
||||
wizardTemplates WizardTemplate[]
|
||||
|
||||
@@unique([name, year])
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
model WizardTemplate {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
description String?
|
||||
config Json @db.JsonB
|
||||
isGlobal Boolean @default(false)
|
||||
programId String?
|
||||
program Program? @relation(fields: [programId], references: [id], onDelete: Cascade)
|
||||
createdBy String
|
||||
creator User @relation("WizardTemplateCreatedBy", fields: [createdBy], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([programId])
|
||||
@@index([isGlobal])
|
||||
}
|
||||
|
||||
model Round {
|
||||
id String @id @default(cuid())
|
||||
programId String
|
||||
@@ -461,7 +452,8 @@ model EvaluationForm {
|
||||
|
||||
model Project {
|
||||
id String @id @default(cuid())
|
||||
roundId String
|
||||
programId String
|
||||
roundId String?
|
||||
status ProjectStatus @default(SUBMITTED)
|
||||
|
||||
// Core fields
|
||||
@@ -507,11 +499,6 @@ model Project {
|
||||
logoKey String? // Storage key (e.g., "logos/project456/1234567890.png")
|
||||
logoProvider String? // Storage provider used: 's3' or 'local'
|
||||
|
||||
// Draft saving (F11)
|
||||
isDraft Boolean @default(false)
|
||||
draftDataJson Json? @db.JsonB
|
||||
draftExpiresAt DateTime?
|
||||
|
||||
// Flexible fields
|
||||
tags String[] @default([]) // "Ocean Conservation", "Tech", etc.
|
||||
metadataJson Json? @db.JsonB // Custom fields from Typeform, etc.
|
||||
@@ -521,7 +508,8 @@ model Project {
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
|
||||
program Program @relation(fields: [programId], references: [id], onDelete: Cascade)
|
||||
round Round? @relation(fields: [roundId], references: [id], onDelete: SetNull)
|
||||
files ProjectFile[]
|
||||
assignments Assignment[]
|
||||
submittedBy User? @relation("ProjectSubmittedBy", fields: [submittedByUserId], references: [id], onDelete: SetNull)
|
||||
@@ -535,9 +523,10 @@ model Project {
|
||||
statusHistory ProjectStatusHistory[]
|
||||
mentorMessages MentorMessage[]
|
||||
evaluationSummaries EvaluationSummary[]
|
||||
discussions EvaluationDiscussion[]
|
||||
|
||||
@@index([programId])
|
||||
@@index([roundId])
|
||||
@@index([programId, roundId])
|
||||
@@index([status])
|
||||
@@index([tags])
|
||||
@@index([submissionSource])
|
||||
@@ -564,10 +553,6 @@ model ProjectFile {
|
||||
|
||||
isLate Boolean @default(false) // Uploaded after round deadline
|
||||
|
||||
// File versioning (F7)
|
||||
version Int @default(1)
|
||||
replacedById String? // Points to newer version of this file
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
// Relations
|
||||
@@ -717,10 +702,6 @@ model AuditLog {
|
||||
// Details
|
||||
detailsJson Json? @db.JsonB // Before/after values, additional context
|
||||
|
||||
// Audit enhancements (F14)
|
||||
sessionId String? // Groups actions in same user session
|
||||
previousDataJson Json? @db.JsonB // Snapshot of data before change
|
||||
|
||||
// Request info
|
||||
ipAddress String?
|
||||
userAgent String?
|
||||
@@ -997,12 +978,6 @@ model LiveVotingSession {
|
||||
votingEndsAt DateTime?
|
||||
projectOrderJson Json? @db.JsonB // Array of project IDs in presentation order
|
||||
|
||||
// Live voting UX enhancements (F5/F6)
|
||||
presentationSettingsJson Json? @db.JsonB // theme, auto-advance, branding
|
||||
allowAudienceVotes Boolean @default(false)
|
||||
audienceVoteWeight Float @default(0) // 0-1 weight relative to jury
|
||||
tieBreakerMethod String @default("admin_decides") // admin_decides, highest_individual, revote
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@ -1014,13 +989,12 @@ model LiveVotingSession {
|
||||
}
|
||||
|
||||
model LiveVote {
|
||||
id String @id @default(cuid())
|
||||
sessionId String
|
||||
projectId String
|
||||
userId String
|
||||
score Int // 1-10
|
||||
isAudienceVote Boolean @default(false) // F6: audience voting
|
||||
votedAt DateTime @default(now())
|
||||
id String @id @default(cuid())
|
||||
sessionId String
|
||||
projectId String
|
||||
userId String
|
||||
score Int // 1-10
|
||||
votedAt DateTime @default(now())
|
||||
|
||||
// Relations
|
||||
session LiveVotingSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
||||
@@ -1030,7 +1004,6 @@ model LiveVote {
|
||||
@@index([sessionId])
|
||||
@@index([projectId])
|
||||
@@index([userId])
|
||||
@@index([isAudienceVote])
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -1074,15 +1047,9 @@ model MentorAssignment {
|
||||
expertiseMatchScore Float?
|
||||
aiReasoning String? @db.Text
|
||||
|
||||
// Mentor dashboard enhancements (F8)
|
||||
lastViewedAt DateTime?
|
||||
completionStatus String @default("in_progress") // in_progress, completed, paused
|
||||
|
||||
// Relations
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
mentor User @relation("MentorAssignments", fields: [mentorId], references: [id])
|
||||
notes MentorNote[]
|
||||
milestoneCompletions MentorMilestoneCompletion[]
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
mentor User @relation("MentorAssignments", fields: [mentorId], references: [id])
|
||||
|
||||
@@index([mentorId])
|
||||
@@index([method])
|
||||
@@ -1487,242 +1454,3 @@ model MentorMessage {
|
||||
|
||||
@@index([projectId, createdAt])
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DIGEST LOGS (F1: Email Digest)
|
||||
// =============================================================================
|
||||
|
||||
model DigestLog {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
digestType String // "daily", "weekly"
|
||||
contentJson Json @db.JsonB
|
||||
sentAt DateTime @default(now())
|
||||
|
||||
// Relations
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
@@index([sentAt])
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// ROUND TEMPLATES (F3)
|
||||
// =============================================================================
|
||||
|
||||
model RoundTemplate {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
description String? @db.Text
|
||||
programId String? // null = global template
|
||||
roundType RoundType @default(EVALUATION)
|
||||
criteriaJson Json @db.JsonB
|
||||
settingsJson Json? @db.JsonB
|
||||
assignmentConfig Json? @db.JsonB
|
||||
createdBy String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([programId])
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// MENTOR NOTES & MILESTONES (F8)
|
||||
// =============================================================================
|
||||
|
||||
model MentorNote {
|
||||
id String @id @default(cuid())
|
||||
mentorAssignmentId String
|
||||
authorId String
|
||||
content String @db.Text
|
||||
isVisibleToAdmin Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
mentorAssignment MentorAssignment @relation(fields: [mentorAssignmentId], references: [id], onDelete: Cascade)
|
||||
author User @relation("MentorNoteAuthor", fields: [authorId], references: [id])
|
||||
|
||||
@@index([mentorAssignmentId])
|
||||
}
|
||||
|
||||
model MentorMilestone {
|
||||
id String @id @default(cuid())
|
||||
programId String
|
||||
name String
|
||||
description String? @db.Text
|
||||
isRequired Boolean @default(false)
|
||||
deadlineOffsetDays Int?
|
||||
sortOrder Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
program Program @relation(fields: [programId], references: [id], onDelete: Cascade)
|
||||
completions MentorMilestoneCompletion[]
|
||||
|
||||
@@index([programId])
|
||||
@@index([sortOrder])
|
||||
}
|
||||
|
||||
model MentorMilestoneCompletion {
|
||||
id String @id @default(cuid())
|
||||
milestoneId String
|
||||
mentorAssignmentId String
|
||||
completedAt DateTime @default(now())
|
||||
completedById String
|
||||
|
||||
// Relations
|
||||
milestone MentorMilestone @relation(fields: [milestoneId], references: [id], onDelete: Cascade)
|
||||
mentorAssignment MentorAssignment @relation(fields: [mentorAssignmentId], references: [id], onDelete: Cascade)
|
||||
completedBy User @relation("MilestoneCompletedByUser", fields: [completedById], references: [id])
|
||||
|
||||
@@unique([milestoneId, mentorAssignmentId])
|
||||
@@index([mentorAssignmentId])
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COMMUNICATION HUB (F9)
|
||||
// =============================================================================
|
||||
|
||||
model Message {
|
||||
id String @id @default(cuid())
|
||||
senderId String
|
||||
recipientType String // USER, ROLE, ROUND_JURY, PROGRAM_TEAM, ALL
|
||||
recipientFilter Json? @db.JsonB
|
||||
roundId String?
|
||||
templateId String?
|
||||
subject String
|
||||
body String @db.Text
|
||||
deliveryChannels String[]
|
||||
scheduledAt DateTime?
|
||||
sentAt DateTime?
|
||||
metadata Json? @db.JsonB
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
// Relations
|
||||
sender User @relation("MessageSender", fields: [senderId], references: [id])
|
||||
template MessageTemplate? @relation(fields: [templateId], references: [id])
|
||||
recipients MessageRecipient[]
|
||||
|
||||
@@index([senderId])
|
||||
@@index([sentAt])
|
||||
@@index([scheduledAt])
|
||||
}
|
||||
|
||||
model MessageTemplate {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
category String
|
||||
subject String
|
||||
body String @db.Text
|
||||
variables Json? @db.JsonB
|
||||
isActive Boolean @default(true)
|
||||
createdBy String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
messages Message[]
|
||||
|
||||
@@index([category])
|
||||
@@index([isActive])
|
||||
}
|
||||
|
||||
model MessageRecipient {
|
||||
id String @id @default(cuid())
|
||||
messageId String
|
||||
userId String
|
||||
channel String // EMAIL, IN_APP, WHATSAPP
|
||||
isRead Boolean @default(false)
|
||||
readAt DateTime?
|
||||
deliveredAt DateTime?
|
||||
|
||||
// Relations
|
||||
message Message @relation(fields: [messageId], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([messageId])
|
||||
@@index([userId, isRead])
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// WEBHOOKS (F12)
|
||||
// =============================================================================
|
||||
|
||||
model Webhook {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
url String
|
||||
secret String // HMAC signing key
|
||||
events String[]
|
||||
headers Json? @db.JsonB
|
||||
isActive Boolean @default(true)
|
||||
maxRetries Int @default(3)
|
||||
createdById String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
// Relations
|
||||
createdBy User @relation("WebhookCreatedBy", fields: [createdById], references: [id])
|
||||
deliveries WebhookDelivery[]
|
||||
|
||||
@@index([isActive])
|
||||
}
|
||||
|
||||
model WebhookDelivery {
|
||||
id String @id @default(cuid())
|
||||
webhookId String
|
||||
event String
|
||||
payload Json @db.JsonB
|
||||
responseStatus Int?
|
||||
responseBody String? @db.Text
|
||||
attempts Int @default(0)
|
||||
lastAttemptAt DateTime?
|
||||
status String @default("PENDING") // PENDING, DELIVERED, FAILED
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
// Relations
|
||||
webhook Webhook @relation(fields: [webhookId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([webhookId])
|
||||
@@index([status])
|
||||
@@index([createdAt])
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// PEER REVIEW / EVALUATION DISCUSSIONS (F13)
|
||||
// =============================================================================
|
||||
|
||||
model EvaluationDiscussion {
|
||||
id String @id @default(cuid())
|
||||
projectId String
|
||||
roundId String
|
||||
status String @default("open") // open, closed
|
||||
createdAt DateTime @default(now())
|
||||
closedAt DateTime?
|
||||
closedById String?
|
||||
|
||||
// Relations
|
||||
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
closedBy User? @relation("DiscussionClosedBy", fields: [closedById], references: [id])
|
||||
comments DiscussionComment[]
|
||||
|
||||
@@unique([projectId, roundId])
|
||||
@@index([roundId])
|
||||
@@index([status])
|
||||
}
|
||||
|
||||
model DiscussionComment {
|
||||
id String @id @default(cuid())
|
||||
discussionId String
|
||||
userId String
|
||||
content String @db.Text
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
// Relations
|
||||
discussion EvaluationDiscussion @relation(fields: [discussionId], references: [id], onDelete: Cascade)
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@index([discussionId, createdAt])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user