@openmrs/esm-fast-data-entry-app 1.0.0-pre.9 → 1.0.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 (214) hide show
  1. package/README.md +58 -12
  2. package/__mocks__/react-i18next.js +9 -14
  3. package/dist/101.js +1 -0
  4. package/dist/101.js.map +1 -0
  5. package/dist/132.js +1 -0
  6. package/dist/188.js +1 -0
  7. package/dist/188.js.map +1 -0
  8. package/dist/197.js +1 -0
  9. package/dist/219.js +1 -0
  10. package/dist/219.js.map +1 -0
  11. package/dist/221.js +1 -0
  12. package/dist/221.js.map +1 -0
  13. package/dist/259.js +1 -0
  14. package/dist/259.js.map +1 -0
  15. package/dist/29.js +2 -0
  16. package/dist/29.js.LICENSE.txt +3 -0
  17. package/dist/29.js.map +1 -0
  18. package/dist/300.js +1 -0
  19. package/dist/326.js +1 -0
  20. package/dist/326.js.map +1 -0
  21. package/dist/335.js +1 -0
  22. package/dist/367.js +1 -0
  23. package/dist/367.js.map +1 -0
  24. package/dist/480.js +1 -0
  25. package/dist/540.js +2 -0
  26. package/dist/{382.js.LICENSE.txt → 540.js.LICENSE.txt} +3 -2
  27. package/dist/540.js.map +1 -0
  28. package/dist/55.js +1 -0
  29. package/dist/564.js +1 -0
  30. package/dist/564.js.map +1 -0
  31. package/dist/602.js +1 -0
  32. package/dist/602.js.map +1 -0
  33. package/dist/626.js +2 -0
  34. package/dist/{294.js.LICENSE.txt → 626.js.LICENSE.txt} +3 -8
  35. package/dist/626.js.map +1 -0
  36. package/dist/652.js +1 -0
  37. package/dist/685.js +1 -0
  38. package/dist/685.js.map +1 -0
  39. package/dist/773.js +2 -0
  40. package/dist/773.js.LICENSE.txt +32 -0
  41. package/dist/773.js.map +1 -0
  42. package/dist/893.js +1 -0
  43. package/dist/893.js.map +1 -0
  44. package/dist/91.js +1 -0
  45. package/dist/91.js.map +1 -0
  46. package/dist/941.js +2 -0
  47. package/dist/941.js.LICENSE.txt +30 -0
  48. package/dist/941.js.map +1 -0
  49. package/dist/961.js +2 -0
  50. package/dist/{735.js.LICENSE.txt → 961.js.LICENSE.txt} +6 -16
  51. package/dist/961.js.map +1 -0
  52. package/dist/99.js +1 -0
  53. package/dist/99.js.map +1 -0
  54. package/dist/991.js +1 -0
  55. package/dist/991.js.map +1 -0
  56. package/dist/main.js +1 -0
  57. package/dist/main.js.map +1 -0
  58. package/dist/openmrs-esm-fast-data-entry-app.js +1 -1
  59. package/dist/openmrs-esm-fast-data-entry-app.js.buildmanifest.json +537 -95
  60. package/dist/openmrs-esm-fast-data-entry-app.js.map +1 -1
  61. package/dist/routes.json +1 -0
  62. package/docs/config-icrc-forms.png +0 -0
  63. package/docs/config-other-forms.png +0 -0
  64. package/docs/configuring-form-categories.md +77 -0
  65. package/docs/fde-workflow.mov +0 -0
  66. package/docs/form-workflow-state-diagram.png +0 -0
  67. package/jest.config.json +21 -18
  68. package/package.json +101 -106
  69. package/prettier.config.js +8 -0
  70. package/src/CancelModal.tsx +42 -0
  71. package/src/CompleteModal.tsx +35 -0
  72. package/src/FormBootstrap.tsx +179 -0
  73. package/src/Root.tsx +11 -5
  74. package/src/add-group-modal/AddGroupModal.tsx +249 -0
  75. package/src/add-group-modal/styles.scss +49 -0
  76. package/src/config-schema.ts +124 -31
  77. package/src/constant.ts +1 -1
  78. package/src/context/FormWorkflowContext.tsx +113 -0
  79. package/src/context/FormWorkflowReducer.ts +263 -0
  80. package/src/context/GroupFormWorkflowContext.tsx +155 -0
  81. package/src/context/GroupFormWorkflowReducer.ts +405 -0
  82. package/src/declarations.d.ts +4 -0
  83. package/src/empty-state/EmptyDataIllustration.tsx +39 -0
  84. package/src/empty-state/EmptyState.tsx +28 -0
  85. package/src/empty-state/styles.scss +55 -0
  86. package/src/form-entry-workflow/FormEntryWorkflow.tsx +184 -0
  87. package/src/form-entry-workflow/form-review-card/FormReviewCard.tsx +50 -0
  88. package/src/form-entry-workflow/form-review-card/index.ts +3 -0
  89. package/src/form-entry-workflow/form-review-card/styles.scss +37 -0
  90. package/src/form-entry-workflow/index.ts +3 -0
  91. package/src/form-entry-workflow/patient-banner/PatientBanner.test.tsx +9 -0
  92. package/src/form-entry-workflow/patient-banner/PatientBanner.tsx +73 -0
  93. package/src/form-entry-workflow/patient-banner/index.ts +3 -0
  94. package/src/form-entry-workflow/patient-banner/styles.scss +44 -0
  95. package/src/form-entry-workflow/patient-search-header/PatientSearchHeader.tsx +54 -0
  96. package/src/form-entry-workflow/patient-search-header/index.ts +3 -0
  97. package/src/form-entry-workflow/patient-search-header/styles.scss +25 -0
  98. package/src/form-entry-workflow/styles.scss +63 -0
  99. package/src/form-entry-workflow/workflow-review/WorkflowReview.tsx +37 -0
  100. package/src/form-entry-workflow/workflow-review/index.ts +3 -0
  101. package/src/form-entry-workflow/workflow-review/styles.scss +30 -0
  102. package/src/forms-app-menu-link.tsx +6 -7
  103. package/src/forms-page/FormsPage.tsx +106 -0
  104. package/src/forms-page/forms-table/FormsTable.tsx +117 -0
  105. package/src/forms-page/forms-table/index.ts +3 -0
  106. package/src/forms-page/forms-table/styles.scss +19 -0
  107. package/src/forms-page/index.ts +3 -0
  108. package/src/forms-page/styles.scss +9 -0
  109. package/src/group-form-entry-workflow/GroupFormEntryWorkflow.tsx +26 -0
  110. package/src/group-form-entry-workflow/GroupSessionWorkspace.tsx +207 -0
  111. package/src/group-form-entry-workflow/SessionDetailsForm.tsx +154 -0
  112. package/src/group-form-entry-workflow/SessionMetaWorkspace.tsx +99 -0
  113. package/src/group-form-entry-workflow/attendance-table/AttendanceTable.tsx +130 -0
  114. package/src/group-form-entry-workflow/attendance-table/index.ts +1 -0
  115. package/src/group-form-entry-workflow/configurable-questions/ConfigurableQuestionsSection.tsx +41 -0
  116. package/src/group-form-entry-workflow/group-display-header/GroupDisplayHeader.test.tsx +9 -0
  117. package/src/group-form-entry-workflow/group-display-header/GroupDisplayHeader.tsx +55 -0
  118. package/src/group-form-entry-workflow/group-display-header/index.ts +3 -0
  119. package/src/group-form-entry-workflow/group-display-header/styles.scss +60 -0
  120. package/src/group-form-entry-workflow/group-search/CompactGroupResults.tsx +128 -0
  121. package/src/group-form-entry-workflow/group-search/CompactGroupSearch.tsx +66 -0
  122. package/src/group-form-entry-workflow/group-search/GroupSearch.tsx +134 -0
  123. package/src/group-form-entry-workflow/group-search/compact-group-result.scss +63 -0
  124. package/src/group-form-entry-workflow/group-search/compact-group-search.scss +34 -0
  125. package/src/group-form-entry-workflow/group-search/group-search.scss +93 -0
  126. package/src/group-form-entry-workflow/group-search-header/GroupSearchHeader.tsx +72 -0
  127. package/src/group-form-entry-workflow/group-search-header/index.ts +3 -0
  128. package/src/group-form-entry-workflow/group-search-header/styles.scss +20 -0
  129. package/src/group-form-entry-workflow/index.ts +3 -0
  130. package/src/group-form-entry-workflow/styles.scss +94 -0
  131. package/src/hooks/index.ts +8 -0
  132. package/src/hooks/useForm.ts +56 -0
  133. package/src/hooks/useFormState.ts +23 -0
  134. package/src/hooks/useGetAllForms.ts +37 -0
  135. package/src/hooks/useGetEncounter.ts +21 -0
  136. package/src/hooks/useGetPatient.ts +23 -0
  137. package/src/hooks/useGetPatients.ts +32 -0
  138. package/src/hooks/useGetSystemSetting.ts +36 -0
  139. package/src/hooks/useKeyPress.ts +31 -0
  140. package/src/hooks/usePostEndpoint.ts +76 -0
  141. package/src/hooks/useSearchEndpoint.ts +103 -0
  142. package/src/hooks/useStartVisit.ts +82 -0
  143. package/src/index.ts +18 -66
  144. package/src/patient-card/PatientCard.tsx +55 -0
  145. package/src/patient-card/index.ts +3 -0
  146. package/src/patient-card/styles.scss +44 -0
  147. package/src/routes.json +24 -0
  148. package/src/setup-tests.ts +1 -1
  149. package/src/types.ts +20 -0
  150. package/tools/i18next-parser.config.js +93 -0
  151. package/translations/am.json +75 -0
  152. package/translations/ar.json +75 -0
  153. package/translations/en.json +75 -4
  154. package/translations/es.json +75 -0
  155. package/translations/fr.json +75 -0
  156. package/translations/he.json +75 -0
  157. package/translations/km.json +75 -0
  158. package/tsconfig.json +26 -23
  159. package/turbo.json +18 -0
  160. package/webpack.config.js +1 -1
  161. package/.editorconfig +0 -12
  162. package/.eslintignore +0 -2
  163. package/.eslintrc +0 -4
  164. package/.github/workflows/node.js.yml +0 -79
  165. package/.husky/pre-commit +0 -6
  166. package/.husky/pre-push +0 -6
  167. package/.prettierignore +0 -14
  168. package/dist/24.js +0 -3
  169. package/dist/24.js.LICENSE.txt +0 -16
  170. package/dist/24.js.map +0 -1
  171. package/dist/294.js +0 -3
  172. package/dist/294.js.map +0 -1
  173. package/dist/296.js +0 -2
  174. package/dist/296.js.map +0 -1
  175. package/dist/299.js +0 -2
  176. package/dist/299.js.map +0 -1
  177. package/dist/382.js +0 -3
  178. package/dist/382.js.map +0 -1
  179. package/dist/415.js +0 -2
  180. package/dist/415.js.map +0 -1
  181. package/dist/574.js +0 -1
  182. package/dist/595.js +0 -3
  183. package/dist/595.js.LICENSE.txt +0 -1
  184. package/dist/595.js.map +0 -1
  185. package/dist/69.js +0 -2
  186. package/dist/69.js.map +0 -1
  187. package/dist/735.js +0 -3
  188. package/dist/735.js.map +0 -1
  189. package/dist/777.js +0 -2
  190. package/dist/777.js.map +0 -1
  191. package/dist/860.js +0 -2
  192. package/dist/860.js.map +0 -1
  193. package/dist/906.js +0 -2
  194. package/dist/906.js.map +0 -1
  195. package/dist/openmrs-esm-fast-data-entry-app.old +0 -2
  196. package/src/boxes/extensions/blue-box.tsx +0 -15
  197. package/src/boxes/extensions/box.scss +0 -23
  198. package/src/boxes/extensions/brand-box.tsx +0 -15
  199. package/src/boxes/extensions/red-box.tsx +0 -15
  200. package/src/boxes/slot/boxes.css +0 -23
  201. package/src/boxes/slot/boxes.tsx +0 -19
  202. package/src/declarations.d.tsx +0 -2
  203. package/src/forms/FormsRoot.tsx +0 -32
  204. package/src/forms/FormsTable.tsx +0 -64
  205. package/src/forms/mockData.ts +0 -43
  206. package/src/greeter/greeter.css +0 -4
  207. package/src/greeter/greeter.test.tsx +0 -29
  208. package/src/greeter/greeter.tsx +0 -25
  209. package/src/hello.css +0 -3
  210. package/src/hello.test.tsx +0 -45
  211. package/src/hello.tsx +0 -30
  212. package/src/patient-getter/patient-getter.resource.ts +0 -31
  213. package/src/patient-getter/patient-getter.test.tsx +0 -28
  214. package/src/patient-getter/patient-getter.tsx +0 -28
@@ -0,0 +1,134 @@
1
+ import React, { useCallback, useRef } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Layer, Tile, Loading } from '@carbon/react';
4
+ import styles from './group-search.scss';
5
+ import { EmptyDataIllustration } from '../../empty-state/EmptyDataIllustration';
6
+ import CompactGroupResults, { SearchResultSkeleton } from './CompactGroupResults';
7
+ import { type GroupType } from '../../context/GroupFormWorkflowContext';
8
+ import { useSearchCohortInfinite } from '../../hooks/useSearchEndpoint';
9
+
10
+ interface GroupSearchProps {
11
+ query: string;
12
+ selectGroupAction?: (group: GroupType) => void;
13
+ }
14
+
15
+ const GroupSearch: React.FC<GroupSearchProps> = ({ query = '', selectGroupAction }) => {
16
+ const { t } = useTranslation();
17
+ const {
18
+ isLoading,
19
+ data: results,
20
+ error,
21
+ loadingNewData,
22
+ setPage,
23
+ hasMore,
24
+ totalResults,
25
+ } = useSearchCohortInfinite({
26
+ searchTerm: query,
27
+ searching: !!query,
28
+ parameters: {
29
+ v: 'full',
30
+ },
31
+ });
32
+
33
+ const lastItem = useRef(null);
34
+ const observer = useRef(null);
35
+ const loadingRef = useCallback(
36
+ (node) => {
37
+ if (loadingNewData) {
38
+ return;
39
+ }
40
+ if (observer.current) {
41
+ observer.current.disconnect();
42
+ }
43
+ observer.current = new IntersectionObserver(
44
+ (entries) => {
45
+ if (entries[0].isIntersecting && hasMore) {
46
+ setPage((page) => page + 1);
47
+ }
48
+ },
49
+ {
50
+ threshold: 0.75,
51
+ },
52
+ );
53
+ if (node) {
54
+ observer.current.observe(node);
55
+ }
56
+ },
57
+ [loadingNewData, hasMore, setPage],
58
+ );
59
+
60
+ if (error) {
61
+ return (
62
+ <div className={styles.searchResults}>
63
+ <Layer>
64
+ <Tile className={styles.emptySearchResultsTile}>
65
+ <EmptyDataIllustration />
66
+ <div>
67
+ <p className={styles.errorMessage}>{t('error', 'Error')}</p>
68
+ <p className={styles.errorCopy}>
69
+ {t(
70
+ 'errorCopy',
71
+ 'Sorry, there was an error. You can try to reload this page, or contact the site administrator and quote the error code above.',
72
+ )}
73
+ </p>
74
+ </div>
75
+ </Tile>
76
+ </Layer>
77
+ </div>
78
+ );
79
+ }
80
+
81
+ if (isLoading) {
82
+ return (
83
+ <div className={styles.searchResultsContainer}>
84
+ <SearchResultSkeleton />
85
+ <SearchResultSkeleton />
86
+ <SearchResultSkeleton />
87
+ <SearchResultSkeleton />
88
+ <SearchResultSkeleton />
89
+ </div>
90
+ );
91
+ }
92
+
93
+ if (results?.length === 0) {
94
+ return (
95
+ <div className={styles.searchResults}>
96
+ <Layer>
97
+ <Tile className={styles.emptySearchResultsTile}>
98
+ <EmptyDataIllustration />
99
+ <p className={styles.emptyResultText}>{t('noGroupsFoundMessage', 'Sorry, no groups have been found')}</p>
100
+ <p className={styles.actionText}>
101
+ <span>{t('trySearchWithPatientUniqueID', "Try searching with the cohort's description")}</span>
102
+ <br />
103
+ <span>{t('orLabelName', 'OR label name')}</span>
104
+ </p>
105
+ </Tile>
106
+ </Layer>
107
+ </div>
108
+ );
109
+ }
110
+
111
+ return (
112
+ <div className={styles.searchResultsContainer}>
113
+ <div
114
+ className={styles.searchResults}
115
+ style={{
116
+ maxHeight: '22rem',
117
+ }}
118
+ >
119
+ <p className={styles.resultsText}>
120
+ {totalResults} {t('searchResultsText', 'search result(s)')}
121
+ </p>
122
+ <CompactGroupResults groups={results} selectGroupAction={selectGroupAction} lastRef={lastItem} />
123
+ <div ref={lastItem}>
124
+ <div className={styles.lastItem} ref={loadingRef}>
125
+ {hasMore && <Loading withOverlay={false} small />}
126
+ {!hasMore && <p>{t('noMoreResults', 'End of search results')}</p>}
127
+ </div>
128
+ </div>
129
+ </div>
130
+ </div>
131
+ );
132
+ };
133
+
134
+ export default GroupSearch;
@@ -0,0 +1,63 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .patientSearchResult {
6
+ text-decoration: none;
7
+ display: flex;
8
+ align-items: center;
9
+ border-bottom: 1px solid colors.$gray-20;
10
+ padding: 0 layout.$spacing-04;
11
+
12
+ &:hover,
13
+ &:focus {
14
+ background-color: colors.$gray-10;
15
+ }
16
+ }
17
+
18
+ .patientSearchResultSelected {
19
+ background-color: colors.$gray-20;
20
+ }
21
+
22
+ .patientBanner {
23
+ display: flex;
24
+ }
25
+
26
+ .patientName {
27
+ @include type.type-style('heading-02');
28
+ }
29
+
30
+ .patientAvatar {
31
+ width: layout.$spacing-09;
32
+ height: layout.$spacing-09;
33
+ margin: layout.$spacing-03 layout.$spacing-05 layout.$spacing-03 0;
34
+ border-radius: 1px;
35
+ }
36
+
37
+ .patientInfo {
38
+ width: 100%;
39
+ display: flex;
40
+ flex-flow: column wrap;
41
+ margin: layout.$spacing-05;
42
+ cursor: pointer;
43
+ }
44
+
45
+ .demographics {
46
+ margin-top: layout.$spacing-03;
47
+ @include type.type-style('body-compact-02');
48
+ color: colors.$gray-70;
49
+ }
50
+
51
+ .identifiers {
52
+ @include type.type-style('body-compact-02');
53
+ color: colors.$gray-50;
54
+ }
55
+
56
+ .actionsContainer {
57
+ padding-top: layout.$spacing-03;
58
+ margin-top: layout.$spacing-05;
59
+ }
60
+
61
+ .middot {
62
+ margin: 0 layout.$spacing-03;
63
+ }
@@ -0,0 +1,34 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .patientSearchBar {
6
+ width: 50vw;
7
+ position: relative;
8
+ }
9
+
10
+ .floatingSearchResultsContainer {
11
+ position: absolute;
12
+ overflow-y: auto;
13
+ box-shadow: 0 layout.$spacing-03 layout.$spacing-05 colors.$gray-20;
14
+ z-index: 99;
15
+ border: 0 1px 1px 1px solid colors.$gray-20;
16
+ width: 100%;
17
+ background-color: colors.$white-0;
18
+ }
19
+
20
+ .searchArea {
21
+ width: inherit;
22
+ display: flex;
23
+ justify-content: center;
24
+ align-items: center;
25
+ border: 1px solid colors.$gray-50;
26
+ }
27
+
28
+ .patientSearchInput {
29
+ border: none;
30
+ }
31
+
32
+ .patientSearchInput input:focus {
33
+ outline: 1px solid 1px solid colors.$orange-40;
34
+ }
@@ -0,0 +1,93 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .searchResultsContainer {
6
+ width: 100%;
7
+ background-color: colors.$white-0;
8
+
9
+ a {
10
+ text-decoration: none;
11
+ @include type.type-style('heading-02');
12
+ color: colors.$gray-70;
13
+ margin: 0rem;
14
+ }
15
+ }
16
+
17
+ :global(.omrs-breakpoint-lt-desktop) .searchResultsContainer {
18
+ top: 6.25rem;
19
+ }
20
+
21
+ :global(.omrs-breakpoint-gt-tablet) .searchResultsContainer {
22
+ padding: 0;
23
+ top: layout.$spacing-09;
24
+ }
25
+
26
+ .searchResults {
27
+ width: 100%;
28
+ }
29
+
30
+
31
+ .searchTerm {
32
+ @include type.type-style('heading-03');
33
+ margin-top: 0.375rem;
34
+ }
35
+
36
+ .resultsText {
37
+ @include type.type-style('label-01');
38
+ color: colors.$gray-70;
39
+ line-height: layout.$spacing-05;
40
+ margin: layout.$spacing-03 layout.$spacing-05;
41
+ }
42
+
43
+ .helperText {
44
+ color: var(--omrs-color-ink-medium-contrast);
45
+ margin-left: 2.375rem;
46
+ }
47
+
48
+ .emptyResultText {
49
+ @include type.type-style('heading-compact-01');
50
+ color: colors.$gray-70;
51
+ margin-top: layout.$spacing-05;
52
+ margin-bottom: 0.313rem;
53
+ }
54
+
55
+ .actionText {
56
+ @include type.type-style('body-01');
57
+ color: colors.$gray-70;
58
+ }
59
+
60
+ .pagination {
61
+ display: flex;
62
+ justify-content: space-evenly;
63
+ padding: 4.688rem 0 layout.$spacing-06;
64
+ }
65
+
66
+ .emptySearchResultsTile {
67
+ text-align: center;
68
+ margin-top: layout.$spacing-05;
69
+ padding: layout.$spacing-09 0rem;
70
+ }
71
+
72
+ :global(.omrs-breakpoint-gt-tablet) .emptySearchResultsTile {
73
+ margin: layout.$spacing-05;
74
+ }
75
+
76
+ .errorMessage {
77
+ @include type.type-style('heading-compact-02');
78
+ margin-top: 2.25rem;
79
+ margin-bottom: layout.$spacing-03;
80
+ }
81
+
82
+ .errorCopy {
83
+ margin-bottom: layout.$spacing-03;
84
+ @include type.type-style('body-01');
85
+ color: colors.$gray-70;
86
+ }
87
+
88
+ .lastItem {
89
+ padding: layout.$spacing-05;
90
+ display: flex;
91
+ justify-content: center;
92
+ align-items: center;
93
+ }
@@ -0,0 +1,72 @@
1
+ import { Close, Add } from '@carbon/react/icons';
2
+ import { Button } from '@carbon/react';
3
+ import React, { useCallback, useContext, useState } from 'react';
4
+ import GroupFormWorkflowContext from '../../context/GroupFormWorkflowContext';
5
+ import styles from './styles.scss';
6
+ import { useTranslation } from 'react-i18next';
7
+ import CompactGroupSearch from '../group-search/CompactGroupSearch';
8
+ import AddGroupModal from '../../add-group-modal/AddGroupModal';
9
+
10
+ const GroupSearchHeader = () => {
11
+ const { t } = useTranslation();
12
+ const { activeGroupUuid, setGroup, destroySession } = useContext(GroupFormWorkflowContext);
13
+ const [isOpen, setOpen] = useState(false);
14
+ const handleSelectGroup = (group) => {
15
+ group.cohortMembers.sort((a, b) => {
16
+ const aName = a?.patient?.person?.names?.[0]?.display;
17
+ const bName = b?.patient?.person?.names?.[0]?.display;
18
+ return aName.localeCompare(bName, undefined, { sensitivity: 'base' });
19
+ });
20
+ setGroup(group);
21
+ };
22
+
23
+ const handleCancel = useCallback(() => {
24
+ setOpen(false);
25
+ }, []);
26
+
27
+ const onPostSubmit = useCallback(() => {
28
+ setOpen(false);
29
+ }, []);
30
+
31
+ const handleOpenClick = useCallback(() => {
32
+ setOpen(true);
33
+ }, []);
34
+
35
+ if (activeGroupUuid) return null;
36
+
37
+ return (
38
+ <div className={styles.searchHeaderContainer}>
39
+ <span className={styles.padded}>{t('findGroup', 'Find group')}:</span>
40
+ <span className={styles.searchBarWrapper}>
41
+ <CompactGroupSearch selectGroupAction={handleSelectGroup} />
42
+ </span>
43
+ <span className={styles.padded}>{t('or', 'or')}</span>
44
+ <span>
45
+ <Button onClick={handleOpenClick} renderIcon={Add} iconDescription="Add">
46
+ {t('createNewGroup', 'Create New Group')}
47
+ </Button>
48
+ <AddGroupModal
49
+ {...{
50
+ isCreate: true,
51
+ isOpen: isOpen,
52
+ onPostCancel: handleCancel,
53
+ onPostSubmit: onPostSubmit,
54
+ }}
55
+ />
56
+ </span>
57
+ <span style={{ flexGrow: 1 }} />
58
+ <span>
59
+ <Button
60
+ kind="ghost"
61
+ onClick={() => {
62
+ destroySession();
63
+ }}
64
+ >
65
+ {t('cancel', 'Cancel')} <Close size={20} />
66
+ </Button>
67
+ </span>
68
+ </div>
69
+ );
70
+ };
71
+
72
+ export default GroupSearchHeader;
@@ -0,0 +1,3 @@
1
+ import GroupSearchHeader from './GroupSearchHeader';
2
+
3
+ export default GroupSearchHeader;
@@ -0,0 +1,20 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+
4
+ .searchHeaderContainer {
5
+ height: layout.$spacing-11;
6
+ display: flex;
7
+ align-items: center;
8
+ background-color: colors.$white-0;
9
+ border-top: 0.0125rem solid colors.$gray-20;
10
+ border-bottom: 0.0125rem solid colors.$gray-20;
11
+ padding: 0 layout.$spacing-05;
12
+ }
13
+
14
+ .searchBarWrapper {
15
+ min-width: 35rem;
16
+ }
17
+
18
+ .padded {
19
+ padding: layout.$spacing-05;
20
+ }
@@ -0,0 +1,3 @@
1
+ import GroupFormEntryWorkflow from './GroupFormEntryWorkflow';
2
+
3
+ export default GroupFormEntryWorkflow;
@@ -0,0 +1,94 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .breadcrumbsContainer > div > div > nav {
6
+ background-color: colors.$white-0;
7
+ padding: layout.$spacing-04 layout.$spacing-05;
8
+ height: layout.$spacing-08;
9
+ }
10
+
11
+ .workspaceWrapper {
12
+ display: flex;
13
+ justify-content: center;
14
+ }
15
+
16
+ .workspace {
17
+ width: 1100px;
18
+ }
19
+
20
+ :global(.omrs-breakpoint-lt-large-desktop) .workspace {
21
+ width: 1000px;
22
+ }
23
+
24
+ :global(.omrs-breakpoint-lt-small-desktop) .workspace {
25
+ // there's only so much we can do here. Currenlty the design does not support tablet
26
+ width: 100vw;
27
+ padding: 0 layout.$spacing-04;
28
+ }
29
+
30
+ .selectPatientMessage {
31
+ @include type.type-style('heading-03');
32
+ margin: layout.$spacing-07;
33
+ text-align: center;
34
+ }
35
+
36
+ .formMainContent {
37
+ display: flex;
38
+ text-align: center;
39
+ margin-top: layout.$spacing-05;
40
+ column-gap: layout.$spacing-05;
41
+ }
42
+
43
+ .formContainer {
44
+ flex-grow: 1;
45
+ max-height: calc(100vh - 14rem);
46
+ overflow-y: scroll;
47
+ text-align: left;
48
+ }
49
+
50
+ .formContainer :global(.cds--form-item) :global(.question-area) {
51
+ max-width: 100%;
52
+ }
53
+
54
+ .rightPanel {
55
+ min-width: 13rem;
56
+ text-align: left;
57
+ overflow-y: scroll;
58
+ display: flex;
59
+ flex-direction: column;
60
+ row-gap: layout.$spacing-05;
61
+ }
62
+
63
+ .patientCardsSection {
64
+ margin: layout.$spacing-05 0;
65
+ border-bottom: 1px solid colors.$gray-10;
66
+ }
67
+
68
+ .rightPanelActionButtons {
69
+ display: flex;
70
+ flex-direction: column;
71
+ row-gap: layout.$spacing-03;
72
+ & button {
73
+ width: 100%;
74
+ text-decoration: "none";
75
+ }
76
+ }
77
+
78
+ .formSection {
79
+ display: flex;
80
+ flex-direction: column;
81
+ row-gap: 1rem;
82
+ justify-content: flex-start;
83
+ align-items: flex-start;
84
+ text-align: left;
85
+ }
86
+
87
+ .formSectionTile {
88
+ width: 500px;
89
+ }
90
+
91
+ .formError {
92
+ @include type.type-style("helper-text-02");
93
+ color: colors.$red-60;
94
+ }
@@ -0,0 +1,8 @@
1
+ import useGetAllForms from './useGetAllForms';
2
+ import useGetPatient from './useGetPatient';
3
+ import useFormState from './useFormState';
4
+ import useGetEncounter from './useGetEncounter';
5
+ import useForm from './useForm';
6
+
7
+ export { useGetAllForms, useGetPatient, useFormState, useGetEncounter, useForm };
8
+ export * from './usePostEndpoint';
@@ -0,0 +1,56 @@
1
+ import { type FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
+ import useSWR from 'swr';
3
+ import { type SpecificQuestion, type SpecificQuestionConfig } from '../types';
4
+ import { useMemo } from 'react';
5
+
6
+ const formUrl = `${restBaseUrl}/o3/forms`;
7
+
8
+ export const useSpecificQuestions = (formUuid: string, specificQuestionConfig: Array<SpecificQuestionConfig>) => {
9
+ const specificQuestionsToLoad = useMemo(
10
+ () => getQuestionIdsByFormId(formUuid, specificQuestionConfig),
11
+ [formUuid, specificQuestionConfig],
12
+ );
13
+
14
+ const { data, error } = useSWR<FetchResponse, Error>(
15
+ specificQuestionsToLoad ? `${formUrl}/${formUuid}` : null,
16
+ openmrsFetch,
17
+ );
18
+
19
+ const specificQuestions = getQuestionsByIds(specificQuestionsToLoad, data?.data);
20
+
21
+ return {
22
+ questions: specificQuestions || null,
23
+ isError: error,
24
+ isLoading: !data && !error,
25
+ };
26
+ };
27
+
28
+ function getQuestionIdsByFormId(formUuid: string, specificQuestionConfig: Array<SpecificQuestionConfig>) {
29
+ const matchingQuestions = specificQuestionConfig.filter((question) => question.forms.includes(formUuid));
30
+ return matchingQuestions.map((question) => question.questionId);
31
+ }
32
+
33
+ function getQuestionsByIds(questionIds, formSchema): Array<SpecificQuestion> {
34
+ if (!formSchema || questionIds.lenght <= 0) {
35
+ return [];
36
+ }
37
+ const conceptLabels = formSchema.conceptReferences;
38
+ return formSchema.pages.flatMap((page) =>
39
+ page.sections.flatMap((section) =>
40
+ section.questions
41
+ .filter((question) => questionIds.includes(question.id))
42
+ .map((question) => ({
43
+ question: {
44
+ display: question.label ?? conceptLabels[question.questionOptions.concept]?.display,
45
+ id: question.id,
46
+ },
47
+ answers: (question.questionOptions.answers ?? []).map((answer) => ({
48
+ value: answer.concept,
49
+ display: answer.label ?? conceptLabels[answer.concept]?.display,
50
+ })),
51
+ })),
52
+ ),
53
+ );
54
+ }
55
+
56
+ export default useSpecificQuestions;
@@ -0,0 +1,23 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ const useFormState = (formUuid) => {
4
+ const [state, setState] = useState(null);
5
+
6
+ useEffect(() => {
7
+ const handler = (e) => {
8
+ if (e.detail?.formUuid === formUuid) {
9
+ setState(e.detail?.state);
10
+ }
11
+ };
12
+
13
+ window.addEventListener('ampath-form-state', handler);
14
+
15
+ return () => {
16
+ window.removeEventListener('ampath-form-state', handler);
17
+ };
18
+ }, [formUuid]);
19
+
20
+ return state;
21
+ };
22
+
23
+ export default useFormState;
@@ -0,0 +1,37 @@
1
+ import { openmrsFetch, userHasAccess, useSession, restBaseUrl } from '@openmrs/esm-framework';
2
+ import useSWR from 'swr';
3
+
4
+ const customFormRepresentation =
5
+ '(uuid,name,display,encounterType:(uuid,name,viewPrivilege,editPrivilege),version,published,retired,resources:(uuid,name,dataType,valueReference))';
6
+
7
+ const formEncounterUrl = `${restBaseUrl}/form?v=custom:${customFormRepresentation}`;
8
+ const formEncounterUrlPoc = `${restBaseUrl}/form?v=custom:${customFormRepresentation}&q=poc`;
9
+
10
+ export function useGetAllForms(cachedOfflineFormsOnly = false) {
11
+ const session = useSession();
12
+ const showHtmlFormEntryForms = true;
13
+ const url = showHtmlFormEntryForms ? formEncounterUrl : formEncounterUrlPoc;
14
+ const { data, error } = useSWR([url, cachedOfflineFormsOnly], async () => {
15
+ const res = await openmrsFetch(url);
16
+ // show published forms, and hide component forms, and filter based on privileges
17
+ const forms =
18
+ res.data?.results?.filter(
19
+ (form) =>
20
+ // forms should be published
21
+ form.published &&
22
+ // forms should not be component forms
23
+ !/component/i.test(form.name),
24
+ // user should have privileges to edit forms
25
+ ) ?? [];
26
+
27
+ return forms;
28
+ });
29
+
30
+ return {
31
+ forms: data?.filter((form) => Boolean(userHasAccess(form.encounterType?.editPrivilege?.display, session?.user))),
32
+ isLoading: !error && !data,
33
+ error,
34
+ };
35
+ }
36
+
37
+ export default useGetAllForms;
@@ -0,0 +1,21 @@
1
+ import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
+ import useSWR from 'swr';
3
+
4
+ const encounterUrl = `${restBaseUrl}/encounter/`;
5
+
6
+ const useGetEncounter = (encounterUuid) => {
7
+ const url = `${encounterUrl}${encounterUuid}`;
8
+ const { data, error } = useSWR(url, async () => {
9
+ const res = await openmrsFetch(url);
10
+ const encounter = res.data || null;
11
+ return encounter;
12
+ });
13
+
14
+ return {
15
+ encounter: data,
16
+ isLoading: !error && data,
17
+ error,
18
+ };
19
+ };
20
+
21
+ export default useGetEncounter;
@@ -0,0 +1,23 @@
1
+ import { fetchCurrentPatient } from '@openmrs/esm-framework';
2
+ import { useEffect, useState } from 'react';
3
+
4
+ const useGetPatient = (patientUuid) => {
5
+ const [patient, setPatient] = useState(null);
6
+
7
+ useEffect(() => {
8
+ if (!patientUuid) {
9
+ setPatient(null);
10
+ } else {
11
+ getPatient(patientUuid);
12
+ }
13
+ }, [patientUuid]);
14
+
15
+ const getPatient = async (uuid) => {
16
+ const result = await fetchCurrentPatient(uuid);
17
+ setPatient(result);
18
+ };
19
+
20
+ return patient;
21
+ };
22
+
23
+ export default useGetPatient;