feat: edit-attendees dialog + roster card on applicant dashboard

Adds applicant.getMyFinalistConfirmation query (returns roster + cutoff
metadata for the team's confirmation, or null). New AttendingMembersCard
shows the confirmed attendee list and surfaces an Edit dialog to the
team lead — disabled past the editable cutoff. Card auto-hides until the
confirmation reaches CONFIRMED status.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt
2026-04-28 18:54:40 +02:00
parent 5b642c3d50
commit a6284e5c66
4 changed files with 373 additions and 0 deletions

View File

@@ -2705,4 +2705,73 @@ export const applicantRouter = router({
unreadCount,
}
}),
/**
* Returns the caller's project's finalist confirmation (if any) plus the
* data needed by the team-lead's "Edit attendees" dialog: the team roster,
* the current AttendingMember rows, the program cap, and the editable
* cutoff derived from the LIVE_FINAL round window.
*
* Returns null when the caller is not on a team with a confirmation.
*/
getMyFinalistConfirmation: protectedProcedure.query(async ({ ctx }) => {
const project = await ctx.prisma.project.findFirst({
where: { teamMembers: { some: { userId: ctx.user.id } } },
include: {
program: { select: { id: true, defaultAttendeeCap: true } },
teamMembers: {
include: {
user: { select: { id: true, name: true, email: true } },
},
orderBy: { joinedAt: 'asc' },
},
finalistConfirmation: {
include: {
attendingMembers: { select: { userId: true, needsVisa: true } },
},
},
},
orderBy: { createdAt: 'desc' },
})
if (!project || !project.finalistConfirmation) return null
const callerMember = project.teamMembers.find((tm) => tm.userId === ctx.user.id)
const isLead = callerMember?.role === 'LEAD'
let cutoffAt: Date | null = null
let editableNow = true
const round = await ctx.prisma.round.findFirst({
where: { competition: { programId: project.program.id }, roundType: 'LIVE_FINAL' },
orderBy: { sortOrder: 'desc' },
select: { windowOpenAt: true, configJson: true },
})
if (round?.windowOpenAt) {
const cfg = (round.configJson ?? {}) as { attendeeEditCutoffHours?: number }
const cutoffHours = cfg.attendeeEditCutoffHours ?? 48
cutoffAt = new Date(round.windowOpenAt.getTime() - cutoffHours * 3_600_000)
editableNow = Date.now() <= cutoffAt.getTime()
}
return {
project: {
id: project.id,
title: project.title,
teamMembers: project.teamMembers.map((tm) => ({
userId: tm.userId,
role: tm.role,
user: tm.user,
})),
program: { defaultAttendeeCap: project.program.defaultAttendeeCap },
},
confirmation: {
id: project.finalistConfirmation.id,
status: project.finalistConfirmation.status,
attendingMembers: project.finalistConfirmation.attendingMembers,
},
isLead,
cutoffAt,
editableNow,
}
}),
})