# Grand Final: judge-visible document curation + optional revised uploads

**Date:** 2026-06-09
**Status:** Approved (Matt, this session)
**Builds on:** `2026-06-09-grand-final-documents-design.md` and the same-day pivot (commits `f8f2d77`, `8a4184d`)

## Problem

Feedback from the other program admin:

> Jury actually need to see BP + Exec summary + 1min video — the ones they uploaded already. And candidates should be able to upload their PDF pres + video — optional, as some sent it another way.

Two gaps against what is deployed:

1. **Judges see too much.** `listFinalistDocumentsForReview` returns *every* file each finalist team ever submitted (5–7 per team on prod: Pitch Deck, Intro Video, Executive Summary, Business Plan, Promotional Video, plus any Grand Final uploads). The admin wants judges to see a curated subset (BP + exec summary + 1-min video). There is no way to choose which prior documents are surfaced.
2. **"Optional uploads" mode renders wrong.** The three modes the admin wants are: no new uploads (toggle OFF — works), mandatory uploads (toggle ON + slots required — works), and optional uploads (toggle ON + slots marked not-required). In the all-optional case, `FinalDocumentStatus.allRequiredUploaded` is hardcoded `false` when zero slots are required, so the finalist banner/panel never reach a settled state and the copy implies the docs are mandatory.

Prod facts (verified 2026-06-09 via read-only query): 9 finalist teams, 48 prior files + 3 Grand Final uploads. Every team has all 5 prior doc types. The Business Plan and the 1-minute promo video live under *two different* `FileRequirement` rows depending on the team's path (Semi-Finals Document Submission for 8 teams, Spotlight on Africa Submission Round for 1 team — Blue Fields Company).

## Part 1 — Admin curation of judge-visible documents

### Storage

New optional key on the LIVE_FINAL round's `configJson`:

```ts
reviewVisibleRequirementIds?: string[]  // FileRequirement ids from prior rounds
```

Semantics:
- **absent / null** → show all prior files (current behavior; safe default, no migration needed)
- **non-empty array** → show only prior files whose `requirementId` is in the list
- **empty array** → hide all prior files (only Grand Final uploads remain visible)
- **Grand Final round uploads are always shown**, regardless of the selection — they are what the team explicitly submitted for the finale
- Prior files with no `requirementId` (fileType-only) are excluded whenever a selection is active. (All 48 prod files have a requirement, so nothing is lost in practice.)

### Service (`src/server/services/final-documents.ts`)

`listFinalistDocumentsForReview` adds `requirementId` to its file select and applies the filter above using the finale round's `configJson`. No signature change; `ReviewPayload` unchanged.

New helper to power the admin picker: list the distinct prior-round requirement slots referenced by the finalist teams' files — `{ requirementId, name, roundName, fileCount }`, ordered by round sort then name. Derived from the same file query, so the picker only offers slots that actually have files.

### tRPC (`src/server/routers/finalist.ts`, adminProcedure)

- `getReviewDocSettings` → `{ options: Slot[], selectedIds: string[] | null }` (null = "all" mode)
- `setReviewVisibleRequirements({ requirementIds: string[] | null })` → writes/clears the configJson key (null clears back to "show all"). Audited like `setRevisedUploadSetting`.

### Admin UI

New card "Documents shown to judges" placed next to the existing revised-uploads toggle (`src/components/admin/grand-finale/final-docs-uploads-toggle.tsx`, rendered on the LIVE_FINAL round admin page `src/app/(admin)/admin/rounds/[roundId]/page.tsx`):

- A "Show all submitted documents" master state (the default), and beneath it a checkbox per slot labeled `"<requirement name> — <round name>"` with the file count (e.g. "Business Plan — Semi-Finals Document Submission (8 files)").
- Unchecking the master switches to curated mode with all boxes ticked; the admin then unticks what judges shouldn't see. Re-checking the master clears the selection (back to null/"all").
- Copy notes that Grand Final uploads are always visible to judges.

For the admin's stated goal, they'd switch to curated mode and leave 5 boxes ticked: Executive Summary (Intake), Business Plan (Semi-Finals + Spotlight), Promotional Video (Semi-Finals) and 1 Minute Promotional Video (Spotlight) — judges then see exactly BP + exec summary + 1-min video per team, plus any finale uploads.

## Part 2 — All-optional upload mode fix

`FinalDocumentStatus` (in `final-documents.ts`) gains:

```ts
hasRequired: boolean   // any slot with isRequired
allUploaded: boolean   // requirements.length > 0 && every slot has a file, required or not
```

`allRequiredUploaded` keeps its current semantics (meaningful only when `hasRequired`). Edge case: if the toggle is ON but no slots are defined at all (`requirements.length === 0`), the banner and panel render nothing — no vacuous "(0 of 0)" complete state.

UI changes:
- **Banner** (`src/components/applicant/final-documents-banner.tsx`): when `hasRequired` is false — title "Upload updated Grand Final documents (optional)", same neutral blue styling, keep per-doc checklist/count/deadline/upload button; green settled state ("Grand Final documents uploaded") only when `allUploaded`.
- **Panel** (`src/components/applicant/final-documents-panel.tsx`, team + mentor variants): "Submitted" badge driven by `hasRequired ? allRequiredUploaded : allUploaded`; description gains "(optional)" when nothing is required.

Reminders need **no change** — verified: the cron and the untargeted manual blast already skip teams with no missing *required* docs, so all-optional mode never nags; an explicitly targeted manual reminder still sends (intentional admin override).

## Out of scope (admin actions in existing UI, not code)

- Flipping `allowFinalistRevisedUploads` ON
- Creating/adjusting the finale upload slots (PDF presentation + 1-min video, "Required" off) in the round's file-requirements editor
- Ticking the curation checkboxes
- Populating the Finals Jury group (still open from the previous ship)

## Testing

Vitest service tests (extend `tests/` final-documents coverage):
- Curation: null selection → all files; selection → only matching prior files + finale uploads always; empty array → finale uploads only; file without requirementId excluded under a selection.
- Picker helper returns distinct slots with correct counts.
- `setReviewVisibleRequirements` round-trips null/array through configJson without clobbering other keys (`allowFinalistRevisedUploads`).
- Status: `hasRequired`/`allUploaded` across mixed, all-optional (0 required), and fully-uploaded fixtures.

## Risks

- **configJson clobbering:** both toggles write the same JSON column — read-modify-write must preserve sibling keys (existing `setRevisedUploadSetting` pattern already does this; reuse it).
- **Stale selection:** if a selected requirement is later deleted, its files simply stop matching; "all" fallback never breaks. No cleanup needed.
