163 lines
5.1 KiB
TypeScript
163 lines
5.1 KiB
TypeScript
|
|
'use client'
|
||
|
|
|
||
|
|
import { useState } from 'react'
|
||
|
|
import { trpc } from '@/lib/trpc/client'
|
||
|
|
import {
|
||
|
|
AlertDialog,
|
||
|
|
AlertDialogContent,
|
||
|
|
AlertDialogDescription,
|
||
|
|
AlertDialogFooter,
|
||
|
|
AlertDialogHeader,
|
||
|
|
AlertDialogTitle,
|
||
|
|
} from '@/components/ui/alert-dialog'
|
||
|
|
import { Button } from '@/components/ui/button'
|
||
|
|
import { Label } from '@/components/ui/label'
|
||
|
|
import { Textarea } from '@/components/ui/textarea'
|
||
|
|
import {
|
||
|
|
Select,
|
||
|
|
SelectContent,
|
||
|
|
SelectItem,
|
||
|
|
SelectTrigger,
|
||
|
|
SelectValue,
|
||
|
|
} from '@/components/ui/select'
|
||
|
|
import { Loader2, ShieldAlert } from 'lucide-react'
|
||
|
|
import { toast } from 'sonner'
|
||
|
|
|
||
|
|
interface COIDeclarationDialogProps {
|
||
|
|
open: boolean
|
||
|
|
assignmentId: string
|
||
|
|
projectTitle: string
|
||
|
|
onComplete: (hasConflict: boolean) => void
|
||
|
|
}
|
||
|
|
|
||
|
|
export function COIDeclarationDialog({
|
||
|
|
open,
|
||
|
|
assignmentId,
|
||
|
|
projectTitle,
|
||
|
|
onComplete,
|
||
|
|
}: COIDeclarationDialogProps) {
|
||
|
|
const [hasConflict, setHasConflict] = useState<boolean | null>(null)
|
||
|
|
const [conflictType, setConflictType] = useState<string>('')
|
||
|
|
const [description, setDescription] = useState('')
|
||
|
|
|
||
|
|
const declareCOI = trpc.evaluation.declareCOI.useMutation({
|
||
|
|
onSuccess: (data) => {
|
||
|
|
if (data.hasConflict) {
|
||
|
|
toast.info('Conflict of interest recorded. An admin will review your declaration.')
|
||
|
|
}
|
||
|
|
onComplete(data.hasConflict)
|
||
|
|
},
|
||
|
|
onError: (error) => {
|
||
|
|
toast.error(error.message || 'Failed to submit COI declaration')
|
||
|
|
},
|
||
|
|
})
|
||
|
|
|
||
|
|
const handleSubmit = () => {
|
||
|
|
if (hasConflict === null) return
|
||
|
|
|
||
|
|
declareCOI.mutate({
|
||
|
|
assignmentId,
|
||
|
|
hasConflict,
|
||
|
|
conflictType: hasConflict ? conflictType : undefined,
|
||
|
|
description: hasConflict ? description : undefined,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
const canSubmit =
|
||
|
|
hasConflict !== null &&
|
||
|
|
(!hasConflict || (hasConflict && conflictType)) &&
|
||
|
|
!declareCOI.isPending
|
||
|
|
|
||
|
|
return (
|
||
|
|
<AlertDialog open={open}>
|
||
|
|
<AlertDialogContent className="max-w-md">
|
||
|
|
<AlertDialogHeader>
|
||
|
|
<AlertDialogTitle className="flex items-center gap-2">
|
||
|
|
<ShieldAlert className="h-5 w-5 text-amber-500" />
|
||
|
|
Conflict of Interest Declaration
|
||
|
|
</AlertDialogTitle>
|
||
|
|
<AlertDialogDescription>
|
||
|
|
Before evaluating “{projectTitle}”, please declare whether
|
||
|
|
you have any conflict of interest with this project.
|
||
|
|
</AlertDialogDescription>
|
||
|
|
</AlertDialogHeader>
|
||
|
|
|
||
|
|
<div className="space-y-4 py-2">
|
||
|
|
<div className="space-y-3">
|
||
|
|
<Label className="text-sm font-medium">
|
||
|
|
Do you have a conflict of interest with this project?
|
||
|
|
</Label>
|
||
|
|
<div className="flex gap-3">
|
||
|
|
<Button
|
||
|
|
type="button"
|
||
|
|
variant={hasConflict === false ? 'default' : 'outline'}
|
||
|
|
className="flex-1"
|
||
|
|
onClick={() => setHasConflict(false)}
|
||
|
|
>
|
||
|
|
No Conflict
|
||
|
|
</Button>
|
||
|
|
<Button
|
||
|
|
type="button"
|
||
|
|
variant={hasConflict === true ? 'destructive' : 'outline'}
|
||
|
|
className="flex-1"
|
||
|
|
onClick={() => setHasConflict(true)}
|
||
|
|
>
|
||
|
|
Yes, I Have a Conflict
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{hasConflict && (
|
||
|
|
<>
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="conflict-type">Type of Conflict</Label>
|
||
|
|
<Select value={conflictType} onValueChange={setConflictType}>
|
||
|
|
<SelectTrigger id="conflict-type">
|
||
|
|
<SelectValue placeholder="Select conflict type..." />
|
||
|
|
</SelectTrigger>
|
||
|
|
<SelectContent>
|
||
|
|
<SelectItem value="financial">Financial Interest</SelectItem>
|
||
|
|
<SelectItem value="personal">Personal Relationship</SelectItem>
|
||
|
|
<SelectItem value="organizational">Organizational Affiliation</SelectItem>
|
||
|
|
<SelectItem value="other">Other</SelectItem>
|
||
|
|
</SelectContent>
|
||
|
|
</Select>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="space-y-2">
|
||
|
|
<Label htmlFor="conflict-description">
|
||
|
|
Description <span className="text-muted-foreground">(optional)</span>
|
||
|
|
</Label>
|
||
|
|
<Textarea
|
||
|
|
id="conflict-description"
|
||
|
|
placeholder="Briefly describe the nature of your conflict..."
|
||
|
|
value={description}
|
||
|
|
onChange={(e) => setDescription(e.target.value)}
|
||
|
|
rows={3}
|
||
|
|
maxLength={1000}
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<AlertDialogFooter>
|
||
|
|
<Button
|
||
|
|
onClick={handleSubmit}
|
||
|
|
disabled={!canSubmit}
|
||
|
|
>
|
||
|
|
{declareCOI.isPending && (
|
||
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||
|
|
)}
|
||
|
|
{hasConflict === null
|
||
|
|
? 'Select an option'
|
||
|
|
: hasConflict
|
||
|
|
? 'Submit Declaration'
|
||
|
|
: 'Confirm No Conflict'}
|
||
|
|
</Button>
|
||
|
|
</AlertDialogFooter>
|
||
|
|
</AlertDialogContent>
|
||
|
|
</AlertDialog>
|
||
|
|
)
|
||
|
|
}
|