import { Questionnaire, QuestionnaireItem } from 'fhir/r4'; import { COLUMN_GRID, COLUMN_SPAN_EXTENSION_URL, normalizeColumnSpan } from './questionnaireLayout'; import { EMAIL_ADDRESS_REGEX, REGEX_EXTENSION_URL } from './questionnaireEmailField'; // Sanitized, author-side snapshot of a Questionnaire's structure sent to the AWS // `public-form/register` endpoint. Only the fields an anonymous public form renderer needs to // display + validate the form survive here - server-authoring fields (code, initial, prefix, // readOnly, repeats, etc.) are intentionally dropped. // // Extensions are dropped as a rule, with TWO deliberate exceptions, each decoded into a flat field: the // column-span layout hint into `columnSpan`, because the public form must render the layout the author // built, and the email rule into `inputKind`, because the visitor's entry must be checked as an address. See docs/_CLAUDE/2026-07-16-questionnaire-grid-columns-design.md. export interface PublicFormItem { linkId: string; text?: string; type: string; required?: boolean; columnSpan?: number; inputKind?: 'email'; answerOption?: any[]; enableWhen?: any[]; item?: PublicFormItem[]; } // Mirrors the `_help` synthetic-child filter used elsewhere (see // QuestionnaireService.tsx's `transformFromFhirQuestionnaireItem`): help-text display items are // synthesized server/client-side as a child with linkId `${parent.linkId}_help` and are not // real authored questions, so they're excluded from the public snapshot. const isHelpChild = (parentLinkId: string, childLinkId: string): boolean => childLinkId === `${parentLinkId}_help`; const buildPublicFormItem = (item: QuestionnaireItem): PublicFormItem => { const children = (item.item ?? []).filter((childItem) => !isHelpChild(item.linkId, childItem.linkId)); const columnSpan = normalizeColumnSpan( item.extension?.find((extension) => extension.url === COLUMN_SPAN_EXTENSION_URL)?.valueInteger ); const isEmail = item.type === 'string' && item.extension?.some((extension) => extension.url === REGEX_EXTENSION_URL && extension.valueString === EMAIL_ADDRESS_REGEX); return { linkId: item.linkId, ...(item.text !== undefined && { text: item.text }), type: item.type, ...(item.required !== undefined && { required: item.required }), // Full width is the renderer's default, so only a non-default span is worth the bytes. ...(columnSpan !== COLUMN_GRID && { columnSpan }), ...(isEmail && { inputKind: 'email' as const }), ...(item.answerOption && item.answerOption.length !== 0 && { answerOption: item.answerOption }), ...(item.enableWhen && item.enableWhen.length !== 0 && { enableWhen: item.enableWhen }), ...(children.length !== 0 && { item: children.map(buildPublicFormItem) }) }; }; export function buildPublicFormSnapshot(questionnaire: Questionnaire): { title: string; description?: string; items: PublicFormItem[] } { const description = questionnaire.description?.trim(); return { title: questionnaire.title ?? '', ...(description && { description }), items: (questionnaire.item ?? []).map(buildPublicFormItem) }; }