From d38fe7887a10434492675376a260d250089051d5 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 10 Jun 2026 15:02:19 +0200 Subject: [PATCH] feat(final-docs): optional-mode banner/panel + admin 'Documents shown to judges' picker - finalist banner/panel render an optional-uploads state (settle to green via hasRequired ? allRequiredUploaded : allUploaded; 'optional' copy when nothing required) - ReviewDocsPicker admin card on the LIVE_FINAL round page to curate judge-visible docs --- .../(admin)/admin/rounds/[roundId]/page.tsx | 2 + .../admin/grand-finale/review-docs-picker.tsx | 79 +++++++++++++++++++ .../applicant/final-documents-banner.tsx | 9 ++- .../applicant/final-documents-panel.tsx | 10 ++- 4 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 src/components/admin/grand-finale/review-docs-picker.tsx diff --git a/src/app/(admin)/admin/rounds/[roundId]/page.tsx b/src/app/(admin)/admin/rounds/[roundId]/page.tsx index 6effb46..c9bbcfd 100644 --- a/src/app/(admin)/admin/rounds/[roundId]/page.tsx +++ b/src/app/(admin)/admin/rounds/[roundId]/page.tsx @@ -98,6 +98,7 @@ import { WaitlistCard } from '@/components/admin/grand-finale/waitlist-card' import { FinalistEnrollmentCard } from '@/components/admin/grand-finale/finalist-enrollment-card' import { FinalDocsReminderButton } from '@/components/admin/grand-finale/final-docs-reminder-button' import { FinalDocsUploadsToggle } from '@/components/admin/grand-finale/final-docs-uploads-toggle' +import { ReviewDocsPicker } from '@/components/admin/grand-finale/review-docs-picker' import { RankingDashboard } from '@/components/admin/round/ranking-dashboard' import { CoverageReport } from '@/components/admin/assignment/coverage-report' import { AssignmentPreviewSheet } from '@/components/admin/assignment/assignment-preview-sheet' @@ -1543,6 +1544,7 @@ export default function RoundDetailPage() { +
diff --git a/src/components/admin/grand-finale/review-docs-picker.tsx b/src/components/admin/grand-finale/review-docs-picker.tsx new file mode 100644 index 0000000..4dbe710 --- /dev/null +++ b/src/components/admin/grand-finale/review-docs-picker.tsx @@ -0,0 +1,79 @@ +'use client' + +import { trpc } from '@/lib/trpc/client' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Checkbox } from '@/components/ui/checkbox' +import { Switch } from '@/components/ui/switch' +import { Label } from '@/components/ui/label' +import { toast } from 'sonner' +import { Eye } from 'lucide-react' + +/** + * Admin picker: which previously-submitted documents finale judges see on the + * review page. Default (switch off) shows everything; switching to curated + * mode starts with all slots ticked, and the admin unticks what to hide. + * Grand Final round uploads are always visible regardless. + */ +export function ReviewDocsPicker({ programId, roundId }: { programId: string; roundId: string }) { + const utils = trpc.useUtils() + const { data } = trpc.finalist.getReviewDocSettings.useQuery({ programId, roundId }) + const set = trpc.finalist.setReviewVisibleRequirements.useMutation({ + onSuccess: () => utils.finalist.getReviewDocSettings.invalidate({ programId, roundId }), + onError: (e) => toast.error(e.message), + }) + if (!data || data.options.length === 0) return null + + const curated = data.selectedIds !== null + const selected = new Set(data.selectedIds ?? data.options.map((o) => o.requirementId)) + const toggleSlot = (id: string, on: boolean) => { + const next = new Set(selected) + if (on) next.add(id) + else next.delete(id) + set.mutate({ roundId, requirementIds: [...next] }) + } + + return ( + + + + Documents shown to judges + + + Choose which previously submitted documents judges see on the finalist review page. + Documents uploaded directly to this Grand Final round are always visible. + + + +
+ + set.mutate({ roundId, requirementIds: v ? data.options.map((o) => o.requirementId) : null })} + /> + +
+ {curated && ( +
+ {data.options.map((o) => ( + + ))} +
+ )} +
+
+ ) +} diff --git a/src/components/applicant/final-documents-banner.tsx b/src/components/applicant/final-documents-banner.tsx index 614be06..aadd850 100644 --- a/src/components/applicant/final-documents-banner.tsx +++ b/src/components/applicant/final-documents-banner.tsx @@ -8,14 +8,15 @@ import { FileText, Video, CheckCircle2, Circle, Clock, Upload } from 'lucide-rea export function FinalDocumentsBanner() { const { data: status } = trpc.applicant.getFinalDocumentStatus.useQuery() - if (!status) return null + if (!status || status.requirements.length === 0) return null const fmt = new Intl.DateTimeFormat(undefined, { dateStyle: 'long', timeStyle: 'short' }) const zone = new Intl.DateTimeFormat(undefined, { timeZoneName: 'short' }) .formatToParts(new Date()).find((p) => p.type === 'timeZoneName')?.value const uploadedCount = status.requirements.filter((r) => r.uploaded).length const total = status.requirements.length - const done = status.allRequiredUploaded + const optionalMode = !status.hasRequired + const done = status.hasRequired ? status.allRequiredUploaded : status.allUploaded return ( @@ -24,7 +25,9 @@ export function FinalDocumentsBanner() {
{done ? : } - {done ? 'Grand Final documents submitted' : 'Upload your Grand Final documents'} + {done + ? optionalMode ? 'Grand Final documents uploaded' : 'Grand Final documents submitted' + : optionalMode ? 'Upload updated Grand Final documents (optional)' : 'Upload your Grand Final documents'} ({uploadedCount} of {total})
diff --git a/src/components/applicant/final-documents-panel.tsx b/src/components/applicant/final-documents-panel.tsx index fbe0902..934eff3 100644 --- a/src/components/applicant/final-documents-panel.tsx +++ b/src/components/applicant/final-documents-panel.tsx @@ -18,7 +18,8 @@ export function FinalDocumentsPanel(props: Props) { { enabled: props.variant === 'mentor' }, ) const status = props.variant === 'team' ? teamQuery.data : mentorQuery.data - if (!status) return null + if (!status || status.requirements.length === 0) return null + const done = status.hasRequired ? status.allRequiredUploaded : status.allUploaded const fmt = new Intl.DateTimeFormat(undefined, { dateStyle: 'long', timeStyle: 'short' }) return ( @@ -26,8 +27,8 @@ export function FinalDocumentsPanel(props: Props) {
Final Documents - {status.allRequiredUploaded - ? Submitted + {done + ? {status.hasRequired ? 'Submitted' : 'Uploaded'} : status.deadline && ( Due {fmt.format(new Date(status.deadline))} @@ -36,6 +37,7 @@ export function FinalDocumentsPanel(props: Props) {
{props.variant === 'team' ? 'Your final deliverables for the Grand Finale.' : 'This team\'s final deliverables for the Grand Finale.'} + {!status.hasRequired && ' These uploads are optional.'}
@@ -48,7 +50,7 @@ export function FinalDocumentsPanel(props: Props) { {r.file?.fileName ?? 'Not yet uploaded'}
))} - {props.variant === 'team' && !status.allRequiredUploaded && ( + {props.variant === 'team' && !done && (