7.0 KiB
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:
- Judges see too much.
listFinalistDocumentsForReviewreturns 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. - "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.allRequiredUploadedis hardcodedfalsewhen 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:
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
requirementIdis 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 likesetRevisedUploadSetting.
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:
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): whenhasRequiredis 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 whenallUploaded. - Panel (
src/components/applicant/final-documents-panel.tsx, team + mentor variants): "Submitted" badge driven byhasRequired ? 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
allowFinalistRevisedUploadsON - 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.
setReviewVisibleRequirementsround-trips null/array through configJson without clobbering other keys (allowFinalistRevisedUploads).- Status:
hasRequired/allUploadedacross 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
setRevisedUploadSettingpattern 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.