From 0edb50cd3a41470769ac403cbbfe07ccf699bd13 Mon Sep 17 00:00:00 2001 From: Matt Date: Wed, 25 Feb 2026 15:04:58 +0100 Subject: [PATCH] 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 --- .../forms/evaluation-form-builder.tsx | 68 +++++++++++++++++-- src/server/routers/evaluation.ts | 2 +- 2 files changed, 63 insertions(+), 7 deletions(-) diff --git a/src/components/forms/evaluation-form-builder.tsx b/src/components/forms/evaluation-form-builder.tsx index 53314c4..77966bf 100644 --- a/src/components/forms/evaluation-form-builder.tsx +++ b/src/components/forms/evaluation-form-builder.tsx @@ -51,10 +51,11 @@ import { Heading, ThumbsUp, ThumbsDown, + ArrowUpCircle, } from 'lucide-react' 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 { criterionId: string @@ -107,6 +108,8 @@ function createDefaultCriterion(type: CriterionType = 'numeric'): Criterion { return { ...base, maxLength: 1000, placeholder: '', required: true } case 'boolean': 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': return { ...base, required: false } default: @@ -236,7 +239,7 @@ export function EvaluationFormBuilder({ {/* Type indicator */}
- {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')}
@@ -413,8 +416,33 @@ export function EvaluationFormBuilder({ )} - {/* Condition builder - available for all types except section_header */} - {(editDraft.type || 'numeric') !== 'section_header' && criteria.filter((c) => c.id !== editDraft.id).length > 0 && ( + {editDraft.type === 'advance' && ( +
+
+ + updateDraft({ trueLabel: e.target.value })} + placeholder="Yes" + disabled={disabled} + /> +
+
+ + updateDraft({ falseLabel: e.target.value })} + placeholder="No" + disabled={disabled} + /> +
+
+ )} + + {/* 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 && (
@@ -554,11 +582,11 @@ export function EvaluationFormBuilder({ {(() => { 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 ( - {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} ) })()} @@ -684,6 +712,22 @@ export function EvaluationFormBuilder({ ))} + + {criteria.length > 0 && ( )} @@ -796,6 +840,18 @@ function PreviewDialog({ criteria }: { criteria: Criterion[] }) {
)} + {type === 'advance' && ( +
+
+ + {criterion.trueLabel || 'Yes'} +
+
+ + {criterion.falseLabel || 'No'} +
+
+ )} ) diff --git a/src/server/routers/evaluation.ts b/src/server/routers/evaluation.ts index fea76b1..79113da 100644 --- a/src/server/routers/evaluation.ts +++ b/src/server/routers/evaluation.ts @@ -1227,7 +1227,7 @@ export const evaluationRouter = router({ id: z.string(), label: z.string().min(1).max(255), 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 weight: z.number().min(0).max(100).optional(), minScore: z.number().int().min(0).optional(),