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:
@@ -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,
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user