COI gate + admin review, mobile file viewer fixes for iOS
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m12s

- Integrate COI declaration dialog into jury evaluate page (blocks evaluation until declared)
- Add COI review section to admin round page with clear/reassign/note actions
- Fix mobile: remove inline preview (viewport too small), add labeled buttons
- Fix iOS: open-in-new-tab uses synchronous window.open to avoid popup blocker
- Fix iOS: download falls back to direct link if fetch+blob fails (CORS/Safari)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt
2026-02-18 14:34:27 +01:00
parent 0f6473c999
commit 8d28104d51
3 changed files with 298 additions and 44 deletions

View File

@@ -15,7 +15,8 @@ import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { cn } from '@/lib/utils'
import { MultiWindowDocViewer } from '@/components/jury/multi-window-doc-viewer'
import { Badge } from '@/components/ui/badge'
import { ArrowLeft, Save, Send, AlertCircle, ThumbsUp, ThumbsDown, Clock, CheckCircle2 } from 'lucide-react'
import { COIDeclarationDialog } from '@/components/forms/coi-declaration-dialog'
import { ArrowLeft, Save, Send, AlertCircle, ThumbsUp, ThumbsDown, Clock, CheckCircle2, ShieldAlert } from 'lucide-react'
import { toast } from 'sonner'
import type { EvaluationConfig } from '@/types/competition-configs'
@@ -68,6 +69,14 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
{ enabled: !!myAssignment?.id }
)
// COI (Conflict of Interest) check
const { data: coiStatus, isLoading: coiLoading } = trpc.evaluation.getCOIStatus.useQuery(
{ assignmentId: myAssignment?.id ?? '' },
{ enabled: !!myAssignment?.id }
)
const [coiCompleted, setCOICompleted] = useState(false)
const [coiHasConflict, setCOIHasConflict] = useState(false)
// Fetch the active evaluation form for this round
const { data: activeForm } = trpc.evaluation.getStageForm.useQuery(
{ roundId },
@@ -385,6 +394,13 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
)
}
// COI config
const coiRequired = evalConfig?.coiRequired ?? true
// Determine COI state: declared via server or just completed in this session
const coiDeclared = coiCompleted || coiStatus !== undefined
const coiConflict = coiHasConflict || (coiStatus?.hasConflict ?? false)
// Check if round is active
const isRoundActive = round.status === 'ROUND_ACTIVE'
@@ -421,6 +437,79 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
)
}
// COI gate: if COI is required, not yet declared, and we have an assignment
if (coiRequired && myAssignment && !coiLoading && !coiDeclared) {
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Button variant="ghost" size="sm" asChild>
<Link href={`/jury/competitions/${roundId}/projects/${projectId}` as Route}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Project
</Link>
</Button>
<div>
<h1 className="text-2xl font-bold tracking-tight text-brand-blue dark:text-foreground">
Evaluate Project
</h1>
<p className="text-muted-foreground mt-1">{project.title}</p>
</div>
</div>
<COIDeclarationDialog
open={true}
assignmentId={myAssignment.id}
projectTitle={project.title}
onComplete={(hasConflict) => {
setCOICompleted(true)
setCOIHasConflict(hasConflict)
}}
/>
</div>
)
}
// COI conflict declared — block evaluation
if (coiRequired && coiConflict) {
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Button variant="ghost" size="sm" asChild>
<Link href={`/jury/competitions/${roundId}/projects/${projectId}` as Route}>
<ArrowLeft className="mr-2 h-4 w-4" />
Back to Project
</Link>
</Button>
<div>
<h1 className="text-2xl font-bold tracking-tight text-brand-blue dark:text-foreground">
Evaluate Project
</h1>
<p className="text-muted-foreground mt-1">{project.title}</p>
</div>
</div>
<Card className="border-l-4 border-l-amber-500">
<CardContent className="flex items-start gap-4 p-6">
<div className="rounded-xl bg-amber-50 dark:bg-amber-950/40 p-3">
<ShieldAlert className="h-6 w-6 text-amber-600" />
</div>
<div>
<h2 className="text-lg font-semibold">Conflict of Interest Declared</h2>
<p className="text-sm text-muted-foreground mt-1">
You declared a conflict of interest for this project. An administrator will
review your declaration. You cannot evaluate this project while the conflict
is under review.
</p>
<Button variant="outline" size="sm" className="mt-4" asChild>
<Link href={`/jury/competitions/${roundId}` as Route}>
Back to Round
</Link>
</Button>
</div>
</CardContent>
</Card>
</div>
)
}
return (
<div className="space-y-6">
<div className="flex items-center gap-4">