feat: program.getEditionSettings + updateEditionSettings
Backs the new consolidated Edition tab on /admin/settings. getEditionSettings returns a merged view of Program-level fields (defaultAttendeeCap, visaStatusVisibleToMembers) plus LIVE_FINAL round config (attendeeEditCutoffHours, confirmationWindowHours, with sensible defaults). Round-derived values are null when the round doesn't exist yet. updateEditionSettings is partial — only supplied fields are written. Round config writes merge into the existing configJson so other keys are preserved. Audit-logged as PROGRAM_EDITION_SETTINGS_UPDATE. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -281,4 +281,114 @@ export const programRouter = router({
|
||||
|
||||
return { success: true }
|
||||
}),
|
||||
|
||||
/**
|
||||
* Returns the merged edition-settings view for the admin Settings page:
|
||||
* Program fields (defaultAttendeeCap, visaStatusVisibleToMembers) plus the
|
||||
* LIVE_FINAL round's configJson values (attendeeEditCutoffHours and
|
||||
* confirmationWindowHours, with sensible defaults). Round-derived values
|
||||
* are null when the LIVE_FINAL round doesn't exist yet.
|
||||
*/
|
||||
getEditionSettings: adminProcedure
|
||||
.input(z.object({ programId: z.string() }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const program = await ctx.prisma.program.findUniqueOrThrow({
|
||||
where: { id: input.programId },
|
||||
select: {
|
||||
id: true,
|
||||
defaultAttendeeCap: true,
|
||||
visaStatusVisibleToMembers: true,
|
||||
},
|
||||
})
|
||||
const round = await ctx.prisma.round.findFirst({
|
||||
where: {
|
||||
competition: { programId: input.programId },
|
||||
roundType: 'LIVE_FINAL',
|
||||
},
|
||||
orderBy: { sortOrder: 'desc' },
|
||||
select: { id: true, configJson: true },
|
||||
})
|
||||
const cfg = (round?.configJson ?? {}) as Record<string, unknown>
|
||||
return {
|
||||
programId: program.id,
|
||||
defaultAttendeeCap: program.defaultAttendeeCap,
|
||||
visaStatusVisibleToMembers: program.visaStatusVisibleToMembers,
|
||||
liveFinalRoundId: round?.id ?? null,
|
||||
attendeeEditCutoffHours: round
|
||||
? ((cfg.attendeeEditCutoffHours as number | undefined) ?? 48)
|
||||
: null,
|
||||
confirmationWindowHours: round
|
||||
? ((cfg.confirmationWindowHours as number | undefined) ?? 24)
|
||||
: null,
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Partial update for edition settings. Writes Program fields directly and
|
||||
* merges round-config keys (attendeeEditCutoffHours, confirmationWindowHours)
|
||||
* into the LIVE_FINAL round's configJson, preserving any unrelated keys
|
||||
* already in the JSON blob.
|
||||
*/
|
||||
updateEditionSettings: adminProcedure
|
||||
.input(
|
||||
z.object({
|
||||
programId: z.string(),
|
||||
defaultAttendeeCap: z.number().int().min(1).max(20).optional(),
|
||||
visaStatusVisibleToMembers: z.boolean().optional(),
|
||||
attendeeEditCutoffHours: z.number().int().min(0).max(720).optional(),
|
||||
confirmationWindowHours: z.number().int().min(1).max(720).optional(),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const programData: Record<string, unknown> = {}
|
||||
if (input.defaultAttendeeCap !== undefined) {
|
||||
programData.defaultAttendeeCap = input.defaultAttendeeCap
|
||||
}
|
||||
if (input.visaStatusVisibleToMembers !== undefined) {
|
||||
programData.visaStatusVisibleToMembers = input.visaStatusVisibleToMembers
|
||||
}
|
||||
|
||||
if (Object.keys(programData).length > 0) {
|
||||
await ctx.prisma.program.update({
|
||||
where: { id: input.programId },
|
||||
data: programData,
|
||||
})
|
||||
}
|
||||
|
||||
const roundConfigKeys = ['attendeeEditCutoffHours', 'confirmationWindowHours'] as const
|
||||
const roundUpdates: Record<string, number> = {}
|
||||
for (const k of roundConfigKeys) {
|
||||
const v = input[k]
|
||||
if (v !== undefined) roundUpdates[k] = v
|
||||
}
|
||||
if (Object.keys(roundUpdates).length > 0) {
|
||||
const round = await ctx.prisma.round.findFirst({
|
||||
where: {
|
||||
competition: { programId: input.programId },
|
||||
roundType: 'LIVE_FINAL',
|
||||
},
|
||||
orderBy: { sortOrder: 'desc' },
|
||||
select: { id: true, configJson: true },
|
||||
})
|
||||
if (round) {
|
||||
const existing = (round.configJson ?? {}) as Record<string, unknown>
|
||||
const merged = { ...existing, ...roundUpdates }
|
||||
await ctx.prisma.round.update({
|
||||
where: { id: round.id },
|
||||
data: { configJson: merged as Prisma.InputJsonValue },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
await logAudit({
|
||||
prisma: ctx.prisma,
|
||||
userId: ctx.user.id,
|
||||
action: 'PROGRAM_EDITION_SETTINGS_UPDATE',
|
||||
entityType: 'Program',
|
||||
entityId: input.programId,
|
||||
detailsJson: { ...input },
|
||||
})
|
||||
|
||||
return { ok: true }
|
||||
}),
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user