@openmrs/esm-form-engine-lib 2.1.0-pre.1362

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 (272) hide show
  1. package/.editorconfig +12 -0
  2. package/.eslintignore +2 -0
  3. package/.eslintrc +58 -0
  4. package/.husky/pre-commit +6 -0
  5. package/.husky/pre-push +6 -0
  6. package/.prettierignore +4 -0
  7. package/LICENSE.txt +401 -0
  8. package/README.md +136 -0
  9. package/__mocks__/concepts.mock.json +140 -0
  10. package/__mocks__/forms/afe-forms/component_art.json +38 -0
  11. package/__mocks__/forms/afe-forms/component_preclinic-review.json +38 -0
  12. package/__mocks__/forms/afe-forms/demo_hts-form.json +62 -0
  13. package/__mocks__/forms/afe-forms/form-component.json +38 -0
  14. package/__mocks__/forms/afe-forms/mini-form.json +31 -0
  15. package/__mocks__/forms/afe-forms/nested-form1.json +38 -0
  16. package/__mocks__/forms/afe-forms/nested-form2.json +38 -0
  17. package/__mocks__/forms/afe-forms/test-orders.json +72 -0
  18. package/__mocks__/forms/afe-forms/test-schema-transformer-form.json +88 -0
  19. package/__mocks__/forms/rfe-forms/age-validation-form.json +58 -0
  20. package/__mocks__/forms/rfe-forms/bmi-test-form.json +69 -0
  21. package/__mocks__/forms/rfe-forms/bsa-test-form.json +69 -0
  22. package/__mocks__/forms/rfe-forms/component_art.json +1705 -0
  23. package/__mocks__/forms/rfe-forms/component_preclinic-review.json +480 -0
  24. package/__mocks__/forms/rfe-forms/conditional-answered-form.json +97 -0
  25. package/__mocks__/forms/rfe-forms/conditional-required-form.json +281 -0
  26. package/__mocks__/forms/rfe-forms/demo_hts-form.json +346 -0
  27. package/__mocks__/forms/rfe-forms/edd-test-form.json +88 -0
  28. package/__mocks__/forms/rfe-forms/external_data_source_form.json +43 -0
  29. package/__mocks__/forms/rfe-forms/filter-answer-options-test-form.json +87 -0
  30. package/__mocks__/forms/rfe-forms/form-component.json +43 -0
  31. package/__mocks__/forms/rfe-forms/forms-loader.test.schema.ts +209 -0
  32. package/__mocks__/forms/rfe-forms/historical-expressions-form.json +170 -0
  33. package/__mocks__/forms/rfe-forms/labour_and_delivery_test_form.json +374 -0
  34. package/__mocks__/forms/rfe-forms/mini-form.json +29 -0
  35. package/__mocks__/forms/rfe-forms/mockHistoricalvisitsEncounter.json +89 -0
  36. package/__mocks__/forms/rfe-forms/months-on-art-form.json +90 -0
  37. package/__mocks__/forms/rfe-forms/multi-select-form.json +86 -0
  38. package/__mocks__/forms/rfe-forms/nested-form1.json +43 -0
  39. package/__mocks__/forms/rfe-forms/nested-form2.json +43 -0
  40. package/__mocks__/forms/rfe-forms/next-visit-test-form.json +78 -0
  41. package/__mocks__/forms/rfe-forms/obs-group-test_form.json +137 -0
  42. package/__mocks__/forms/rfe-forms/obs-list-data.ts +37 -0
  43. package/__mocks__/forms/rfe-forms/post-submission-test-form.json +116 -0
  44. package/__mocks__/forms/rfe-forms/reference-by-mapping-form.json +54 -0
  45. package/__mocks__/forms/rfe-forms/required-form.json +50 -0
  46. package/__mocks__/forms/rfe-forms/sample_fields.json +36 -0
  47. package/__mocks__/forms/rfe-forms/test-enrolment-form.json +241 -0
  48. package/__mocks__/forms/rfe-forms/treatment-end-date-test-form.json +121 -0
  49. package/__mocks__/forms/rfe-forms/viral-load-status-form.json +75 -0
  50. package/__mocks__/forms/rfe-forms/zscore-bmi-for-age-form.json +79 -0
  51. package/__mocks__/forms/rfe-forms/zscore-height-for-age-form.json +79 -0
  52. package/__mocks__/forms/rfe-forms/zscore-weight-height-form.json +77 -0
  53. package/__mocks__/packages/hiv/forms/hts_poc/1.0.json +8 -0
  54. package/__mocks__/packages/hiv/forms/hts_poc/1.1.json +91 -0
  55. package/__mocks__/packages/test-forms-registry.ts +12 -0
  56. package/__mocks__/patient.mock.ts +173 -0
  57. package/__mocks__/react-i18next.js +49 -0
  58. package/__mocks__/react-markdown.tsx +5 -0
  59. package/__mocks__/session.mock.ts +117 -0
  60. package/__mocks__/single-spa-react.js +11 -0
  61. package/__mocks__/use-initial-values/encounter.mock.json +963 -0
  62. package/__mocks__/use-initial-values/patient.mock.json +73 -0
  63. package/__mocks__/visit.mock.ts +19 -0
  64. package/dist/openmrs-esm-form-engine-lib.js +1 -0
  65. package/jest.config.js +30 -0
  66. package/package.json +104 -0
  67. package/prettier.config.js +8 -0
  68. package/readme/form-engine.jpeg +0 -0
  69. package/src/adapters/control-adapter.ts +29 -0
  70. package/src/adapters/encounter-datetime-adapter.ts +38 -0
  71. package/src/adapters/encounter-location-adapter.ts +39 -0
  72. package/src/adapters/encounter-provider-adapter.ts +48 -0
  73. package/src/adapters/encounter-role-adapter.ts +54 -0
  74. package/src/adapters/inline-date-adapter.ts +58 -0
  75. package/src/adapters/obs-adapter.ts +280 -0
  76. package/src/adapters/obs-comment-adapter.ts +60 -0
  77. package/src/adapters/orders-adapter.ts +75 -0
  78. package/src/adapters/patient-identifier-adapter.ts +40 -0
  79. package/src/adapters/program-state-adapter.ts +52 -0
  80. package/src/api/index.ts +178 -0
  81. package/src/components/error/error-modal.component.tsx +37 -0
  82. package/src/components/error/error.scss +4 -0
  83. package/src/components/extension/extension-parcel.component.tsx +32 -0
  84. package/src/components/field-label/field-label.component.tsx +32 -0
  85. package/src/components/field-label/field-label.scss +11 -0
  86. package/src/components/group/obs-group.component.tsx +29 -0
  87. package/src/components/group/obs-group.scss +12 -0
  88. package/src/components/inputs/content-switcher/content-switcher.component.tsx +71 -0
  89. package/src/components/inputs/content-switcher/content-switcher.scss +55 -0
  90. package/src/components/inputs/date/date.component.tsx +149 -0
  91. package/src/components/inputs/date/date.scss +36 -0
  92. package/src/components/inputs/file/camera/camera.component.tsx +34 -0
  93. package/src/components/inputs/file/camera/camera.scss +3 -0
  94. package/src/components/inputs/file/file.component.tsx +159 -0
  95. package/src/components/inputs/file/file.scss +101 -0
  96. package/src/components/inputs/fixed-value/fixed-value.component.tsx +19 -0
  97. package/src/components/inputs/markdown/markdown-wrapper.component.tsx +14 -0
  98. package/src/components/inputs/markdown/markdown.component.tsx +8 -0
  99. package/src/components/inputs/multi-select/multi-select.component.tsx +151 -0
  100. package/src/components/inputs/multi-select/multi-select.scss +25 -0
  101. package/src/components/inputs/multi-select/multi-select.test.tsx +90 -0
  102. package/src/components/inputs/number/number.component.tsx +69 -0
  103. package/src/components/inputs/number/number.scss +15 -0
  104. package/src/components/inputs/radio/radio.component.tsx +79 -0
  105. package/src/components/inputs/radio/radio.scss +36 -0
  106. package/src/components/inputs/select/dropdown.component.tsx +73 -0
  107. package/src/components/inputs/select/dropdown.scss +11 -0
  108. package/src/components/inputs/select/dropdown.test.tsx +120 -0
  109. package/src/components/inputs/text/text.component.tsx +65 -0
  110. package/src/components/inputs/text/text.scss +15 -0
  111. package/src/components/inputs/text/text.test.tsx +104 -0
  112. package/src/components/inputs/text-area/text-area.component.tsx +63 -0
  113. package/src/components/inputs/text-area/text-area.scss +11 -0
  114. package/src/components/inputs/toggle/toggle.component.tsx +66 -0
  115. package/src/components/inputs/toggle/toggle.scss +12 -0
  116. package/src/components/inputs/tooltip/tooltip.component.tsx +23 -0
  117. package/src/components/inputs/tooltip/tooltip.scss +8 -0
  118. package/src/components/inputs/ui-select-extended/ui-select-extended.component.tsx +187 -0
  119. package/src/components/inputs/ui-select-extended/ui-select-extended.scss +15 -0
  120. package/src/components/inputs/ui-select-extended/ui-select-extended.test.tsx +211 -0
  121. package/src/components/inputs/unspecified/unspecified.component.tsx +74 -0
  122. package/src/components/inputs/unspecified/unspecified.scss +7 -0
  123. package/src/components/inputs/unspecified/unspecified.test.tsx +95 -0
  124. package/src/components/inputs/workspace-launcher/workspace-launcher.component.tsx +35 -0
  125. package/src/components/inputs/workspace-launcher/workspace-launcher.scss +15 -0
  126. package/src/components/label/label.component.tsx +20 -0
  127. package/src/components/label/label.scss +11 -0
  128. package/src/components/loaders/loader.component.tsx +16 -0
  129. package/src/components/loaders/loader.scss +20 -0
  130. package/src/components/patient-banner/patient-banner.component.tsx +20 -0
  131. package/src/components/patient-banner/patient-banner.scss +12 -0
  132. package/src/components/previous-value-review/previous-value-review.component.tsx +49 -0
  133. package/src/components/previous-value-review/previous-value-review.scss +36 -0
  134. package/src/components/processor-factory/form-processor-factory.component.tsx +127 -0
  135. package/src/components/renderer/custom-hooks-renderer.component.tsx +30 -0
  136. package/src/components/renderer/field/fieldLogic.ts +214 -0
  137. package/src/components/renderer/field/form-field-renderer.component.tsx +281 -0
  138. package/src/components/renderer/field/form-field-renderer.scss +5 -0
  139. package/src/components/renderer/form/form-renderer.component.tsx +89 -0
  140. package/src/components/renderer/form/state.ts +54 -0
  141. package/src/components/renderer/page/page.renderer.component.tsx +50 -0
  142. package/src/components/renderer/page/page.renderer.scss +36 -0
  143. package/src/components/renderer/section/section-renderer.component.tsx +21 -0
  144. package/src/components/renderer/section/section-renderer.scss +19 -0
  145. package/src/components/repeat/helpers.test.ts +29 -0
  146. package/src/components/repeat/helpers.ts +68 -0
  147. package/src/components/repeat/repeat-controls.component.tsx +38 -0
  148. package/src/components/repeat/repeat-controls.scss +7 -0
  149. package/src/components/repeat/repeat.component.tsx +201 -0
  150. package/src/components/repeat/repeat.scss +30 -0
  151. package/src/components/repeat/repeat.test.ts +29 -0
  152. package/src/components/sidebar/sidebar.component.tsx +134 -0
  153. package/src/components/sidebar/sidebar.scss +121 -0
  154. package/src/components/value/value.component.tsx +27 -0
  155. package/src/components/value/value.scss +17 -0
  156. package/src/components/value/view/field-value-view.component.tsx +33 -0
  157. package/src/components/value/view/field-value-view.scss +31 -0
  158. package/src/constants.ts +12 -0
  159. package/src/datasources/concept-data-source.ts +42 -0
  160. package/src/datasources/data-source.ts +23 -0
  161. package/src/datasources/encounter-role-datasource.ts +15 -0
  162. package/src/datasources/historical-data-source.ts +11 -0
  163. package/src/datasources/location-data-source.ts +27 -0
  164. package/src/datasources/provider-datasource.ts +15 -0
  165. package/src/datasources/select-concept-answers-datasource.ts +15 -0
  166. package/src/declarations.d.ts +4 -0
  167. package/src/external-function-context.tsx +8 -0
  168. package/src/form-context.tsx +42 -0
  169. package/src/form-engine.component.tsx +178 -0
  170. package/src/form-engine.scss +140 -0
  171. package/src/form-engine.test.tsx +817 -0
  172. package/src/globals.ts +1 -0
  173. package/src/hooks/useClobData.tsx +21 -0
  174. package/src/hooks/useConcepts.tsx +55 -0
  175. package/src/hooks/useDatasourceDependentValue.ts +16 -0
  176. package/src/hooks/useEncounter.tsx +32 -0
  177. package/src/hooks/useEncounterRole.tsx +15 -0
  178. package/src/hooks/useEvaluateFormFieldExpressions.ts +138 -0
  179. package/src/hooks/useFieldValidationResults.ts +18 -0
  180. package/src/hooks/useFormCollapse.tsx +36 -0
  181. package/src/hooks/useFormFieldValidators.ts +22 -0
  182. package/src/hooks/useFormFieldValueAdapters.ts +24 -0
  183. package/src/hooks/useFormFields.ts +37 -0
  184. package/src/hooks/useFormFieldsMeta.ts +48 -0
  185. package/src/hooks/useFormJson.test.tsx +173 -0
  186. package/src/hooks/useFormJson.tsx +237 -0
  187. package/src/hooks/useFormStateHelpers.ts +50 -0
  188. package/src/hooks/useFormsConfig.tsx +27 -0
  189. package/src/hooks/useInitialValues.ts +38 -0
  190. package/src/hooks/usePatientData.tsx +32 -0
  191. package/src/hooks/usePatientPrograms.ts +32 -0
  192. package/src/hooks/usePostSubmissionActions.test.tsx +42 -0
  193. package/src/hooks/usePostSubmissionActions.ts +31 -0
  194. package/src/hooks/useProcessorDependencies.ts +30 -0
  195. package/src/hooks/useRestMaxResultsCount.ts +5 -0
  196. package/src/hooks/useSystemSetting.ts +36 -0
  197. package/src/hooks/useWorkspaceLayout.ts +29 -0
  198. package/src/index.ts +12 -0
  199. package/src/lifecycle.ts +33 -0
  200. package/src/post-submission-actions/program-enrollment-action.ts +138 -0
  201. package/src/processors/encounter/encounter-form-processor.ts +337 -0
  202. package/src/processors/encounter/encounter-processor-helper.ts +320 -0
  203. package/src/processors/form-processor.ts +41 -0
  204. package/src/provider/form-factory-helper.ts +100 -0
  205. package/src/provider/form-factory-provider.tsx +169 -0
  206. package/src/provider/form-provider.tsx +37 -0
  207. package/src/registry/inbuilt-components/InbuiltPostSubmissionActions.ts +9 -0
  208. package/src/registry/inbuilt-components/control-templates.ts +57 -0
  209. package/src/registry/inbuilt-components/inbuiltControls.ts +99 -0
  210. package/src/registry/inbuilt-components/inbuiltDataSources.ts +41 -0
  211. package/src/registry/inbuilt-components/inbuiltFieldValueAdapters.ts +64 -0
  212. package/src/registry/inbuilt-components/inbuiltTransformers.ts +10 -0
  213. package/src/registry/inbuilt-components/inbuiltValidators.ts +33 -0
  214. package/src/registry/inbuilt-components/template-component-map.ts +28 -0
  215. package/src/registry/registry.test.ts +20 -0
  216. package/src/registry/registry.ts +261 -0
  217. package/src/routes.json +1 -0
  218. package/src/setupI18n.ts +16 -0
  219. package/src/setupTests.ts +5 -0
  220. package/src/transformers/default-schema-transformer.test.ts +155 -0
  221. package/src/transformers/default-schema-transformer.ts +239 -0
  222. package/src/types/domain.ts +129 -0
  223. package/src/types/index.ts +130 -0
  224. package/src/types/schema.ts +238 -0
  225. package/src/typings.d.ts +9 -0
  226. package/src/utils/boolean-utils.ts +25 -0
  227. package/src/utils/common-expression-helpers.ts +503 -0
  228. package/src/utils/common-utils.test.ts +136 -0
  229. package/src/utils/common-utils.ts +55 -0
  230. package/src/utils/error-utils.ts +37 -0
  231. package/src/utils/expression-parser.test.ts +308 -0
  232. package/src/utils/expression-parser.ts +158 -0
  233. package/src/utils/expression-runner.test.ts +387 -0
  234. package/src/utils/expression-runner.ts +219 -0
  235. package/src/utils/form-helper.test.ts +482 -0
  236. package/src/utils/form-helper.ts +210 -0
  237. package/src/utils/form-page-utils.ts +13 -0
  238. package/src/utils/forms-loader.test.ts +323 -0
  239. package/src/utils/forms-loader.ts +306 -0
  240. package/src/utils/post-submission-action-helper.ts +71 -0
  241. package/src/utils/test-utils.ts +54 -0
  242. package/src/utils/zscore-service.ts +59 -0
  243. package/src/validators/conditional-answered-validator.test.ts +61 -0
  244. package/src/validators/conditional-answered-validator.ts +17 -0
  245. package/src/validators/date-validator.test.ts +46 -0
  246. package/src/validators/date-validator.ts +19 -0
  247. package/src/validators/default-value-validator.test.ts +90 -0
  248. package/src/validators/default-value-validator.ts +36 -0
  249. package/src/validators/form-validator.test.ts +188 -0
  250. package/src/validators/form-validator.ts +95 -0
  251. package/src/validators/js-expression-validator.test.ts +118 -0
  252. package/src/validators/js-expression-validator.ts +44 -0
  253. package/src/validators/schema.ts +34 -0
  254. package/src/zscore/bfa_boys_5_above.json +2522 -0
  255. package/src/zscore/bfa_girls_5_above.json +2522 -0
  256. package/src/zscore/hfa_boys_5_above.json +2186 -0
  257. package/src/zscore/hfa_boys_below5.json +22286 -0
  258. package/src/zscore/hfa_girls_5_above.json +2186 -0
  259. package/src/zscore/hfa_girls_below5.json +22286 -0
  260. package/src/zscore/wfl_boys_below5.json +7814 -0
  261. package/src/zscore/wfl_girls_below5.json +7814 -0
  262. package/src/zscore-tests/bmi-age.test.tsx +88 -0
  263. package/src/zscore-tests/height-age.test.tsx +96 -0
  264. package/src/zscore-tests/weight-height.test.tsx +87 -0
  265. package/tools/i18next-parser.config.js +93 -0
  266. package/translations/en.json +47 -0
  267. package/translations/es.json +38 -0
  268. package/translations/fr.json +38 -0
  269. package/translations/km.json +38 -0
  270. package/tsconfig.json +19 -0
  271. package/turbo.json +15 -0
  272. package/webpack.config.js +1 -0
@@ -0,0 +1,138 @@
1
+ import dayjs from 'dayjs';
2
+ import { showSnackbar, translateFrom } from '@openmrs/esm-framework';
3
+ import { getPatientEnrolledPrograms, saveProgramEnrollment } from '../api';
4
+ import { type PostSubmissionAction, type PatientProgramPayload } from '../types';
5
+ import { moduleName } from '../globals';
6
+ import { extractErrorMessagesFromResponse } from '../utils/error-utils';
7
+
8
+ export const ProgramEnrollmentSubmissionAction: PostSubmissionAction = {
9
+ applyAction: async function ({ patient, encounters, sessionMode }, config) {
10
+ const encounter = encounters[0];
11
+ const encounterLocation = encounter.location['uuid'];
12
+ const translateFn = (key, defaultValue?) => translateFrom(moduleName, key, defaultValue);
13
+ const programUuid = config.programUuid;
14
+
15
+ if (sessionMode === 'view') {
16
+ return;
17
+ }
18
+ if (!programUuid) {
19
+ throw new Error('Program UUID not configured');
20
+ }
21
+
22
+ const enrollmentDate = encounter.obs?.find((item) =>
23
+ item.formFieldPath.includes(config.enrollmentDate || null),
24
+ )?.value;
25
+ const completionDate = encounter.obs?.find((item) =>
26
+ item.formFieldPath.includes(config.completionDate || null),
27
+ )?.value;
28
+
29
+ const abortController = new AbortController();
30
+ let payload: PatientProgramPayload = {
31
+ patient: patient.id,
32
+ program: programUuid,
33
+ dateEnrolled: enrollmentDate ?? dayjs().format(),
34
+ location: encounterLocation,
35
+ };
36
+ const patientPrograms = await getPatientEnrolledPrograms(patient.id);
37
+ const existingProgramEnrollment = patientPrograms?.results.find(
38
+ (enrollment) => enrollment.program.uuid === programUuid && !enrollment.dateCompleted,
39
+ );
40
+
41
+ if (config.completionDate) {
42
+ if (!completionDate) {
43
+ throw new Error('Completion date was not found in the encounter');
44
+ }
45
+ if (existingProgramEnrollment) {
46
+ payload = {
47
+ uuid: existingProgramEnrollment.uuid,
48
+ dateCompleted: updateTimeToNow(completionDate),
49
+ };
50
+ } else {
51
+ showSnackbar({
52
+ title: translateFn('enrollmentDiscontinuationNotAllowed', 'Enrollment discontinuation not allowed'),
53
+ subtitle: translateFn('cannotDiscontinueEnrollment', 'Cannot discontinue an enrollment that does not exist'),
54
+ kind: 'error',
55
+ isLowContrast: false,
56
+ });
57
+ return;
58
+ }
59
+ }
60
+
61
+ if (existingProgramEnrollment) {
62
+ if (!existingProgramEnrollment.dateCompleted && !completionDate) {
63
+ // The patient is already enrolled in the program and there is no completion date provided.
64
+ if (sessionMode === 'enter') {
65
+ showSnackbar({
66
+ title: translateFn('enrollmentNotAllowed', 'Enrollment not allowed'),
67
+ subtitle: translateFn(
68
+ 'alreadyEnrolledDescription',
69
+ 'This patient is already enrolled in the selected program and cannot be enrolled again.',
70
+ ),
71
+ kind: 'error',
72
+ isLowContrast: false,
73
+ });
74
+ }
75
+ return;
76
+ } else if (existingProgramEnrollment.dateCompleted) {
77
+ // The enrollment has already been completed
78
+ if (sessionMode === 'enter') {
79
+ showSnackbar({
80
+ title: translateFn('enrollmentAlreadyDiscontinued', 'Enrollment already discontinued'),
81
+ subtitle: translateFn(
82
+ 'alreadyDiscontinuedDescription',
83
+ 'This patient is already enrolled in the selected program and has already been discontinued.',
84
+ ),
85
+ kind: 'error',
86
+ isLowContrast: false,
87
+ });
88
+ }
89
+ return;
90
+ }
91
+ }
92
+ saveProgramEnrollment(payload, abortController).then(
93
+ (response) => {
94
+ showSnackbar({
95
+ kind: 'success',
96
+ title: getSnackTitle(translateFn, response),
97
+ isLowContrast: true,
98
+ });
99
+ },
100
+ (err) => {
101
+ showSnackbar({
102
+ title: translateFn('errorSavingEnrollment', 'Error saving enrollment'),
103
+ subtitle: extractErrorMessagesFromResponse(err).join(', '),
104
+ kind: 'error',
105
+ isLowContrast: false,
106
+ });
107
+ },
108
+ );
109
+ },
110
+ };
111
+
112
+ function getSnackTitle(translateFn, response) {
113
+ if (response.data.dateCompleted) {
114
+ return translateFn(
115
+ 'enrollmentDiscontinued',
116
+ "The patient's program enrollment has been successfully discontinued.",
117
+ );
118
+ }
119
+ return translateFn('enrolledToProgram', 'The patient has been successfully enrolled in the program.');
120
+ }
121
+
122
+ function updateTimeToNow(dateString) {
123
+ // Check if the input date string has the time set to midnight (00:00:00.000+0000)
124
+ if (!dateString.endsWith('T00:00:00.000+0000')) {
125
+ return dateString;
126
+ }
127
+ const now = dayjs();
128
+ const originalDate = dayjs(dateString);
129
+ const updatedDate = originalDate
130
+ .hour(now.hour())
131
+ .minute(now.minute())
132
+ .second(now.second())
133
+ .millisecond(now.millisecond());
134
+
135
+ return updatedDate.format();
136
+ }
137
+
138
+ export default ProgramEnrollmentSubmissionAction;
@@ -0,0 +1,337 @@
1
+ import {
2
+ type ValueAndDisplay,
3
+ type FormField,
4
+ type FormProcessorContextProps,
5
+ type FormPage,
6
+ type FormSection,
7
+ } from '../../types';
8
+ import { usePatientPrograms } from '../../hooks/usePatientPrograms';
9
+ import { useEffect, useState } from 'react';
10
+ import { useEncounter } from '../../hooks/useEncounter';
11
+ import { isEmpty } from '../../validators/form-validator';
12
+ import { type FormSchema } from '../../types';
13
+ import { type FormContextProps } from '../../provider/form-provider';
14
+ import { FormProcessor } from '../form-processor';
15
+ import {
16
+ getMutableSessionProps,
17
+ hydrateRepeatField,
18
+ inferInitialValueFromDefaultFieldValue,
19
+ prepareEncounter,
20
+ preparePatientIdentifiers,
21
+ preparePatientPrograms,
22
+ saveAttachments,
23
+ savePatientIdentifiers,
24
+ savePatientPrograms,
25
+ } from './encounter-processor-helper';
26
+ import { type OpenmrsResource, showSnackbar, translateFrom } from '@openmrs/esm-framework';
27
+ import { moduleName } from '../../globals';
28
+ import { extractErrorMessagesFromResponse } from '../../utils/error-utils';
29
+ import { getPreviousEncounter, saveEncounter } from '../../api';
30
+ import { useEncounterRole } from '../../hooks/useEncounterRole';
31
+ import { type FormNode, evaluateAsyncExpression, evaluateExpression } from '../../utils/expression-runner';
32
+ import { hasRendering } from '../../utils/common-utils';
33
+
34
+ function useCustomHooks(context: Partial<FormProcessorContextProps>) {
35
+ const [isLoading, setIsLoading] = useState(true);
36
+ const { encounter, isLoading: isLoadingEncounter } = useEncounter(context.formJson);
37
+ const { encounterRole, isLoading: isLoadingEncounterRole } = useEncounterRole();
38
+ const { isLoading: isLoadingPatientPrograms, patientPrograms } = usePatientPrograms(
39
+ context.patient?.id,
40
+ context.formJson,
41
+ );
42
+
43
+ useEffect(() => {
44
+ setIsLoading(isLoadingPatientPrograms || isLoadingEncounter || isLoadingEncounterRole);
45
+ }, [isLoadingPatientPrograms, isLoadingEncounter, isLoadingEncounterRole]);
46
+
47
+ return {
48
+ data: { encounter, patientPrograms, encounterRole },
49
+ isLoading,
50
+ error: null,
51
+ updateContext: (setContext: React.Dispatch<React.SetStateAction<FormProcessorContextProps>>) => {
52
+ setContext((context) => {
53
+ context.processor.domainObjectValue = encounter as OpenmrsResource;
54
+ return {
55
+ ...context,
56
+ domainObjectValue: encounter as OpenmrsResource,
57
+ customDependencies: {
58
+ ...context.customDependencies,
59
+ patientPrograms: patientPrograms,
60
+ defaultEncounterRole: encounterRole,
61
+ },
62
+ };
63
+ });
64
+ },
65
+ };
66
+ }
67
+
68
+ const emptyValues = {
69
+ checkbox: [],
70
+ toggle: false,
71
+ text: '',
72
+ };
73
+
74
+ const contextInitializableTypes = [
75
+ 'encounterProvider',
76
+ 'encounterDatetime',
77
+ 'encounterLocation',
78
+ 'patientIdentifier',
79
+ 'encounterRole',
80
+ ];
81
+ export class EncounterFormProcessor extends FormProcessor {
82
+ prepareFormSchema(schema: FormSchema) {
83
+ schema.pages.forEach((page) => {
84
+ page.sections.forEach((section) => {
85
+ section.questions.forEach((question) => {
86
+ prepareFormField(question, section, page, schema);
87
+ });
88
+ });
89
+ });
90
+
91
+ function prepareFormField(field: FormField, section: FormSection, page: FormPage, schema: FormSchema) {
92
+ // inherit inlineRendering and readonly from parent section and page if not set
93
+ field.inlineRendering =
94
+ field.inlineRendering ?? section.inlineRendering ?? page.inlineRendering ?? schema.inlineRendering;
95
+ field.readonly = field.readonly ?? section.readonly ?? page.readonly ?? schema.readonly;
96
+ if (field.questionOptions?.rendering == 'fixed-value' && !field.meta.fixedValue) {
97
+ field.meta.fixedValue = field.value;
98
+ delete field.value;
99
+ }
100
+ if (field.questionOptions?.rendering == 'group') {
101
+ field.questions?.forEach((child) => {
102
+ child.readonly = child.readonly ?? field.readonly;
103
+ return prepareFormField(child, section, page, schema);
104
+ });
105
+ }
106
+ }
107
+ return schema;
108
+ }
109
+
110
+ async processSubmission(context: FormContextProps, abortController: AbortController) {
111
+ const { encounterRole, encounterProvider, encounterDate, encounterLocation } = getMutableSessionProps(context);
112
+ const translateFn = (key, defaultValue?) => translateFrom(moduleName, key, defaultValue);
113
+ const patientIdentifiers = preparePatientIdentifiers(context.formFields, encounterLocation);
114
+ const encounter = prepareEncounter(context, encounterDate, encounterRole, encounterProvider, encounterLocation);
115
+
116
+ // save patient identifiers
117
+ try {
118
+ await Promise.all(savePatientIdentifiers(context.patient, patientIdentifiers));
119
+ if (patientIdentifiers?.length) {
120
+ showSnackbar({
121
+ title: translateFn('patientIdentifiersSaved', 'Patient identifier(s) saved successfully'),
122
+ kind: 'success',
123
+ isLowContrast: true,
124
+ });
125
+ }
126
+ } catch (error) {
127
+ const errorMessages = extractErrorMessagesFromResponse(error);
128
+ return Promise.reject({
129
+ title: translateFn('errorSavingPatientIdentifiers', 'Error saving patient identifiers'),
130
+ description: errorMessages.join(', '),
131
+ kind: 'error',
132
+ critical: true,
133
+ });
134
+ }
135
+
136
+ // save patient programs
137
+ try {
138
+ const programs = preparePatientPrograms(
139
+ context.formFields,
140
+ context.patient,
141
+ context.customDependencies.patientPrograms,
142
+ );
143
+ const savedPrograms = await await savePatientPrograms(programs);
144
+ if (savedPrograms?.length) {
145
+ showSnackbar({
146
+ title: translateFn('patientProgramsSaved', 'Patient program(s) saved successfully'),
147
+ kind: 'success',
148
+ isLowContrast: true,
149
+ });
150
+ }
151
+ } catch (error) {
152
+ const errorMessages = extractErrorMessagesFromResponse(error);
153
+ return Promise.reject({
154
+ title: translateFn('errorSavingPatientPrograms', 'Error saving patient program(s)'),
155
+ description: errorMessages.join(', '),
156
+ kind: 'error',
157
+ critical: true,
158
+ });
159
+ }
160
+
161
+ // save encounter
162
+ try {
163
+ const { data: savedEncounter } = await saveEncounter(abortController, encounter, encounter.uuid);
164
+ const saveOrders = savedEncounter.orders.map((order) => order.orderNumber);
165
+ if (saveOrders.length) {
166
+ showSnackbar({
167
+ title: translateFn('ordersSaved', 'Order(s) saved successfully'),
168
+ subtitle: saveOrders.join(', '),
169
+ kind: 'success',
170
+ isLowContrast: true,
171
+ });
172
+ }
173
+ // handle attachments
174
+ try {
175
+ const attachmentsResponse = await Promise.all(
176
+ saveAttachments(context.formFields, savedEncounter, abortController),
177
+ );
178
+ if (attachmentsResponse?.length) {
179
+ showSnackbar({
180
+ title: translateFn('attachmentsSaved', 'Attachment(s) saved successfully'),
181
+ kind: 'success',
182
+ isLowContrast: true,
183
+ });
184
+ }
185
+ } catch (error) {
186
+ const errorMessages = extractErrorMessagesFromResponse(error);
187
+ return Promise.reject({
188
+ title: translateFn('errorSavingAttachments', 'Error saving attachment(s)'),
189
+ description: errorMessages.join(', '),
190
+ kind: 'error',
191
+ critical: true,
192
+ });
193
+ }
194
+ return savedEncounter;
195
+ } catch (error) {
196
+ const errorMessages = extractErrorMessagesFromResponse(error);
197
+ return Promise.reject({
198
+ title: translateFn('errorSavingEncounter', 'Error saving encounter'),
199
+ description: errorMessages.join(', '),
200
+ kind: 'error',
201
+ critical: true,
202
+ });
203
+ }
204
+ }
205
+
206
+ getCustomHooks() {
207
+ return { useCustomHooks };
208
+ }
209
+
210
+ async getInitialValues(context: FormProcessorContextProps) {
211
+ const { domainObjectValue: encounter, formFields, formFieldAdapters } = context;
212
+ const initialValues = {};
213
+ const repeatableFields = [];
214
+ if (encounter) {
215
+ const filteredFields = formFields.filter((field) => isEmpty(field.meta?.previousValue));
216
+ await Promise.all(
217
+ filteredFields.map(async (field) => {
218
+ const adapter = formFieldAdapters[field.type];
219
+ if (adapter) {
220
+ if (hasRendering(field, 'repeating') && !field.meta?.repeat?.isClone) {
221
+ repeatableFields.push(field);
222
+ }
223
+ let value = null;
224
+ try {
225
+ value = await adapter.getInitialValue(field, encounter, context);
226
+ } catch (error) {
227
+ console.error(error);
228
+ }
229
+ if (field.type === 'obsGroup') {
230
+ return;
231
+ }
232
+ if (!isEmpty(value)) {
233
+ initialValues[field.id] = value;
234
+ } else if (!isEmpty(field.questionOptions.defaultValue)) {
235
+ initialValues[field.id] = inferInitialValueFromDefaultFieldValue(field);
236
+ } else {
237
+ initialValues[field.id] = emptyValues[field.questionOptions.rendering] ?? '';
238
+ }
239
+ if (field.questionOptions.calculate?.calculateExpression) {
240
+ await evaluateCalculateExpression(field, initialValues, context);
241
+ }
242
+ } else {
243
+ console.warn(`No adapter found for field type ${field.type}`);
244
+ }
245
+ }),
246
+ );
247
+ const flattenedRepeatableFields = await Promise.all(
248
+ repeatableFields.flatMap((field) => hydrateRepeatField(field, encounter, initialValues, context)),
249
+ ).then((results) => results.flat());
250
+ formFields.push(...flattenedRepeatableFields);
251
+ } else {
252
+ const filteredFields = formFields.filter(
253
+ (field) => field.questionOptions.rendering !== 'group' && field.type !== 'obsGroup',
254
+ );
255
+ await Promise.all(
256
+ filteredFields.map(async (field) => {
257
+ const adapter = formFieldAdapters[field.type];
258
+ initialValues[field.id] = emptyValues[field.questionOptions.rendering] ?? null;
259
+ if (field.questionOptions.calculate?.calculateExpression) {
260
+ await evaluateCalculateExpression(field, initialValues, context);
261
+ }
262
+ if (isEmpty(initialValues[field.id]) && contextInitializableTypes.includes(field.type)) {
263
+ try {
264
+ initialValues[field.id] = await adapter.getInitialValue(field, null, context);
265
+ } catch (error) {
266
+ console.error(error);
267
+ }
268
+ }
269
+ }),
270
+ );
271
+ }
272
+ return initialValues;
273
+ }
274
+
275
+ async loadDependencies(
276
+ context: FormContextProps,
277
+ setContext: React.Dispatch<React.SetStateAction<FormProcessorContextProps>>,
278
+ ) {
279
+ const { patient, formJson } = context;
280
+ const encounter = await getPreviousEncounter(patient?.id, formJson.encounterType);
281
+ setContext((context) => {
282
+ return {
283
+ ...context,
284
+ previousDomainObjectValue: encounter,
285
+ };
286
+ });
287
+ return context;
288
+ }
289
+
290
+ async getHistoricalValue(field: FormField, context: FormContextProps): Promise<ValueAndDisplay> {
291
+ const {
292
+ formFields,
293
+ sessionMode,
294
+ patient,
295
+ methods: { getValues },
296
+ formFieldAdapters,
297
+ previousDomainObjectValue,
298
+ } = context;
299
+ const node: FormNode = { value: field, type: 'field' };
300
+ const adapter = formFieldAdapters[field.type];
301
+ if (field.historicalExpression) {
302
+ const value = await evaluateAsyncExpression(field.historicalExpression, node, formFields, getValues(), {
303
+ mode: sessionMode,
304
+ patient: patient,
305
+ previousEncounter: previousDomainObjectValue,
306
+ });
307
+ return value;
308
+ }
309
+ if (previousDomainObjectValue && field.questionOptions.enablePreviousValue) {
310
+ return await adapter.getPreviousValue(field, previousDomainObjectValue, context);
311
+ }
312
+ return null;
313
+ }
314
+ }
315
+
316
+ async function evaluateCalculateExpression(
317
+ field: FormField,
318
+ values: Record<string, any>,
319
+ formContext: FormProcessorContextProps,
320
+ ) {
321
+ const { formFields, sessionMode, patient } = formContext;
322
+ const expression = field.questionOptions.calculate.calculateExpression;
323
+ const node: FormNode = { value: field, type: 'field' };
324
+ const context = {
325
+ mode: sessionMode,
326
+ patient: patient,
327
+ };
328
+ let value = null;
329
+ if (field.questionOptions.calculate.calculateExpression.includes('resolve(')) {
330
+ value = await evaluateAsyncExpression(expression, node, formFields, values, context);
331
+ } else {
332
+ value = evaluateExpression(expression, node, formFields, values, context);
333
+ }
334
+ if (!isEmpty(value)) {
335
+ values[field.id] = value;
336
+ }
337
+ }