@welshare/questionnaire 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +151 -0
  2. package/dist/esm/components/bmi-form.d.ts +68 -0
  3. package/dist/esm/components/bmi-form.d.ts.map +1 -0
  4. package/dist/esm/components/bmi-form.js +138 -0
  5. package/dist/esm/components/question-renderer.d.ts +6 -1
  6. package/dist/esm/components/question-renderer.d.ts.map +1 -1
  7. package/dist/esm/components/question-renderer.js +25 -14
  8. package/dist/esm/components/questions/decimal-question.d.ts +8 -1
  9. package/dist/esm/components/questions/decimal-question.d.ts.map +1 -1
  10. package/dist/esm/components/questions/decimal-question.js +19 -1
  11. package/dist/esm/components/questions/multiple-choice-question.d.ts.map +1 -1
  12. package/dist/esm/components/questions/multiple-choice-question.js +2 -2
  13. package/dist/esm/contexts/questionnaire-context.d.ts.map +1 -1
  14. package/dist/esm/contexts/questionnaire-context.js +3 -2
  15. package/dist/esm/index.d.ts +6 -2
  16. package/dist/esm/index.d.ts.map +1 -1
  17. package/dist/esm/index.js +5 -1
  18. package/dist/esm/lib/bmi-helpers.d.ts +50 -0
  19. package/dist/esm/lib/bmi-helpers.d.ts.map +1 -0
  20. package/dist/esm/lib/bmi-helpers.js +69 -0
  21. package/dist/esm/lib/constants.d.ts +94 -0
  22. package/dist/esm/lib/constants.d.ts.map +1 -0
  23. package/dist/esm/lib/constants.js +93 -0
  24. package/dist/esm/lib/questionnaire-utils.d.ts +21 -1
  25. package/dist/esm/lib/questionnaire-utils.d.ts.map +1 -1
  26. package/dist/esm/lib/questionnaire-utils.js +85 -4
  27. package/dist/esm/types/fhir.d.ts +1 -0
  28. package/dist/esm/types/fhir.d.ts.map +1 -1
  29. package/dist/esm/types/index.d.ts +25 -0
  30. package/dist/esm/types/index.d.ts.map +1 -1
  31. package/dist/styles.css +108 -0
  32. package/package.json +17 -6
  33. package/dist/node_modules/@welshare/questionnaire/.tshy/build.json +0 -8
  34. package/dist/node_modules/@welshare/questionnaire/.tshy/esm.json +0 -16
  35. package/dist/node_modules/@welshare/questionnaire/LICENSE +0 -7
  36. package/dist/node_modules/@welshare/questionnaire/README.md +0 -173
  37. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/debug-section.d.ts +0 -44
  38. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/debug-section.d.ts.map +0 -1
  39. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/debug-section.js +0 -28
  40. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/question-renderer.d.ts +0 -80
  41. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/question-renderer.d.ts.map +0 -1
  42. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/question-renderer.js +0 -183
  43. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/boolean-question.d.ts +0 -15
  44. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/boolean-question.d.ts.map +0 -1
  45. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/boolean-question.js +0 -19
  46. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/choice-question.d.ts +0 -19
  47. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/choice-question.d.ts.map +0 -1
  48. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/choice-question.js +0 -23
  49. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/decimal-question.d.ts +0 -12
  50. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/decimal-question.d.ts.map +0 -1
  51. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/decimal-question.js +0 -7
  52. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/integer-question.d.ts +0 -18
  53. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/integer-question.d.ts.map +0 -1
  54. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/integer-question.js +0 -24
  55. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/multiple-choice-question.d.ts +0 -20
  56. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/multiple-choice-question.d.ts.map +0 -1
  57. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/multiple-choice-question.js +0 -39
  58. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/string-question.d.ts +0 -12
  59. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/string-question.d.ts.map +0 -1
  60. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/string-question.js +0 -7
  61. package/dist/node_modules/@welshare/questionnaire/dist/esm/contexts/questionnaire-context.d.ts +0 -41
  62. package/dist/node_modules/@welshare/questionnaire/dist/esm/contexts/questionnaire-context.d.ts.map +0 -1
  63. package/dist/node_modules/@welshare/questionnaire/dist/esm/contexts/questionnaire-context.js +0 -350
  64. package/dist/node_modules/@welshare/questionnaire/dist/esm/index.d.ts +0 -7
  65. package/dist/node_modules/@welshare/questionnaire/dist/esm/index.d.ts.map +0 -1
  66. package/dist/node_modules/@welshare/questionnaire/dist/esm/index.js +0 -6
  67. package/dist/node_modules/@welshare/questionnaire/dist/esm/lib/questionnaire-utils.d.ts +0 -33
  68. package/dist/node_modules/@welshare/questionnaire/dist/esm/lib/questionnaire-utils.d.ts.map +0 -1
  69. package/dist/node_modules/@welshare/questionnaire/dist/esm/lib/questionnaire-utils.js +0 -99
  70. package/dist/node_modules/@welshare/questionnaire/dist/esm/package.json +0 -3
  71. package/dist/node_modules/@welshare/questionnaire/dist/esm/types/fhir.d.ts +0 -117
  72. package/dist/node_modules/@welshare/questionnaire/dist/esm/types/fhir.d.ts.map +0 -1
  73. package/dist/node_modules/@welshare/questionnaire/dist/esm/types/fhir.js +0 -3
  74. package/dist/node_modules/@welshare/questionnaire/dist/esm/types/index.d.ts +0 -51
  75. package/dist/node_modules/@welshare/questionnaire/dist/esm/types/index.d.ts.map +0 -1
  76. package/dist/node_modules/@welshare/questionnaire/dist/esm/types/index.js +0 -1
  77. package/dist/node_modules/@welshare/questionnaire/dist/styles.css +0 -467
  78. package/dist/node_modules/@welshare/questionnaire/dist/tokens.css +0 -130
  79. package/dist/node_modules/@welshare/questionnaire/package.json +0 -85
  80. package/dist/node_modules/@welshare/questionnaire/src/components/debug-section.tsx +0 -116
  81. package/dist/node_modules/@welshare/questionnaire/src/components/question-renderer.tsx +0 -391
  82. package/dist/node_modules/@welshare/questionnaire/src/components/questionnaire-styles.css +0 -467
  83. package/dist/node_modules/@welshare/questionnaire/src/components/questionnaire-tokens.css +0 -130
  84. package/dist/node_modules/@welshare/questionnaire/src/components/questions/boolean-question.tsx +0 -72
  85. package/dist/node_modules/@welshare/questionnaire/src/components/questions/choice-question.tsx +0 -68
  86. package/dist/node_modules/@welshare/questionnaire/src/components/questions/decimal-question.tsx +0 -32
  87. package/dist/node_modules/@welshare/questionnaire/src/components/questions/integer-question.tsx +0 -87
  88. package/dist/node_modules/@welshare/questionnaire/src/components/questions/multiple-choice-question.tsx +0 -119
  89. package/dist/node_modules/@welshare/questionnaire/src/components/questions/string-question.tsx +0 -31
  90. package/dist/node_modules/@welshare/questionnaire/src/contexts/questionnaire-context.tsx +0 -499
  91. package/dist/node_modules/@welshare/questionnaire/src/index.ts +0 -41
  92. package/dist/node_modules/@welshare/questionnaire/src/lib/__tests__/questionnaire-utils.test.ts +0 -578
  93. package/dist/node_modules/@welshare/questionnaire/src/lib/questionnaire-utils.ts +0 -122
  94. package/dist/node_modules/@welshare/questionnaire/src/types/fhir.ts +0 -126
  95. package/dist/node_modules/@welshare/questionnaire/src/types/index.ts +0 -44
  96. package/dist/node_modules/@welshare/questionnaire/tsconfig.json +0 -16
@@ -1,85 +0,0 @@
1
- {
2
- "name": "@welshare/questionnaire",
3
- "version": "0.1.2",
4
- "description": "FHIR Questionnaire components for React with state management and validation",
5
- "keywords": [
6
- "react",
7
- "fhir",
8
- "questionnaire",
9
- "survey",
10
- "forms",
11
- "healthcare"
12
- ],
13
- "author": "Welshare UG (haftungsbeschränkt)",
14
- "license": "MIT",
15
- "repository": {
16
- "type": "git",
17
- "url": "https://github.com/welshare/surveys-monorepo.git",
18
- "directory": "packages/welshare-questionnaire"
19
- },
20
- "bugs": {
21
- "url": "https://github.com/welshare/surveys-monorepo/issues"
22
- },
23
- "homepage": "https://welshare.health",
24
- "type": "module",
25
- "scripts": {
26
- "lint": "eslint . --max-warnings 25",
27
- "build": "tshy && npm run build:css",
28
- "build:css": "cp src/components/questionnaire-tokens.css dist/tokens.css && cp src/components/questionnaire-styles.css dist/styles.css",
29
- "build:clean": "rm -rf ./dist",
30
- "test": "vitest",
31
- "test:run": "vitest run",
32
- "test:coverage": "vitest run --coverage",
33
- "prepublishOnly": "npm run build:clean && npm run build"
34
- },
35
- "peerDependencies": {
36
- "react": "^19",
37
- "react-dom": "^19"
38
- },
39
- "devDependencies": {
40
- "@types/node": "^22",
41
- "@types/react": "^19",
42
- "@workspace/eslint-config": "workspace:*",
43
- "@workspace/typescript-config": "workspace:*",
44
- "tshy": "^3.0.2",
45
- "typescript": "^5.7.3",
46
- "vitest": "^3.2.4",
47
- "@vitest/ui": "^3.2.4",
48
- "@vitest/coverage-v8": "^3.2.4",
49
- "jsdom": "^26.1.0"
50
- },
51
- "tshy": {
52
- "dialects": [
53
- "esm"
54
- ],
55
- "exclude": [
56
- "src/**/__tests__/**"
57
- ],
58
- "exports": {
59
- "./package.json": "./package.json",
60
- "./tokens.css": "./dist/tokens.css",
61
- "./styles.css": "./dist/styles.css",
62
- ".": "./src/index.ts"
63
- }
64
- },
65
- "module": "./dist/esm/index.js",
66
- "exports": {
67
- "./package.json": "./package.json",
68
- "./tokens.css": "./dist/tokens.css",
69
- "./styles.css": "./dist/styles.css",
70
- ".": {
71
- "import": {
72
- "types": "./dist/esm/index.d.ts",
73
- "default": "./dist/esm/index.js"
74
- }
75
- }
76
- },
77
- "files": [
78
- "dist/**/*",
79
- "README.md",
80
- "LICENSE"
81
- ],
82
- "engines": {
83
- "node": "^22.0.0"
84
- }
85
- }
@@ -1,116 +0,0 @@
1
- import { useState } from "react";
2
- import type {
3
- QuestionnaireItem,
4
- QuestionnaireResponseAnswer,
5
- Coding,
6
- } from "../types/fhir.js";
7
-
8
- export interface DebugSectionProps {
9
- /**
10
- * The questionnaire item to display debug information for
11
- */
12
- item: QuestionnaireItem;
13
- /**
14
- * The current answer (for single-answer questions)
15
- */
16
- currentAnswer?: QuestionnaireResponseAnswer;
17
- /**
18
- * The current answers (for multi-answer questions)
19
- */
20
- currentAnswers?: QuestionnaireResponseAnswer[];
21
- /**
22
- * Optional LOINC code to display
23
- */
24
- loincCode?: Coding;
25
- /**
26
- * Additional CSS class names
27
- */
28
- className?: string;
29
- }
30
-
31
- /**
32
- * DebugSection component displays debug information for a questionnaire item.
33
- *
34
- * This component is designed to be reusable and can be moved to the shared package.
35
- * It displays:
36
- * - LOINC code link (if available)
37
- * - Collapsible FHIR question definition
38
- * - Collapsible selected answer(s) in FHIR format
39
- *
40
- * @example
41
- * ```tsx
42
- * <DebugSection
43
- * item={questionItem}
44
- * currentAnswer={answer}
45
- * currentAnswers={answers}
46
- * loincCode={loincCode}
47
- * />
48
- * ```
49
- */
50
- export const DebugSection = ({
51
- item,
52
- currentAnswer,
53
- currentAnswers = [],
54
- loincCode,
55
- className = "",
56
- }: DebugSectionProps) => {
57
- const [isQuestionDefOpen, setIsQuestionDefOpen] = useState(false);
58
- const [isAnswersOpen, setIsAnswersOpen] = useState(false);
59
-
60
- return (
61
- <div className={`wq-debug-section ${className}`}>
62
- {loincCode && loincCode.code && (
63
- <div className="wq-loinc-link-section">
64
- <a
65
- href={`https://loinc.org/${loincCode.code}`}
66
- target="_blank"
67
- rel="noopener noreferrer"
68
- className="wq-loinc-link"
69
- >
70
- 📋 LOINC Specification: {loincCode.code}
71
- </a>
72
- </div>
73
- )}
74
-
75
- {/* Collapsible FHIR Question Definition */}
76
- <div className="wq-debug-collapsible">
77
- <button
78
- className="wq-debug-collapsible-header"
79
- onClick={() => setIsQuestionDefOpen(!isQuestionDefOpen)}
80
- type="button"
81
- >
82
- <span className={`chevron ${isQuestionDefOpen ? "open" : ""}`}>
83
-
84
- </span>
85
- <span className="wq-debug-title">FHIR Question Definition</span>
86
- </button>
87
- {isQuestionDefOpen && (
88
- <pre className="wq-debug-json">{JSON.stringify(item, null, 2)}</pre>
89
- )}
90
- </div>
91
-
92
- {/* Collapsible Selected Answer(s) */}
93
- {(currentAnswer || currentAnswers.length > 0) && (
94
- <div className="wq-debug-collapsible">
95
- <button
96
- className="wq-debug-collapsible-header"
97
- onClick={() => setIsAnswersOpen(!isAnswersOpen)}
98
- type="button"
99
- >
100
- <span className={`chevron ${isAnswersOpen ? "open" : ""}`}>▶</span>
101
- <span className="wq-debug-title">
102
- Selected Answer(s) (FHIR Format)
103
- </span>
104
- </button>
105
- {isAnswersOpen && (
106
- <pre className="wq-debug-json">
107
- {item.repeats
108
- ? JSON.stringify(currentAnswers, null, 2)
109
- : JSON.stringify(currentAnswer, null, 2)}
110
- </pre>
111
- )}
112
- </div>
113
- )}
114
- </div>
115
- );
116
- };
@@ -1,391 +0,0 @@
1
- import { type ReactNode } from "react";
2
- import { useQuestionnaire } from "../contexts/questionnaire-context.js";
3
- import type { QuestionnaireItem } from "../types/fhir.js";
4
- import { DebugSection } from "./debug-section.js";
5
- import { ChoiceQuestion } from "./questions/choice-question.js";
6
- import { MultipleChoiceQuestion } from "./questions/multiple-choice-question.js";
7
- import { IntegerQuestion } from "./questions/integer-question.js";
8
- import { DecimalQuestion } from "./questions/decimal-question.js";
9
- import { StringQuestion } from "./questions/string-question.js";
10
- import { BooleanQuestion } from "./questions/boolean-question.js";
11
- import { RadioInputProps, CheckboxInputProps } from "../types/index.js";
12
-
13
- export interface QuestionRendererProps {
14
- item: QuestionnaireItem;
15
- /**
16
- * Additional class names for the question container
17
- * Will be appended to the default wq-question-container class
18
- */
19
- className?: string;
20
- /**
21
- * Additional class names for input elements
22
- * Will be appended to the default wq-question-input class
23
- */
24
- inputClassName?: string;
25
- /**
26
- * Additional class names for choice options
27
- * Will be appended to the default wq-choice-option class
28
- */
29
- choiceClassName?: string;
30
- /**
31
- * Custom renderer for radio button inputs.
32
- * When provided, this function will be called to render each radio option,
33
- * giving clients full control over styling and structure.
34
- *
35
- * @example
36
- * ```tsx
37
- * <QuestionRenderer
38
- * item={item}
39
- * renderRadioInput={(props) => (
40
- * <div className="custom-radio-wrapper">
41
- * <input
42
- * type="radio"
43
- * checked={props.checked}
44
- * onChange={props.onChange}
45
- * className="custom-radio"
46
- * />
47
- * <label>{props.label}</label>
48
- * </div>
49
- * )}
50
- * />
51
- * ```
52
- */
53
- renderRadioInput?: (props: RadioInputProps) => ReactNode;
54
- /**
55
- * Custom renderer for checkbox inputs.
56
- * When provided, this function will be called to render each checkbox option,
57
- * giving clients full control over styling and structure.
58
- *
59
- * @example
60
- * ```tsx
61
- * <QuestionRenderer
62
- * item={item}
63
- * renderCheckboxInput={(props) => (
64
- * <div className="custom-checkbox-wrapper">
65
- * <input
66
- * type="checkbox"
67
- * checked={props.checked}
68
- * onChange={props.onChange}
69
- * className="custom-checkbox"
70
- * />
71
- * <label>{props.label}</label>
72
- * </div>
73
- * )}
74
- * />
75
- * ```
76
- */
77
- renderCheckboxInput?: (props: CheckboxInputProps) => ReactNode;
78
- }
79
-
80
- /**
81
- * Wrapper component that combines the library's QuestionRenderer with debug functionality.
82
- *
83
- * This component:
84
- * - Renders the library's QuestionRenderer with custom radio/checkbox renderers
85
- * - Adds DebugSection below it when debug mode is enabled
86
- *
87
- * Uses custom input renderers to maintain the app's design system styling.
88
- */
89
- export const QuestionRenderer = (rendererProps: QuestionRendererProps) => {
90
- const { getAnswer, getAnswers, debugMode } = useQuestionnaire();
91
-
92
- const { item } = rendererProps;
93
- const currentAnswer = getAnswer(item.linkId);
94
- const currentAnswers = getAnswers(item.linkId);
95
-
96
- // Find LOINC code if present
97
- const loincCode = item.code?.find(
98
- (coding) => coding.system === "http://loinc.org"
99
- );
100
-
101
- return (
102
- <div>
103
- <QuestionRendererInternal {...rendererProps} />
104
- {debugMode && (
105
- <DebugSection
106
- item={item}
107
- currentAnswer={currentAnswer}
108
- currentAnswers={currentAnswers}
109
- loincCode={loincCode}
110
- />
111
- )}
112
- </div>
113
- );
114
- };
115
-
116
- /**
117
- * QuestionRenderer component renders different types of questionnaire items
118
- * Supports: choice, integer, decimal, string, text, boolean
119
- * Features:
120
- * - Single and multiple choice (radio/checkbox)
121
- * - Slider controls for integer inputs
122
- * - Exclusive option support for multi-select
123
- * - Auto-populated (hidden) fields
124
- * - Validation error display
125
- */
126
- const QuestionRendererInternal = ({
127
- item,
128
- className = "",
129
- inputClassName = "",
130
- choiceClassName = "",
131
- renderRadioInput,
132
- renderCheckboxInput,
133
- }: QuestionRendererProps) => {
134
- const {
135
- updateAnswer,
136
- updateMultipleAnswers,
137
- getAnswer,
138
- getAnswers,
139
- hasValidationError,
140
- } = useQuestionnaire();
141
-
142
- const currentAnswer = getAnswer(item.linkId);
143
- const currentAnswers = getAnswers(item.linkId);
144
- const hasError = hasValidationError(item.linkId);
145
-
146
- // Check if this field should be hidden (auto-populated)
147
- const isAutoPopulated = item.extension?.some(
148
- (ext) =>
149
- ext.url ===
150
- "http://hl7.org/fhir/StructureDefinition/questionnaire-hidden" &&
151
- ext.valueBoolean === true
152
- );
153
-
154
- // Don't render auto-populated fields
155
- if (isAutoPopulated) {
156
- return null;
157
- }
158
-
159
- const handleChoiceChange = (
160
- valueCoding: { system?: string; code?: string; display?: string },
161
- valueInteger?: number
162
- ) => {
163
- // For choice questions, only set one field: valueCoding takes precedence
164
- if (valueCoding && (valueCoding.code || valueCoding.display || valueCoding.system)) {
165
- updateAnswer(item.linkId, { valueCoding });
166
- } else if (valueInteger !== undefined) {
167
- updateAnswer(item.linkId, { valueInteger });
168
- }
169
- };
170
-
171
- const handleMultipleChoiceToggle = (
172
- valueCoding: { system?: string; code?: string; display?: string },
173
- valueInteger?: number
174
- ) => {
175
- const isSelected = currentAnswers.some(
176
- (answer) => answer.valueCoding?.code === valueCoding.code
177
- );
178
-
179
- // Check if there's an exclusive option extension
180
- const exclusiveOptionExt = item.extension?.find(
181
- (ext) =>
182
- ext.url ===
183
- "http://codes.welshare.app/StructureDefinition/questionnaire-exclusive-option"
184
- );
185
- const exclusiveOptionCode = exclusiveOptionExt?.valueString;
186
-
187
- let newAnswers;
188
- if (isSelected) {
189
- // Remove the answer
190
- newAnswers = currentAnswers.filter(
191
- (answer) => answer.valueCoding?.code !== valueCoding.code
192
- );
193
- } else {
194
- // Check if we're selecting the exclusive option
195
- if (exclusiveOptionCode && valueCoding.code === exclusiveOptionCode) {
196
- // Clear all other answers and only keep this one
197
- // For choice questions, only set one field: valueCoding takes precedence
198
- const answer = valueCoding && (valueCoding.code || valueCoding.display || valueCoding.system)
199
- ? { valueCoding }
200
- : valueInteger !== undefined
201
- ? { valueInteger }
202
- : { valueCoding };
203
- newAnswers = [answer];
204
- } else {
205
- // Check if exclusive option is currently selected
206
- const exclusiveSelected = currentAnswers.some(
207
- (answer) => answer.valueCoding?.code === exclusiveOptionCode
208
- );
209
-
210
- if (exclusiveSelected) {
211
- // Remove exclusive option and add new selection
212
- newAnswers = currentAnswers.filter(
213
- (answer) => answer.valueCoding?.code !== exclusiveOptionCode
214
- );
215
- // For choice questions, only set one field: valueCoding takes precedence
216
- const answer = valueCoding && (valueCoding.code || valueCoding.display || valueCoding.system)
217
- ? { valueCoding }
218
- : valueInteger !== undefined
219
- ? { valueInteger }
220
- : { valueCoding };
221
- newAnswers = [...newAnswers, answer];
222
- } else {
223
- // Normal behavior: Add the answer (respecting maxAnswers limit)
224
- const maxAnswers = item.maxAnswers || Number.MAX_SAFE_INTEGER;
225
- if (currentAnswers.length < maxAnswers) {
226
- // For choice questions, only set one field: valueCoding takes precedence
227
- const answer = valueCoding && (valueCoding.code || valueCoding.display || valueCoding.system)
228
- ? { valueCoding }
229
- : valueInteger !== undefined
230
- ? { valueInteger }
231
- : { valueCoding };
232
- newAnswers = [...currentAnswers, answer];
233
- } else {
234
- // Already at max, don't add
235
- return;
236
- }
237
- }
238
- }
239
- }
240
-
241
- updateMultipleAnswers(item.linkId, newAnswers);
242
- };
243
-
244
- const handleIntegerChange = (value: string) => {
245
- // Allow clearing the controlled input
246
- if (value === "") {
247
- updateAnswer(item.linkId, {});
248
- return;
249
- }
250
- const numValue = parseInt(value);
251
- if (!isNaN(numValue)) {
252
- updateAnswer(item.linkId, { valueInteger: numValue });
253
- }
254
- };
255
-
256
- const handleDecimalChange = (value: string) => {
257
- // Allow clearing the controlled input
258
- if (value === "") {
259
- updateAnswer(item.linkId, {});
260
- return;
261
- }
262
- const numValue = parseFloat(value);
263
- if (!isNaN(numValue)) {
264
- updateAnswer(item.linkId, { valueDecimal: numValue });
265
- }
266
- };
267
-
268
- const handleStringChange = (value: string) => {
269
- updateAnswer(item.linkId, { valueString: value });
270
- };
271
-
272
- const handleBooleanChange = (value: boolean) => {
273
- updateAnswer(item.linkId, { valueBoolean: value });
274
- };
275
-
276
- // Check if this field should use a slider control
277
- const getSliderConfig = () => {
278
- const sliderExt = item.extension?.find(
279
- (ext) =>
280
- ext.url ===
281
- "http://codes.welshare.app/StructureDefinition/questionnaire-slider-control"
282
- );
283
-
284
- if (!sliderExt?.extension) return null;
285
-
286
- const minValue =
287
- sliderExt.extension.find((e) => e.url === "minValue")?.valueInteger ?? 0;
288
- const maxValue =
289
- sliderExt.extension.find((e) => e.url === "maxValue")?.valueInteger ??
290
- 100;
291
- const step =
292
- sliderExt.extension.find((e) => e.url === "step")?.valueInteger ?? 1;
293
- const unit =
294
- sliderExt.extension.find((e) => e.url === "unit")?.valueString ?? "";
295
-
296
- return { minValue, maxValue, step, unit };
297
- };
298
-
299
- const sliderConfig = getSliderConfig();
300
-
301
- const renderQuestion = () => {
302
- switch (item.type) {
303
- case "choice":
304
- // Multi-select with checkboxes when repeats is true
305
- if (item.repeats) {
306
- return (
307
- <MultipleChoiceQuestion
308
- item={item}
309
- currentAnswers={currentAnswers}
310
- onMultipleChoiceToggle={handleMultipleChoiceToggle}
311
- choiceClassName={choiceClassName}
312
- renderCheckboxInput={renderCheckboxInput}
313
- />
314
- );
315
- }
316
-
317
- // Single-select with radio buttons (default)
318
- return (
319
- <ChoiceQuestion
320
- item={item}
321
- currentAnswer={currentAnswer}
322
- onChoiceChange={handleChoiceChange}
323
- choiceClassName={choiceClassName}
324
- renderRadioInput={renderRadioInput}
325
- />
326
- );
327
-
328
- case "integer":
329
- return (
330
- <IntegerQuestion
331
- item={item}
332
- currentAnswer={currentAnswer}
333
- onIntegerChange={handleIntegerChange}
334
- inputClassName={inputClassName}
335
- sliderConfig={sliderConfig}
336
- />
337
- );
338
-
339
- case "decimal":
340
- return (
341
- <DecimalQuestion
342
- item={item}
343
- currentAnswer={currentAnswer}
344
- onDecimalChange={handleDecimalChange}
345
- inputClassName={inputClassName}
346
- />
347
- );
348
-
349
- case "string":
350
- case "text":
351
- return (
352
- <StringQuestion
353
- item={item}
354
- currentAnswer={currentAnswer}
355
- onStringChange={handleStringChange}
356
- inputClassName={inputClassName}
357
- />
358
- );
359
-
360
- case "boolean":
361
- return (
362
- <BooleanQuestion
363
- item={item}
364
- currentAnswer={currentAnswer}
365
- onBooleanChange={handleBooleanChange}
366
- choiceClassName={choiceClassName}
367
- renderRadioInput={renderRadioInput}
368
- />
369
- );
370
-
371
- default:
372
- return (
373
- <div className="wq-unsupported-type">
374
- Unsupported question type: {item.type}
375
- </div>
376
- );
377
- }
378
- };
379
-
380
- return (
381
- <div
382
- className={`wq-question-container ${hasError ? "wq-has-error" : ""} ${className}`}
383
- >
384
- <div className="wq-question-text">
385
- {item.text}
386
- {item.required && <span className="wq-required-indicator">*</span>}
387
- </div>
388
- {renderQuestion()}
389
- </div>
390
- );
391
- };