Add notification bell system and MOPC onboarding form
Notification System: - Add InAppNotification and NotificationEmailSetting database models - Create notification service with 60+ notification types for all user roles - Add notification router with CRUD endpoints - Build NotificationBell UI component with dropdown and unread count - Integrate bell into admin, jury, mentor, and observer navs - Add notification email settings admin UI in Settings > Notifications - Add notification triggers to filtering router (complete/failed) - Add sendNotificationEmail function to email library - Add formatRelativeTime utility function MOPC Onboarding Form: - Create /apply landing page with auto-redirect for single form - Create seed script for MOPC 2026 application form (6 steps) - Create seed script for default notification email settings Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "InAppNotification" (
|
||||
"id" TEXT NOT NULL,
|
||||
"userId" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"priority" TEXT NOT NULL DEFAULT 'normal',
|
||||
"icon" TEXT,
|
||||
"title" TEXT NOT NULL,
|
||||
"message" TEXT NOT NULL,
|
||||
"linkUrl" TEXT,
|
||||
"linkLabel" TEXT,
|
||||
"metadata" JSONB,
|
||||
"groupKey" TEXT,
|
||||
"isRead" BOOLEAN NOT NULL DEFAULT false,
|
||||
"readAt" TIMESTAMP(3),
|
||||
"expiresAt" TIMESTAMP(3),
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "InAppNotification_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "NotificationEmailSetting" (
|
||||
"id" TEXT NOT NULL,
|
||||
"notificationType" TEXT NOT NULL,
|
||||
"category" TEXT NOT NULL,
|
||||
"label" TEXT NOT NULL,
|
||||
"description" TEXT,
|
||||
"sendEmail" BOOLEAN NOT NULL DEFAULT true,
|
||||
"emailSubject" TEXT,
|
||||
"emailTemplate" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
"updatedById" TEXT,
|
||||
|
||||
CONSTRAINT "NotificationEmailSetting_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "InAppNotification_userId_isRead_idx" ON "InAppNotification"("userId", "isRead");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "InAppNotification_userId_createdAt_idx" ON "InAppNotification"("userId", "createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "InAppNotification_groupKey_idx" ON "InAppNotification"("groupKey");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "NotificationEmailSetting_notificationType_key" ON "NotificationEmailSetting"("notificationType");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "NotificationEmailSetting_category_idx" ON "NotificationEmailSetting"("category");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "InAppNotification" ADD CONSTRAINT "InAppNotification_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "NotificationEmailSetting" ADD CONSTRAINT "NotificationEmailSetting_updatedById_fkey" FOREIGN KEY ("updatedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
@@ -278,6 +278,10 @@ model User {
|
||||
// Award overrides
|
||||
awardEligibilityOverrides AwardEligibility[] @relation("AwardEligibilityOverriddenBy")
|
||||
|
||||
// In-app notifications
|
||||
notifications InAppNotification[] @relation("UserNotifications")
|
||||
notificationSettingsUpdated NotificationEmailSetting[] @relation("NotificationSettingUpdater")
|
||||
|
||||
// NextAuth relations
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
@@ -759,6 +763,55 @@ model NotificationLog {
|
||||
@@index([createdAt])
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// IN-APP NOTIFICATIONS
|
||||
// =============================================================================
|
||||
|
||||
model InAppNotification {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
type String // FILTERING_COMPLETE, NEW_APPLICATION, ASSIGNED_TO_PROJECT, etc.
|
||||
priority String @default("normal") // low, normal, high, urgent
|
||||
icon String? // lucide icon name
|
||||
title String
|
||||
message String @db.Text
|
||||
linkUrl String? // Where to navigate when clicked
|
||||
linkLabel String? // CTA text
|
||||
metadata Json? @db.JsonB // Extra context (projectId, roundId, etc.)
|
||||
groupKey String? // For batching similar notifications
|
||||
|
||||
isRead Boolean @default(false)
|
||||
readAt DateTime?
|
||||
expiresAt DateTime? // Auto-dismiss after date
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
// Relations
|
||||
user User @relation("UserNotifications", fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId, isRead])
|
||||
@@index([userId, createdAt])
|
||||
@@index([groupKey])
|
||||
}
|
||||
|
||||
model NotificationEmailSetting {
|
||||
id String @id @default(cuid())
|
||||
notificationType String @unique // e.g., "ADVANCED_TO_ROUND", "ASSIGNED_TO_PROJECT"
|
||||
category String // "team", "jury", "mentor", "admin"
|
||||
label String // Human-readable label for admin UI
|
||||
description String? // Help text
|
||||
sendEmail Boolean @default(true)
|
||||
emailSubject String? // Custom subject template (optional)
|
||||
emailTemplate String? @db.Text // Custom body template (optional)
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
updatedById String?
|
||||
updatedBy User? @relation("NotificationSettingUpdater", fields: [updatedById], references: [id])
|
||||
|
||||
@@index([category])
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// LEARNING HUB (Phase 2)
|
||||
// =============================================================================
|
||||
|
||||
456
prisma/seed-mopc-onboarding.ts
Normal file
456
prisma/seed-mopc-onboarding.ts
Normal file
@@ -0,0 +1,456 @@
|
||||
/**
|
||||
* 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<string, unknown>
|
||||
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<string, unknown>
|
||||
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()
|
||||
})
|
||||
253
prisma/seed-notification-settings.ts
Normal file
253
prisma/seed-notification-settings.ts
Normal file
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* Seed script for notification email settings
|
||||
*
|
||||
* Run with: npx tsx prisma/seed-notification-settings.ts
|
||||
*/
|
||||
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
// Default notification email settings by category
|
||||
const NOTIFICATION_EMAIL_SETTINGS = [
|
||||
// Team / Applicant notifications
|
||||
{
|
||||
notificationType: 'APPLICATION_SUBMITTED',
|
||||
category: 'team',
|
||||
label: 'Application Submitted',
|
||||
description: 'When a team submits their application',
|
||||
sendEmail: true,
|
||||
},
|
||||
{
|
||||
notificationType: 'TEAM_INVITE_RECEIVED',
|
||||
category: 'team',
|
||||
label: 'Team Invitation Received',
|
||||
description: 'When someone is invited to join a team',
|
||||
sendEmail: true,
|
||||
},
|
||||
{
|
||||
notificationType: 'TEAM_MEMBER_JOINED',
|
||||
category: 'team',
|
||||
label: 'Team Member Joined',
|
||||
description: 'When a new member joins the team',
|
||||
sendEmail: false,
|
||||
},
|
||||
{
|
||||
notificationType: 'ADVANCED_SEMIFINAL',
|
||||
category: 'team',
|
||||
label: 'Advanced to Semi-Finals',
|
||||
description: 'When a project advances to semi-finals',
|
||||
sendEmail: true,
|
||||
},
|
||||
{
|
||||
notificationType: 'ADVANCED_FINAL',
|
||||
category: 'team',
|
||||
label: 'Selected as Finalist',
|
||||
description: 'When a project is selected as a finalist',
|
||||
sendEmail: true,
|
||||
},
|
||||
{
|
||||
notificationType: 'MENTOR_ASSIGNED',
|
||||
category: 'team',
|
||||
label: 'Mentor Assigned',
|
||||
description: 'When a mentor is assigned to the team',
|
||||
sendEmail: true,
|
||||
},
|
||||
{
|
||||
notificationType: 'NOT_SELECTED',
|
||||
category: 'team',
|
||||
label: 'Not Selected',
|
||||
description: 'When a project is not selected for the next round',
|
||||
sendEmail: true,
|
||||
},
|
||||
{
|
||||
notificationType: 'FEEDBACK_AVAILABLE',
|
||||
category: 'team',
|
||||
label: 'Feedback Available',
|
||||
description: 'When jury feedback becomes available',
|
||||
sendEmail: true,
|
||||
},
|
||||
{
|
||||
notificationType: 'WINNER_ANNOUNCEMENT',
|
||||
category: 'team',
|
||||
label: 'Winner Announcement',
|
||||
description: 'When a project wins an award',
|
||||
sendEmail: true,
|
||||
},
|
||||
|
||||
// Jury notifications
|
||||
{
|
||||
notificationType: 'ASSIGNED_TO_PROJECT',
|
||||
category: 'jury',
|
||||
label: 'Assigned to Project',
|
||||
description: 'When a jury member is assigned to a project',
|
||||
sendEmail: true,
|
||||
},
|
||||
{
|
||||
notificationType: 'BATCH_ASSIGNED',
|
||||
category: 'jury',
|
||||
label: 'Batch Assignment',
|
||||
description: 'When multiple projects are assigned at once',
|
||||
sendEmail: true,
|
||||
},
|
||||
{
|
||||
notificationType: 'ROUND_NOW_OPEN',
|
||||
category: 'jury',
|
||||
label: 'Round Now Open',
|
||||
description: 'When a round opens for evaluation',
|
||||
sendEmail: true,
|
||||
},
|
||||
{
|
||||
notificationType: 'REMINDER_24H',
|
||||
category: 'jury',
|
||||
label: 'Reminder (24h)',
|
||||
description: 'Reminder 24 hours before deadline',
|
||||
sendEmail: true,
|
||||
},
|
||||
{
|
||||
notificationType: 'REMINDER_1H',
|
||||
category: 'jury',
|
||||
label: 'Reminder (1h)',
|
||||
description: 'Urgent reminder 1 hour before deadline',
|
||||
sendEmail: true,
|
||||
},
|
||||
{
|
||||
notificationType: 'ROUND_CLOSED',
|
||||
category: 'jury',
|
||||
label: 'Round Closed',
|
||||
description: 'When a round closes',
|
||||
sendEmail: false,
|
||||
},
|
||||
{
|
||||
notificationType: 'AWARD_VOTING_OPEN',
|
||||
category: 'jury',
|
||||
label: 'Award Voting Open',
|
||||
description: 'When special award voting opens',
|
||||
sendEmail: true,
|
||||
},
|
||||
|
||||
// Mentor notifications
|
||||
{
|
||||
notificationType: 'MENTEE_ASSIGNED',
|
||||
category: 'mentor',
|
||||
label: 'Mentee Assigned',
|
||||
description: 'When assigned as mentor to a project',
|
||||
sendEmail: true,
|
||||
},
|
||||
{
|
||||
notificationType: 'MENTEE_UPLOADED_DOCS',
|
||||
category: 'mentor',
|
||||
label: 'Mentee Documents Updated',
|
||||
description: 'When a mentee uploads new documents',
|
||||
sendEmail: false,
|
||||
},
|
||||
{
|
||||
notificationType: 'MENTEE_ADVANCED',
|
||||
category: 'mentor',
|
||||
label: 'Mentee Advanced',
|
||||
description: 'When a mentee advances to the next round',
|
||||
sendEmail: true,
|
||||
},
|
||||
{
|
||||
notificationType: 'MENTEE_FINALIST',
|
||||
category: 'mentor',
|
||||
label: 'Mentee is Finalist',
|
||||
description: 'When a mentee is selected as finalist',
|
||||
sendEmail: true,
|
||||
},
|
||||
{
|
||||
notificationType: 'MENTEE_WON',
|
||||
category: 'mentor',
|
||||
label: 'Mentee Won',
|
||||
description: 'When a mentee wins an award',
|
||||
sendEmail: true,
|
||||
},
|
||||
|
||||
// Observer notifications
|
||||
{
|
||||
notificationType: 'ROUND_STARTED',
|
||||
category: 'observer',
|
||||
label: 'Round Started',
|
||||
description: 'When a new round begins',
|
||||
sendEmail: false,
|
||||
},
|
||||
{
|
||||
notificationType: 'ROUND_COMPLETED',
|
||||
category: 'observer',
|
||||
label: 'Round Completed',
|
||||
description: 'When a round is completed',
|
||||
sendEmail: true,
|
||||
},
|
||||
{
|
||||
notificationType: 'FINALISTS_ANNOUNCED',
|
||||
category: 'observer',
|
||||
label: 'Finalists Announced',
|
||||
description: 'When finalists are announced',
|
||||
sendEmail: true,
|
||||
},
|
||||
{
|
||||
notificationType: 'WINNERS_ANNOUNCED',
|
||||
category: 'observer',
|
||||
label: 'Winners Announced',
|
||||
description: 'When winners are announced',
|
||||
sendEmail: true,
|
||||
},
|
||||
|
||||
// Admin notifications (in-app only by default)
|
||||
{
|
||||
notificationType: 'FILTERING_COMPLETE',
|
||||
category: 'admin',
|
||||
label: 'AI Filtering Complete',
|
||||
description: 'When AI filtering job completes',
|
||||
sendEmail: false,
|
||||
},
|
||||
{
|
||||
notificationType: 'FILTERING_FAILED',
|
||||
category: 'admin',
|
||||
label: 'AI Filtering Failed',
|
||||
description: 'When AI filtering job fails',
|
||||
sendEmail: true,
|
||||
},
|
||||
{
|
||||
notificationType: 'NEW_APPLICATION',
|
||||
category: 'admin',
|
||||
label: 'New Application',
|
||||
description: 'When a new application is received',
|
||||
sendEmail: false,
|
||||
},
|
||||
{
|
||||
notificationType: 'SYSTEM_ERROR',
|
||||
category: 'admin',
|
||||
label: 'System Error',
|
||||
description: 'When a system error occurs',
|
||||
sendEmail: true,
|
||||
},
|
||||
]
|
||||
|
||||
async function main() {
|
||||
console.log('Seeding notification email settings...')
|
||||
|
||||
for (const setting of NOTIFICATION_EMAIL_SETTINGS) {
|
||||
await prisma.notificationEmailSetting.upsert({
|
||||
where: { notificationType: setting.notificationType },
|
||||
update: {
|
||||
category: setting.category,
|
||||
label: setting.label,
|
||||
description: setting.description,
|
||||
},
|
||||
create: setting,
|
||||
})
|
||||
console.log(` - ${setting.label} (${setting.notificationType})`)
|
||||
}
|
||||
|
||||
console.log(`\nSeeded ${NOTIFICATION_EMAIL_SETTINGS.length} notification email settings.`)
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e)
|
||||
process.exit(1)
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect()
|
||||
})
|
||||
Reference in New Issue
Block a user