feat: add advance criterion type to evaluation form builder
Adds a new 'advance' criterion type representing "should this project advance to the next round?". Only one advance criterion is allowed per form (button disabled once added). No weight, no condition fields, always required. Also updates the upsertForm Zod schema to accept the new type. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -51,10 +51,11 @@ import {
|
|||||||
Heading,
|
Heading,
|
||||||
ThumbsUp,
|
ThumbsUp,
|
||||||
ThumbsDown,
|
ThumbsDown,
|
||||||
|
ArrowUpCircle,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
export type CriterionType = 'numeric' | 'text' | 'boolean' | 'section_header'
|
export type CriterionType = 'numeric' | 'text' | 'boolean' | 'advance' | 'section_header'
|
||||||
|
|
||||||
export interface CriterionCondition {
|
export interface CriterionCondition {
|
||||||
criterionId: string
|
criterionId: string
|
||||||
@@ -107,6 +108,8 @@ function createDefaultCriterion(type: CriterionType = 'numeric'): Criterion {
|
|||||||
return { ...base, maxLength: 1000, placeholder: '', required: true }
|
return { ...base, maxLength: 1000, placeholder: '', required: true }
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
return { ...base, trueLabel: 'Yes', falseLabel: 'No', required: true }
|
return { ...base, trueLabel: 'Yes', falseLabel: 'No', required: true }
|
||||||
|
case 'advance':
|
||||||
|
return { ...base, label: 'Advance to next round?', trueLabel: 'Yes', falseLabel: 'No', required: true }
|
||||||
case 'section_header':
|
case 'section_header':
|
||||||
return { ...base, required: false }
|
return { ...base, required: false }
|
||||||
default:
|
default:
|
||||||
@@ -236,7 +239,7 @@ export function EvaluationFormBuilder({
|
|||||||
{/* Type indicator */}
|
{/* Type indicator */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
{CRITERION_TYPE_OPTIONS.find((t) => t.value === (editDraft.type || 'numeric'))?.label ?? 'Numeric Score'}
|
{editDraft.type === 'advance' ? 'Advance to Next Round?' : (CRITERION_TYPE_OPTIONS.find((t) => t.value === (editDraft.type || 'numeric'))?.label ?? 'Numeric Score')}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -413,8 +416,33 @@ export function EvaluationFormBuilder({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Condition builder - available for all types except section_header */}
|
{editDraft.type === 'advance' && (
|
||||||
{(editDraft.type || 'numeric') !== 'section_header' && criteria.filter((c) => c.id !== editDraft.id).length > 0 && (
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`trueLabel-${criterion.id}`}>Yes Label</Label>
|
||||||
|
<Input
|
||||||
|
id={`trueLabel-${criterion.id}`}
|
||||||
|
value={editDraft.trueLabel || 'Yes'}
|
||||||
|
onChange={(e) => updateDraft({ trueLabel: e.target.value })}
|
||||||
|
placeholder="Yes"
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor={`falseLabel-${criterion.id}`}>No Label</Label>
|
||||||
|
<Input
|
||||||
|
id={`falseLabel-${criterion.id}`}
|
||||||
|
value={editDraft.falseLabel || 'No'}
|
||||||
|
onChange={(e) => updateDraft({ falseLabel: e.target.value })}
|
||||||
|
placeholder="No"
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Condition builder - available for all types except section_header and advance */}
|
||||||
|
{(editDraft.type || 'numeric') !== 'section_header' && editDraft.type !== 'advance' && criteria.filter((c) => c.id !== editDraft.id).length > 0 && (
|
||||||
<div className="space-y-2 border-t pt-4">
|
<div className="space-y-2 border-t pt-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Label>Conditional Visibility</Label>
|
<Label>Conditional Visibility</Label>
|
||||||
@@ -554,11 +582,11 @@ export function EvaluationFormBuilder({
|
|||||||
</span>
|
</span>
|
||||||
{(() => {
|
{(() => {
|
||||||
const type = criterion.type || 'numeric'
|
const type = criterion.type || 'numeric'
|
||||||
const TypeIcon = CRITERION_TYPE_OPTIONS.find((t) => t.value === type)?.icon ?? Hash
|
const TypeIcon = type === 'advance' ? ArrowUpCircle : (CRITERION_TYPE_OPTIONS.find((t) => t.value === type)?.icon ?? Hash)
|
||||||
return (
|
return (
|
||||||
<Badge variant="secondary" className="shrink-0 text-xs gap-1">
|
<Badge variant="secondary" className="shrink-0 text-xs gap-1">
|
||||||
<TypeIcon className="h-3 w-3" />
|
<TypeIcon className="h-3 w-3" />
|
||||||
{type === 'numeric' ? `1-${criterion.scale ?? 5}` : CRITERION_TYPE_OPTIONS.find((t) => t.value === type)?.label}
|
{type === 'numeric' ? `1-${criterion.scale ?? 5}` : type === 'advance' ? 'Advance to Next Round?' : CRITERION_TYPE_OPTIONS.find((t) => t.value === type)?.label}
|
||||||
</Badge>
|
</Badge>
|
||||||
)
|
)
|
||||||
})()}
|
})()}
|
||||||
@@ -684,6 +712,22 @@ export function EvaluationFormBuilder({
|
|||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => addCriterion('advance')}
|
||||||
|
disabled={editingId !== null || criteria.some((c) => c.type === 'advance')}
|
||||||
|
title={criteria.some((c) => c.type === 'advance') ? 'Only one advance criterion allowed per form' : undefined}
|
||||||
|
className={cn(
|
||||||
|
'border-brand-blue/40 text-brand-blue hover:bg-brand-blue/5',
|
||||||
|
criteria.some((c) => c.type === 'advance') && 'opacity-50 cursor-not-allowed'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ArrowUpCircle className="mr-1 h-4 w-4" />
|
||||||
|
Advance to Next Round?
|
||||||
|
</Button>
|
||||||
|
|
||||||
{criteria.length > 0 && (
|
{criteria.length > 0 && (
|
||||||
<PreviewDialog criteria={criteria} />
|
<PreviewDialog criteria={criteria} />
|
||||||
)}
|
)}
|
||||||
@@ -796,6 +840,18 @@ function PreviewDialog({ criteria }: { criteria: Criterion[] }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{type === 'advance' && (
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<div className="flex-1 h-14 rounded-lg border-2 border-emerald-300 bg-emerald-50/50 flex items-center justify-center text-sm font-semibold text-emerald-700">
|
||||||
|
<ThumbsUp className="mr-2 h-4 w-4" />
|
||||||
|
{criterion.trueLabel || 'Yes'}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 h-14 rounded-lg border-2 border-red-300 bg-red-50/50 flex items-center justify-center text-sm font-semibold text-red-700">
|
||||||
|
<ThumbsDown className="mr-2 h-4 w-4" />
|
||||||
|
{criterion.falseLabel || 'No'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1227,7 +1227,7 @@ export const evaluationRouter = router({
|
|||||||
id: z.string(),
|
id: z.string(),
|
||||||
label: z.string().min(1).max(255),
|
label: z.string().min(1).max(255),
|
||||||
description: z.string().max(2000).optional(),
|
description: z.string().max(2000).optional(),
|
||||||
type: z.enum(['numeric', 'text', 'boolean', 'section_header']).optional(),
|
type: z.enum(['numeric', 'text', 'boolean', 'advance', 'section_header']).optional(),
|
||||||
// Numeric fields
|
// Numeric fields
|
||||||
weight: z.number().min(0).max(100).optional(),
|
weight: z.number().min(0).max(100).optional(),
|
||||||
minScore: z.number().int().min(0).optional(),
|
minScore: z.number().int().min(0).optional(),
|
||||||
|
|||||||
Reference in New Issue
Block a user