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

- 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:
2026-02-23 17:29:13 +01:00
parent ab2c73bad2
commit 3bc6552f47
3 changed files with 51 additions and 12 deletions

View File

@@ -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
const buildSavePayload = useCallback(() => {
return {
@@ -370,13 +388,17 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
}
}
submitMutation.mutate({
try {
await submitMutation.mutateAsync({
id: evaluationId,
criterionScoresJson: scoringMode === 'criteria' ? criteriaValues : {},
globalScore: scoringMode === 'global' ? parseInt(globalScore, 10) : computedGlobalScore,
binaryDecision: scoringMode === 'binary' ? binaryDecision === 'accept' : true,
feedbackText: feedbackText || 'No feedback provided',
})
} catch {
// Error toast already handled by onError callback
}
}
if (!round || !project) {
@@ -840,7 +862,7 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
</Button>
<Button
onClick={handleSubmit}
disabled={submitMutation.isPending || autosaveMutation.isPending}
disabled={submitMutation.isPending || autosaveMutation.isPending || startMutation.isPending}
className="bg-brand-blue hover:bg-brand-blue-light"
>
<Send className="mr-2 h-4 w-4" />

View File

@@ -26,6 +26,7 @@ import {
Zap,
BarChart3,
Waves,
Send,
} from 'lucide-react'
import { formatDateOnly } from '@/lib/utils'
import { CountdownTimer } from '@/components/shared/countdown-timer'
@@ -390,6 +391,11 @@ async function JuryDashboardContent() {
<CheckCircle2 className="mr-1 h-3 w-3" />
Done
</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 ? (
<Badge variant="warning" className="text-xs">
<Clock className="mr-1 h-3 w-3" />
@@ -404,10 +410,17 @@ async function JuryDashboardContent() {
View
</Link>
</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 ? (
<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`}>
{isDraft ? 'Continue' : 'Evaluate'}
Evaluate
</Link>
</Button>
) : (

View File

@@ -303,7 +303,10 @@ export function EvaluationForm({
// Submit handler
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 {
await submit.mutateAsync({
@@ -325,6 +328,7 @@ export function EvaluationForm({
})
} catch (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>
<Button
type="button"
disabled={!isValid || submit.isPending}
disabled={!isValid || submit.isPending || startEvaluation.isPending}
>
{submit.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
@@ -678,7 +682,7 @@ export function EvaluationForm({
<Button
type="button"
size="lg"
disabled={!isValid || submit.isPending}
disabled={!isValid || submit.isPending || startEvaluation.isPending}
className="w-full sm:w-auto"
>
{submit.isPending ? (