feat: extend notification system with batch sender, bulk dialog, and logging
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>
This commit is contained in:
112
scripts/backfill-intake-round.ts
Normal file
112
scripts/backfill-intake-round.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* 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())
|
||||
Reference in New Issue
Block a user