Add background filtering jobs, improved date picker, AI reasoning display

- Implement background job system for AI filtering to avoid HTTP timeouts
- Add FilteringJob model to track progress of long-running filtering operations
- Add real-time progress polling for filtering operations on round details page
- Create custom DateTimePicker component with calendar popup (no year picker hassle)
- Fix round date persistence bug (refetchOnWindowFocus was resetting form state)
- Integrate filtering controls into round details page for filtering rounds
- Display AI reasoning for flagged/filtered projects in results table
- Add onboarding system scaffolding (schema, routes, basic UI)
- Allow setting round dates in the past for manual overrides

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-03 19:48:41 +01:00
parent 8be740a4fb
commit e2782b2b19
24 changed files with 3692 additions and 443 deletions

View File

@@ -0,0 +1,147 @@
-- Add Onboarding System Schema Changes
-- This migration adds the onboarding configuration system for the public application wizard
-- CreateEnum: SpecialFieldType
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'SpecialFieldType') THEN
CREATE TYPE "SpecialFieldType" AS ENUM (
'TEAM_MEMBERS',
'COMPETITION_CATEGORY',
'OCEAN_ISSUE',
'FILE_UPLOAD',
'GDPR_CONSENT',
'COUNTRY_SELECT'
);
END IF;
END $$;
-- CreateTable: OnboardingStep
CREATE TABLE IF NOT EXISTS "OnboardingStep" (
"id" TEXT NOT NULL,
"formId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"isOptional" BOOLEAN NOT NULL DEFAULT false,
"conditionJson" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "OnboardingStep_pkey" PRIMARY KEY ("id")
);
-- Add columns to ApplicationForm
DO $$
BEGIN
-- roundId column (unique)
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'ApplicationForm' AND column_name = 'roundId'
) THEN
ALTER TABLE "ApplicationForm" ADD COLUMN "roundId" TEXT;
END IF;
-- sendConfirmationEmail column
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'ApplicationForm' AND column_name = 'sendConfirmationEmail'
) THEN
ALTER TABLE "ApplicationForm" ADD COLUMN "sendConfirmationEmail" BOOLEAN NOT NULL DEFAULT true;
END IF;
-- sendTeamInviteEmails column
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'ApplicationForm' AND column_name = 'sendTeamInviteEmails'
) THEN
ALTER TABLE "ApplicationForm" ADD COLUMN "sendTeamInviteEmails" BOOLEAN NOT NULL DEFAULT true;
END IF;
-- confirmationEmailSubject column
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'ApplicationForm' AND column_name = 'confirmationEmailSubject'
) THEN
ALTER TABLE "ApplicationForm" ADD COLUMN "confirmationEmailSubject" TEXT;
END IF;
-- confirmationEmailBody column
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'ApplicationForm' AND column_name = 'confirmationEmailBody'
) THEN
ALTER TABLE "ApplicationForm" ADD COLUMN "confirmationEmailBody" TEXT;
END IF;
END $$;
-- Add columns to ApplicationFormField
DO $$
BEGIN
-- stepId column
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'ApplicationFormField' AND column_name = 'stepId'
) THEN
ALTER TABLE "ApplicationFormField" ADD COLUMN "stepId" TEXT;
END IF;
-- projectMapping column
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'ApplicationFormField' AND column_name = 'projectMapping'
) THEN
ALTER TABLE "ApplicationFormField" ADD COLUMN "projectMapping" TEXT;
END IF;
-- specialType column
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'ApplicationFormField' AND column_name = 'specialType'
) THEN
ALTER TABLE "ApplicationFormField" ADD COLUMN "specialType" "SpecialFieldType";
END IF;
END $$;
-- Create indexes for OnboardingStep
CREATE INDEX IF NOT EXISTS "OnboardingStep_formId_idx" ON "OnboardingStep"("formId");
CREATE INDEX IF NOT EXISTS "OnboardingStep_sortOrder_idx" ON "OnboardingStep"("sortOrder");
-- Create index for ApplicationForm.roundId
CREATE UNIQUE INDEX IF NOT EXISTS "ApplicationForm_roundId_key" ON "ApplicationForm"("roundId");
CREATE INDEX IF NOT EXISTS "ApplicationForm_roundId_idx" ON "ApplicationForm"("roundId");
-- Create index for ApplicationFormField.stepId
CREATE INDEX IF NOT EXISTS "ApplicationFormField_stepId_idx" ON "ApplicationFormField"("stepId");
-- Add foreign key constraints
DO $$
BEGIN
-- OnboardingStep -> ApplicationForm
IF NOT EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'OnboardingStep_formId_fkey'
) THEN
ALTER TABLE "OnboardingStep" ADD CONSTRAINT "OnboardingStep_formId_fkey"
FOREIGN KEY ("formId") REFERENCES "ApplicationForm"("id") ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
-- ApplicationFormField -> OnboardingStep
IF NOT EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'ApplicationFormField_stepId_fkey'
) THEN
ALTER TABLE "ApplicationFormField" ADD CONSTRAINT "ApplicationFormField_stepId_fkey"
FOREIGN KEY ("stepId") REFERENCES "OnboardingStep"("id") ON DELETE SET NULL ON UPDATE CASCADE;
END IF;
-- ApplicationForm -> Round
IF NOT EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE constraint_name = 'ApplicationForm_roundId_fkey'
) THEN
ALTER TABLE "ApplicationForm" ADD CONSTRAINT "ApplicationForm_roundId_fkey"
FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE SET NULL ON UPDATE CASCADE;
END IF;
END $$;

View File

@@ -0,0 +1,38 @@
-- Create FilteringJobStatus enum if not exists
DO $$ BEGIN
CREATE TYPE "FilteringJobStatus" AS ENUM ('PENDING', 'RUNNING', 'COMPLETED', 'FAILED');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Create FilteringJob table if not exists
CREATE TABLE IF NOT EXISTS "FilteringJob" (
"id" TEXT NOT NULL,
"roundId" TEXT NOT NULL,
"status" "FilteringJobStatus" NOT NULL DEFAULT 'PENDING',
"totalProjects" INTEGER NOT NULL DEFAULT 0,
"totalBatches" INTEGER NOT NULL DEFAULT 0,
"currentBatch" INTEGER NOT NULL DEFAULT 0,
"processedCount" INTEGER NOT NULL DEFAULT 0,
"passedCount" INTEGER NOT NULL DEFAULT 0,
"filteredCount" INTEGER NOT NULL DEFAULT 0,
"flaggedCount" INTEGER NOT NULL DEFAULT 0,
"errorMessage" TEXT,
"startedAt" TIMESTAMP(3),
"completedAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "FilteringJob_pkey" PRIMARY KEY ("id")
);
-- Add indexes
CREATE INDEX IF NOT EXISTS "FilteringJob_roundId_idx" ON "FilteringJob"("roundId");
CREATE INDEX IF NOT EXISTS "FilteringJob_status_idx" ON "FilteringJob"("status");
-- Add foreign key if not exists
DO $$ BEGIN
ALTER TABLE "FilteringJob" ADD CONSTRAINT "FilteringJob_roundId_fkey" FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
EXCEPTION
WHEN duplicate_object THEN null;
END $$;

View File

@@ -167,6 +167,15 @@ enum FormFieldType {
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
// =============================================================================
@@ -376,14 +385,16 @@ model Round {
updatedAt DateTime @updatedAt
// Relations
program Program @relation(fields: [programId], references: [id], onDelete: Cascade)
roundProjects RoundProject[]
assignments Assignment[]
evaluationForms EvaluationForm[]
gracePeriods GracePeriod[]
program Program @relation(fields: [programId], references: [id], onDelete: Cascade)
roundProjects RoundProject[]
assignments Assignment[]
evaluationForms EvaluationForm[]
gracePeriods GracePeriod[]
liveVotingSession LiveVotingSession?
filteringRules FilteringRule[]
filteringResults FilteringResult[]
filteringRules FilteringRule[]
filteringResults FilteringResult[]
filteringJobs FilteringJob[]
applicationForm ApplicationForm?
@@index([programId])
@@index([status])
@@ -858,22 +869,35 @@ model ApplicationForm {
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")
@@ -889,6 +913,10 @@ model ApplicationFormField {
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
@@ -896,7 +924,30 @@ model ApplicationFormField {
updatedAt DateTime @updatedAt
// Relations
form ApplicationForm @relation(fields: [formId], references: [id], onDelete: Cascade)
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])
@@ -1117,6 +1168,39 @@ model FilteringResult {
@@index([outcome])
}
// Tracks progress of long-running filtering jobs
model FilteringJob {
id String @id @default(cuid())
roundId String
status FilteringJobStatus @default(PENDING)
totalProjects Int @default(0)
totalBatches Int @default(0)
currentBatch Int @default(0)
processedCount Int @default(0)
passedCount Int @default(0)
filteredCount Int @default(0)
flaggedCount Int @default(0)
errorMessage String? @db.Text
startedAt DateTime?
completedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
@@index([roundId])
@@index([status])
}
enum FilteringJobStatus {
PENDING
RUNNING
COMPLETED
FAILED
}
// =============================================================================
// SPECIAL AWARDS SYSTEM
// =============================================================================