diff --git a/src/app/(admin)/admin/finals-documents/page.tsx b/src/app/(admin)/admin/finals-documents/page.tsx new file mode 100644 index 0000000..3ffb106 --- /dev/null +++ b/src/app/(admin)/admin/finals-documents/page.tsx @@ -0,0 +1,7 @@ +import { FinalsDocumentsReview } from '@/components/finals/finals-documents-review' + +export const dynamic = 'force-dynamic' + +export default function AdminFinalsDocumentsPage() { + return +} diff --git a/src/app/(admin)/admin/rounds/[roundId]/page.tsx b/src/app/(admin)/admin/rounds/[roundId]/page.tsx index 072a2d9..c4e6d0f 100644 --- a/src/app/(admin)/admin/rounds/[roundId]/page.tsx +++ b/src/app/(admin)/admin/rounds/[roundId]/page.tsx @@ -1532,7 +1532,7 @@ export default function RoundDetailPage() {
- )} -
- {doc.file ? ( - - ) : ( -

- Not yet uploaded -

- )} - - ))} - - - ))} - - ) + return } diff --git a/src/components/finals/finals-documents-review.tsx b/src/components/finals/finals-documents-review.tsx new file mode 100644 index 0000000..0f59293 --- /dev/null +++ b/src/components/finals/finals-documents-review.tsx @@ -0,0 +1,147 @@ +'use client' + +import { trpc } from '@/lib/trpc/client' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' +import { Button } from '@/components/ui/button' +import { Skeleton } from '@/components/ui/skeleton' +import { FilePreview } from '@/components/shared/file-viewer' +import { FileText, Download, ShieldAlert } from 'lucide-react' + +/** + * Read-only judge review of finalist grand-final documents. Self-resolves the + * active program and self-gates on the `finalist.listReviewDocuments` FORBIDDEN + * check, so it can be mounted on any route reachable by either the finals jury + * (`/jury/finals-documents`) or program admins (`/admin/finals-documents`). + */ +export function FinalsDocumentsReview() { + const { data: programId, isLoading: programLoading } = + trpc.competition.getActiveProgramId.useQuery() + const { data, isLoading, error } = trpc.finalist.listReviewDocuments.useQuery( + { programId: programId! }, + { enabled: !!programId, retry: false }, + ) + + if (error?.data?.code === 'FORBIDDEN') { + return ( + + + +

No access

+

+ This review is for the Grand-Final jury and program admins. +

+
+
+ ) + } + + // No active program resolved — nothing to review. + if (!programLoading && !programId) { + return ( + + + +

No active program

+

+ Finalist documents will appear here once a program is active. +

+
+
+ ) + } + + if (isLoading || !data) { + return ( +
+ + +
+ ) + } + + const fmt = new Intl.DateTimeFormat(undefined, { + dateStyle: 'long', + timeStyle: 'short', + }) + return ( +
+
+

+ Finalist Documents +

+

+ {data.submittedCount} of {data.totalCount} teams complete + {data.round.deadline + ? ` · due ${fmt.format(new Date(data.round.deadline))}` + : ''} +

+
+ {data.teams.map((team) => ( + + + {team.teamName} +
+ {team.category && ( + {team.category} + )} + + {team.submitted ? 'Complete' : 'Incomplete'} + +
+
+ + {team.documents.map((doc) => ( +
+
+ + {doc.requirementName} + + {doc.file && ( + + )} +
+ {doc.file ? ( + + ) : ( +

+ Not yet uploaded +

+ )} +
+ ))} +
+
+ ))} +
+ ) +}