feat: render advance criterion on juror evaluation page and fix related renderers
- Jury evaluate page: add prominent advance criterion block (h-14, brand-blue border) before boolean block, fix type cast to include 'advance', add advance to required-field validation - evaluation-form.tsx: add 'advance' to CriterionType, schema, default values, progress tracking, rendering via new AdvanceCriterionField component with prominent styling - Admin project detail: treat advance same as boolean in EvaluationDetailSheet criterion score display - Observer project detail: treat advance same as boolean in evaluation criterion score display Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -927,7 +927,7 @@ function EvaluationDetailSheet({
|
|||||||
|
|
||||||
if (type === 'section_header') return null
|
if (type === 'section_header') return null
|
||||||
|
|
||||||
if (type === 'boolean') {
|
if (type === 'boolean' || type === 'advance') {
|
||||||
return (
|
return (
|
||||||
<div key={key} className="flex items-center justify-between p-2.5 rounded-lg border">
|
<div key={key} className="flex items-center justify-between p-2.5 rounded-lg border">
|
||||||
<span className="text-sm">{label}</span>
|
<span className="text-sm">{label}</span>
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
|
|||||||
id: c.id,
|
id: c.id,
|
||||||
label: c.label,
|
label: c.label,
|
||||||
description: c.description,
|
description: c.description,
|
||||||
type: type as 'numeric' | 'text' | 'boolean' | 'section_header',
|
type: type as 'numeric' | 'text' | 'boolean' | 'advance' | 'section_header',
|
||||||
weight: c.weight,
|
weight: c.weight,
|
||||||
minScore,
|
minScore,
|
||||||
maxScore,
|
maxScore,
|
||||||
@@ -352,7 +352,7 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
|
|||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (c.type === 'boolean' && val === undefined) {
|
if ((c.type === 'boolean' || c.type === 'advance') && val === undefined) {
|
||||||
toast.error(`Please answer "${c.label}"`)
|
toast.error(`Please answer "${c.label}"`)
|
||||||
isSubmittingRef.current = false
|
isSubmittingRef.current = false
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
@@ -657,6 +657,51 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (criterion.type === 'advance') {
|
||||||
|
const currentValue = criteriaValues[criterion.id]
|
||||||
|
return (
|
||||||
|
<div key={criterion.id} className="space-y-3 p-5 border-2 border-brand-blue/30 rounded-xl bg-brand-blue/5">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label className="text-base font-semibold text-brand-blue">
|
||||||
|
{criterion.label}
|
||||||
|
<span className="text-destructive ml-1">*</span>
|
||||||
|
</Label>
|
||||||
|
{criterion.description && (
|
||||||
|
<p className="text-sm text-muted-foreground">{criterion.description}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleCriterionChange(criterion.id, true)}
|
||||||
|
className={cn(
|
||||||
|
'flex-1 h-14 rounded-xl border-2 flex items-center justify-center text-base font-semibold transition-all',
|
||||||
|
currentValue === true
|
||||||
|
? 'border-emerald-500 bg-emerald-50 text-emerald-700 shadow-sm ring-2 ring-emerald-200'
|
||||||
|
: 'border-border hover:border-emerald-300 hover:bg-emerald-50/50'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ThumbsUp className="mr-2 h-5 w-5" />
|
||||||
|
{criterion.trueLabel || 'Yes'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleCriterionChange(criterion.id, false)}
|
||||||
|
className={cn(
|
||||||
|
'flex-1 h-14 rounded-xl border-2 flex items-center justify-center text-base font-semibold transition-all',
|
||||||
|
currentValue === false
|
||||||
|
? 'border-red-500 bg-red-50 text-red-700 shadow-sm ring-2 ring-red-200'
|
||||||
|
: 'border-border hover:border-red-300 hover:bg-red-50/50'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ThumbsDown className="mr-2 h-5 w-5" />
|
||||||
|
{criterion.falseLabel || 'No'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (criterion.type === 'boolean') {
|
if (criterion.type === 'boolean') {
|
||||||
const currentValue = criteriaValues[criterion.id]
|
const currentValue = criteriaValues[criterion.id]
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ import { toast } from 'sonner'
|
|||||||
import { CountdownTimer } from '@/components/shared/countdown-timer'
|
import { CountdownTimer } from '@/components/shared/countdown-timer'
|
||||||
|
|
||||||
// Define criterion type from the evaluation form JSON
|
// Define criterion type from the evaluation form JSON
|
||||||
type CriterionType = 'numeric' | 'text' | 'boolean' | 'section_header'
|
type CriterionType = 'numeric' | 'text' | 'boolean' | 'advance' | 'section_header'
|
||||||
|
|
||||||
interface CriterionCondition {
|
interface CriterionCondition {
|
||||||
criterionId: string
|
criterionId: string
|
||||||
@@ -96,7 +96,7 @@ const createEvaluationSchema = (criteria: Criterion[]) => {
|
|||||||
criterionFields[c.id] = z.number()
|
criterionFields[c.id] = z.number()
|
||||||
} else if (type === 'text') {
|
} else if (type === 'text') {
|
||||||
criterionFields[c.id] = z.string()
|
criterionFields[c.id] = z.string()
|
||||||
} else if (type === 'boolean') {
|
} else if (type === 'boolean' || type === 'advance') {
|
||||||
criterionFields[c.id] = z.boolean()
|
criterionFields[c.id] = z.boolean()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,7 +180,7 @@ export function EvaluationForm({
|
|||||||
defaultCriterionScores[c.id] = typeof existing === 'number' ? existing : Math.ceil((c.scale ?? 5) / 2)
|
defaultCriterionScores[c.id] = typeof existing === 'number' ? existing : Math.ceil((c.scale ?? 5) / 2)
|
||||||
} else if (type === 'text') {
|
} else if (type === 'text') {
|
||||||
defaultCriterionScores[c.id] = typeof existing === 'string' ? existing : ''
|
defaultCriterionScores[c.id] = typeof existing === 'string' ? existing : ''
|
||||||
} else if (type === 'boolean') {
|
} else if (type === 'boolean' || type === 'advance') {
|
||||||
defaultCriterionScores[c.id] = typeof existing === 'boolean' ? existing : false
|
defaultCriterionScores[c.id] = typeof existing === 'boolean' ? existing : false
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -225,7 +225,7 @@ export function EvaluationForm({
|
|||||||
} else if (type === 'text') {
|
} else if (type === 'text') {
|
||||||
const val = watchedScores?.[c.id]
|
const val = watchedScores?.[c.id]
|
||||||
if (typeof val === 'string' && val.length > 0) criteriaDone++
|
if (typeof val === 'string' && val.length > 0) criteriaDone++
|
||||||
} else if (type === 'boolean') {
|
} else if (type === 'boolean' || type === 'advance') {
|
||||||
if (touchedCriteria.has(c.id)) criteriaDone++
|
if (touchedCriteria.has(c.id)) criteriaDone++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -470,6 +470,19 @@ export function EvaluationForm({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Advance (prominent boolean-valued criterion)
|
||||||
|
if (type === 'advance') {
|
||||||
|
return (
|
||||||
|
<AdvanceCriterionField
|
||||||
|
key={criterion.id}
|
||||||
|
criterion={criterion}
|
||||||
|
control={control}
|
||||||
|
disabled={isReadOnly}
|
||||||
|
onTouch={onCriterionTouch}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return null
|
return null
|
||||||
})}
|
})}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -947,6 +960,88 @@ function BooleanCriterionField({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Advance criterion field component (prominent boolean — "should this project advance?")
|
||||||
|
function AdvanceCriterionField({
|
||||||
|
criterion,
|
||||||
|
control,
|
||||||
|
disabled,
|
||||||
|
onTouch,
|
||||||
|
}: {
|
||||||
|
criterion: Criterion
|
||||||
|
control: any
|
||||||
|
disabled: boolean
|
||||||
|
onTouch: (criterionId: string) => void
|
||||||
|
}) {
|
||||||
|
const trueLabel = criterion.trueLabel || 'Yes'
|
||||||
|
const falseLabel = criterion.falseLabel || 'No'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
name={`criterionScores.${criterion.id}`}
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<div className="space-y-3 p-5 border-2 border-brand-blue/30 rounded-xl bg-brand-blue/5">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Label className="text-base font-semibold text-brand-blue">{criterion.label}</Label>
|
||||||
|
{criterion.required && (
|
||||||
|
<Badge variant="destructive" className="text-xs">Required</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{criterion.description && (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{criterion.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={field.value === true ? 'default' : 'outline'}
|
||||||
|
className={cn(
|
||||||
|
'flex-1 h-14 rounded-xl border-2 text-base font-semibold transition-all',
|
||||||
|
field.value === true
|
||||||
|
? 'border-emerald-500 bg-emerald-50 text-emerald-700 shadow-sm ring-2 ring-emerald-200 hover:bg-emerald-100'
|
||||||
|
: 'border-border hover:border-emerald-300 hover:bg-emerald-50/50'
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
if (!disabled) {
|
||||||
|
field.onChange(true)
|
||||||
|
onTouch(criterion.id)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<ThumbsUp className="mr-2 h-5 w-5" />
|
||||||
|
{trueLabel}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={field.value === false ? 'default' : 'outline'}
|
||||||
|
className={cn(
|
||||||
|
'flex-1 h-14 rounded-xl border-2 text-base font-semibold transition-all',
|
||||||
|
field.value === false
|
||||||
|
? 'border-red-500 bg-red-50 text-red-700 shadow-sm ring-2 ring-red-200 hover:bg-red-100'
|
||||||
|
: 'border-border hover:border-red-300 hover:bg-red-50/50'
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
if (!disabled) {
|
||||||
|
field.onChange(false)
|
||||||
|
onTouch(criterion.id)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
<ThumbsDown className="mr-2 h-5 w-5" />
|
||||||
|
{falseLabel}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Progress indicator component
|
// Progress indicator component
|
||||||
function ProgressIndicator({
|
function ProgressIndicator({
|
||||||
percentage,
|
percentage,
|
||||||
|
|||||||
@@ -737,7 +737,7 @@ export function ObserverProjectDetail({ projectId }: { projectId: string }) {
|
|||||||
|
|
||||||
if (type === 'section_header') return null
|
if (type === 'section_header') return null
|
||||||
|
|
||||||
if (type === 'boolean') {
|
if (type === 'boolean' || type === 'advance') {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={key}
|
key={key}
|
||||||
|
|||||||
Reference in New Issue
Block a user