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 === 'boolean') {
|
||||
if (type === 'boolean' || type === 'advance') {
|
||||
return (
|
||||
<div key={key} className="flex items-center justify-between p-2.5 rounded-lg border">
|
||||
<span className="text-sm">{label}</span>
|
||||
|
||||
@@ -164,7 +164,7 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
|
||||
id: c.id,
|
||||
label: c.label,
|
||||
description: c.description,
|
||||
type: type as 'numeric' | 'text' | 'boolean' | 'section_header',
|
||||
type: type as 'numeric' | 'text' | 'boolean' | 'advance' | 'section_header',
|
||||
weight: c.weight,
|
||||
minScore,
|
||||
maxScore,
|
||||
@@ -352,7 +352,7 @@ export default function JuryEvaluatePage({ params: paramsPromise }: PageProps) {
|
||||
setIsSubmitting(false)
|
||||
return
|
||||
}
|
||||
if (c.type === 'boolean' && val === undefined) {
|
||||
if ((c.type === 'boolean' || c.type === 'advance') && val === undefined) {
|
||||
toast.error(`Please answer "${c.label}"`)
|
||||
isSubmittingRef.current = 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') {
|
||||
const currentValue = criteriaValues[criterion.id]
|
||||
return (
|
||||
|
||||
@@ -42,7 +42,7 @@ import { toast } from 'sonner'
|
||||
import { CountdownTimer } from '@/components/shared/countdown-timer'
|
||||
|
||||
// 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 {
|
||||
criterionId: string
|
||||
@@ -96,7 +96,7 @@ const createEvaluationSchema = (criteria: Criterion[]) => {
|
||||
criterionFields[c.id] = z.number()
|
||||
} else if (type === 'text') {
|
||||
criterionFields[c.id] = z.string()
|
||||
} else if (type === 'boolean') {
|
||||
} else if (type === 'boolean' || type === 'advance') {
|
||||
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)
|
||||
} else if (type === 'text') {
|
||||
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
|
||||
}
|
||||
})
|
||||
@@ -225,7 +225,7 @@ export function EvaluationForm({
|
||||
} else if (type === 'text') {
|
||||
const val = watchedScores?.[c.id]
|
||||
if (typeof val === 'string' && val.length > 0) criteriaDone++
|
||||
} else if (type === 'boolean') {
|
||||
} else if (type === 'boolean' || type === 'advance') {
|
||||
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
|
||||
})}
|
||||
</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
|
||||
function ProgressIndicator({
|
||||
percentage,
|
||||
|
||||
@@ -737,7 +737,7 @@ export function ObserverProjectDetail({ projectId }: { projectId: string }) {
|
||||
|
||||
if (type === 'section_header') return null
|
||||
|
||||
if (type === 'boolean') {
|
||||
if (type === 'boolean' || type === 'advance') {
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
|
||||
Reference in New Issue
Block a user