Implement Prototype 1 improvements: unified members, project filters, audit expansion, filtering rounds, special awards

- Unified Member Management: merge /admin/users and /admin/mentors into /admin/members with role tabs, search, pagination
- Project List Filters: add search, multi-status filter, round/category/country selects, boolean toggles, URL persistence
- Audit Log Expansion: track logins, round state changes, evaluation submissions, file access, role changes via shared logAudit utility
- Founding Date Field: add foundedAt to Project model with CSV import support
- Filtering Round System: configurable rules (field-based, document check, AI screening), execution engine, results review with override/reinstate
- Special Awards System: named awards with eligibility criteria, dedicated jury, PICK_WINNER/RANKED/SCORED voting modes, AI eligibility
- Dashboard resilience: wrap heavy queries in try-catch to prevent error boundary on transient DB failures
- Reusable pagination component extracted to src/components/shared/pagination.tsx
- Old /admin/users and /admin/mentors routes redirect to /admin/members
- Prisma migration for all schema additions (additive, no data loss)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-02 16:58:29 +01:00
parent 8fda8deded
commit 90e3adfab2
44 changed files with 7268 additions and 2154 deletions

View File

@@ -0,0 +1,221 @@
-- CreateEnum
CREATE TYPE "FilteringOutcome" AS ENUM ('PASSED', 'FILTERED_OUT', 'FLAGGED');
-- CreateEnum
CREATE TYPE "FilteringRuleType" AS ENUM ('FIELD_BASED', 'DOCUMENT_CHECK', 'AI_SCREENING');
-- CreateEnum
CREATE TYPE "AwardScoringMode" AS ENUM ('PICK_WINNER', 'RANKED', 'SCORED');
-- CreateEnum
CREATE TYPE "AwardStatus" AS ENUM ('DRAFT', 'NOMINATIONS_OPEN', 'VOTING_OPEN', 'CLOSED', 'ARCHIVED');
-- CreateEnum
CREATE TYPE "EligibilityMethod" AS ENUM ('AUTO', 'MANUAL');
-- AlterTable
ALTER TABLE "Project" ADD COLUMN "foundedAt" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "User" ADD COLUMN "inviteToken" TEXT,
ADD COLUMN "inviteTokenExpiresAt" TIMESTAMP(3);
-- CreateTable
CREATE TABLE "FilteringRule" (
"id" TEXT NOT NULL,
"roundId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"ruleType" "FilteringRuleType" NOT NULL,
"configJson" JSONB NOT NULL,
"priority" INTEGER NOT NULL DEFAULT 0,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "FilteringRule_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "FilteringResult" (
"id" TEXT NOT NULL,
"roundId" TEXT NOT NULL,
"projectId" TEXT NOT NULL,
"outcome" "FilteringOutcome" NOT NULL,
"ruleResultsJson" JSONB,
"aiScreeningJson" JSONB,
"overriddenBy" TEXT,
"overriddenAt" TIMESTAMP(3),
"overrideReason" TEXT,
"finalOutcome" "FilteringOutcome",
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "FilteringResult_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "SpecialAward" (
"id" TEXT NOT NULL,
"programId" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"status" "AwardStatus" NOT NULL DEFAULT 'DRAFT',
"criteriaText" TEXT,
"autoTagRulesJson" JSONB,
"scoringMode" "AwardScoringMode" NOT NULL DEFAULT 'PICK_WINNER',
"maxRankedPicks" INTEGER,
"votingStartAt" TIMESTAMP(3),
"votingEndAt" TIMESTAMP(3),
"evaluationFormId" TEXT,
"winnerProjectId" TEXT,
"winnerOverridden" BOOLEAN NOT NULL DEFAULT false,
"winnerOverriddenBy" TEXT,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SpecialAward_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AwardEligibility" (
"id" TEXT NOT NULL,
"awardId" TEXT NOT NULL,
"projectId" TEXT NOT NULL,
"method" "EligibilityMethod" NOT NULL DEFAULT 'AUTO',
"eligible" BOOLEAN NOT NULL DEFAULT false,
"aiReasoningJson" JSONB,
"overriddenBy" TEXT,
"overriddenAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "AwardEligibility_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AwardJuror" (
"id" TEXT NOT NULL,
"awardId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AwardJuror_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "AwardVote" (
"id" TEXT NOT NULL,
"awardId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"projectId" TEXT NOT NULL,
"rank" INTEGER,
"votedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AwardVote_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "FilteringRule_roundId_idx" ON "FilteringRule"("roundId");
-- CreateIndex
CREATE INDEX "FilteringRule_priority_idx" ON "FilteringRule"("priority");
-- CreateIndex
CREATE INDEX "FilteringResult_roundId_idx" ON "FilteringResult"("roundId");
-- CreateIndex
CREATE INDEX "FilteringResult_projectId_idx" ON "FilteringResult"("projectId");
-- CreateIndex
CREATE INDEX "FilteringResult_outcome_idx" ON "FilteringResult"("outcome");
-- CreateIndex
CREATE UNIQUE INDEX "FilteringResult_roundId_projectId_key" ON "FilteringResult"("roundId", "projectId");
-- CreateIndex
CREATE INDEX "SpecialAward_programId_idx" ON "SpecialAward"("programId");
-- CreateIndex
CREATE INDEX "SpecialAward_status_idx" ON "SpecialAward"("status");
-- CreateIndex
CREATE INDEX "SpecialAward_sortOrder_idx" ON "SpecialAward"("sortOrder");
-- CreateIndex
CREATE INDEX "AwardEligibility_awardId_idx" ON "AwardEligibility"("awardId");
-- CreateIndex
CREATE INDEX "AwardEligibility_projectId_idx" ON "AwardEligibility"("projectId");
-- CreateIndex
CREATE INDEX "AwardEligibility_eligible_idx" ON "AwardEligibility"("eligible");
-- CreateIndex
CREATE UNIQUE INDEX "AwardEligibility_awardId_projectId_key" ON "AwardEligibility"("awardId", "projectId");
-- CreateIndex
CREATE INDEX "AwardJuror_awardId_idx" ON "AwardJuror"("awardId");
-- CreateIndex
CREATE INDEX "AwardJuror_userId_idx" ON "AwardJuror"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "AwardJuror_awardId_userId_key" ON "AwardJuror"("awardId", "userId");
-- CreateIndex
CREATE INDEX "AwardVote_awardId_idx" ON "AwardVote"("awardId");
-- CreateIndex
CREATE INDEX "AwardVote_userId_idx" ON "AwardVote"("userId");
-- CreateIndex
CREATE INDEX "AwardVote_projectId_idx" ON "AwardVote"("projectId");
-- CreateIndex
CREATE UNIQUE INDEX "AwardVote_awardId_userId_projectId_key" ON "AwardVote"("awardId", "userId", "projectId");
-- CreateIndex
CREATE UNIQUE INDEX "User_inviteToken_key" ON "User"("inviteToken");
-- AddForeignKey
ALTER TABLE "FilteringRule" ADD CONSTRAINT "FilteringRule_roundId_fkey" FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "FilteringResult" ADD CONSTRAINT "FilteringResult_roundId_fkey" FOREIGN KEY ("roundId") REFERENCES "Round"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "FilteringResult" ADD CONSTRAINT "FilteringResult_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "FilteringResult" ADD CONSTRAINT "FilteringResult_overriddenBy_fkey" FOREIGN KEY ("overriddenBy") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "SpecialAward" ADD CONSTRAINT "SpecialAward_programId_fkey" FOREIGN KEY ("programId") REFERENCES "Program"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "SpecialAward" ADD CONSTRAINT "SpecialAward_winnerProjectId_fkey" FOREIGN KEY ("winnerProjectId") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AwardEligibility" ADD CONSTRAINT "AwardEligibility_awardId_fkey" FOREIGN KEY ("awardId") REFERENCES "SpecialAward"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AwardEligibility" ADD CONSTRAINT "AwardEligibility_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AwardEligibility" ADD CONSTRAINT "AwardEligibility_overriddenBy_fkey" FOREIGN KEY ("overriddenBy") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AwardJuror" ADD CONSTRAINT "AwardJuror_awardId_fkey" FOREIGN KEY ("awardId") REFERENCES "SpecialAward"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AwardJuror" ADD CONSTRAINT "AwardJuror_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AwardVote" ADD CONSTRAINT "AwardVote_awardId_fkey" FOREIGN KEY ("awardId") REFERENCES "SpecialAward"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AwardVote" ADD CONSTRAINT "AwardVote_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "AwardVote" ADD CONSTRAINT "AwardVote_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

View File

@@ -259,6 +259,16 @@ model User {
teamMemberships TeamMember[]
mentorAssignments MentorAssignment[] @relation("MentorAssignments")
// Awards
awardJurorships AwardJuror[]
awardVotes AwardVote[]
// Filtering overrides
filteringOverrides FilteringResult[] @relation("FilteringOverriddenBy")
// Award overrides
awardEligibilityOverrides AwardEligibility[] @relation("AwardEligibilityOverriddenBy")
// NextAuth relations
accounts Account[]
sessions Session[]
@@ -328,6 +338,7 @@ model Program {
learningResources LearningResource[]
partners Partner[]
applicationForms ApplicationForm[]
specialAwards SpecialAward[]
@@unique([name, year])
@@index([status])
@@ -369,6 +380,8 @@ model Round {
evaluationForms EvaluationForm[]
gracePeriods GracePeriod[]
liveVotingSession LiveVotingSession?
filteringRules FilteringRule[]
filteringResults FilteringResult[]
@@index([programId])
@@index([status])
@@ -428,6 +441,9 @@ model Project {
// Mentorship
wantsMentorship Boolean @default(false)
// Founding date
foundedAt DateTime? // When the project/company was founded
// Submission links (external, from CSV)
phase1SubmissionUrl String?
phase2SubmissionUrl String?
@@ -464,6 +480,10 @@ model Project {
submittedBy User? @relation("ProjectSubmittedBy", fields: [submittedByUserId], references: [id], onDelete: SetNull)
teamMembers TeamMember[]
mentorAssignment MentorAssignment?
filteringResults FilteringResult[]
awardEligibilities AwardEligibility[]
awardVotes AwardVote[]
wonAwards SpecialAward[] @relation("AwardWinner")
@@index([roundId])
@@index([status])
@@ -975,3 +995,194 @@ model MentorAssignment {
@@index([mentorId])
@@index([method])
}
// =============================================================================
// FILTERING ROUND SYSTEM
// =============================================================================
enum FilteringOutcome {
PASSED
FILTERED_OUT
FLAGGED
}
enum FilteringRuleType {
FIELD_BASED
DOCUMENT_CHECK
AI_SCREENING
}
model FilteringRule {
id String @id @default(cuid())
roundId String
name String
ruleType FilteringRuleType
configJson Json @db.JsonB // Conditions, logic, action per rule type
priority Int @default(0)
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
@@index([roundId])
@@index([priority])
}
model FilteringResult {
id String @id @default(cuid())
roundId String
projectId String
outcome FilteringOutcome
ruleResultsJson Json? @db.JsonB // Per-rule results
aiScreeningJson Json? @db.JsonB // AI screening details
// Admin override
overriddenBy String?
overriddenAt DateTime?
overrideReason String? @db.Text
finalOutcome FilteringOutcome?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
round Round @relation(fields: [roundId], references: [id], onDelete: Cascade)
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
overriddenByUser User? @relation("FilteringOverriddenBy", fields: [overriddenBy], references: [id], onDelete: SetNull)
@@unique([roundId, projectId])
@@index([roundId])
@@index([projectId])
@@index([outcome])
}
// =============================================================================
// SPECIAL AWARDS SYSTEM
// =============================================================================
enum AwardScoringMode {
PICK_WINNER
RANKED
SCORED
}
enum AwardStatus {
DRAFT
NOMINATIONS_OPEN
VOTING_OPEN
CLOSED
ARCHIVED
}
enum EligibilityMethod {
AUTO
MANUAL
}
model SpecialAward {
id String @id @default(cuid())
programId String
name String
description String? @db.Text
status AwardStatus @default(DRAFT)
// Criteria
criteriaText String? @db.Text // Plain-language criteria for AI
autoTagRulesJson Json? @db.JsonB // Deterministic eligibility rules
// Scoring
scoringMode AwardScoringMode @default(PICK_WINNER)
maxRankedPicks Int? // For RANKED mode
// Voting window
votingStartAt DateTime?
votingEndAt DateTime?
// Evaluation form (for SCORED mode)
evaluationFormId String?
// Winner
winnerProjectId String?
winnerOverridden Boolean @default(false)
winnerOverriddenBy String?
sortOrder Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
program Program @relation(fields: [programId], references: [id], onDelete: Cascade)
winnerProject Project? @relation("AwardWinner", fields: [winnerProjectId], references: [id], onDelete: SetNull)
eligibilities AwardEligibility[]
jurors AwardJuror[]
votes AwardVote[]
@@index([programId])
@@index([status])
@@index([sortOrder])
}
model AwardEligibility {
id String @id @default(cuid())
awardId String
projectId String
method EligibilityMethod @default(AUTO)
eligible Boolean @default(false)
aiReasoningJson Json? @db.JsonB
// Admin override
overriddenBy String?
overriddenAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
award SpecialAward @relation(fields: [awardId], references: [id], onDelete: Cascade)
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
overriddenByUser User? @relation("AwardEligibilityOverriddenBy", fields: [overriddenBy], references: [id], onDelete: SetNull)
@@unique([awardId, projectId])
@@index([awardId])
@@index([projectId])
@@index([eligible])
}
model AwardJuror {
id String @id @default(cuid())
awardId String
userId String
createdAt DateTime @default(now())
// Relations
award SpecialAward @relation(fields: [awardId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([awardId, userId])
@@index([awardId])
@@index([userId])
}
model AwardVote {
id String @id @default(cuid())
awardId String
userId String
projectId String
rank Int? // For RANKED mode
votedAt DateTime @default(now())
// Relations
award SpecialAward @relation(fields: [awardId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@unique([awardId, userId, projectId])
@@index([awardId])
@@index([userId])
@@index([projectId])
}