Fix multi-click submit bug and add draft submit indicator on juror dashboard
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m24s
All checks were successful
Build and Push Docker Image / build (push) Successful in 8m24s
- Initialize slider values to midpoint so visual matches stored value (root cause: sliders appeared filled but criteriaValues was undefined) - Use mutateAsync for submit to properly await and prevent double-clicks - Add startMutation.isPending to submit button disabled state - Add error toast in evaluation-form.tsx catch block (was silent) - Show "Ready to submit" badge and "Review & Submit" button for drafts on juror dashboard Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -173,6 +173,24 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Initialize numeric criteria with midpoint values so slider visual matches stored value.
|
||||||
|
const criteriaInitializedRef = useRef(false)
|
||||||
|
useEffect(() => {
|
||||||
|
if (criteriaInitializedRef.current || criteria.length === 0) return
|
||||||
|
if (existingEvaluation?.criterionScoresJson) return
|
||||||
|
criteriaInitializedRef.current = true
|
||||||
|
|
||||||
|
const defaults: Record<string, number | boolean | string> = {}
|
||||||
|
for (const c of criteria) {
|
||||||
|
if (c.type === 'numeric') {
|
||||||
|
defaults[c.id] = Math.ceil((c.minScore + c.maxScore) / 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Object.keys(defaults).length > 0) {
|
||||||
|
setCriteriaValues((prev) => ({ ...defaults, ...prev }))
|
||||||
|
}
|
||||||
|
}, [criteria, existingEvaluation?.criterionScoresJson])
|
||||||
|
|
||||||
// Build current form data for autosave
|
// Build current form data for autosave
|
||||||
const buildSavePayload = useCallback(() => {
|
const buildSavePayload = useCallback(() => {
|
||||||
return {
|
return {
|
||||||
@@ -370,13 +388,17 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
submitMutation.mutate({
|
try {
|
||||||
id: evaluationId,
|
await submitMutation.mutateAsync({
|
||||||
criterionScoresJson: scoringMode === 'criteria' ? criteriaValues : {},
|
id: evaluationId,
|
||||||
globalScore: scoringMode === 'global' ? parseInt(globalScore, 10) : computedGlobalScore,
|
criterionScoresJson: scoringMode === 'criteria' ? criteriaValues : {},
|
||||||
binaryDecision: scoringMode === 'binary' ? binaryDecision === 'accept' : true,
|
globalScore: scoringMode === 'global' ? parseInt(globalScore, 10) : computedGlobalScore,
|
||||||
feedbackText: feedbackText || 'No feedback provided',
|
binaryDecision: scoringMode === 'binary' ? binaryDecision === 'accept' : true,
|
||||||
})
|
feedbackText: feedbackText || 'No feedback provided',
|
||||||
|
})
|
||||||
|
} catch {
|
||||||
|
// Error toast already handled by onError callback
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!round || !project) {
|
if (!round || !project) {
|
||||||
@@ -840,7 +862,7 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
disabled={submitMutation.isPending || autosaveMutation.isPending}
|
disabled={submitMutation.isPending || autosaveMutation.isPending || startMutation.isPending}
|
||||||
className="bg-brand-blue hover:bg-brand-blue-light"
|
className="bg-brand-blue hover:bg-brand-blue-light"
|
||||||
>
|
>
|
||||||
<Send className="mr-2 h-4 w-4" />
|
<Send className="mr-2 h-4 w-4" />
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
Zap,
|
Zap,
|
||||||
BarChart3,
|
BarChart3,
|
||||||
Waves,
|
Waves,
|
||||||
|
Send,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { formatDateOnly } from '@/lib/utils'
|
import { formatDateOnly } from '@/lib/utils'
|
||||||
import { CountdownTimer } from '@/components/shared/countdown-timer'
|
import { CountdownTimer } from '@/components/shared/countdown-timer'
|
||||||
@@ -390,6 +391,11 @@ async function JuryDashboardContent() {
|
|||||||
<CheckCircle2 className="mr-1 h-3 w-3" />
|
<CheckCircle2 className="mr-1 h-3 w-3" />
|
||||||
Done
|
Done
|
||||||
</Badge>
|
</Badge>
|
||||||
|
) : isDraft && isVotingOpen ? (
|
||||||
|
<Badge className="text-xs bg-amber-100 text-amber-800 border-amber-300 dark:bg-amber-950 dark:text-amber-300 dark:border-amber-700 animate-pulse">
|
||||||
|
<Send className="mr-1 h-3 w-3" />
|
||||||
|
Ready to submit
|
||||||
|
</Badge>
|
||||||
) : isDraft ? (
|
) : isDraft ? (
|
||||||
<Badge variant="warning" className="text-xs">
|
<Badge variant="warning" className="text-xs">
|
||||||
<Clock className="mr-1 h-3 w-3" />
|
<Clock className="mr-1 h-3 w-3" />
|
||||||
@@ -404,10 +410,17 @@ async function JuryDashboardContent() {
|
|||||||
View
|
View
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
) : isVotingOpen && isDraft ? (
|
||||||
|
<Button size="sm" asChild className="h-7 px-3 bg-amber-600 hover:bg-amber-700 text-white shadow-sm">
|
||||||
|
<Link href={`/jury/competitions/${assignment.round.id}/projects/${assignment.project.id}/evaluate`}>
|
||||||
|
<Send className="mr-1 h-3 w-3" />
|
||||||
|
Review & Submit
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
) : isVotingOpen ? (
|
) : isVotingOpen ? (
|
||||||
<Button size="sm" asChild className="h-7 px-3 bg-brand-blue hover:bg-brand-blue-light shadow-sm">
|
<Button size="sm" asChild className="h-7 px-3 bg-brand-blue hover:bg-brand-blue-light shadow-sm">
|
||||||
<Link href={`/jury/competitions/${assignment.round.id}/projects/${assignment.project.id}/evaluate`}>
|
<Link href={`/jury/competitions/${assignment.round.id}/projects/${assignment.project.id}/evaluate`}>
|
||||||
{isDraft ? 'Continue' : 'Evaluate'}
|
Evaluate
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -303,7 +303,10 @@ export function EvaluationForm({
|
|||||||
|
|
||||||
// Submit handler
|
// Submit handler
|
||||||
const onSubmit = async (data: EvaluationFormData) => {
|
const onSubmit = async (data: EvaluationFormData) => {
|
||||||
if (!currentEvaluationId) return
|
if (!currentEvaluationId) {
|
||||||
|
toast.error('Evaluation is still being created. Please wait a moment and try again.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await submit.mutateAsync({
|
await submit.mutateAsync({
|
||||||
@@ -325,6 +328,7 @@ export function EvaluationForm({
|
|||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Submit failed:', error)
|
console.error('Submit failed:', error)
|
||||||
|
toast.error(error instanceof Error ? error.message : 'Failed to submit evaluation. Please try again.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,7 +363,7 @@ export function EvaluationForm({
|
|||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={!isValid || submit.isPending}
|
disabled={!isValid || submit.isPending || startEvaluation.isPending}
|
||||||
>
|
>
|
||||||
{submit.isPending ? (
|
{submit.isPending ? (
|
||||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
@@ -678,7 +682,7 @@ export function EvaluationForm({
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size="lg"
|
size="lg"
|
||||||
disabled={!isValid || submit.isPending}
|
disabled={!isValid || submit.isPending || startEvaluation.isPending}
|
||||||
className="w-full sm:w-auto"
|
className="w-full sm:w-auto"
|
||||||
>
|
>
|
||||||
{submit.isPending ? (
|
{submit.isPending ? (
|
||||||
|
|||||||
Reference in New Issue
Block a user