docs(grand-final): 4-doc set (PDF-only), thin dedicated judge page rationale

Confirmed document set: Final Presentation, Final Business Plan, 1-min Video,
Executive Summary (all required, PDF-only docs), same for both categories.
Judge page stays a thin dedicated page reusing the existing doc viewer because
the finale has 0 assignments / empty jury group (group-based, not assignment-based).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt
2026-06-09 14:49:26 +02:00
parent 537de07245
commit c2afa58606

View File

@@ -13,7 +13,7 @@ Nine finalist teams must submit two final deliverables ahead of the Grand Final
The data layer already exists and is correctly set up:
- **"Grand Final" (LIVE_FINAL) round is `ROUND_ACTIVE`** with `windowCloseAt = 2026-06-11 21:00 UTC` and the empty **"Finals Jury"** group attached (`juryGroupId` set, **0 members**).
- **Two `FileRequirement` rows already exist on the Grand Final round** (legacy per-round system): **"PDF presentation support"** (`application/pdf`) and **"1 minute video"** (`video/*`). Both currently `required = false`, `maxSizeMB = null`, **0 files uploaded**.
- **Two `FileRequirement` rows already exist on the Grand Final round** (legacy per-round system): **"PDF presentation support"** (`application/pdf`) and **"1 minute video"** (`video/*`). Both currently `required = false`, `maxSizeMB = null`, **0 files uploaded**. This set is being expanded to the confirmed 4-document set below.
- **All 9 finalist teams are correctly enrolled**: each has a `ProjectRoundState` in both the (closed) Mentoring round and the (active) Grand Final round, and all 9 have `FinalistConfirmation.status = CONFIRMED`. No mismatches (confirmed↔enrolled↔in-mentor all aligned). All 9 share one mentor, **Camille Lopez**. Attendee counts 14 (program `defaultAttendeeCap = 4`).
- Auto-enroll (confirmed + in mentor round → Grand Final round) is working via `finalist.enrollFinalists`; the **admin override already exists** (`finalist.adminConfirm` to mark attending without a token; `finalist.unenroll` to remove) in the `/admin/logistics` **Confirmations tab** + attendance dialog.
@@ -30,6 +30,17 @@ The gaps are therefore: **discoverability** (no banner/notification), **judge re
3. Surface the final documents (and a pre-deadline cue) inside **the mentor section**, on both the team's and the mentor's views.
4. Add **email + in-app notifications**, triggered **automatically** (pre-deadline reminder cron) and **manually** (admin blast).
## Document set (confirmed)
The Grand Final round's `FileRequirement` rows are reconfigured to **four required documents**, identical for both categories (STARTUP and BUSINESS_CONCEPT) — the per-round `FileRequirement` model already applies one set to all teams in the round:
1. **Final Presentation**`application/pdf` (rename of the existing "PDF presentation support" row)
2. **Final Business Plan**`application/pdf` (new)
3. **1-minute Video**`video/*` (existing "1 minute video" row)
4. **Executive Summary**`application/pdf` (new)
All four `required = true`. PDF-only for the three document slots (no Word). This is an additive/safe prod data change (0 files currently uploaded). If per-category document sets are ever needed, that is out of scope here (the per-round model does not support it without extra work).
## Non-goals (YAGNI)
- Admin approve / needs-changes review workflow on documents.
@@ -63,7 +74,7 @@ A new service centralizes the logic, wrapped by thin tRPC procedures:
- New auto-hiding banner component (pattern of `LunchBanner`/`MyLogisticsCard`: returns `null` when not applicable) on `src/app/(applicant)/applicant/page.tsx`.
- Backed by a new query **`applicant.getFinalDocumentStatus`** (wraps `getFinalDocumentStatusForProject` for the caller's project).
- Shows: heading ("Upload your Grand Final documents"), the two deliverables each with a ✓ / empty state, deadline in browser-local time + zone, and a CTA button → `/applicant/documents`. Collapses to a "✓ Submitted" confirmation once all required documents are uploaded. Non-dismissible while incomplete.
- Shows: heading ("Upload your Grand Final documents"), each required document with a ✓ / empty state (e.g. "2 of 4 uploaded"), deadline in browser-local time + zone, and a CTA button → `/applicant/documents`. Collapses to a "✓ Submitted" confirmation once all required documents are uploaded. Non-dismissible while incomplete.
### 2. Mentor-section "Final Documents" panel (team + mentor)
@@ -74,9 +85,13 @@ A new read-only `FinalDocumentsPanel` component rendered on **both** mentor surf
Behavior: before upload + as the deadline nears, a **visual cue** ("Final grand-final documents due [date] — upload now", with an upload CTA on the team view); after upload, the PDF + video appear as the team's read-only "final documents" (inline preview / video player / download), visible to both the team and their mentor. Same underlying `ProjectFile`s — no duplicate storage.
### 3. Judge review page
### 3. Judge review page (thin dedicated page, reusing existing components)
- New read-only page (e.g. `src/app/(jury)/jury/finals-documents/page.tsx`) listing all finalist teams grouped by category, each with its two documents: PDF via inline `FilePreview`, video via inline `<video>` player, plus download. Missing documents show "Not yet uploaded". Header shows the deadline and "X of N submitted".
**Why dedicated rather than baking into the existing per-project jury page** (verified in prod): the finale has **0 `Assignment` rows**, the "Finals Jury" group has **0 members**, and no `LiveVotingSession` exists. The existing jury flow is assignment-gated — the round page lists `roundAssignment.getMyAssignments` (empty for the finale) and `file.listByProject` 403s any juror without an `Assignment` to the project. The finale runs on a **group + live-session** model, not per-project assignments. Baking in would require either fabricating an assignment per judge × finalist or rewiring the assignment-based access path — more work and risk, and a worse UX (one project at a time vs. all finalists at once). So a dedicated page is the better fit *because* the existing page's access model does not apply to the finale.
It stays thin by **reusing existing components** — the same `MultiWindowDocViewer` / `FilePreview` / `<video>` / `file.getDownloadUrl` used on the per-project page — laid out as a consolidated finale list. Not a reinvented viewer.
- New read-only page (e.g. `src/app/(jury)/jury/finals-documents/page.tsx`) listing all finalist teams grouped by category, each with its four documents (3 PDFs + video) via the reused viewer/preview components plus download. Missing documents show "Not yet uploaded". Header shows the deadline and "X of N submitted".
- Backed by a new **`finalist.listReviewDocuments`** procedure (wraps `listFinalistDocumentsForReview`). Authorization: **SUPER_ADMIN / PROGRAM_ADMIN, OR a `JuryGroupMember` of the active LIVE_FINAL round's jury group**, via a new `assertFinalsReviewAccess(ctx)` helper. Unauthorized → access-denied state.
- Entry points: a jury sidebar link ("Finalist Documents") shown when an active LIVE_FINAL round exists, and a "Review finalist documents" link on the admin Grand Final round overview.
- **Implementation note:** verify the `(jury)` route-group layout does not hard-redirect admins; if it does, either relax it for this page or mount the page on a neutral path reachable by both. Authorization is enforced by the procedure regardless.
@@ -90,7 +105,7 @@ Behavior: before upload + as the deadline nears, a **visual cue** ("Final grand-
### 5. Minor polish
- Flip the two `FileRequirement` rows to `required = true` (guarded prod data update at ship time, or via the admin round file-requirement editor if present).
- Reconfigure the round's `FileRequirement` rows to the 4-document set (rename "PDF presentation support" → "Final Presentation"; add "Final Business Plan" + "Executive Summary" as `application/pdf`; keep "1-minute Video"), all `required = true` (guarded prod data update at ship time, or via the admin round file-requirement editor if present). Additive/safe — 0 files uploaded.
- Confirm admins can edit the round's `windowCloseAt` in round settings (the admin-set deadline). If no input exists, add a small one; likely already present.
## Data model changes
@@ -115,7 +130,7 @@ Behavior: before upload + as the deadline nears, a **visual cue** ("Final grand-
Vitest (sequential, factories per `tests/helpers.ts`):
- `getFinalDocumentStatusForProject`: both uploaded / one uploaded / none; returns `null` for a non-confirmed team and when no active LIVE_FINAL round; `deadlinePassed` reflects `windowCloseAt`.
- `getFinalDocumentStatusForProject`: all 4 required uploaded / partial / none (`allRequiredUploaded` correct); returns `null` for a non-confirmed team and when no active LIVE_FINAL round; `deadlinePassed` reflects `windowCloseAt`.
- `listFinalistDocumentsForReview`: returns all finalist teams with correct per-requirement file mapping and `submittedCount`.
- Authorization matrix for `finalist.listReviewDocuments`: admin ✓, finale jury-group member ✓, non-finale jury member ✗, applicant ✗.
- `sendDueFinalDocReminders`: targets only CONFIRMED finalists with missing required docs inside the window; stamps `finalDocsReminderSentAt`; idempotent (no double-send).
@@ -127,7 +142,7 @@ Live-UI smoke on dev (lesson learned — catches what tests/build miss): banner
1. **Populate the "Finals Jury" group** with the actual judges (existing jury-group admin UI) — required before the review page is useful to them.
2. **Extend the Grand Final `windowCloseAt`** (currently 2026-06-11, ~2 days out) to the intended deadline.
3. Verify/flip the two requirements to `required = true`.
3. Reconfigure the round's file requirements to the 4-document set (Final Presentation, Final Business Plan, 1-minute Video, Executive Summary), all `required = true` — I can do this as a guarded prod update.
## Build sequence (shippable in phases; deadline is imminent)