import { Autocomplete, Checkbox, Group, Select, Stack, Switch, Text, TextInput } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import type { FieldCondition, FormSchemaPalette } from '@ema-platform/api'; import { useLocalized } from '@ema-platform/api'; import type { ConditionTarget } from '../config/schema-paths'; /** `FieldCondition` plus the renewal-only extra `DocumentRequirement.conditionExpression` carries. */ export type ConditionValue = FieldCondition & { previousDocExpired?: string }; type Operator = 'equals' | 'notEquals' | 'in' | 'isSet'; function operatorOf(condition: ConditionValue | undefined): Operator | null { if (!condition) return null; if (condition.isSet !== undefined) return 'isSet'; if (condition.equals !== undefined) return 'equals'; if (condition.notEquals !== undefined) return 'notEquals'; if (condition.in !== undefined) return 'in'; return null; } /** Best-effort type for a raw stored value, so re-editing an existing * condition renders a number input for a numeric value rather than text. */ function coerce(raw: string, targetType: string | undefined): string | number | boolean { if (targetType === 'BOOLEAN') return raw === 'true'; if (targetType === 'NUMBER' || targetType === 'MONEY') { const n = Number(raw); return Number.isFinite(n) && raw.trim() !== '' ? n : raw; } return raw; } /** * Authors one `FieldCondition` (`showWhen` on a section/field, or * `conditionExpression` on a document requirement). * * The field-path box autocompletes from every field already defined in this * licence type's schema (`targets`); when the chosen path resolves to a * SELECT field, the value picker switches to that field's own options * instead of free text — the condition can only ever reference an answer * that could actually be chosen. */ export function ConditionBuilder({ value, onChange, targets, palette, allowClear = true, }: { value: ConditionValue | null; onChange: (value: ConditionValue | null) => void; targets: ConditionTarget[]; palette: FormSchemaPalette | undefined; /** Hide the "no condition" toggle — used where a condition is mandatory (CONDITIONAL document mode). */ allowClear?: boolean; }) { const { t } = useTranslation(); const localized = useLocalized(); const active = value !== null; const operator = operatorOf(value ?? undefined) ?? 'equals'; const target = targets.find((c) => c.path === value?.field); const operators = palette?.conditionOperators ?? ['equals', 'notEquals', 'in', 'isSet']; function setField(field: string) { onChange({ field, ...(operator === 'isSet' ? { isSet: true } : { equals: '' }) }); } function setOperator(next: Operator) { if (!value?.field) return; const base: ConditionValue = { field: value.field }; if (next === 'isSet') base.isSet = true; else if (next === 'in') base.in = []; else if (next === 'notEquals') base.notEquals = ''; else base.equals = ''; onChange(base); } function setValueRaw(raw: string) { if (!value?.field) return; const coerced = coerce(raw, target?.field.type); if (operator === 'equals') onChange({ field: value.field, equals: coerced }); else if (operator === 'notEquals') onChange({ field: value.field, notEquals: coerced }); } function setInValues(raws: string[]) { if (!value?.field) return; onChange({ field: value.field, in: raws.map((r) => coerce(r, target?.field.type)) as (string | number)[], }); } return ( {allowClear && ( onChange(e.currentTarget.checked ? { field: '', equals: '' } : null)} /> )} {active && ( c.path)} value={value?.field ?? ''} onChange={setField} /> ({ value: o.value, label: localized(o.label) || o.value, }))} value={String(value?.equals ?? value?.notEquals ?? '')} onChange={(v) => v !== null && setValueRaw(v)} /> )} {operator !== 'isSet' && operator !== 'in' && target?.field.type !== 'SELECT' && ( target?.field.type === 'BOOLEAN' ? ( setValueRaw(String(e.currentTarget.checked))} /> ) : ( setValueRaw(e.currentTarget.value)} /> ) )} {operator === 'in' && target?.field.type === 'SELECT' && (