Add NotificationLog schema extensions (nullable userId, email, roundId, projectId, batchId fields), batch notification sender service, and bulk notification dialog UI. Include utility scripts for debugging and seeding. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
113 lines
3.3 KiB
TypeScript
113 lines
3.3 KiB
TypeScript
/**
|
|
* Backfill all projects into the intake round (and any intermediate rounds
|
|
* between intake and their earliest assigned round) with COMPLETED state.
|
|
*
|
|
* Usage: npx tsx scripts/backfill-intake-round.ts
|
|
* Add --dry-run to preview without making changes.
|
|
*/
|
|
|
|
import { PrismaClient } from '@prisma/client'
|
|
|
|
const prisma = new PrismaClient()
|
|
const dryRun = process.argv.includes('--dry-run')
|
|
|
|
async function main() {
|
|
console.log(dryRun ? '🔍 DRY RUN — no changes will be made\n' : '🚀 Backfilling intake round states...\n')
|
|
|
|
// Find the intake round
|
|
const intakeRound = await prisma.round.findFirst({
|
|
where: { roundType: 'INTAKE' },
|
|
select: { id: true, name: true, sortOrder: true, competitionId: true },
|
|
})
|
|
|
|
if (!intakeRound) {
|
|
console.log('❌ No INTAKE round found')
|
|
return
|
|
}
|
|
|
|
console.log(`Intake round: "${intakeRound.name}" (sortOrder: ${intakeRound.sortOrder})`)
|
|
|
|
// Get all rounds in the competition ordered by sortOrder
|
|
const allRounds = await prisma.round.findMany({
|
|
where: { competitionId: intakeRound.competitionId },
|
|
select: { id: true, name: true, sortOrder: true },
|
|
orderBy: { sortOrder: 'asc' },
|
|
})
|
|
|
|
// Find all projects NOT in the intake round
|
|
const projects = await prisma.project.findMany({
|
|
where: {
|
|
projectRoundStates: {
|
|
none: { roundId: intakeRound.id },
|
|
},
|
|
},
|
|
select: {
|
|
id: true,
|
|
title: true,
|
|
projectRoundStates: {
|
|
select: { roundId: true, round: { select: { sortOrder: true } } },
|
|
orderBy: { round: { sortOrder: 'asc' } },
|
|
},
|
|
},
|
|
})
|
|
|
|
console.log(`${projects.length} projects not in intake round\n`)
|
|
|
|
if (projects.length === 0) {
|
|
console.log('✅ All projects already in intake round')
|
|
return
|
|
}
|
|
|
|
// For each project, create COMPLETED states for intake + any intermediate rounds
|
|
const toCreate: Array<{ projectId: string; roundId: string; state: 'COMPLETED' }> = []
|
|
|
|
for (const project of projects) {
|
|
// Find the earliest round this project is already in
|
|
const earliestSortOrder = project.projectRoundStates.length > 0
|
|
? Math.min(...project.projectRoundStates.map(ps => ps.round.sortOrder))
|
|
: Infinity
|
|
|
|
const existingRoundIds = new Set(project.projectRoundStates.map(ps => ps.roundId))
|
|
|
|
// Add COMPLETED for intake + all intermediate rounds before the earliest assigned round
|
|
for (const round of allRounds) {
|
|
if (round.sortOrder >= earliestSortOrder) break
|
|
if (existingRoundIds.has(round.id)) continue
|
|
|
|
toCreate.push({
|
|
projectId: project.id,
|
|
roundId: round.id,
|
|
state: 'COMPLETED',
|
|
})
|
|
}
|
|
}
|
|
|
|
console.log(`Creating ${toCreate.length} ProjectRoundState records...`)
|
|
|
|
if (!dryRun) {
|
|
await prisma.projectRoundState.createMany({
|
|
data: toCreate,
|
|
skipDuplicates: true,
|
|
})
|
|
}
|
|
|
|
// Summary by round
|
|
const byRound = new Map<string, number>()
|
|
for (const r of toCreate) {
|
|
const name = allRounds.find(ar => ar.id === r.roundId)?.name ?? r.roundId
|
|
byRound.set(name, (byRound.get(name) ?? 0) + 1)
|
|
}
|
|
for (const [name, count] of byRound) {
|
|
console.log(` ${name}: ${count} projects`)
|
|
}
|
|
|
|
console.log(`\n✅ Done! ${toCreate.length} records ${dryRun ? 'would be' : ''} created`)
|
|
}
|
|
|
|
main()
|
|
.catch((e) => {
|
|
console.error('❌ Error:', e)
|
|
process.exit(1)
|
|
})
|
|
.finally(() => prisma.$disconnect())
|