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:
Matt
2026-04-28 19:59:31 +02:00
parent 62ab27a05a
commit 78992a493a
3 changed files with 429 additions and 0 deletions

View File

@@ -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 }
}),
})