@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.
- package/README.md +151 -0
- package/dist/esm/components/bmi-form.d.ts +68 -0
- package/dist/esm/components/bmi-form.d.ts.map +1 -0
- package/dist/esm/components/bmi-form.js +138 -0
- package/dist/esm/components/question-renderer.d.ts +6 -1
- package/dist/esm/components/question-renderer.d.ts.map +1 -1
- package/dist/esm/components/question-renderer.js +25 -14
- package/dist/esm/components/questions/decimal-question.d.ts +8 -1
- package/dist/esm/components/questions/decimal-question.d.ts.map +1 -1
- package/dist/esm/components/questions/decimal-question.js +19 -1
- package/dist/esm/components/questions/multiple-choice-question.d.ts.map +1 -1
- package/dist/esm/components/questions/multiple-choice-question.js +2 -2
- package/dist/esm/contexts/questionnaire-context.d.ts.map +1 -1
- package/dist/esm/contexts/questionnaire-context.js +3 -2
- package/dist/esm/index.d.ts +6 -2
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +5 -1
- package/dist/esm/lib/bmi-helpers.d.ts +50 -0
- package/dist/esm/lib/bmi-helpers.d.ts.map +1 -0
- package/dist/esm/lib/bmi-helpers.js +69 -0
- package/dist/esm/lib/constants.d.ts +94 -0
- package/dist/esm/lib/constants.d.ts.map +1 -0
- package/dist/esm/lib/constants.js +93 -0
- package/dist/esm/lib/questionnaire-utils.d.ts +21 -1
- package/dist/esm/lib/questionnaire-utils.d.ts.map +1 -1
- package/dist/esm/lib/questionnaire-utils.js +85 -4
- package/dist/esm/types/fhir.d.ts +1 -0
- package/dist/esm/types/fhir.d.ts.map +1 -1
- package/dist/esm/types/index.d.ts +25 -0
- package/dist/esm/types/index.d.ts.map +1 -1
- package/dist/styles.css +108 -0
- package/package.json +17 -6
- package/dist/node_modules/@welshare/questionnaire/.tshy/build.json +0 -8
- package/dist/node_modules/@welshare/questionnaire/.tshy/esm.json +0 -16
- package/dist/node_modules/@welshare/questionnaire/LICENSE +0 -7
- package/dist/node_modules/@welshare/questionnaire/README.md +0 -173
- package/dist/node_modules/@welshare/questionnaire/dist/esm/components/debug-section.d.ts +0 -44
- package/dist/node_modules/@welshare/questionnaire/dist/esm/components/debug-section.d.ts.map +0 -1
- package/dist/node_modules/@welshare/questionnaire/dist/esm/components/debug-section.js +0 -28
- package/dist/node_modules/@welshare/questionnaire/dist/esm/components/question-renderer.d.ts +0 -80
- package/dist/node_modules/@welshare/questionnaire/dist/esm/components/question-renderer.d.ts.map +0 -1
- package/dist/node_modules/@welshare/questionnaire/dist/esm/components/question-renderer.js +0 -183
- package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/boolean-question.d.ts +0 -15
- package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/boolean-question.d.ts.map +0 -1
- package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/boolean-question.js +0 -19
- package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/choice-question.d.ts +0 -19
- package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/choice-question.d.ts.map +0 -1
- package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/choice-question.js +0 -23
- package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/decimal-question.d.ts +0 -12
- package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/decimal-question.d.ts.map +0 -1
- package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/decimal-question.js +0 -7
- package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/integer-question.d.ts +0 -18
- package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/integer-question.d.ts.map +0 -1
- package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/integer-question.js +0 -24
- package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/multiple-choice-question.d.ts +0 -20
- package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/multiple-choice-question.d.ts.map +0 -1
- package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/multiple-choice-question.js +0 -39
- package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/string-question.d.ts +0 -12
- package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/string-question.d.ts.map +0 -1
- package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/string-question.js +0 -7
- package/dist/node_modules/@welshare/questionnaire/dist/esm/contexts/questionnaire-context.d.ts +0 -41
- package/dist/node_modules/@welshare/questionnaire/dist/esm/contexts/questionnaire-context.d.ts.map +0 -1
- package/dist/node_modules/@welshare/questionnaire/dist/esm/contexts/questionnaire-context.js +0 -350
- package/dist/node_modules/@welshare/questionnaire/dist/esm/index.d.ts +0 -7
- package/dist/node_modules/@welshare/questionnaire/dist/esm/index.d.ts.map +0 -1
- package/dist/node_modules/@welshare/questionnaire/dist/esm/index.js +0 -6
- package/dist/node_modules/@welshare/questionnaire/dist/esm/lib/questionnaire-utils.d.ts +0 -33
- package/dist/node_modules/@welshare/questionnaire/dist/esm/lib/questionnaire-utils.d.ts.map +0 -1
- package/dist/node_modules/@welshare/questionnaire/dist/esm/lib/questionnaire-utils.js +0 -99
- package/dist/node_modules/@welshare/questionnaire/dist/esm/package.json +0 -3
- package/dist/node_modules/@welshare/questionnaire/dist/esm/types/fhir.d.ts +0 -117
- package/dist/node_modules/@welshare/questionnaire/dist/esm/types/fhir.d.ts.map +0 -1
- package/dist/node_modules/@welshare/questionnaire/dist/esm/types/fhir.js +0 -3
- package/dist/node_modules/@welshare/questionnaire/dist/esm/types/index.d.ts +0 -51
- package/dist/node_modules/@welshare/questionnaire/dist/esm/types/index.d.ts.map +0 -1
- package/dist/node_modules/@welshare/questionnaire/dist/esm/types/index.js +0 -1
- package/dist/node_modules/@welshare/questionnaire/dist/styles.css +0 -467
- package/dist/node_modules/@welshare/questionnaire/dist/tokens.css +0 -130
- package/dist/node_modules/@welshare/questionnaire/package.json +0 -85
- package/dist/node_modules/@welshare/questionnaire/src/components/debug-section.tsx +0 -116
- package/dist/node_modules/@welshare/questionnaire/src/components/question-renderer.tsx +0 -391
- package/dist/node_modules/@welshare/questionnaire/src/components/questionnaire-styles.css +0 -467
- package/dist/node_modules/@welshare/questionnaire/src/components/questionnaire-tokens.css +0 -130
- package/dist/node_modules/@welshare/questionnaire/src/components/questions/boolean-question.tsx +0 -72
- package/dist/node_modules/@welshare/questionnaire/src/components/questions/choice-question.tsx +0 -68
- package/dist/node_modules/@welshare/questionnaire/src/components/questions/decimal-question.tsx +0 -32
- package/dist/node_modules/@welshare/questionnaire/src/components/questions/integer-question.tsx +0 -87
- package/dist/node_modules/@welshare/questionnaire/src/components/questions/multiple-choice-question.tsx +0 -119
- package/dist/node_modules/@welshare/questionnaire/src/components/questions/string-question.tsx +0 -31
- package/dist/node_modules/@welshare/questionnaire/src/contexts/questionnaire-context.tsx +0 -499
- package/dist/node_modules/@welshare/questionnaire/src/index.ts +0 -41
- package/dist/node_modules/@welshare/questionnaire/src/lib/__tests__/questionnaire-utils.test.ts +0 -578
- package/dist/node_modules/@welshare/questionnaire/src/lib/questionnaire-utils.ts +0 -122
- package/dist/node_modules/@welshare/questionnaire/src/types/fhir.ts +0 -126
- package/dist/node_modules/@welshare/questionnaire/src/types/index.ts +0 -44
- package/dist/node_modules/@welshare/questionnaire/tsconfig.json +0 -16
package/README.md
CHANGED
|
@@ -97,12 +97,74 @@ const {
|
|
|
97
97
|
|
|
98
98
|
**Supported Types:** `choice`, `boolean`, `integer`, `decimal`, `string`, `text`
|
|
99
99
|
|
|
100
|
+
### BmiForm
|
|
101
|
+
|
|
102
|
+
A controlled component for collecting height, weight, and calculating BMI. The component acts as an input helper that manages BMI calculation internally and reports changes to the parent via callbacks. Unit system (metric/imperial) is managed internally and automatically clears all fields when switched.
|
|
103
|
+
|
|
104
|
+
**Props:**
|
|
105
|
+
- `height: number` - Current height value (0 when empty)
|
|
106
|
+
- `weight: number` - Current weight value (0 when empty)
|
|
107
|
+
- `bmi: number` - Current BMI value (calculated and set by the component, 0 when not calculated)
|
|
108
|
+
- `onHeightChange: (value: number, unit: "cm" | "in") => void` - Called when height changes, includes current unit
|
|
109
|
+
- `onWeightChange: (value: number, unit: "kg" | "lb") => void` - Called when weight changes, includes current unit
|
|
110
|
+
- `onBmiChange: (value: number) => void` - Called when BMI is calculated or cleared
|
|
111
|
+
- `className?: string` - Optional CSS classes
|
|
112
|
+
|
|
113
|
+
**Features:**
|
|
114
|
+
- Controlled numeric values (height, weight, bmi) managed by parent
|
|
115
|
+
- BMI calculation handled internally by the component
|
|
116
|
+
- Unit system managed internally (defaults to metric)
|
|
117
|
+
- Automatically clears all fields (sets to 0) when switching unit systems
|
|
118
|
+
- No unit conversion - values are cleared on unit system change
|
|
119
|
+
- Uses consistent styling with questionnaire components
|
|
120
|
+
|
|
121
|
+
**Example:**
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
import { useState } from 'react';
|
|
125
|
+
import { BmiForm, getBmiCategory } from '@welshare/questionnaire';
|
|
126
|
+
import '@welshare/questionnaire/tokens.css';
|
|
127
|
+
import '@welshare/questionnaire/styles.css';
|
|
128
|
+
|
|
129
|
+
function MyComponent() {
|
|
130
|
+
const [height, setHeight] = useState(0);
|
|
131
|
+
const [weight, setWeight] = useState(0);
|
|
132
|
+
const [bmi, setBmi] = useState(0);
|
|
133
|
+
|
|
134
|
+
const handleBmiChange = (value: number) => {
|
|
135
|
+
setBmi(value);
|
|
136
|
+
|
|
137
|
+
// Optional: Get BMI category
|
|
138
|
+
if (value) {
|
|
139
|
+
const category = getBmiCategory(value);
|
|
140
|
+
console.log(`BMI Category: ${category}`);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
return (
|
|
145
|
+
<BmiForm
|
|
146
|
+
height={height}
|
|
147
|
+
weight={weight}
|
|
148
|
+
bmi={bmi}
|
|
149
|
+
onHeightChange={(value, unit) => setHeight(value)}
|
|
150
|
+
onWeightChange={(value, unit) => setWeight(value)}
|
|
151
|
+
onBmiChange={handleBmiChange}
|
|
152
|
+
/>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
100
157
|
### Utilities
|
|
101
158
|
|
|
159
|
+
**Questionnaire Utilities:**
|
|
102
160
|
- `getVisiblePages(questionnaire)` - Get visible page groups
|
|
103
161
|
- `calculateProgress(currentIndex, total)` - Calculate progress percentage
|
|
104
162
|
- `getAllQuestionsFromPage(pageItem)` - Get all questions from a page
|
|
105
163
|
|
|
164
|
+
**BMI Helper Functions:**
|
|
165
|
+
- `calculateBmi(height, weight, unitSystem)` - Calculate BMI from height and weight
|
|
166
|
+
- `getBmiCategory(bmi)` - Get WHO BMI category (Underweight, Normal weight, Overweight, Obese)
|
|
167
|
+
|
|
106
168
|
## Theming
|
|
107
169
|
|
|
108
170
|
Override CSS custom properties:
|
|
@@ -168,6 +230,95 @@ Override CSS custom properties:
|
|
|
168
230
|
}
|
|
169
231
|
```
|
|
170
232
|
|
|
233
|
+
### Input Helpers
|
|
234
|
+
|
|
235
|
+
Input helpers allow you to provide auxiliary UI (like calculators or lookup dialogs) to assist users in filling out form fields. The library detects the extension and calls your `renderHelperTrigger` function, but you control the dialog/modal implementation.
|
|
236
|
+
|
|
237
|
+
**FHIR Extension:**
|
|
238
|
+
```json
|
|
239
|
+
{
|
|
240
|
+
"linkId": "bmi",
|
|
241
|
+
"text": "Body Mass Index",
|
|
242
|
+
"type": "decimal",
|
|
243
|
+
"extension": [{
|
|
244
|
+
"url": "http://codes.welshare.app/StructureDefinition/questionnaire-inputHelper",
|
|
245
|
+
"valueCodeableConcept": {
|
|
246
|
+
"coding": [{
|
|
247
|
+
"system": "http://codes.welshare.app/input-helper-type",
|
|
248
|
+
"code": "bmi-calculator",
|
|
249
|
+
"display": "BMI Calculator"
|
|
250
|
+
}],
|
|
251
|
+
"text": "Calculate BMI from height and weight"
|
|
252
|
+
}
|
|
253
|
+
}]
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
**Implementation:**
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
import { useState } from 'react';
|
|
261
|
+
import { QuestionRenderer, BmiForm, type HelperTriggerProps } from '@welshare/questionnaire';
|
|
262
|
+
|
|
263
|
+
function MyQuestionnaireRenderer({ item }) {
|
|
264
|
+
const [showBmiDialog, setShowBmiDialog] = useState(false);
|
|
265
|
+
const [helperCallback, setHelperCallback] = useState<((v: string | number) => void) | null>(null);
|
|
266
|
+
|
|
267
|
+
const handleHelperTrigger = ({ helper, onValueSelected }: HelperTriggerProps) => {
|
|
268
|
+
if (helper.type === 'bmi-calculator') {
|
|
269
|
+
return (
|
|
270
|
+
<button
|
|
271
|
+
type="button"
|
|
272
|
+
onClick={() => {
|
|
273
|
+
setHelperCallback(() => onValueSelected);
|
|
274
|
+
setShowBmiDialog(true);
|
|
275
|
+
}}
|
|
276
|
+
>
|
|
277
|
+
{helper.display || 'Calculate'}
|
|
278
|
+
</button>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
return null;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<>
|
|
286
|
+
<QuestionRenderer
|
|
287
|
+
item={item}
|
|
288
|
+
renderHelperTrigger={handleHelperTrigger}
|
|
289
|
+
/>
|
|
290
|
+
|
|
291
|
+
{showBmiDialog && (
|
|
292
|
+
<Dialog onClose={() => setShowBmiDialog(false)}>
|
|
293
|
+
<BmiForm
|
|
294
|
+
onSubmit={({ bmi }) => {
|
|
295
|
+
helperCallback?.(bmi);
|
|
296
|
+
setShowBmiDialog(false);
|
|
297
|
+
}}
|
|
298
|
+
/>
|
|
299
|
+
</Dialog>
|
|
300
|
+
)}
|
|
301
|
+
</>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**Helper Trigger Props:**
|
|
307
|
+
- `helper: InputHelperConfig` - Configuration from the extension
|
|
308
|
+
- `type: string` - Helper identifier (e.g., "bmi-calculator")
|
|
309
|
+
- `display?: string` - Display name from the extension
|
|
310
|
+
- `description?: string` - Description/tooltip text
|
|
311
|
+
- `linkId: string` - The question's linkId
|
|
312
|
+
- `currentValue?: T` - Current field value (if any), where T defaults to `string | number`
|
|
313
|
+
- `onValueSelected: (value: T) => void` - Callback to update the field
|
|
314
|
+
|
|
315
|
+
**Note:** `HelperTriggerProps<T>` is a generic interface. For decimal questions, it's specialized as `HelperTriggerProps<number>`, but you can use different types for other question types (e.g., `HelperTriggerProps<string>` for text inputs).
|
|
316
|
+
|
|
317
|
+
**Note:** The library only renders the trigger element you provide. You are responsible for implementing the dialog/modal UI, as this allows you to use your preferred dialog system (e.g., shadcn/ui, Radix UI, MUI, etc.).
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
|
|
171
322
|
## License
|
|
172
323
|
|
|
173
324
|
MIT © Welshare UG (haftungsbeschränkt)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result object returned on submission - all values in metric units
|
|
3
|
+
*/
|
|
4
|
+
export interface BmiSubmissionResult {
|
|
5
|
+
/** Height in centimeters */
|
|
6
|
+
heightCm: number;
|
|
7
|
+
/** Weight in kilograms */
|
|
8
|
+
weightKg: number;
|
|
9
|
+
/** Calculated BMI (kg/m²) */
|
|
10
|
+
bmi: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Initial values for the BMI form - always in metric units
|
|
14
|
+
*/
|
|
15
|
+
export interface BmiInitialValues {
|
|
16
|
+
/** Height in centimeters */
|
|
17
|
+
heightCm?: number;
|
|
18
|
+
/** Weight in kilograms */
|
|
19
|
+
weightKg?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface BmiFormProps {
|
|
22
|
+
/**
|
|
23
|
+
* Initial values in metric units (cm, kg).
|
|
24
|
+
* These are used to populate the form on mount.
|
|
25
|
+
* If unitPreference is "imperial", they will be converted for display.
|
|
26
|
+
*/
|
|
27
|
+
initialValues?: BmiInitialValues;
|
|
28
|
+
/**
|
|
29
|
+
* Preferred unit system for initial display.
|
|
30
|
+
* When "imperial", initial metric values are converted for display.
|
|
31
|
+
* Defaults to "metric".
|
|
32
|
+
*/
|
|
33
|
+
unitPreference?: "metric" | "imperial";
|
|
34
|
+
/**
|
|
35
|
+
* Callback fired when user submits/confirms the BMI values.
|
|
36
|
+
* Returns all values in metric units (cm, kg).
|
|
37
|
+
*/
|
|
38
|
+
onSubmit?: (result: BmiSubmissionResult) => void;
|
|
39
|
+
/**
|
|
40
|
+
* Callback fired whenever any value changes.
|
|
41
|
+
* Returns all values in metric units (cm, kg).
|
|
42
|
+
* Use this for real-time updates without requiring explicit submission.
|
|
43
|
+
*/
|
|
44
|
+
onChange?: (result: BmiSubmissionResult) => void;
|
|
45
|
+
/**
|
|
46
|
+
* Additional CSS class names
|
|
47
|
+
*/
|
|
48
|
+
className?: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* BMI Form Component
|
|
52
|
+
*
|
|
53
|
+
* A self-contained form component for entering height and weight measurements.
|
|
54
|
+
* Manages all state internally and supports both metric (cm/kg) and imperial (ft+in/lb) input.
|
|
55
|
+
*
|
|
56
|
+
* **Clinical Standard Compliance:**
|
|
57
|
+
* - All external communication uses metric units (cm, kg)
|
|
58
|
+
* - Imperial inputs are UI conveniences that convert to metric internally
|
|
59
|
+
* - Initial values must always be provided in metric
|
|
60
|
+
* - BMI is always calculated using the metric formula: kg / m²
|
|
61
|
+
*
|
|
62
|
+
* **Unit System Behavior:**
|
|
63
|
+
* - Switching unit systems clears all input fields (no automatic conversion)
|
|
64
|
+
* - Initial values provided in metric are converted to imperial for display
|
|
65
|
+
* only when unitPreference is "imperial"
|
|
66
|
+
*/
|
|
67
|
+
export declare const BmiForm: ({ initialValues, unitPreference, onSubmit, onChange, className, }: BmiFormProps) => import("react/jsx-runtime").JSX.Element;
|
|
68
|
+
//# sourceMappingURL=bmi-form.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bmi-form.d.ts","sourceRoot":"","sources":["../../../src/components/bmi-form.tsx"],"names":[],"mappings":"AASA;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,4BAA4B;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,0BAA0B;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,6BAA6B;IAC7B,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,4BAA4B;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0BAA0B;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,aAAa,CAAC,EAAE,gBAAgB,CAAC;IAEjC;;;;OAIG;IACH,cAAc,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAC;IAEvC;;;OAGG;IACH,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAC;IAEjD;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAC;IAEjD;;OAEG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,OAAO,GAAI,mEAMrB,YAAY,4CAmSd,CAAC"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
3
|
+
import { calculateBmi, cmToFeetAndInches, feetAndInchesToCm, kgToLbs, lbsToKg, } from "../lib/bmi-helpers.js";
|
|
4
|
+
/**
|
|
5
|
+
* BMI Form Component
|
|
6
|
+
*
|
|
7
|
+
* A self-contained form component for entering height and weight measurements.
|
|
8
|
+
* Manages all state internally and supports both metric (cm/kg) and imperial (ft+in/lb) input.
|
|
9
|
+
*
|
|
10
|
+
* **Clinical Standard Compliance:**
|
|
11
|
+
* - All external communication uses metric units (cm, kg)
|
|
12
|
+
* - Imperial inputs are UI conveniences that convert to metric internally
|
|
13
|
+
* - Initial values must always be provided in metric
|
|
14
|
+
* - BMI is always calculated using the metric formula: kg / m²
|
|
15
|
+
*
|
|
16
|
+
* **Unit System Behavior:**
|
|
17
|
+
* - Switching unit systems clears all input fields (no automatic conversion)
|
|
18
|
+
* - Initial values provided in metric are converted to imperial for display
|
|
19
|
+
* only when unitPreference is "imperial"
|
|
20
|
+
*/
|
|
21
|
+
export const BmiForm = ({ initialValues, unitPreference = "metric", onSubmit, onChange, className = "", }) => {
|
|
22
|
+
// Track if this is the initial render to handle initial value conversion
|
|
23
|
+
const isInitialRender = useRef(true);
|
|
24
|
+
// Unit system state - starts with preference
|
|
25
|
+
const [unitSystem, setUnitSystem] = useState(unitPreference);
|
|
26
|
+
// Internal state for metric display (cm)
|
|
27
|
+
const [heightCmInput, setHeightCmInput] = useState("");
|
|
28
|
+
// Internal state for imperial height display (ft + in)
|
|
29
|
+
const [feetInput, setFeetInput] = useState("");
|
|
30
|
+
const [inchesInput, setInchesInput] = useState("");
|
|
31
|
+
// Internal state for metric weight display (kg)
|
|
32
|
+
const [weightKgInput, setWeightKgInput] = useState("");
|
|
33
|
+
// Internal state for imperial weight display (lb)
|
|
34
|
+
const [weightLbInput, setWeightLbInput] = useState("");
|
|
35
|
+
// The canonical metric values (source of truth for calculations)
|
|
36
|
+
const [heightCm, setHeightCm] = useState(0);
|
|
37
|
+
const [weightKg, setWeightKg] = useState(0);
|
|
38
|
+
// Calculated BMI (always from metric values)
|
|
39
|
+
const [bmi, setBmi] = useState(0);
|
|
40
|
+
// Initialize form with provided values (converted to display units if needed)
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (!isInitialRender.current)
|
|
43
|
+
return;
|
|
44
|
+
isInitialRender.current = false;
|
|
45
|
+
const initialHeightCm = initialValues?.heightCm ?? 0;
|
|
46
|
+
const initialWeightKg = initialValues?.weightKg ?? 0;
|
|
47
|
+
// Set canonical metric values
|
|
48
|
+
setHeightCm(initialHeightCm);
|
|
49
|
+
setWeightKg(initialWeightKg);
|
|
50
|
+
if (unitPreference === "metric") {
|
|
51
|
+
// Display as metric
|
|
52
|
+
if (initialHeightCm > 0) {
|
|
53
|
+
setHeightCmInput(String(initialHeightCm));
|
|
54
|
+
}
|
|
55
|
+
if (initialWeightKg > 0) {
|
|
56
|
+
setWeightKgInput(String(initialWeightKg));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
// Convert metric to imperial for display
|
|
61
|
+
if (initialHeightCm > 0) {
|
|
62
|
+
const { feet, inches } = cmToFeetAndInches(initialHeightCm);
|
|
63
|
+
setFeetInput(String(feet));
|
|
64
|
+
setInchesInput(String(inches));
|
|
65
|
+
}
|
|
66
|
+
if (initialWeightKg > 0) {
|
|
67
|
+
const lbs = Math.round(kgToLbs(initialWeightKg) * 10) / 10;
|
|
68
|
+
setWeightLbInput(String(lbs));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}, [initialValues, unitPreference]);
|
|
72
|
+
// Calculate BMI whenever canonical metric values change
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
const calculatedBmi = calculateBmi(heightCm, weightKg);
|
|
75
|
+
setBmi(calculatedBmi);
|
|
76
|
+
}, [heightCm, weightKg]);
|
|
77
|
+
// Notify parent of changes
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (onChange && !isInitialRender.current) {
|
|
80
|
+
onChange({ heightCm, weightKg, bmi });
|
|
81
|
+
}
|
|
82
|
+
}, [heightCm, weightKg, bmi, onChange]);
|
|
83
|
+
// Handle unit system change - clear all fields
|
|
84
|
+
const handleUnitSystemChange = useCallback((newSystem) => {
|
|
85
|
+
if (newSystem === unitSystem)
|
|
86
|
+
return;
|
|
87
|
+
// Clear all inputs
|
|
88
|
+
setHeightCmInput("");
|
|
89
|
+
setFeetInput("");
|
|
90
|
+
setInchesInput("");
|
|
91
|
+
setWeightKgInput("");
|
|
92
|
+
setWeightLbInput("");
|
|
93
|
+
// Clear canonical values
|
|
94
|
+
setHeightCm(0);
|
|
95
|
+
setWeightKg(0);
|
|
96
|
+
setBmi(0);
|
|
97
|
+
setUnitSystem(newSystem);
|
|
98
|
+
}, [unitSystem]);
|
|
99
|
+
// Metric height change (cm)
|
|
100
|
+
const handleHeightCmChange = useCallback((value) => {
|
|
101
|
+
setHeightCmInput(value);
|
|
102
|
+
const numValue = parseFloat(value);
|
|
103
|
+
setHeightCm(isNaN(numValue) ? 0 : numValue);
|
|
104
|
+
}, []);
|
|
105
|
+
// Imperial height change (feet)
|
|
106
|
+
const handleFeetChange = useCallback((value) => {
|
|
107
|
+
setFeetInput(value);
|
|
108
|
+
const feet = parseFloat(value) || 0;
|
|
109
|
+
const inches = parseFloat(inchesInput) || 0;
|
|
110
|
+
setHeightCm(feetAndInchesToCm(feet, inches));
|
|
111
|
+
}, [inchesInput]);
|
|
112
|
+
// Imperial height change (inches)
|
|
113
|
+
const handleInchesChange = useCallback((value) => {
|
|
114
|
+
setInchesInput(value);
|
|
115
|
+
const feet = parseFloat(feetInput) || 0;
|
|
116
|
+
const inches = parseFloat(value) || 0;
|
|
117
|
+
setHeightCm(feetAndInchesToCm(feet, inches));
|
|
118
|
+
}, [feetInput]);
|
|
119
|
+
// Metric weight change (kg)
|
|
120
|
+
const handleWeightKgChange = useCallback((value) => {
|
|
121
|
+
setWeightKgInput(value);
|
|
122
|
+
const numValue = parseFloat(value);
|
|
123
|
+
setWeightKg(isNaN(numValue) ? 0 : numValue);
|
|
124
|
+
}, []);
|
|
125
|
+
// Imperial weight change (lb)
|
|
126
|
+
const handleWeightLbChange = useCallback((value) => {
|
|
127
|
+
setWeightLbInput(value);
|
|
128
|
+
const numValue = parseFloat(value);
|
|
129
|
+
setWeightKg(isNaN(numValue) ? 0 : lbsToKg(numValue));
|
|
130
|
+
}, []);
|
|
131
|
+
// Handle form submission
|
|
132
|
+
const handleSubmit = useCallback(() => {
|
|
133
|
+
if (onSubmit) {
|
|
134
|
+
onSubmit({ heightCm, weightKg, bmi });
|
|
135
|
+
}
|
|
136
|
+
}, [onSubmit, heightCm, weightKg, bmi]);
|
|
137
|
+
return (_jsxs("div", { className: `wq-bmi-form ${className}`, children: [_jsxs("div", { className: "wq-bmi-unit-toggle", children: [_jsx("button", { type: "button", className: `wq-bmi-unit-option ${unitSystem === "metric" ? "wq-selected" : ""}`, onClick: () => handleUnitSystemChange("metric"), "aria-pressed": unitSystem === "metric", children: "Metric" }), _jsx("button", { type: "button", className: `wq-bmi-unit-option ${unitSystem === "imperial" ? "wq-selected" : ""}`, onClick: () => handleUnitSystemChange("imperial"), "aria-pressed": unitSystem === "imperial", children: "Imperial" })] }), _jsxs("div", { className: "wq-bmi-field", children: [_jsx("label", { htmlFor: "bmi-height", className: "wq-bmi-label", children: unitSystem === "metric" ? "Height (cm)" : "Height (ft + in)" }), unitSystem === "metric" ? (_jsxs("div", { className: "wq-bmi-input-group", children: [_jsx("input", { id: "bmi-height", type: "number", className: "wq-question-input", value: heightCmInput, onChange: (e) => handleHeightCmChange(e.target.value), placeholder: "Enter height in cm", step: "0.1", min: "0" }), _jsx("span", { className: "wq-bmi-unit", children: "cm" })] })) : (_jsxs("div", { className: "wq-bmi-imperial-height", children: [_jsxs("div", { className: "wq-bmi-input-group", children: [_jsx("input", { id: "bmi-height-feet", type: "number", className: "wq-question-input", value: feetInput, onChange: (e) => handleFeetChange(e.target.value), placeholder: "Feet", step: "1", min: "0" }), _jsx("span", { className: "wq-bmi-unit", children: "ft" })] }), _jsxs("div", { className: "wq-bmi-input-group", children: [_jsx("input", { id: "bmi-height-inches", type: "number", className: "wq-question-input", value: inchesInput, onChange: (e) => handleInchesChange(e.target.value), placeholder: "Inches", step: "0.1", min: "0", max: "11.9" }), _jsx("span", { className: "wq-bmi-unit", children: "in" })] })] }))] }), _jsxs("div", { className: "wq-bmi-field", children: [_jsx("label", { htmlFor: "bmi-weight", className: "wq-bmi-label", children: unitSystem === "metric" ? "Weight (kg)" : "Weight (lbs)" }), _jsx("div", { className: "wq-bmi-input-group", children: unitSystem === "metric" ? (_jsxs(_Fragment, { children: [_jsx("input", { id: "bmi-weight", type: "number", className: "wq-question-input", value: weightKgInput, onChange: (e) => handleWeightKgChange(e.target.value), placeholder: "Enter weight in kg", step: "0.1", min: "0" }), _jsx("span", { className: "wq-bmi-unit", children: "kg" })] })) : (_jsxs(_Fragment, { children: [_jsx("input", { id: "bmi-weight", type: "number", className: "wq-question-input", value: weightLbInput, onChange: (e) => handleWeightLbChange(e.target.value), placeholder: "Enter weight in lbs", step: "0.1", min: "0" }), _jsx("span", { className: "wq-bmi-unit", children: "lbs" })] })) })] }), _jsxs("div", { className: "wq-bmi-field", children: [_jsx("label", { htmlFor: "bmi-result", className: "wq-bmi-label", children: "BMI" }), _jsxs("div", { className: "wq-bmi-input-group", children: [_jsx("input", { id: "bmi-result", type: "text", className: "wq-question-input wq-bmi-result", value: bmi ? bmi.toFixed(1) : "", placeholder: "Calculated automatically", readOnly: true, disabled: true }), _jsx("span", { className: "wq-bmi-unit", children: "kg/m\u00B2" })] })] }), bmi > 0 && (_jsx("div", { className: "wq-bmi-info", children: _jsx("p", { className: "wq-text-secondary", children: "BMI is calculated automatically from height and weight measurements." }) })), onSubmit && (_jsx("div", { className: "wq-bmi-actions", children: _jsx("button", { type: "button", className: "wq-button wq-button-primary", onClick: handleSubmit, disabled: !bmi, children: "Confirm Measurements" }) }))] }));
|
|
138
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
2
|
import type { QuestionnaireItem } from "../types/fhir.js";
|
|
3
|
-
import { RadioInputProps, CheckboxInputProps } from "../types/index.js";
|
|
3
|
+
import { RadioInputProps, CheckboxInputProps, HelperTriggerProps } from "../types/index.js";
|
|
4
4
|
export interface QuestionRendererProps {
|
|
5
5
|
item: QuestionnaireItem;
|
|
6
6
|
/**
|
|
@@ -66,6 +66,11 @@ export interface QuestionRendererProps {
|
|
|
66
66
|
* ```
|
|
67
67
|
*/
|
|
68
68
|
renderCheckboxInput?: (props: CheckboxInputProps) => ReactNode;
|
|
69
|
+
/**
|
|
70
|
+
* Custom renderer for helper triggers on decimal inputs.
|
|
71
|
+
* Called when a decimal question has an inputHelper extension.
|
|
72
|
+
*/
|
|
73
|
+
renderHelperTrigger?: (props: HelperTriggerProps) => ReactNode;
|
|
69
74
|
}
|
|
70
75
|
/**
|
|
71
76
|
* Wrapper component that combines the library's QuestionRenderer with debug functionality.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"question-renderer.d.ts","sourceRoot":"","sources":["../../../src/components/question-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"question-renderer.d.ts","sourceRoot":"","sources":["../../../src/components/question-renderer.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAU1D,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAE5F,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,iBAAiB,CAAC;IACxB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,SAAS,CAAC;IACzD;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,SAAS,CAAC;IAC/D;;;OAGG;IACH,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,SAAS,CAAC;CAChE;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,GAAI,eAAe,qBAAqB,4CAyBpE,CAAC"}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useQuestionnaire } from "../contexts/questionnaire-context.js";
|
|
3
|
+
import { isQuestionEnabled } from "../lib/questionnaire-utils.js";
|
|
4
|
+
import { FHIR_EXTENSIONS, WELSHARE_EXTENSIONS, FHIR_CODE_SYSTEMS } from "../lib/constants.js";
|
|
3
5
|
import { DebugSection } from "./debug-section.js";
|
|
4
6
|
import { ChoiceQuestion } from "./questions/choice-question.js";
|
|
5
7
|
import { MultipleChoiceQuestion } from "./questions/multiple-choice-question.js";
|
|
6
8
|
import { IntegerQuestion } from "./questions/integer-question.js";
|
|
7
|
-
import { DecimalQuestion } from "./questions/decimal-question.js";
|
|
9
|
+
import { DecimalQuestion, getDecimalHelperTriggerProps } from "./questions/decimal-question.js";
|
|
8
10
|
import { StringQuestion } from "./questions/string-question.js";
|
|
9
11
|
import { BooleanQuestion } from "./questions/boolean-question.js";
|
|
10
12
|
/**
|
|
@@ -22,7 +24,7 @@ export const QuestionRenderer = (rendererProps) => {
|
|
|
22
24
|
const currentAnswer = getAnswer(item.linkId);
|
|
23
25
|
const currentAnswers = getAnswers(item.linkId);
|
|
24
26
|
// Find LOINC code if present
|
|
25
|
-
const loincCode = item.code?.find((coding) => coding.system ===
|
|
27
|
+
const loincCode = item.code?.find((coding) => coding.system === FHIR_CODE_SYSTEMS.LOINC);
|
|
26
28
|
return (_jsxs("div", { children: [_jsx(QuestionRendererInternal, { ...rendererProps }), debugMode && (_jsx(DebugSection, { item: item, currentAnswer: currentAnswer, currentAnswers: currentAnswers, loincCode: loincCode }))] }));
|
|
27
29
|
};
|
|
28
30
|
/**
|
|
@@ -35,22 +37,26 @@ export const QuestionRenderer = (rendererProps) => {
|
|
|
35
37
|
* - Auto-populated (hidden) fields
|
|
36
38
|
* - Validation error display
|
|
37
39
|
*/
|
|
38
|
-
const QuestionRendererInternal = ({ item, className = "", inputClassName = "", choiceClassName = "", renderRadioInput, renderCheckboxInput, }) => {
|
|
40
|
+
const QuestionRendererInternal = ({ item, className = "", inputClassName = "", choiceClassName = "", renderRadioInput, renderCheckboxInput, renderHelperTrigger, }) => {
|
|
39
41
|
const { updateAnswer, updateMultipleAnswers, getAnswer, getAnswers, hasValidationError, } = useQuestionnaire();
|
|
40
42
|
const currentAnswer = getAnswer(item.linkId);
|
|
41
43
|
const currentAnswers = getAnswers(item.linkId);
|
|
42
44
|
const hasError = hasValidationError(item.linkId);
|
|
43
45
|
// Check if this field should be hidden (auto-populated)
|
|
44
|
-
const isAutoPopulated = item.extension?.some((ext) => ext.url ===
|
|
45
|
-
"http://hl7.org/fhir/StructureDefinition/questionnaire-hidden" &&
|
|
46
|
+
const isAutoPopulated = item.extension?.some((ext) => ext.url === FHIR_EXTENSIONS.QUESTIONNAIRE_HIDDEN &&
|
|
46
47
|
ext.valueBoolean === true);
|
|
47
48
|
// Don't render auto-populated fields
|
|
48
49
|
if (isAutoPopulated) {
|
|
49
50
|
return null;
|
|
50
51
|
}
|
|
52
|
+
// Don't render questions that don't meet their enableWhen conditions
|
|
53
|
+
if (!isQuestionEnabled(item, getAnswer)) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
51
56
|
const handleChoiceChange = (valueCoding, valueInteger) => {
|
|
52
57
|
// For choice questions, only set one field: valueCoding takes precedence
|
|
53
|
-
if (valueCoding &&
|
|
58
|
+
if (valueCoding &&
|
|
59
|
+
(valueCoding.code || valueCoding.display || valueCoding.system)) {
|
|
54
60
|
updateAnswer(item.linkId, { valueCoding });
|
|
55
61
|
}
|
|
56
62
|
else if (valueInteger !== undefined) {
|
|
@@ -60,8 +66,7 @@ const QuestionRendererInternal = ({ item, className = "", inputClassName = "", c
|
|
|
60
66
|
const handleMultipleChoiceToggle = (valueCoding, valueInteger) => {
|
|
61
67
|
const isSelected = currentAnswers.some((answer) => answer.valueCoding?.code === valueCoding.code);
|
|
62
68
|
// Check if there's an exclusive option extension
|
|
63
|
-
const exclusiveOptionExt = item.extension?.find((ext) => ext.url ===
|
|
64
|
-
"http://codes.welshare.app/StructureDefinition/questionnaire-exclusive-option");
|
|
69
|
+
const exclusiveOptionExt = item.extension?.find((ext) => ext.url === WELSHARE_EXTENSIONS.EXCLUSIVE_OPTION);
|
|
65
70
|
const exclusiveOptionCode = exclusiveOptionExt?.valueString;
|
|
66
71
|
let newAnswers;
|
|
67
72
|
if (isSelected) {
|
|
@@ -73,7 +78,8 @@ const QuestionRendererInternal = ({ item, className = "", inputClassName = "", c
|
|
|
73
78
|
if (exclusiveOptionCode && valueCoding.code === exclusiveOptionCode) {
|
|
74
79
|
// Clear all other answers and only keep this one
|
|
75
80
|
// For choice questions, only set one field: valueCoding takes precedence
|
|
76
|
-
const answer = valueCoding &&
|
|
81
|
+
const answer = valueCoding &&
|
|
82
|
+
(valueCoding.code || valueCoding.display || valueCoding.system)
|
|
77
83
|
? { valueCoding }
|
|
78
84
|
: valueInteger !== undefined
|
|
79
85
|
? { valueInteger }
|
|
@@ -87,7 +93,8 @@ const QuestionRendererInternal = ({ item, className = "", inputClassName = "", c
|
|
|
87
93
|
// Remove exclusive option and add new selection
|
|
88
94
|
newAnswers = currentAnswers.filter((answer) => answer.valueCoding?.code !== exclusiveOptionCode);
|
|
89
95
|
// For choice questions, only set one field: valueCoding takes precedence
|
|
90
|
-
const answer = valueCoding &&
|
|
96
|
+
const answer = valueCoding &&
|
|
97
|
+
(valueCoding.code || valueCoding.display || valueCoding.system)
|
|
91
98
|
? { valueCoding }
|
|
92
99
|
: valueInteger !== undefined
|
|
93
100
|
? { valueInteger }
|
|
@@ -99,7 +106,8 @@ const QuestionRendererInternal = ({ item, className = "", inputClassName = "", c
|
|
|
99
106
|
const maxAnswers = item.maxAnswers || Number.MAX_SAFE_INTEGER;
|
|
100
107
|
if (currentAnswers.length < maxAnswers) {
|
|
101
108
|
// For choice questions, only set one field: valueCoding takes precedence
|
|
102
|
-
const answer = valueCoding &&
|
|
109
|
+
const answer = valueCoding &&
|
|
110
|
+
(valueCoding.code || valueCoding.display || valueCoding.system)
|
|
103
111
|
? { valueCoding }
|
|
104
112
|
: valueInteger !== undefined
|
|
105
113
|
? { valueInteger }
|
|
@@ -145,8 +153,7 @@ const QuestionRendererInternal = ({ item, className = "", inputClassName = "", c
|
|
|
145
153
|
};
|
|
146
154
|
// Check if this field should use a slider control
|
|
147
155
|
const getSliderConfig = () => {
|
|
148
|
-
const sliderExt = item.extension?.find((ext) => ext.url ===
|
|
149
|
-
"http://codes.welshare.app/StructureDefinition/questionnaire-slider-control");
|
|
156
|
+
const sliderExt = item.extension?.find((ext) => ext.url === WELSHARE_EXTENSIONS.SLIDER_CONTROL);
|
|
150
157
|
if (!sliderExt?.extension)
|
|
151
158
|
return null;
|
|
152
159
|
const minValue = sliderExt.extension.find((e) => e.url === "minValue")?.valueInteger ?? 0;
|
|
@@ -179,5 +186,9 @@ const QuestionRendererInternal = ({ item, className = "", inputClassName = "", c
|
|
|
179
186
|
return (_jsxs("div", { className: "wq-unsupported-type", children: ["Unsupported question type: ", item.type] }));
|
|
180
187
|
}
|
|
181
188
|
};
|
|
182
|
-
|
|
189
|
+
// Get helper trigger for decimal questions
|
|
190
|
+
const helperTriggerProps = item.type === "decimal"
|
|
191
|
+
? getDecimalHelperTriggerProps(item, currentAnswer, handleDecimalChange)
|
|
192
|
+
: null;
|
|
193
|
+
return (_jsxs("div", { className: `wq-question-container ${hasError ? "wq-has-error" : ""} ${className}`, children: [_jsxs("div", { className: "wq-question-label-row", children: [_jsxs("div", { className: "wq-question-text", children: [item.text, item.required && _jsx("span", { className: "wq-required-indicator", children: "*" })] }), helperTriggerProps && renderHelperTrigger && (_jsx("div", { className: "wq-question-helper-trigger", children: renderHelperTrigger(helperTriggerProps) }))] }), renderQuestion()] }));
|
|
183
194
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { QuestionnaireItem, QuestionnaireResponseAnswer } from "../../types/fhir.js";
|
|
2
|
+
import type { HelperTriggerProps } from "../../types/index.js";
|
|
2
3
|
export interface DecimalQuestionProps {
|
|
3
4
|
item: QuestionnaireItem;
|
|
4
5
|
currentAnswer?: QuestionnaireResponseAnswer;
|
|
@@ -7,6 +8,12 @@ export interface DecimalQuestionProps {
|
|
|
7
8
|
}
|
|
8
9
|
/**
|
|
9
10
|
* Renders a decimal question with number input
|
|
11
|
+
* Helper trigger is now rendered in the question label row by the parent component
|
|
10
12
|
*/
|
|
11
|
-
export declare const DecimalQuestion: ({
|
|
13
|
+
export declare const DecimalQuestion: ({ currentAnswer, onDecimalChange, inputClassName, }: DecimalQuestionProps) => import("react/jsx-runtime").JSX.Element;
|
|
14
|
+
/**
|
|
15
|
+
* Get helper trigger props for a decimal question if available
|
|
16
|
+
* Returns null if no helper is configured
|
|
17
|
+
*/
|
|
18
|
+
export declare const getDecimalHelperTriggerProps: (item: QuestionnaireItem, currentAnswer: QuestionnaireResponseAnswer | undefined, onDecimalChange: (value: string) => void) => HelperTriggerProps | null;
|
|
12
19
|
//# sourceMappingURL=decimal-question.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decimal-question.d.ts","sourceRoot":"","sources":["../../../../src/components/questions/decimal-question.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EACjB,2BAA2B,EAC5B,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"decimal-question.d.ts","sourceRoot":"","sources":["../../../../src/components/questions/decimal-question.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EACjB,2BAA2B,EAC5B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAG/D,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,iBAAiB,CAAC;IACxB,aAAa,CAAC,EAAE,2BAA2B,CAAC;IAC5C,eAAe,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,eAAO,MAAM,eAAe,GAAI,qDAI7B,oBAAoB,4CAWtB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACvC,MAAM,iBAAiB,EACvB,eAAe,2BAA2B,GAAG,SAAS,EACtD,iBAAiB,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,KACvC,kBAAkB,GAAG,IAavB,CAAC"}
|
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { getInputHelper } from "../../lib/questionnaire-utils.js";
|
|
2
3
|
/**
|
|
3
4
|
* Renders a decimal question with number input
|
|
5
|
+
* Helper trigger is now rendered in the question label row by the parent component
|
|
4
6
|
*/
|
|
5
|
-
export const DecimalQuestion = ({
|
|
7
|
+
export const DecimalQuestion = ({ currentAnswer, onDecimalChange, inputClassName = "", }) => {
|
|
6
8
|
return (_jsx("input", { type: "number", className: `wq-question-input ${inputClassName}`, value: currentAnswer?.valueDecimal ?? "", onChange: (e) => onDecimalChange(e.target.value), placeholder: "Enter a number", step: "0.1" }));
|
|
7
9
|
};
|
|
10
|
+
/**
|
|
11
|
+
* Get helper trigger props for a decimal question if available
|
|
12
|
+
* Returns null if no helper is configured
|
|
13
|
+
*/
|
|
14
|
+
export const getDecimalHelperTriggerProps = (item, currentAnswer, onDecimalChange) => {
|
|
15
|
+
const helper = getInputHelper(item);
|
|
16
|
+
if (!helper) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
helper,
|
|
21
|
+
linkId: item.linkId,
|
|
22
|
+
currentValue: currentAnswer?.valueDecimal,
|
|
23
|
+
onValueSelected: (value) => onDecimalChange(String(value)),
|
|
24
|
+
};
|
|
25
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"multiple-choice-question.d.ts","sourceRoot":"","sources":["../../../../src/components/questions/multiple-choice-question.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EACV,iBAAiB,EACjB,2BAA2B,EAC5B,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"multiple-choice-question.d.ts","sourceRoot":"","sources":["../../../../src/components/questions/multiple-choice-question.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EACV,iBAAiB,EACjB,2BAA2B,EAC5B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAE1D,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,iBAAiB,CAAC;IACxB,cAAc,EAAE,2BAA2B,EAAE,CAAC;IAC9C,sBAAsB,EAAE,CACtB,WAAW,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,EACjE,YAAY,CAAC,EAAE,MAAM,KAClB,IAAI,CAAC;IACV,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mBAAmB,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,SAAS,CAAC;CAChE;AAED;;;GAGG;AACH,eAAO,MAAM,sBAAsB,GAAI,yFAMpC,2BAA2B,4CAwF7B,CAAC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { WELSHARE_EXTENSIONS } from "../../lib/constants.js";
|
|
2
3
|
/**
|
|
3
4
|
* Renders a multi-select choice question with checkboxes
|
|
4
5
|
* Supports exclusive options and maxAnswers limits
|
|
@@ -7,8 +8,7 @@ export const MultipleChoiceQuestion = ({ item, currentAnswers, onMultipleChoiceT
|
|
|
7
8
|
const maxAnswers = item.maxAnswers || Number.MAX_SAFE_INTEGER;
|
|
8
9
|
const atMaxAnswers = currentAnswers.length >= maxAnswers;
|
|
9
10
|
// Check if there's an exclusive option extension
|
|
10
|
-
const exclusiveOptionExt = item.extension?.find((ext) => ext.url ===
|
|
11
|
-
"http://codes.welshare.app/StructureDefinition/questionnaire-exclusive-option");
|
|
11
|
+
const exclusiveOptionExt = item.extension?.find((ext) => ext.url === WELSHARE_EXTENSIONS.EXCLUSIVE_OPTION);
|
|
12
12
|
const exclusiveOptionCode = exclusiveOptionExt?.valueString;
|
|
13
13
|
// Check if the exclusive option is currently selected
|
|
14
14
|
const exclusiveSelected = exclusiveOptionCode &&
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"questionnaire-context.d.ts","sourceRoot":"","sources":["../../../src/contexts/questionnaire-context.tsx"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EACV,aAAa,EACb,iBAAiB,EACjB,qBAAqB,EACrB,2BAA2B,EAE5B,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"questionnaire-context.d.ts","sourceRoot":"","sources":["../../../src/contexts/questionnaire-context.tsx"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EACV,aAAa,EACb,iBAAiB,EACjB,qBAAqB,EACrB,2BAA2B,EAE5B,MAAM,kBAAkB,CAAC;AAM1B,MAAM,WAAW,wBAAwB;IACvC,aAAa,EAAE,aAAa,CAAC;IAC7B,QAAQ,EAAE,qBAAqB,CAAC;IAChC,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,2BAA2B,KAAK,IAAI,CAAC;IAC5E,qBAAqB,EAAE,CACrB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,2BAA2B,EAAE,KACnC,IAAI,CAAC;IACV,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,2BAA2B,GAAG,SAAS,CAAC;IACvE,UAAU,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,2BAA2B,EAAE,CAAC;IAC9D,WAAW,EAAE,CAAC,SAAS,EAAE,iBAAiB,EAAE,KAAK,OAAO,CAAC;IACzD,oBAAoB,EAAE,CAAC,SAAS,EAAE,iBAAiB,EAAE,KAAK,iBAAiB,EAAE,CAAC;IAC9E,8BAA8B,EAAE,CAC9B,SAAS,EAAE,iBAAiB,EAAE,KAC3B,iBAAiB,EAAE,CAAC;IACzB,oBAAoB,EAAE,CAAC,SAAS,EAAE,iBAAiB,EAAE,KAAK,IAAI,CAAC;IAC/D,qBAAqB,EAAE,MAAM,IAAI,CAAC;IAClC,kBAAkB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;IAChD,SAAS,EAAE,OAAO,CAAC;IACnB,eAAe,EAAE,MAAM,IAAI,CAAC;CAC7B;AAMD,eAAO,MAAM,gBAAgB,gCAQ5B,CAAC;AAEF,MAAM,WAAW,0BAA0B;IACzC,QAAQ,EAAE,SAAS,CAAC;IACpB;;;OAGG;IACH,aAAa,EAAE,aAAa,CAAC;IAC7B;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,eAAO,MAAM,qBAAqB,GAAI,mEAKnC,0BAA0B,4CAya5B,CAAC"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { createContext, useContext, useEffect, useState, } from "react";
|
|
3
|
-
import { getAllQuestionsFromPage } from "../lib/questionnaire-utils.js";
|
|
3
|
+
import { getAllQuestionsFromPage, isQuestionEnabled, } from "../lib/questionnaire-utils.js";
|
|
4
4
|
const QuestionnaireContext = createContext(undefined);
|
|
5
5
|
export const useQuestionnaire = () => {
|
|
6
6
|
const context = useContext(QuestionnaireContext);
|
|
@@ -258,7 +258,8 @@ export const QuestionnaireProvider = ({ children, questionnaire, questionnaireId
|
|
|
258
258
|
type: "group",
|
|
259
259
|
item: pageItems,
|
|
260
260
|
});
|
|
261
|
-
|
|
261
|
+
// Filter for required questions that are also enabled (meet their enableWhen conditions)
|
|
262
|
+
return allQuestionsOnPage.filter((item) => item.required === true && isQuestionEnabled(item, getAnswer));
|
|
262
263
|
};
|
|
263
264
|
const isNonEmptyString = (value) => typeof value === "string" && value.trim().length > 0;
|
|
264
265
|
const hasMeaningfulAnswer = (answer) => {
|