docs(final-docs): spec for judge-doc curation + optional revised uploads
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user