Auto-assign projects to first round, auto-filter on close, pipeline UX consolidation

- New projects (admin create, CSV import, public form) auto-assign to program's
  first round (by sortOrder) when no round is specified
- Closing a FILTERING round auto-starts filtering job (configurable via
  autoFilterOnClose setting, defaults to true)
- Add SUBMISSION_RECEIVED notification type for confirming submissions
- Replace separate List/Pipeline toggle with integrated pipeline view below
  the sortable round list
- Add autoFilterOnClose toggle to filtering round type settings UI

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-12 15:06:11 +01:00
parent 2a5fa463b3
commit 7b85fd9602
11 changed files with 204 additions and 101 deletions

View File

@@ -4,9 +4,12 @@ import { Prisma } from '@prisma/client'
import { router, protectedProcedure, adminProcedure } from '../trpc'
import {
notifyRoundJury,
notifyAdmins,
NotificationTypes,
} from '../services/in-app-notification'
import { logAudit } from '@/server/utils/audit'
import { runFilteringJob } from './filtering'
import { prisma as globalPrisma } from '@/lib/prisma'
// Valid round status transitions (state machine)
const VALID_ROUND_TRANSITIONS: Record<string, string[]> = {
@@ -437,6 +440,60 @@ export const roundRouter = router({
}
}
// Auto-run filtering when a FILTERING round is closed (if enabled in settings)
const roundSettings = (round.settingsJson as Record<string, unknown>) || {}
const autoFilterEnabled = roundSettings.autoFilterOnClose !== false // Default to true
if (input.status === 'CLOSED' && round.roundType === 'FILTERING' && autoFilterEnabled) {
try {
const [filteringRules, projectCount] = await Promise.all([
ctx.prisma.filteringRule.findMany({
where: { roundId: input.id, isActive: true },
}),
ctx.prisma.project.count({ where: { roundId: input.id } }),
])
// Check for existing running job
const existingJob = await ctx.prisma.filteringJob.findFirst({
where: { roundId: input.id, status: 'RUNNING' },
})
if (filteringRules.length > 0 && projectCount > 0 && !existingJob) {
// Create filtering job
const job = await globalPrisma.filteringJob.create({
data: {
roundId: input.id,
status: 'PENDING',
totalProjects: projectCount,
},
})
// Start background execution (non-blocking)
setImmediate(() => {
runFilteringJob(job.id, input.id, ctx.user.id).catch(console.error)
})
// Notify admins that auto-filtering has started
await notifyAdmins({
type: NotificationTypes.FILTERING_COMPLETE,
title: 'Auto-Filtering Started',
message: `Filtering automatically started for "${round.name}" after closing. ${projectCount} projects will be processed.`,
linkUrl: `/admin/rounds/${input.id}/filtering`,
linkLabel: 'View Progress',
metadata: {
roundId: input.id,
roundName: round.name,
projectCount,
ruleCount: filteringRules.length,
autoTriggered: true,
},
})
}
} catch (error) {
// Auto-filtering failure should not block round closure
console.error('[Auto-Filtering] Failed to start:', error)
}
}
return round
}),