@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,179 @@
1
+ import React, { useContext, useEffect, useState } from 'react';
2
+ import { detach, ExtensionSlot } from '@openmrs/esm-framework';
3
+ import useGetPatient from './hooks/useGetPatient';
4
+ import GroupFormWorkflowContext from './context/GroupFormWorkflowContext';
5
+
6
+ export interface Order {
7
+ uuid: string;
8
+ dateActivated: string;
9
+ dose: number;
10
+ doseUnits: {
11
+ uuid: string;
12
+ display: string;
13
+ };
14
+ orderNumber: number;
15
+ display: string;
16
+ drug: {
17
+ uuid: string;
18
+ name: string;
19
+ strength: string;
20
+ };
21
+ duration: number;
22
+ durationUnits: {
23
+ uuid: string;
24
+ display: string;
25
+ };
26
+ frequency: {
27
+ uuid: string;
28
+ display: string;
29
+ };
30
+ numRefills: number;
31
+ orderer: {
32
+ uuid: string;
33
+ person: {
34
+ uuid: string;
35
+ display: string;
36
+ };
37
+ };
38
+ orderType: {
39
+ uuid: string;
40
+ display: string;
41
+ };
42
+ route: {
43
+ uuid: string;
44
+ display: string;
45
+ };
46
+ auditInfo: {
47
+ dateVoided: string;
48
+ };
49
+ }
50
+
51
+ export interface Observation {
52
+ uuid: string;
53
+ concept: {
54
+ uuid: string;
55
+ display: string;
56
+ conceptClass: {
57
+ uuid: string;
58
+ display: string;
59
+ };
60
+ };
61
+ display: string;
62
+ groupMembers: null | Array<{
63
+ uuid: string;
64
+ concept: {
65
+ uuid: string;
66
+ display: string;
67
+ };
68
+ value: {
69
+ uuid: string;
70
+ display: string;
71
+ };
72
+ }>;
73
+ value: unknown;
74
+ obsDatetime: string;
75
+ }
76
+
77
+ export interface Encounter {
78
+ uuid: string;
79
+ encounterDatetime: string;
80
+ encounterProviders: Array<{
81
+ uuid: string;
82
+ display: string;
83
+ encounterRole: {
84
+ uuid: string;
85
+ display: string;
86
+ };
87
+ provider: {
88
+ uuid: string;
89
+ person: {
90
+ uuid: string;
91
+ display: string;
92
+ };
93
+ };
94
+ }>;
95
+ encounterType: {
96
+ uuid: string;
97
+ display: string;
98
+ };
99
+ obs: Array<Observation>;
100
+ orders: Array<Order>;
101
+ }
102
+
103
+ type PreFilledQuestions = {
104
+ [key: string]: string;
105
+ };
106
+
107
+ interface FormParams {
108
+ formUuid: string;
109
+ patientUuid: string;
110
+ visitUuid?: string;
111
+ visitTypeUuid?: string;
112
+ encounterUuid?: string;
113
+ preFilledQuestions?: PreFilledQuestions;
114
+ showDiscardSubmitButtons?: boolean;
115
+ handlePostResponse?: (Encounter) => void;
116
+ handleEncounterCreate?: (Object) => void;
117
+ handleOnValidate?: (boolean) => void;
118
+ }
119
+
120
+ const FormBootstrap = ({
121
+ formUuid,
122
+ patientUuid,
123
+ visitUuid,
124
+ visitTypeUuid,
125
+ encounterUuid,
126
+ handlePostResponse,
127
+ handleEncounterCreate,
128
+ handleOnValidate,
129
+ }: FormParams) => {
130
+ const patient = useGetPatient(patientUuid);
131
+ const { activeSessionMeta } = useContext(GroupFormWorkflowContext);
132
+
133
+ useEffect(() => {
134
+ return () => detach('form-widget-slot', 'form-widget-slot');
135
+ });
136
+
137
+ // FIXME This should not be necessary
138
+ const [showForm, setShowForm] = useState(true);
139
+
140
+ const updateFormComponent = () => {
141
+ setShowForm(false);
142
+ setTimeout(() => {
143
+ setShowForm(true);
144
+ });
145
+ };
146
+
147
+ return (
148
+ <div>
149
+ {showForm && formUuid && patientUuid && patient && (
150
+ <ExtensionSlot
151
+ name="form-widget-slot"
152
+ state={{
153
+ view: 'form',
154
+ formUuid,
155
+ visitUuid: visitUuid ?? '',
156
+ visitTypeUuid: visitTypeUuid ?? '',
157
+ patientUuid,
158
+ patient,
159
+ encounterUuid: encounterUuid ?? '',
160
+ closeWorkspace: () => undefined,
161
+ handlePostResponse: (encounter) => {
162
+ handlePostResponse(encounter);
163
+ updateFormComponent();
164
+ },
165
+ handleEncounterCreate,
166
+ handleOnValidate,
167
+ showDiscardSubmitButtons: false,
168
+ preFilledQuestions: {
169
+ ...activeSessionMeta,
170
+ encDate: activeSessionMeta.sessionDate,
171
+ },
172
+ }}
173
+ />
174
+ )}
175
+ </div>
176
+ );
177
+ };
178
+
179
+ export default FormBootstrap;
package/src/Root.tsx CHANGED
@@ -1,13 +1,19 @@
1
- import React from "react";
2
- import { BrowserRouter, Route } from "react-router-dom";
3
- import { appPath } from "./constant";
4
- import FormsRoot from "./forms/FormsRoot";
1
+ import React from 'react';
2
+ import { BrowserRouter, Route, Routes } from 'react-router-dom';
3
+ import { appPath } from './constant';
4
+ const FormsPage = React.lazy(() => import('./forms-page'));
5
+ const FormEntryWorkflow = React.lazy(() => import('./form-entry-workflow'));
6
+ const GroupFormEntryWorkflow = React.lazy(() => import('./group-form-entry-workflow'));
5
7
 
6
8
  const Root = () => {
7
9
  return (
8
10
  <main>
9
11
  <BrowserRouter basename={appPath}>
10
- <Route path="/:tab?" component={FormsRoot} />
12
+ <Routes>
13
+ <Route path="/" element={<FormsPage />} />
14
+ <Route path="/form/:formUuid" element={<FormEntryWorkflow />} />
15
+ <Route path="/groupform/:formUuid" element={<GroupFormEntryWorkflow />} />
16
+ </Routes>
11
17
  </BrowserRouter>
12
18
  </main>
13
19
  );
@@ -0,0 +1,249 @@
1
+ import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
2
+ import { ComposedModal, Button, ModalHeader, ModalFooter, ModalBody, TextInput, FormLabel } from '@carbon/react';
3
+ import { TrashCan } from '@carbon/react/icons';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { ExtensionSlot, fetchCurrentPatient, showToast, useConfig, usePatient } from '@openmrs/esm-framework';
6
+ import styles from './styles.scss';
7
+ import GroupFormWorkflowContext from '../context/GroupFormWorkflowContext';
8
+ import { usePostCohort } from '../hooks';
9
+
10
+ const MemExtension = React.memo(ExtensionSlot);
11
+
12
+ const PatientRow = ({ patient, removePatient }) => {
13
+ const { t } = useTranslation();
14
+ const { patient: patientInfo, error, isLoading } = usePatient(patient?.uuid);
15
+ const onClickHandler = useCallback(() => removePatient(patient?.uuid), [patient, removePatient]);
16
+
17
+ const patientDisplay = useMemo(() => {
18
+ if (isLoading || error || !patientInfo) return '';
19
+
20
+ const { identifier, name } = patientInfo;
21
+ const displayIdentifier = identifier?.[0]?.value || '';
22
+ const givenNames = `${(name?.[0]?.given || []).join(' ')} ${name?.[0]?.family || ''}`;
23
+
24
+ return `${displayIdentifier ? `${displayIdentifier} -` : ''}${givenNames ? ` ${givenNames}` : ''}`.trim();
25
+ }, [isLoading, error, patientInfo]);
26
+
27
+ return (
28
+ <li className={styles.patientRow}>
29
+ <span>
30
+ <Button
31
+ kind="tertiary"
32
+ size="sm"
33
+ hasIconOnly
34
+ onClick={onClickHandler}
35
+ renderIcon={TrashCan}
36
+ tooltipAlignment="start"
37
+ tooltipPosition="top"
38
+ iconDescription={t('remove', 'Remove')}
39
+ />
40
+ </span>
41
+ <span className={styles.patientName}>{patientDisplay}</span>
42
+ </li>
43
+ );
44
+ };
45
+
46
+ const NewGroupForm = (props) => {
47
+ const { name, setName, patientList, updatePatientList, errors, validate, removePatient } = props;
48
+ const { t } = useTranslation();
49
+
50
+ return (
51
+ <div
52
+ style={{
53
+ display: 'flex',
54
+ flexDirection: 'column',
55
+ rowGap: '1rem',
56
+ }}
57
+ >
58
+ <TextInput
59
+ labelText={t('newGroupName', 'New Group Name')}
60
+ value={name}
61
+ onChange={(e) => setName(e.target.value)}
62
+ onBlur={() => validate('name')}
63
+ />
64
+ {errors?.name && (
65
+ <p className={styles.formError}>
66
+ {errors.name === 'required' ? t('groupNameError', 'Please enter a group name.') : errors.name}
67
+ </p>
68
+ )}
69
+ <FormLabel>
70
+ {patientList.length} {t('patientsInGroup', 'Patients in group')}
71
+ </FormLabel>
72
+ {errors?.patientList && (
73
+ <p className={styles.formError}>{t('noPatientError', 'Please enter at least one patient.')}</p>
74
+ )}
75
+ {!errors?.patientList && (
76
+ <ul className={styles.patientList}>
77
+ {patientList?.map((patient, index) => (
78
+ <PatientRow patient={patient} removePatient={removePatient} key={patient.uuid} />
79
+ ))}
80
+ </ul>
81
+ )}
82
+
83
+ <FormLabel>{t('searchForPatientsToAddToGroup', 'Search for patients to add to group')}</FormLabel>
84
+ <div className={styles.searchBar}>
85
+ <MemExtension
86
+ extensionSlotName="patient-search-bar-slot"
87
+ state={{
88
+ selectPatientAction: updatePatientList,
89
+ buttonProps: {
90
+ kind: 'secondary',
91
+ },
92
+ }}
93
+ />
94
+ </div>
95
+ </div>
96
+ );
97
+ };
98
+
99
+ const AddGroupModal = ({
100
+ patients = undefined,
101
+ isCreate = undefined,
102
+ groupName = '',
103
+ cohortUuid = undefined,
104
+ isOpen,
105
+ onPostCancel,
106
+ onPostSubmit,
107
+ }) => {
108
+ const { setGroup } = useContext(GroupFormWorkflowContext);
109
+ const { t } = useTranslation();
110
+ const [errors, setErrors] = useState({});
111
+ const [name, setName] = useState(groupName);
112
+ const [patientList, setPatientList] = useState(patients || []);
113
+ const { post, result, error } = usePostCohort();
114
+ const config = useConfig();
115
+
116
+ const removePatient = useCallback(
117
+ (patientUuid: string) =>
118
+ setPatientList((patientList) => patientList.filter((patient) => patient.uuid !== patientUuid)),
119
+ [setPatientList],
120
+ );
121
+
122
+ const validate = useCallback(
123
+ (field?: string | undefined) => {
124
+ let valid = true;
125
+ if (field) {
126
+ valid = field === 'name' ? !!name : !!patientList.length;
127
+ setErrors((errors) => ({
128
+ ...errors,
129
+ [field]: valid ? null : 'required',
130
+ }));
131
+ } else {
132
+ if (!name) {
133
+ setErrors((errors) => ({ ...errors, name: 'required' }));
134
+ valid = false;
135
+ } else {
136
+ setErrors((errors) => ({ ...errors, name: null }));
137
+ }
138
+ if (!patientList.length) {
139
+ setErrors((errors) => ({ ...errors, patientList: 'required' }));
140
+ valid = false;
141
+ } else {
142
+ setErrors((errors) => ({ ...errors, patientList: null }));
143
+ }
144
+ }
145
+ return valid;
146
+ },
147
+ [name, patientList.length],
148
+ );
149
+
150
+ const updatePatientList = useCallback(
151
+ (patientUuid) => {
152
+ function getPatientName(patient) {
153
+ return [patient?.name?.[0]?.given, patient?.name?.[0]?.family].join(' ');
154
+ }
155
+ if (!patientList.find((p) => p.uuid === patientUuid)) {
156
+ fetchCurrentPatient(patientUuid).then((result) => {
157
+ const newPatient = { uuid: patientUuid, ...result };
158
+ setPatientList(
159
+ [...patientList, newPatient].sort((a, b) =>
160
+ getPatientName(a).localeCompare(getPatientName(b), undefined, {
161
+ sensitivity: 'base',
162
+ }),
163
+ ),
164
+ );
165
+ });
166
+ }
167
+ setErrors((errors) => ({ ...errors, patientList: null }));
168
+ },
169
+ [patientList, setPatientList],
170
+ );
171
+
172
+ const handleSubmit = () => {
173
+ if (validate()) {
174
+ post({
175
+ uuid: cohortUuid,
176
+ name: name,
177
+ cohortType: config?.groupSessionConcepts?.cohortTypeId,
178
+ cohortMembers: patientList.map((p) => ({ patient: p.uuid })),
179
+ });
180
+ if (onPostSubmit) {
181
+ onPostSubmit();
182
+ }
183
+ }
184
+ };
185
+
186
+ const handleCancel = () => {
187
+ setPatientList(patients || []);
188
+ if (onPostCancel) {
189
+ onPostCancel();
190
+ }
191
+ };
192
+
193
+ useEffect(() => {
194
+ if (result) {
195
+ setGroup({
196
+ ...result,
197
+ // the result doesn't come with cohortMembers.
198
+ // need to add it in based on our local state
199
+ cohortMembers: patientList.map((p) => ({ patient: { uuid: p.uuid } })),
200
+ });
201
+ }
202
+ }, [result, setGroup, patientList]);
203
+
204
+ useEffect(() => {
205
+ if (error) {
206
+ showToast({
207
+ kind: 'error',
208
+ title: t('postError', 'POST Error'),
209
+ description: error.message ?? t('unknownPostError', 'An unknown error occurred while saving data'),
210
+ });
211
+ if (error.fieldErrors) {
212
+ setErrors(
213
+ Object.fromEntries(Object.entries(error.fieldErrors).map(([key, value]) => [key, value?.[0]?.message])),
214
+ );
215
+ }
216
+ }
217
+ }, [error, t]);
218
+
219
+ return (
220
+ <div className={styles.modal}>
221
+ <ComposedModal open={isOpen} onClose={handleCancel}>
222
+ <ModalHeader>{isCreate ? t('createNewGroup', 'Create New Group') : t('editGroup', 'Edit Group')}</ModalHeader>
223
+ <ModalBody>
224
+ <NewGroupForm
225
+ {...{
226
+ name,
227
+ setName,
228
+ patientList,
229
+ updatePatientList,
230
+ errors,
231
+ validate,
232
+ removePatient,
233
+ }}
234
+ />
235
+ </ModalBody>
236
+ <ModalFooter>
237
+ <Button kind="secondary" onClick={handleCancel}>
238
+ {t('cancel', 'Cancel')}
239
+ </Button>
240
+ <Button kind="primary" onClick={handleSubmit}>
241
+ {isCreate ? t('createGroup', 'Create Group') : t('save', 'Save')}
242
+ </Button>
243
+ </ModalFooter>
244
+ </ComposedModal>
245
+ </div>
246
+ );
247
+ };
248
+
249
+ export default AddGroupModal;
@@ -0,0 +1,49 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+
4
+ .modal {
5
+ :global(.cds--modal) {
6
+ z-index: 90;
7
+ }
8
+ :global(.cds--modal-container) {
9
+ height: 600px;
10
+ }
11
+ :global(.cds--modal-content) {
12
+ height: 100%;
13
+ }
14
+ }
15
+
16
+ .searchBar > div > div > div {
17
+ width: 100%
18
+ }
19
+
20
+ .searchBar > div button {
21
+ height: auto;
22
+ }
23
+
24
+ .formError {
25
+ color: red;
26
+ }
27
+
28
+ .patientRow {
29
+ display: flex;
30
+ align-items: center;
31
+ width: "100%";
32
+ &:nth-child(odd) {
33
+ background-color: colors.$gray-20;
34
+ }
35
+ }
36
+
37
+ .patientName {
38
+ flex-grow: 1;
39
+ padding-left: layout.$spacing-05;
40
+ }
41
+
42
+ .loading {
43
+ display: flex;
44
+ height: 100%;
45
+ flex-direction: column;
46
+ justify-content: center;
47
+ align-items: center;
48
+ row-gap: layout.$spacing-05;
49
+ }
@@ -1,46 +1,139 @@
1
- import { Type, validator } from "@openmrs/esm-framework";
1
+ import { Type } from '@openmrs/esm-framework';
2
2
 
3
3
  /**
4
- * This is the config schema. It expects a configuration object which
5
- * looks like this:
4
+ * This is the config schema.
6
5
  *
7
- * ```json
8
- * { "casualGreeting": true, "whoToGreet": ["Mom"] }
9
- * ```
10
- *
11
- * In OpenMRS Microfrontends, all config parameters are optional. Thus,
12
- * all elements must have a reasonable default. A good default is one
13
- * that works well with the reference application.
14
- *
15
- * To understand the schema below, please read the configuration system
16
- * documentation:
17
- * https://openmrs.github.io/openmrs-esm-core/#/main/config
18
- * Note especially the section "How do I make my module configurable?"
19
- * https://openmrs.github.io/openmrs-esm-core/#/main/config?id=im-developing-an-esm-module-how-do-i-make-it-configurable
20
- * and the Schema Reference
21
- * https://openmrs.github.io/openmrs-esm-core/#/main/config?id=schema-reference
22
6
  */
7
+
23
8
  export const configSchema = {
24
- casualGreeting: {
25
- _type: Type.Boolean,
26
- _default: false,
27
- _description: "Whether to use a casual greeting (or a formal one).",
9
+ formCategories: {
10
+ _type: Type.Array,
11
+ _description: 'Organize forms into categories. A form can belong to multiple categories.',
12
+ _elements: {
13
+ name: {
14
+ _type: Type.String,
15
+ _description: 'Category name',
16
+ },
17
+ forms: {
18
+ _type: Type.Array,
19
+ _description: 'List of forms for this category.',
20
+ _elements: {
21
+ formUUID: {
22
+ _type: Type.UUID,
23
+ _description: 'UUID of form',
24
+ },
25
+ name: {
26
+ _type: Type.String,
27
+ _description: 'Name of form',
28
+ },
29
+ },
30
+ },
31
+ },
32
+ _default: [
33
+ {
34
+ name: 'ICRC Forms',
35
+ forms: [
36
+ {
37
+ formUUID: '0cefb866-110c-4f16-af58-560932a1db1f',
38
+ name: 'Adult Triage',
39
+ },
40
+ ],
41
+ },
42
+ {
43
+ name: 'Distress Scales',
44
+ forms: [
45
+ {
46
+ formUUID: '9f26aad4-244a-46ca-be49-1196df1a8c9a',
47
+ name: 'POC Sample Form 1',
48
+ },
49
+ ],
50
+ },
51
+ ],
28
52
  },
29
- whoToGreet: {
53
+ formCategoriesToShow: {
30
54
  _type: Type.Array,
31
- _default: ["World"],
32
- _description:
33
- "Who should be greeted. Names will be separated by a comma and space.",
55
+ _description: 'Forms to show by default on the forms app home page.',
34
56
  _elements: {
35
57
  _type: Type.String,
58
+ _description: 'Name of category',
36
59
  },
37
- _validators: [
38
- validator((v) => v.length > 0, "At least one person must be greeted."),
39
- ],
60
+ _default: ['ICRC Forms', 'Distress Scales'],
40
61
  },
62
+ groupSessionConcepts: {
63
+ sessionName: {
64
+ _type: Type.UUID,
65
+ _description: 'UUID of concept for Session Name',
66
+ _default: 'e2559620-900b-4f66-ae41-0b9c4adfb654',
67
+ },
68
+ sessionDate: {
69
+ _type: Type.UUID,
70
+ _description: 'UUID of concept for Session Date',
71
+ _default: 'ceaca505-6dff-4940-8a43-8c060a0924d7',
72
+ },
73
+ practitionerName: {
74
+ _type: Type.UUID,
75
+ _description: 'UUID of concept for Practitioner Name',
76
+ _default: 'f1a2d58c-1a0e-4148-931a-aac224649fdc',
77
+ },
78
+ sessionNotes: {
79
+ _type: Type.UUID,
80
+ _description: 'UUID of concept for Session Notes',
81
+ _default: 'fa8fedc0-c066-4da3-8dc1-2ad8621fc480',
82
+ },
83
+ cohortTypeId: {
84
+ _type: Type.UUID,
85
+ _description: 'UUID of cohort type',
86
+ _default: 'eee9970e-7ca0-4e8c-a280-c33e9d5f6a04',
87
+ },
88
+ cohortId: {
89
+ _type: Type.UUID,
90
+ _description: 'UUID of concept for cohort identifier',
91
+ _default: '5461f231-7e59-4be8-93a4-6d49fd13c00a',
92
+ },
93
+ cohortName: {
94
+ _type: Type.UUID,
95
+ _description: 'UUID of concept for cohort name',
96
+ _default: '6029f289-92a6-4a68-80f1-3078d0152449',
97
+ },
98
+ sessionUuid: {
99
+ _type: Type.UUID,
100
+ _description: 'UUID of concept for session identifier',
101
+ _default: '6a803908-8a5b-4598-adea-19358c83529a',
102
+ },
103
+ },
104
+ specificQuestions: {
105
+ _type: Type.Array,
106
+ _description: 'List of specific questions to populate forms.',
107
+ _elements: {
108
+ forms: {
109
+ _type: Type.Array,
110
+ _description: 'List of form UUIDs for which the question applies.',
111
+ _elements: {
112
+ _type: Type.UUID,
113
+ },
114
+ _default: [],
115
+ },
116
+ questionId: {
117
+ _type: Type.String,
118
+ _description: 'ID of the question.',
119
+ _default: '',
120
+ },
121
+ },
122
+ _default: [],
123
+ },
124
+ };
125
+
126
+ export type Form = {
127
+ formUUID: Type.UUID;
128
+ name: Type.String;
129
+ };
130
+
131
+ export type Category = {
132
+ name: string;
133
+ forms: Array<Form>;
41
134
  };
42
135
 
43
136
  export type Config = {
44
- casualGreeting: boolean;
45
- whoToGreet: Array<String>;
137
+ formCategories: Array<Category>;
138
+ formCategoriesToShow: Array<string>;
46
139
  };
package/src/constant.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export const routes = {
2
- allForms: "all-forms",
2
+ allForms: 'all-forms',
3
3
  };
4
4
 
5
5
  export const spaBase = window.spaBase;