143 lines
6.2 KiB
Markdown
143 lines
6.2 KiB
Markdown
|
|
# PR 5: Settings Consolidation Implementation Plan
|
||
|
|
|
||
|
|
> **For agentic workers:** Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
|
|
||
|
|
**Goal:** Consolidate per-edition logistics configuration onto `/admin/settings` so admins find every knob in one place. Remove dead Logistics tabs and move the visa-visibility toggle out of the Visas tab.
|
||
|
|
|
||
|
|
**Architecture:** A new "Edition" tab on `/admin/settings` reads + writes a merged view: `Program` fields (defaultAttendeeCap, visaStatusVisibleToMembers) + the LIVE_FINAL round's `configJson` (attendeeEditCutoffHours, confirmationWindowHours). One reader (`program.getEditionSettings`) + one writer (`program.updateEditionSettings`) so the UI stays simple.
|
||
|
|
|
||
|
|
**Tech Stack:** Prisma 6 (no migration — purely a UX layer over existing fields), tRPC, Vitest 4 for the procedure tests, shadcn/ui for the tab + sub-sections.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Task 1: tRPC procedures for edition settings (TDD)
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Modify: `src/server/routers/program.ts`
|
||
|
|
- Create: `tests/unit/program-edition-settings.test.ts`
|
||
|
|
|
||
|
|
- [ ] **Step 1: Failing tests**
|
||
|
|
|
||
|
|
```ts
|
||
|
|
describe('program.getEditionSettings', () => {
|
||
|
|
it('returns the merged view (program fields + LIVE_FINAL round config)', async () => {
|
||
|
|
// setup: program with defaultAttendeeCap=5, visaStatusVisibleToMembers=false
|
||
|
|
// + LIVE_FINAL round with configJson { attendeeEditCutoffHours: 24, confirmationWindowHours: 12 }
|
||
|
|
// assert: shape { defaultAttendeeCap, visaStatusVisibleToMembers, attendeeEditCutoffHours, confirmationWindowHours }
|
||
|
|
})
|
||
|
|
|
||
|
|
it('falls back to defaults when LIVE_FINAL round has no config', async () => {
|
||
|
|
// assert: attendeeEditCutoffHours = 48, confirmationWindowHours = 24
|
||
|
|
})
|
||
|
|
|
||
|
|
it('returns nulls for round-derived fields if no LIVE_FINAL round exists', async () => {
|
||
|
|
// assert: attendeeEditCutoffHours = null, confirmationWindowHours = null
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
describe('program.updateEditionSettings', () => {
|
||
|
|
it('writes program fields + round configJson + audit-logs', async () => {
|
||
|
|
// call with partial { defaultAttendeeCap: 4, attendeeEditCutoffHours: 36 }
|
||
|
|
// assert: program.defaultAttendeeCap=4, round.configJson.attendeeEditCutoffHours=36
|
||
|
|
// assert: AuditLog action=PROGRAM_EDITION_SETTINGS_UPDATE
|
||
|
|
})
|
||
|
|
|
||
|
|
it('preserves untouched configJson keys', async () => {
|
||
|
|
// round.configJson initially { foo: 'bar', attendeeEditCutoffHours: 48 }
|
||
|
|
// call with { attendeeEditCutoffHours: 24 }
|
||
|
|
// assert: round.configJson = { foo: 'bar', attendeeEditCutoffHours: 24 }
|
||
|
|
})
|
||
|
|
})
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 2: Run failing tests**.
|
||
|
|
|
||
|
|
- [ ] **Step 3: Implement getEditionSettings**
|
||
|
|
|
||
|
|
```ts
|
||
|
|
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,
|
||
|
|
}
|
||
|
|
}),
|
||
|
|
```
|
||
|
|
|
||
|
|
- [ ] **Step 4: Implement updateEditionSettings** with merge semantics on configJson + audit log.
|
||
|
|
|
||
|
|
- [ ] **Step 5: Run tests, expect green**.
|
||
|
|
|
||
|
|
- [ ] **Step 6: Commit**.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Task 2: Edition Settings tab UI
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Create: `src/components/admin/settings/edition-settings-tab.tsx`
|
||
|
|
- Modify: `src/components/settings/settings-content.tsx` (add the new tab + entry)
|
||
|
|
|
||
|
|
- [ ] **Step 1: Build the Edition Settings tab**
|
||
|
|
|
||
|
|
Three sub-sections (Card per section):
|
||
|
|
1. **Grand-finale logistics** — `defaultAttendeeCap` (number input), `attendeeEditCutoffHours` (number input with "hours before LIVE_FINAL" hint), `confirmationWindowHours` (number input with "hours from email send" hint).
|
||
|
|
2. **Visa** — `visaStatusVisibleToMembers` Switch + caption.
|
||
|
|
3. **Coming soon** — placeholder for Lunch + Email Templates that PRs 6 + 7 will fill.
|
||
|
|
|
||
|
|
Each input fires `program.updateEditionSettings.useMutation` debounced or on-blur. Toast on success.
|
||
|
|
|
||
|
|
- [ ] **Step 2: Wire into `/admin/settings`** — add `<TabsTrigger value="edition">` and `<TabsContent value="edition">` in settings-content. Place before existing tabs.
|
||
|
|
|
||
|
|
- [ ] **Step 3: Live smoke** — open settings, verify it renders, toggle visa visibility, verify it persists in the DB and the applicant dashboard hides the surface.
|
||
|
|
|
||
|
|
- [ ] **Step 4: Commit**.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Task 3: Cleanup — remove dead Logistics tabs + visibility toggle
|
||
|
|
|
||
|
|
**Files:**
|
||
|
|
- Modify: `src/app/(admin)/admin/logistics/page.tsx`
|
||
|
|
- Modify: `src/components/admin/logistics/visas-tab.tsx`
|
||
|
|
|
||
|
|
- [ ] **Step 1: Remove disabled tabs**
|
||
|
|
|
||
|
|
Drop the `<TabsTrigger value="documents" disabled>` and `<TabsTrigger value="settings" disabled>` blocks. Also drop their unused imports (`FileText`, `Settings`).
|
||
|
|
|
||
|
|
- [ ] **Step 2: Replace visibility toggle with a hint**
|
||
|
|
|
||
|
|
In `visas-tab.tsx`, swap the inline Switch + Label for a small banner: "Configure visibility in Settings → Edition" with a Link to `/admin/settings?tab=edition`. Drop the `getVisaVisibility` query and `setVisaVisibility` mutation usage from this component (keep the procedures — used by the new settings page).
|
||
|
|
|
||
|
|
- [ ] **Step 3: Live smoke** — confirm Logistics tab bar shows only the active tabs, visas tab no longer has a toggle, settings page does.
|
||
|
|
|
||
|
|
- [ ] **Step 4: Commit**.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Task 4: Final verification
|
||
|
|
|
||
|
|
- [ ] **Step 1: Full vitest** — `npx vitest run`. Expect 161 + new tests (~5).
|
||
|
|
- [ ] **Step 2: Typecheck** — clean.
|
||
|
|
- [ ] **Step 3: Build** — clean.
|
||
|
|
- [ ] **Step 4: E2E smoke** — open `/admin/settings` → Edition tab, change knobs, verify persistence; confirm the Visas tab no longer shows the toggle and the hint links to settings.
|