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

@@ -5,8 +5,10 @@ import { Prisma, CompetitionCategory, OceanIssue, TeamMemberRole } from '@prisma
import {
createNotification,
notifyAdmins,
notifyProjectTeam,
NotificationTypes,
} from '../services/in-app-notification'
import { getFirstRoundForProgram } from '@/server/utils/round-helpers'
import { checkRateLimit } from '@/lib/rate-limit'
import { logAudit } from '@/server/utils/audit'
import { parseWizardConfig } from '@/lib/wizard-config'
@@ -458,6 +460,18 @@ export const applicationRouter = router({
},
})
// Auto-assign to first round if project has no roundId (edition-wide mode)
let assignedRound: { id: string; name: string; entryNotificationType: string | null } | null = null
if (!project.roundId) {
assignedRound = await getFirstRoundForProgram(ctx.prisma, program.id)
if (assignedRound) {
await ctx.prisma.project.update({
where: { id: project.id },
data: { roundId: assignedRound.id },
})
}
}
// Create team lead membership
await ctx.prisma.teamMember.create({
data: {
@@ -510,6 +524,7 @@ export const applicationRouter = router({
source: 'public_application_form',
title: data.projectName,
category: data.competitionCategory,
autoAssignedRound: assignedRound?.name || null,
},
ipAddress: ctx.ip,
userAgent: ctx.userAgent,
@@ -544,6 +559,26 @@ export const applicationRouter = router({
},
})
// Send SUBMISSION_RECEIVED notification if the round is configured for it
if (assignedRound?.entryNotificationType === 'SUBMISSION_RECEIVED') {
try {
await notifyProjectTeam(project.id, {
type: NotificationTypes.SUBMISSION_RECEIVED,
title: 'Submission Received',
message: `Your submission "${data.projectName}" has been received and is now under review.`,
linkUrl: `/team/projects/${project.id}`,
linkLabel: 'View Submission',
metadata: {
projectName: data.projectName,
roundName: assignedRound.name,
programName: program.name,
},
})
} catch {
// Never fail on notification
}
}
return {
success: true,
projectId: project.id,
@@ -816,6 +851,18 @@ export const applicationRouter = router({
},
})
// Auto-assign to first round if project has no roundId
let draftAssignedRound: { id: string; name: string; entryNotificationType: string | null } | null = null
if (!updated.roundId) {
draftAssignedRound = await getFirstRoundForProgram(ctx.prisma, updated.programId)
if (draftAssignedRound) {
await ctx.prisma.project.update({
where: { id: updated.id },
data: { roundId: draftAssignedRound.id },
})
}
}
// Audit log
try {
await logAudit({
@@ -828,6 +875,7 @@ export const applicationRouter = router({
source: 'draft_submission',
title: data.projectName,
category: data.competitionCategory,
autoAssignedRound: draftAssignedRound?.name || null,
},
ipAddress: ctx.ip,
userAgent: ctx.userAgent,
@@ -836,6 +884,25 @@ export const applicationRouter = router({
// Never throw on audit failure
}
// Send SUBMISSION_RECEIVED notification if the round is configured for it
if (draftAssignedRound?.entryNotificationType === 'SUBMISSION_RECEIVED') {
try {
await notifyProjectTeam(updated.id, {
type: NotificationTypes.SUBMISSION_RECEIVED,
title: 'Submission Received',
message: `Your submission "${data.projectName}" has been received and is now under review.`,
linkUrl: `/team/projects/${updated.id}`,
linkLabel: 'View Submission',
metadata: {
projectName: data.projectName,
roundName: draftAssignedRound.name,
},
})
} catch {
// Never fail on notification
}
}
return {
success: true,
projectId: updated.id,