@welshare/questionnaire 0.1.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 (109) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +173 -0
  3. package/dist/esm/components/debug-section.d.ts +44 -0
  4. package/dist/esm/components/debug-section.d.ts.map +1 -0
  5. package/dist/esm/components/debug-section.js +28 -0
  6. package/dist/esm/components/question-renderer.d.ts +80 -0
  7. package/dist/esm/components/question-renderer.d.ts.map +1 -0
  8. package/dist/esm/components/question-renderer.js +159 -0
  9. package/dist/esm/components/questions/boolean-question.d.ts +15 -0
  10. package/dist/esm/components/questions/boolean-question.d.ts.map +1 -0
  11. package/dist/esm/components/questions/boolean-question.js +19 -0
  12. package/dist/esm/components/questions/choice-question.d.ts +19 -0
  13. package/dist/esm/components/questions/choice-question.d.ts.map +1 -0
  14. package/dist/esm/components/questions/choice-question.js +23 -0
  15. package/dist/esm/components/questions/decimal-question.d.ts +12 -0
  16. package/dist/esm/components/questions/decimal-question.d.ts.map +1 -0
  17. package/dist/esm/components/questions/decimal-question.js +7 -0
  18. package/dist/esm/components/questions/integer-question.d.ts +18 -0
  19. package/dist/esm/components/questions/integer-question.d.ts.map +1 -0
  20. package/dist/esm/components/questions/integer-question.js +24 -0
  21. package/dist/esm/components/questions/multiple-choice-question.d.ts +20 -0
  22. package/dist/esm/components/questions/multiple-choice-question.d.ts.map +1 -0
  23. package/dist/esm/components/questions/multiple-choice-question.js +39 -0
  24. package/dist/esm/components/questions/string-question.d.ts +12 -0
  25. package/dist/esm/components/questions/string-question.d.ts.map +1 -0
  26. package/dist/esm/components/questions/string-question.js +7 -0
  27. package/dist/esm/contexts/questionnaire-context.d.ts +41 -0
  28. package/dist/esm/contexts/questionnaire-context.d.ts.map +1 -0
  29. package/dist/esm/contexts/questionnaire-context.js +350 -0
  30. package/dist/esm/index.d.ts +7 -0
  31. package/dist/esm/index.d.ts.map +1 -0
  32. package/dist/esm/index.js +6 -0
  33. package/dist/esm/lib/questionnaire-utils.d.ts +29 -0
  34. package/dist/esm/lib/questionnaire-utils.d.ts.map +1 -0
  35. package/dist/esm/lib/questionnaire-utils.js +80 -0
  36. package/dist/esm/package.json +3 -0
  37. package/dist/esm/types/fhir.d.ts +117 -0
  38. package/dist/esm/types/fhir.d.ts.map +1 -0
  39. package/dist/esm/types/fhir.js +3 -0
  40. package/dist/esm/types/index.d.ts +51 -0
  41. package/dist/esm/types/index.d.ts.map +1 -0
  42. package/dist/esm/types/index.js +1 -0
  43. package/dist/node_modules/@welshare/questionnaire/.tshy/build.json +8 -0
  44. package/dist/node_modules/@welshare/questionnaire/.tshy/esm.json +16 -0
  45. package/dist/node_modules/@welshare/questionnaire/LICENSE +7 -0
  46. package/dist/node_modules/@welshare/questionnaire/README.md +173 -0
  47. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/debug-section.d.ts +44 -0
  48. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/debug-section.d.ts.map +1 -0
  49. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/debug-section.js +28 -0
  50. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/question-renderer.d.ts +80 -0
  51. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/question-renderer.d.ts.map +1 -0
  52. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/question-renderer.js +159 -0
  53. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/boolean-question.d.ts +15 -0
  54. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/boolean-question.d.ts.map +1 -0
  55. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/boolean-question.js +19 -0
  56. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/choice-question.d.ts +19 -0
  57. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/choice-question.d.ts.map +1 -0
  58. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/choice-question.js +23 -0
  59. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/decimal-question.d.ts +12 -0
  60. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/decimal-question.d.ts.map +1 -0
  61. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/decimal-question.js +7 -0
  62. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/integer-question.d.ts +18 -0
  63. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/integer-question.d.ts.map +1 -0
  64. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/integer-question.js +24 -0
  65. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/multiple-choice-question.d.ts +20 -0
  66. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/multiple-choice-question.d.ts.map +1 -0
  67. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/multiple-choice-question.js +39 -0
  68. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/string-question.d.ts +12 -0
  69. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/string-question.d.ts.map +1 -0
  70. package/dist/node_modules/@welshare/questionnaire/dist/esm/components/questions/string-question.js +7 -0
  71. package/dist/node_modules/@welshare/questionnaire/dist/esm/contexts/questionnaire-context.d.ts +41 -0
  72. package/dist/node_modules/@welshare/questionnaire/dist/esm/contexts/questionnaire-context.d.ts.map +1 -0
  73. package/dist/node_modules/@welshare/questionnaire/dist/esm/contexts/questionnaire-context.js +350 -0
  74. package/dist/node_modules/@welshare/questionnaire/dist/esm/index.d.ts +7 -0
  75. package/dist/node_modules/@welshare/questionnaire/dist/esm/index.d.ts.map +1 -0
  76. package/dist/node_modules/@welshare/questionnaire/dist/esm/index.js +6 -0
  77. package/dist/node_modules/@welshare/questionnaire/dist/esm/lib/questionnaire-utils.d.ts +29 -0
  78. package/dist/node_modules/@welshare/questionnaire/dist/esm/lib/questionnaire-utils.d.ts.map +1 -0
  79. package/dist/node_modules/@welshare/questionnaire/dist/esm/lib/questionnaire-utils.js +80 -0
  80. package/dist/node_modules/@welshare/questionnaire/dist/esm/package.json +3 -0
  81. package/dist/node_modules/@welshare/questionnaire/dist/esm/types/fhir.d.ts +117 -0
  82. package/dist/node_modules/@welshare/questionnaire/dist/esm/types/fhir.d.ts.map +1 -0
  83. package/dist/node_modules/@welshare/questionnaire/dist/esm/types/fhir.js +3 -0
  84. package/dist/node_modules/@welshare/questionnaire/dist/esm/types/index.d.ts +51 -0
  85. package/dist/node_modules/@welshare/questionnaire/dist/esm/types/index.d.ts.map +1 -0
  86. package/dist/node_modules/@welshare/questionnaire/dist/esm/types/index.js +1 -0
  87. package/dist/node_modules/@welshare/questionnaire/dist/styles.css +467 -0
  88. package/dist/node_modules/@welshare/questionnaire/dist/tokens.css +130 -0
  89. package/dist/node_modules/@welshare/questionnaire/package.json +85 -0
  90. package/dist/node_modules/@welshare/questionnaire/src/components/debug-section.tsx +116 -0
  91. package/dist/node_modules/@welshare/questionnaire/src/components/question-renderer.tsx +368 -0
  92. package/dist/node_modules/@welshare/questionnaire/src/components/questionnaire-styles.css +467 -0
  93. package/dist/node_modules/@welshare/questionnaire/src/components/questionnaire-tokens.css +130 -0
  94. package/dist/node_modules/@welshare/questionnaire/src/components/questions/boolean-question.tsx +72 -0
  95. package/dist/node_modules/@welshare/questionnaire/src/components/questions/choice-question.tsx +68 -0
  96. package/dist/node_modules/@welshare/questionnaire/src/components/questions/decimal-question.tsx +32 -0
  97. package/dist/node_modules/@welshare/questionnaire/src/components/questions/integer-question.tsx +87 -0
  98. package/dist/node_modules/@welshare/questionnaire/src/components/questions/multiple-choice-question.tsx +119 -0
  99. package/dist/node_modules/@welshare/questionnaire/src/components/questions/string-question.tsx +31 -0
  100. package/dist/node_modules/@welshare/questionnaire/src/contexts/questionnaire-context.tsx +499 -0
  101. package/dist/node_modules/@welshare/questionnaire/src/index.ts +40 -0
  102. package/dist/node_modules/@welshare/questionnaire/src/lib/__tests__/questionnaire-utils.test.ts +578 -0
  103. package/dist/node_modules/@welshare/questionnaire/src/lib/questionnaire-utils.ts +99 -0
  104. package/dist/node_modules/@welshare/questionnaire/src/types/fhir.ts +126 -0
  105. package/dist/node_modules/@welshare/questionnaire/src/types/index.ts +44 -0
  106. package/dist/node_modules/@welshare/questionnaire/tsconfig.json +16 -0
  107. package/dist/styles.css +467 -0
  108. package/dist/tokens.css +130 -0
  109. package/package.json +84 -0
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Renders an integer question with optional slider control
4
+ */
5
+ export const IntegerQuestion = ({ item, currentAnswer, onIntegerChange, inputClassName = "", sliderConfig, }) => {
6
+ // Render slider if slider config is present
7
+ if (sliderConfig) {
8
+ // Use current answer, or fallback to initial value, or fallback to minValue
9
+ const initialValue = item.initial?.[0]?.valueInteger;
10
+ const value = currentAnswer?.valueInteger ?? initialValue ?? sliderConfig.minValue;
11
+ const unit = sliderConfig.unit || "";
12
+ // Create abbreviated unit for range labels
13
+ const unitShort = unit === "minutes"
14
+ ? "min"
15
+ : unit === "hours"
16
+ ? "hr"
17
+ : unit === "days per week"
18
+ ? "days"
19
+ : unit;
20
+ return (_jsxs("div", { className: "wq-question-slider", children: [_jsx("input", { type: "range", className: `wq-slider-input ${inputClassName}`, min: sliderConfig.minValue, max: sliderConfig.maxValue, step: sliderConfig.step, value: value, onChange: (e) => onIntegerChange(e.target.value) }), _jsxs("div", { className: "wq-slider-value-display", children: [_jsx("span", { className: "wq-slider-value", children: value }), unit && _jsx("span", { className: "wq-slider-unit", children: unit })] }), _jsxs("div", { className: "wq-slider-range-labels", children: [_jsxs("span", { className: "wq-slider-min", children: [sliderConfig.minValue, unitShort && ` ${unitShort}`] }), _jsxs("span", { className: "wq-slider-max", children: [sliderConfig.maxValue, unitShort && ` ${unitShort}`] })] })] }));
21
+ }
22
+ // Default number input
23
+ return (_jsx("input", { type: "number", className: `wq-question-input ${inputClassName}`, value: currentAnswer?.valueInteger ?? "", onChange: (e) => onIntegerChange(e.target.value), placeholder: "Enter a number", step: "1" }));
24
+ };
@@ -0,0 +1,20 @@
1
+ import { type ReactNode } from "react";
2
+ import type { QuestionnaireItem, QuestionnaireResponseAnswer } from "../../types/fhir.js";
3
+ import { CheckboxInputProps } from "../../types/index.js";
4
+ export interface MultipleChoiceQuestionProps {
5
+ item: QuestionnaireItem;
6
+ currentAnswers: QuestionnaireResponseAnswer[];
7
+ onMultipleChoiceToggle: (valueCoding: {
8
+ system?: string;
9
+ code?: string;
10
+ display?: string;
11
+ }, valueInteger?: number) => void;
12
+ choiceClassName?: string;
13
+ renderCheckboxInput?: (props: CheckboxInputProps) => ReactNode;
14
+ }
15
+ /**
16
+ * Renders a multi-select choice question with checkboxes
17
+ * Supports exclusive options and maxAnswers limits
18
+ */
19
+ export declare const MultipleChoiceQuestion: ({ item, currentAnswers, onMultipleChoiceToggle, choiceClassName, renderCheckboxInput, }: MultipleChoiceQuestionProps) => import("react/jsx-runtime").JSX.Element;
20
+ //# sourceMappingURL=multiple-choice-question.d.ts.map
@@ -0,0 +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;AAC7B,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,4CA0F7B,CAAC"}
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Renders a multi-select choice question with checkboxes
4
+ * Supports exclusive options and maxAnswers limits
5
+ */
6
+ export const MultipleChoiceQuestion = ({ item, currentAnswers, onMultipleChoiceToggle, choiceClassName = "", renderCheckboxInput, }) => {
7
+ const maxAnswers = item.maxAnswers || Number.MAX_SAFE_INTEGER;
8
+ const atMaxAnswers = currentAnswers.length >= maxAnswers;
9
+ // 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");
12
+ const exclusiveOptionCode = exclusiveOptionExt?.valueString;
13
+ // Check if the exclusive option is currently selected
14
+ const exclusiveSelected = exclusiveOptionCode &&
15
+ currentAnswers.some((answer) => answer.valueCoding?.code === exclusiveOptionCode);
16
+ // Filter options to show: if exclusive option is selected, only show that one
17
+ const optionsToShow = exclusiveSelected
18
+ ? item.answerOption?.filter((opt) => opt.valueCoding?.code === exclusiveOptionCode)
19
+ : item.answerOption;
20
+ return (_jsxs("div", { className: `wq-question-choice ${choiceClassName}`, children: [optionsToShow?.map((option, index) => {
21
+ const isSelected = currentAnswers.some((answer) => answer.valueCoding?.code === option.valueCoding?.code);
22
+ const isDisabled = !isSelected && atMaxAnswers;
23
+ // Use custom renderer if provided
24
+ if (renderCheckboxInput) {
25
+ return (_jsx("div", { children: renderCheckboxInput({
26
+ linkId: item.linkId,
27
+ valueCoding: option.valueCoding,
28
+ valueInteger: option.valueInteger,
29
+ checked: isSelected,
30
+ disabled: isDisabled,
31
+ onChange: () => onMultipleChoiceToggle(option.valueCoding || {}, option.valueInteger),
32
+ label: option.valueCoding?.display || "",
33
+ index,
34
+ }) }, index));
35
+ }
36
+ // Default rendering
37
+ return (_jsxs("label", { className: `wq-choice-option ${isSelected ? "wq-selected" : ""} ${isDisabled ? "wq-disabled" : ""}`, children: [_jsx("input", { type: "checkbox", name: item.linkId, value: option.valueCoding?.code, checked: isSelected, disabled: isDisabled, onChange: () => onMultipleChoiceToggle(option.valueCoding || {}, option.valueInteger), "data-wq-input": "checkbox", "data-wq-selected": isSelected }), _jsx("span", { className: "wq-choice-label", children: option.valueCoding?.display })] }, index));
38
+ }), item.maxAnswers && (_jsxs("div", { className: "wq-max-answers-hint", children: ["Selected: ", currentAnswers.length, " / ", item.maxAnswers] }))] }));
39
+ };
@@ -0,0 +1,12 @@
1
+ import type { QuestionnaireItem, QuestionnaireResponseAnswer } from "../../types/fhir.js";
2
+ export interface StringQuestionProps {
3
+ item: QuestionnaireItem;
4
+ currentAnswer?: QuestionnaireResponseAnswer;
5
+ onStringChange: (value: string) => void;
6
+ inputClassName?: string;
7
+ }
8
+ /**
9
+ * Renders a string or text question with text input
10
+ */
11
+ export declare const StringQuestion: ({ item, currentAnswer, onStringChange, inputClassName, }: StringQuestionProps) => import("react/jsx-runtime").JSX.Element;
12
+ //# sourceMappingURL=string-question.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"string-question.d.ts","sourceRoot":"","sources":["../../../../src/components/questions/string-question.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EACjB,2BAA2B,EAC5B,MAAM,qBAAqB,CAAC;AAE7B,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,iBAAiB,CAAC;IACxB,aAAa,CAAC,EAAE,2BAA2B,CAAC;IAC5C,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;GAEG;AACH,eAAO,MAAM,cAAc,GAAI,0DAK5B,mBAAmB,4CAUrB,CAAC"}
@@ -0,0 +1,7 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * Renders a string or text question with text input
4
+ */
5
+ export const StringQuestion = ({ item, currentAnswer, onStringChange, inputClassName = "", }) => {
6
+ return (_jsx("input", { type: "text", className: `wq-question-input ${inputClassName}`, value: currentAnswer?.valueString ?? "", onChange: (e) => onStringChange(e.target.value), placeholder: "Enter your answer" }));
7
+ };
@@ -0,0 +1,41 @@
1
+ import { type ReactNode } from "react";
2
+ import type { Questionnaire, QuestionnaireItem, QuestionnaireResponse, QuestionnaireResponseAnswer } from "../types/fhir.js";
3
+ export interface QuestionnaireContextType {
4
+ questionnaire: Questionnaire;
5
+ response: QuestionnaireResponse;
6
+ updateAnswer: (linkId: string, answer: QuestionnaireResponseAnswer) => void;
7
+ updateMultipleAnswers: (linkId: string, answers: QuestionnaireResponseAnswer[]) => void;
8
+ getAnswer: (linkId: string) => QuestionnaireResponseAnswer | undefined;
9
+ getAnswers: (linkId: string) => QuestionnaireResponseAnswer[];
10
+ isPageValid: (pageItems: QuestionnaireItem[]) => boolean;
11
+ getRequiredQuestions: (pageItems: QuestionnaireItem[]) => QuestionnaireItem[];
12
+ getUnansweredRequiredQuestions: (pageItems: QuestionnaireItem[]) => QuestionnaireItem[];
13
+ markValidationErrors: (pageItems: QuestionnaireItem[]) => void;
14
+ clearValidationErrors: () => void;
15
+ hasValidationError: (linkId: string) => boolean;
16
+ debugMode: boolean;
17
+ toggleDebugMode: () => void;
18
+ }
19
+ export declare const useQuestionnaire: () => QuestionnaireContextType;
20
+ export interface QuestionnaireProviderProps {
21
+ children: ReactNode;
22
+ /**
23
+ * The FHIR Questionnaire object to render
24
+ * Clients are responsible for loading/fetching this data
25
+ */
26
+ questionnaire: Questionnaire;
27
+ /**
28
+ * Optional questionnaire ID to use in the response
29
+ * If not provided, will use questionnaire.id
30
+ * If neither exists, an error will be thrown
31
+ */
32
+ questionnaireId?: string;
33
+ /**
34
+ * If true, initializes the response with a hierarchical structure matching the questionnaire
35
+ * If false, uses a flat structure
36
+ * @default true
37
+ */
38
+ useNestedStructure?: boolean;
39
+ }
40
+ export declare const QuestionnaireProvider: ({ children, questionnaire, questionnaireId, useNestedStructure, }: QuestionnaireProviderProps) => import("react/jsx-runtime").JSX.Element;
41
+ //# sourceMappingURL=questionnaire-context.d.ts.map
@@ -0,0 +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;AAG1B,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,4CAoa5B,CAAC"}
@@ -0,0 +1,350 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useContext, useEffect, useState, } from "react";
3
+ import { getAllQuestionsFromPage } from "../lib/questionnaire-utils.js";
4
+ const QuestionnaireContext = createContext(undefined);
5
+ export const useQuestionnaire = () => {
6
+ const context = useContext(QuestionnaireContext);
7
+ if (!context) {
8
+ throw new Error("useQuestionnaire must be used within QuestionnaireProvider");
9
+ }
10
+ return context;
11
+ };
12
+ export const QuestionnaireProvider = ({ children, questionnaire, questionnaireId, useNestedStructure = true, }) => {
13
+ const [response, setResponse] = useState({
14
+ resourceType: "QuestionnaireResponse",
15
+ status: "in-progress",
16
+ item: [],
17
+ });
18
+ const [validationErrors, setValidationErrors] = useState(new Set());
19
+ const [debugMode, setDebugMode] = useState(false);
20
+ const toggleDebugMode = () => {
21
+ setDebugMode((prev) => !prev);
22
+ };
23
+ // Determine the questionnaire ID to use
24
+ const effectiveQuestionnaireId = questionnaireId || questionnaire.id;
25
+ if (!effectiveQuestionnaireId) {
26
+ throw new Error("QuestionnaireProvider: questionnaireId prop or questionnaire.id must be provided");
27
+ }
28
+ // Initialize response structure when questionnaire or options change
29
+ useEffect(() => {
30
+ if (useNestedStructure) {
31
+ // Build response structure mirroring questionnaire hierarchy
32
+ const buildResponseStructure = (questionnaireItems) => {
33
+ if (!questionnaireItems)
34
+ return [];
35
+ return questionnaireItems.map((item) => {
36
+ const responseItem = {
37
+ linkId: item.linkId,
38
+ };
39
+ // Add text if present
40
+ if (item.text) {
41
+ responseItem.text = item.text;
42
+ }
43
+ // If item has nested items (group), recursively build structure
44
+ if (item.item && item.item.length > 0) {
45
+ responseItem.item = buildResponseStructure(item.item);
46
+ }
47
+ // If item has initial values, add them as answers
48
+ if (item.initial && item.initial.length > 0) {
49
+ responseItem.answer = item.initial;
50
+ }
51
+ return responseItem;
52
+ });
53
+ };
54
+ const initialItems = buildResponseStructure(questionnaire.item);
55
+ // Initialize response structure with hierarchical structure
56
+ setResponse({
57
+ resourceType: "QuestionnaireResponse",
58
+ questionnaire: effectiveQuestionnaireId,
59
+ status: "in-progress",
60
+ authored: new Date().toISOString(),
61
+ item: initialItems,
62
+ });
63
+ }
64
+ else {
65
+ // Flat structure initialization
66
+ setResponse({
67
+ resourceType: "QuestionnaireResponse",
68
+ questionnaire: effectiveQuestionnaireId,
69
+ status: "in-progress",
70
+ authored: new Date().toISOString(),
71
+ item: [],
72
+ });
73
+ }
74
+ }, [questionnaire, effectiveQuestionnaireId, useNestedStructure]);
75
+ const updateAnswer = (linkId, answer) => {
76
+ setResponse((prev) => {
77
+ const newResponse = { ...prev };
78
+ if (useNestedStructure) {
79
+ // Recursively find and update the item in the nested structure
80
+ const updateNestedItem = (items = []) => {
81
+ return items.map((item) => {
82
+ // If this is the item we're looking for, update it
83
+ if (item.linkId === linkId) {
84
+ return {
85
+ linkId: item.linkId,
86
+ ...(item.definition && { definition: item.definition }),
87
+ ...(item.text && { text: item.text }),
88
+ answer: [answer],
89
+ ...(item.item && { item: item.item }),
90
+ };
91
+ }
92
+ // If this item has nested items, search within them
93
+ if (item.item && item.item.length > 0) {
94
+ return {
95
+ ...item,
96
+ item: updateNestedItem(item.item),
97
+ };
98
+ }
99
+ return item;
100
+ });
101
+ };
102
+ newResponse.item = updateNestedItem(newResponse.item);
103
+ }
104
+ else {
105
+ // Flat structure: find or create the item
106
+ const findOrCreateItem = (items = []) => {
107
+ const existingIndex = items.findIndex((item) => item.linkId === linkId);
108
+ if (existingIndex >= 0) {
109
+ // Update existing item
110
+ const updated = [...items];
111
+ const existingItem = updated[existingIndex];
112
+ updated[existingIndex] = {
113
+ linkId: existingItem.linkId,
114
+ ...(existingItem.definition && {
115
+ definition: existingItem.definition,
116
+ }),
117
+ ...(existingItem.text && { text: existingItem.text }),
118
+ answer: [answer],
119
+ ...(existingItem.item && { item: existingItem.item }),
120
+ };
121
+ return updated;
122
+ }
123
+ else {
124
+ // Create new item
125
+ return [
126
+ ...items,
127
+ {
128
+ linkId,
129
+ answer: [answer],
130
+ },
131
+ ];
132
+ }
133
+ };
134
+ newResponse.item = findOrCreateItem(newResponse.item);
135
+ }
136
+ return newResponse;
137
+ });
138
+ // Clear validation error for this question when answered
139
+ setValidationErrors((prev) => {
140
+ const newErrors = new Set(prev);
141
+ newErrors.delete(linkId);
142
+ return newErrors;
143
+ });
144
+ };
145
+ const updateMultipleAnswers = (linkId, answers) => {
146
+ setResponse((prev) => {
147
+ const newResponse = { ...prev };
148
+ if (useNestedStructure) {
149
+ // Recursively find and update the item in the nested structure
150
+ const updateNestedItem = (items = []) => {
151
+ return items.map((item) => {
152
+ // If this is the item we're looking for, update it
153
+ if (item.linkId === linkId) {
154
+ return {
155
+ linkId: item.linkId,
156
+ ...(item.definition && { definition: item.definition }),
157
+ ...(item.text && { text: item.text }),
158
+ answer: answers,
159
+ ...(item.item && { item: item.item }),
160
+ };
161
+ }
162
+ // If this item has nested items, search within them
163
+ if (item.item && item.item.length > 0) {
164
+ return {
165
+ ...item,
166
+ item: updateNestedItem(item.item),
167
+ };
168
+ }
169
+ return item;
170
+ });
171
+ };
172
+ newResponse.item = updateNestedItem(newResponse.item);
173
+ }
174
+ else {
175
+ // Flat structure
176
+ const findOrCreateItem = (items = []) => {
177
+ const existingIndex = items.findIndex((item) => item.linkId === linkId);
178
+ if (existingIndex >= 0) {
179
+ const updated = [...items];
180
+ const existingItem = updated[existingIndex];
181
+ updated[existingIndex] = {
182
+ linkId: existingItem.linkId,
183
+ ...(existingItem.definition && {
184
+ definition: existingItem.definition,
185
+ }),
186
+ ...(existingItem.text && { text: existingItem.text }),
187
+ answer: answers,
188
+ ...(existingItem.item && { item: existingItem.item }),
189
+ };
190
+ return updated;
191
+ }
192
+ else {
193
+ return [
194
+ ...items,
195
+ {
196
+ linkId,
197
+ answer: answers,
198
+ },
199
+ ];
200
+ }
201
+ };
202
+ newResponse.item = findOrCreateItem(newResponse.item);
203
+ }
204
+ return newResponse;
205
+ });
206
+ // Clear validation error for this question when answered
207
+ setValidationErrors((prev) => {
208
+ const newErrors = new Set(prev);
209
+ newErrors.delete(linkId);
210
+ return newErrors;
211
+ });
212
+ };
213
+ const getAnswer = (linkId) => {
214
+ // Recursively search for the item in nested structure or flat structure
215
+ const findItem = (items) => {
216
+ if (!items)
217
+ return undefined;
218
+ for (const item of items) {
219
+ if (item.linkId === linkId) {
220
+ return item;
221
+ }
222
+ if (item.item && item.item.length > 0) {
223
+ const found = findItem(item.item);
224
+ if (found)
225
+ return found;
226
+ }
227
+ }
228
+ return undefined;
229
+ };
230
+ const item = findItem(response.item);
231
+ return item?.answer?.[0];
232
+ };
233
+ const getAnswers = (linkId) => {
234
+ // Recursively search for the item in nested structure or flat structure
235
+ const findItem = (items) => {
236
+ if (!items)
237
+ return undefined;
238
+ for (const item of items) {
239
+ if (item.linkId === linkId) {
240
+ return item;
241
+ }
242
+ if (item.item && item.item.length > 0) {
243
+ const found = findItem(item.item);
244
+ if (found)
245
+ return found;
246
+ }
247
+ }
248
+ return undefined;
249
+ };
250
+ const item = findItem(response.item);
251
+ return item?.answer || [];
252
+ };
253
+ const getRequiredQuestions = (pageItems) => {
254
+ // Recursively flatten nested groups so required questions inside them
255
+ // are included in page validation.
256
+ const allQuestionsOnPage = getAllQuestionsFromPage({
257
+ linkId: "__page__",
258
+ type: "group",
259
+ item: pageItems,
260
+ });
261
+ return allQuestionsOnPage.filter((item) => item.required === true);
262
+ };
263
+ const isNonEmptyString = (value) => typeof value === "string" && value.trim().length > 0;
264
+ const hasMeaningfulAnswer = (answer) => {
265
+ if (!answer)
266
+ return false;
267
+ // Primitive numeric/boolean values are meaningful even when 0/false.
268
+ if (answer.valueBoolean !== undefined)
269
+ return true;
270
+ if (answer.valueInteger !== undefined)
271
+ return true;
272
+ if (answer.valueDecimal !== undefined)
273
+ return true;
274
+ // String-like answers must be non-empty (trimmed) to be meaningful.
275
+ if (isNonEmptyString(answer.valueString))
276
+ return true;
277
+ if (isNonEmptyString(answer.valueDate))
278
+ return true;
279
+ if (isNonEmptyString(answer.valueDateTime))
280
+ return true;
281
+ if (isNonEmptyString(answer.valueTime))
282
+ return true;
283
+ if (isNonEmptyString(answer.valueUri))
284
+ return true;
285
+ // Coding answers are meaningful when they contain at least one identifier.
286
+ if (answer.valueCoding &&
287
+ (isNonEmptyString(answer.valueCoding.code) ||
288
+ isNonEmptyString(answer.valueCoding.display) ||
289
+ isNonEmptyString(answer.valueCoding.system))) {
290
+ return true;
291
+ }
292
+ // Quantity answers are meaningful when a value is provided.
293
+ if (answer.valueQuantity?.value !== undefined)
294
+ return true;
295
+ return false;
296
+ };
297
+ const isPageValid = (pageItems) => {
298
+ const requiredQuestions = getRequiredQuestions(pageItems);
299
+ return requiredQuestions.every((question) => {
300
+ // For multi-select questions (repeats = true), check if there's at least one answer
301
+ if (question.repeats) {
302
+ const answers = getAnswers(question.linkId);
303
+ return answers.some((a) => hasMeaningfulAnswer(a));
304
+ }
305
+ // For single-answer questions
306
+ const answer = getAnswer(question.linkId);
307
+ return hasMeaningfulAnswer(answer);
308
+ });
309
+ };
310
+ const getUnansweredRequiredQuestions = (pageItems) => {
311
+ const requiredQuestions = getRequiredQuestions(pageItems);
312
+ return requiredQuestions.filter((question) => {
313
+ // For multi-select questions (repeats = true), check if there's at least one answer
314
+ if (question.repeats) {
315
+ const answers = getAnswers(question.linkId);
316
+ return !answers.some((a) => hasMeaningfulAnswer(a));
317
+ }
318
+ // For single-answer questions
319
+ const answer = getAnswer(question.linkId);
320
+ return !hasMeaningfulAnswer(answer);
321
+ });
322
+ };
323
+ const markValidationErrors = (pageItems) => {
324
+ const unansweredRequired = getUnansweredRequiredQuestions(pageItems);
325
+ const errorLinkIds = unansweredRequired.map((q) => q.linkId);
326
+ setValidationErrors(new Set(errorLinkIds));
327
+ };
328
+ const clearValidationErrors = () => {
329
+ setValidationErrors(new Set());
330
+ };
331
+ const hasValidationError = (linkId) => {
332
+ return validationErrors.has(linkId);
333
+ };
334
+ return (_jsx(QuestionnaireContext.Provider, { value: {
335
+ questionnaire,
336
+ response,
337
+ updateAnswer,
338
+ updateMultipleAnswers,
339
+ getAnswer,
340
+ getAnswers,
341
+ isPageValid,
342
+ getRequiredQuestions,
343
+ getUnansweredRequiredQuestions,
344
+ markValidationErrors,
345
+ clearValidationErrors,
346
+ hasValidationError,
347
+ debugMode,
348
+ toggleDebugMode,
349
+ }, children: children }));
350
+ };
@@ -0,0 +1,7 @@
1
+ export { QuestionRenderer } from "./components/question-renderer.js";
2
+ export type { QuestionRendererProps } from "./components/question-renderer.js";
3
+ export { QuestionnaireProvider, useQuestionnaire, type QuestionnaireContextType, type QuestionnaireProviderProps, } from "./contexts/questionnaire-context.js";
4
+ export type { Coding, Extension, Questionnaire, QuestionnaireItem, QuestionnaireItemAnswerOption, QuestionnaireResponse, QuestionnaireResponseAnswer, QuestionnaireResponseItem, } from "./types/fhir.js";
5
+ export type { RadioInputProps, CheckboxInputProps, } from "./types/index.js";
6
+ export { calculateProgress, getAllQuestionsFromPage, getExclusiveOptionCode, getVisiblePages, hasAnswerValue, isQuestionHidden, } from "./lib/questionnaire-utils.js";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AACrE,YAAY,EACV,qBAAqB,EACtB,MAAM,mCAAmC,CAAC;AAG3C,OAAO,EACL,qBAAqB,EACrB,gBAAgB,EAChB,KAAK,wBAAwB,EAC7B,KAAK,0BAA0B,GAChC,MAAM,qCAAqC,CAAC;AAG7C,YAAY,EACV,MAAM,EACN,SAAS,EACT,aAAa,EACb,iBAAiB,EACjB,6BAA6B,EAC7B,qBAAqB,EACrB,2BAA2B,EAC3B,yBAAyB,GAC1B,MAAM,iBAAiB,CAAC;AAEzB,YAAY,EACV,eAAe,EACf,kBAAkB,GACnB,MAAM,kBAAkB,CAAC;AAG1B,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,sBAAsB,EACtB,eAAe,EACf,cAAc,EACd,gBAAgB,GACjB,MAAM,8BAA8B,CAAC"}
@@ -0,0 +1,6 @@
1
+ // Components
2
+ export { QuestionRenderer } from "./components/question-renderer.js";
3
+ // Contexts
4
+ export { QuestionnaireProvider, useQuestionnaire, } from "./contexts/questionnaire-context.js";
5
+ // Utils
6
+ export { calculateProgress, getAllQuestionsFromPage, getExclusiveOptionCode, getVisiblePages, hasAnswerValue, isQuestionHidden, } from "./lib/questionnaire-utils.js";
@@ -0,0 +1,29 @@
1
+ import type { Questionnaire, QuestionnaireItem } from "../types/fhir.js";
2
+ /**
3
+ * Get visible pages from a questionnaire (excludes hidden groups)
4
+ */
5
+ export declare const getVisiblePages: (questionnaire: Questionnaire) => QuestionnaireItem[];
6
+ /**
7
+ * Calculate progress percentage
8
+ * @param currentPageIndex 0-based page index
9
+ * @param totalPages Total number of pages
10
+ * @returns Progress percentage (0-100)
11
+ */
12
+ export declare const calculateProgress: (currentPageIndex: number, totalPages: number) => number;
13
+ /**
14
+ * Get all questions from a page (flattens nested groups)
15
+ */
16
+ export declare const getAllQuestionsFromPage: (pageItem: QuestionnaireItem) => QuestionnaireItem[];
17
+ /**
18
+ * Check if a question has any answer value
19
+ */
20
+ export declare const hasAnswerValue: (answer: any) => boolean;
21
+ /**
22
+ * Check if a question item should be hidden based on extensions
23
+ */
24
+ export declare const isQuestionHidden: (item: QuestionnaireItem) => boolean;
25
+ /**
26
+ * Get the exclusive option code from a question item (if any)
27
+ */
28
+ export declare const getExclusiveOptionCode: (item: QuestionnaireItem) => string | undefined;
29
+ //# sourceMappingURL=questionnaire-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"questionnaire-utils.d.ts","sourceRoot":"","sources":["../../../src/lib/questionnaire-utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAEzE;;GAEG;AACH,eAAO,MAAM,eAAe,GAAI,eAAe,aAAa,KAAG,iBAAiB,EAc/E,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,GAAI,kBAAkB,MAAM,EAAE,YAAY,MAAM,KAAG,MAGhF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,uBAAuB,GAAI,UAAU,iBAAiB,KAAG,iBAAiB,EAsBtF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,cAAc,GAAI,QAAQ,GAAG,KAAG,OAe5C,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,gBAAgB,GAAI,MAAM,iBAAiB,KAAG,OAM1D,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,sBAAsB,GAAI,MAAM,iBAAiB,KAAG,MAAM,GAAG,SAKzE,CAAC"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Get visible pages from a questionnaire (excludes hidden groups)
3
+ */
4
+ export const getVisiblePages = (questionnaire) => {
5
+ if (!questionnaire.item)
6
+ return [];
7
+ return questionnaire.item.filter((item) => {
8
+ // Check if hidden
9
+ const isHidden = item.extension?.some((ext) => ext.url === "http://hl7.org/fhir/StructureDefinition/questionnaire-hidden" &&
10
+ ext.valueBoolean === true);
11
+ // Only include visible group items
12
+ return item.type === "group" && !isHidden;
13
+ });
14
+ };
15
+ /**
16
+ * Calculate progress percentage
17
+ * @param currentPageIndex 0-based page index
18
+ * @param totalPages Total number of pages
19
+ * @returns Progress percentage (0-100)
20
+ */
21
+ export const calculateProgress = (currentPageIndex, totalPages) => {
22
+ if (totalPages === 0)
23
+ return 0;
24
+ return ((currentPageIndex + 1) / totalPages) * 100;
25
+ };
26
+ /**
27
+ * Get all questions from a page (flattens nested groups)
28
+ */
29
+ export const getAllQuestionsFromPage = (pageItem) => {
30
+ const questions = [];
31
+ const collectQuestions = (items) => {
32
+ if (!items)
33
+ return;
34
+ for (const item of items) {
35
+ if (item.type === "group" && item.item) {
36
+ // Recursively collect from nested groups
37
+ collectQuestions(item.item);
38
+ }
39
+ else if (item.type !== "display") {
40
+ // Add non-display items (actual questions)
41
+ questions.push(item);
42
+ }
43
+ }
44
+ };
45
+ if (pageItem.item) {
46
+ collectQuestions(pageItem.item);
47
+ }
48
+ return questions;
49
+ };
50
+ /**
51
+ * Check if a question has any answer value
52
+ */
53
+ export const hasAnswerValue = (answer) => {
54
+ if (!answer)
55
+ return false;
56
+ return (answer.valueBoolean !== undefined ||
57
+ answer.valueInteger !== undefined ||
58
+ answer.valueDecimal !== undefined ||
59
+ answer.valueString !== undefined ||
60
+ answer.valueCoding !== undefined ||
61
+ answer.valueDate !== undefined ||
62
+ answer.valueDateTime !== undefined ||
63
+ answer.valueTime !== undefined ||
64
+ answer.valueUri !== undefined ||
65
+ answer.valueQuantity !== undefined);
66
+ };
67
+ /**
68
+ * Check if a question item should be hidden based on extensions
69
+ */
70
+ export const isQuestionHidden = (item) => {
71
+ return item.extension?.some((ext) => ext.url === "http://hl7.org/fhir/StructureDefinition/questionnaire-hidden" &&
72
+ ext.valueBoolean === true) ?? false;
73
+ };
74
+ /**
75
+ * Get the exclusive option code from a question item (if any)
76
+ */
77
+ export const getExclusiveOptionCode = (item) => {
78
+ const exclusiveExt = item.extension?.find((ext) => ext.url === "http://codes.welshare.app/StructureDefinition/questionnaire-exclusive-option");
79
+ return exclusiveExt?.valueString;
80
+ };
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }