@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,106 @@
1
+ import { useConfig, useSession } from '@openmrs/esm-framework';
2
+ import { Tab, Tabs, TabList, TabPanels, TabPanel } from '@carbon/react';
3
+ import React from 'react';
4
+ import { type Config } from '../config-schema';
5
+ import { useGetAllForms } from '../hooks';
6
+ import FormsTable from './forms-table';
7
+ import styles from './styles.scss';
8
+ import { useTranslation } from 'react-i18next';
9
+ import { fdeWorkflowStorageName, fdeWorkflowStorageVersion } from '../context/FormWorkflowReducer';
10
+ import { fdeGroupWorkflowStorageName, fdeGroupWorkflowStorageVersion } from '../context/GroupFormWorkflowReducer';
11
+
12
+ // helper function useful for debugging
13
+ // given a list of forms, it will organize into permissions
14
+ // and list which forms are associated with that permission
15
+ export const getFormPermissions = (forms) => {
16
+ const output = {};
17
+ forms?.forEach(
18
+ (form) =>
19
+ (output[form.encounterType.editPrivilege.display] = [
20
+ ...(output[form.encounterType.editPrivilege.display] || []),
21
+ form.display,
22
+ ]),
23
+ );
24
+ return output;
25
+ };
26
+
27
+ // Function adds `id` field to rows so they will be accepted by DataTable
28
+ // "display" is prefered for display name if present, otherwise fall back on "name'"
29
+ const prepareRowsForTable = (rawFormData) => {
30
+ if (rawFormData) {
31
+ return rawFormData?.map((form) => ({
32
+ ...form,
33
+ id: form.uuid,
34
+ display: form.display || form.name,
35
+ }));
36
+ }
37
+ return null;
38
+ };
39
+
40
+ const FormsPage = () => {
41
+ const config = useConfig();
42
+ const { t } = useTranslation();
43
+ const { formCategories, formCategoriesToShow } = config;
44
+ const { forms, isLoading, error } = useGetAllForms();
45
+ const cleanRows = prepareRowsForTable(forms);
46
+ const { user } = useSession();
47
+ const savedFormsData = localStorage.getItem(fdeWorkflowStorageName + ':' + user?.uuid);
48
+ const savedGroupFormsData = localStorage.getItem(fdeGroupWorkflowStorageName + ':' + user?.uuid);
49
+ const activeForms = [];
50
+ const activeGroupForms = [];
51
+
52
+ if (savedFormsData && JSON.parse(savedFormsData)?.['_storageVersion'] === fdeWorkflowStorageVersion) {
53
+ Object.entries(JSON.parse(savedFormsData).forms).forEach(
54
+ ([formUuid, form]: [string, { [key: string]: unknown }]) => {
55
+ if (form.workflowState) activeForms.push(formUuid);
56
+ },
57
+ );
58
+ }
59
+ if (savedGroupFormsData && JSON.parse(savedGroupFormsData)?.['_storageVersion'] === fdeGroupWorkflowStorageVersion) {
60
+ Object.entries(JSON.parse(savedGroupFormsData).forms).forEach(
61
+ ([formUuid, form]: [string, { [key: string]: unknown }]) => {
62
+ if (form.workflowState) activeGroupForms.push(formUuid);
63
+ },
64
+ );
65
+ }
66
+
67
+ const categoryRows = formCategoriesToShow.map((name) => {
68
+ const category = formCategories.find((category) => category.name === name);
69
+ let rows = [];
70
+ if (category && cleanRows && cleanRows.length) {
71
+ const uuids = category.forms?.map((form) => form.formUUID);
72
+ rows = cleanRows.filter((row) => uuids.includes(row.uuid));
73
+ }
74
+ return { ...{ name, rows } };
75
+ });
76
+
77
+ return (
78
+ <div className={styles.mainContent}>
79
+ <h3 className={styles.pageTitle}>{t('fastDataEntry', 'Fast Data Entry')}</h3>
80
+ <Tabs>
81
+ <TabList>
82
+ <Tab label={t('allForms', 'All Forms')}>
83
+ {`${t('allForms', 'All Forms')} (${cleanRows ? cleanRows?.length : '??'})`}
84
+ </Tab>
85
+ {categoryRows?.map((category, index) => (
86
+ <Tab label={category.name} key={index}>
87
+ {`${category.name} (${category.rows.length})`}
88
+ </Tab>
89
+ ))}
90
+ </TabList>
91
+ <TabPanels>
92
+ <TabPanel>
93
+ <FormsTable rows={cleanRows} {...{ error, isLoading, activeForms, activeGroupForms }} />
94
+ </TabPanel>
95
+ {categoryRows?.map((category, index) => (
96
+ <TabPanel key={index}>
97
+ <FormsTable rows={category.rows} {...{ error, isLoading, activeForms, activeGroupForms }} />
98
+ </TabPanel>
99
+ ))}
100
+ </TabPanels>
101
+ </Tabs>
102
+ </div>
103
+ );
104
+ };
105
+
106
+ export default FormsPage;
@@ -0,0 +1,117 @@
1
+ import { ErrorState } from '@openmrs/esm-framework';
2
+ import {
3
+ DataTable,
4
+ DataTableSkeleton,
5
+ Table,
6
+ TableBody,
7
+ TableCell,
8
+ TableContainer,
9
+ TableHead,
10
+ TableHeader,
11
+ TableRow,
12
+ TableToolbar,
13
+ TableToolbarContent,
14
+ TableToolbarSearch,
15
+ } from '@carbon/react';
16
+ import React from 'react';
17
+ import { useTranslation } from 'react-i18next';
18
+ import { Link } from 'react-router-dom';
19
+ import EmptyState from '../../empty-state/EmptyState';
20
+ import styles from './styles.scss';
21
+
22
+ const FormsTable = ({ rows, error, isLoading, activeForms, activeGroupForms }) => {
23
+ const { t } = useTranslation();
24
+
25
+ const tableHeaders = [
26
+ {
27
+ key: 'display',
28
+ header: t('formName', 'Form Name'),
29
+ isSortable: true,
30
+ },
31
+ {
32
+ key: 'actions',
33
+ header: t('actions', 'Actions'),
34
+ },
35
+ {
36
+ key: 'actions2',
37
+ header: '',
38
+ },
39
+ ];
40
+
41
+ const augmentedRows = rows?.map((row) => ({
42
+ ...row,
43
+ actions: (
44
+ <Link to={`form/${row.uuid}`}>
45
+ {activeForms.includes(row.uuid) ? t('resumeSession', 'Resume Session') : t('fillForm', 'Fill Form')}
46
+ </Link>
47
+ ),
48
+ actions2: (
49
+ <Link to={`groupform/${row.uuid}`}>
50
+ {activeGroupForms.includes(row.uuid)
51
+ ? t('resumeGroupSession', 'Resume Group Session')
52
+ : t('startGroupSession', 'Start Group Session')}
53
+ </Link>
54
+ ),
55
+ }));
56
+
57
+ if (isLoading) return <DataTableSkeleton />;
58
+ if (error) {
59
+ return <ErrorState headerTitle={t('errorLoadingData', 'Error Loading Data')} error={error} />;
60
+ }
61
+ if (augmentedRows.length === 0) {
62
+ return (
63
+ <EmptyState
64
+ headerTitle={t('noFormsFound', 'No Forms To Show')}
65
+ displayText={t(
66
+ 'noFormsFoundMessage',
67
+ 'No forms could be found for this category. Please double check the form concept uuids and access permissions.',
68
+ )}
69
+ />
70
+ );
71
+ }
72
+ return (
73
+ <DataTable rows={augmentedRows} headers={tableHeaders}>
74
+ {({ rows, headers, getTableProps, getHeaderProps, getRowProps, onInputChange }) => {
75
+ return (
76
+ <TableContainer>
77
+ <div className={styles.toolbarWrapper}>
78
+ <TableToolbar className={styles.tableToolbar}>
79
+ <TableToolbarContent>
80
+ <TableToolbarSearch onChange={onInputChange} />
81
+ </TableToolbarContent>
82
+ </TableToolbar>
83
+ </div>
84
+ <Table {...getTableProps()}>
85
+ <TableHead>
86
+ <TableRow>
87
+ {headers.map((header) => (
88
+ <TableHeader
89
+ {...getHeaderProps({
90
+ header,
91
+ isSortable: header.isSortable,
92
+ })}
93
+ >
94
+ {header.header}
95
+ </TableHeader>
96
+ ))}
97
+ </TableRow>
98
+ </TableHead>
99
+ <TableBody>
100
+ {rows?.map((row) => (
101
+ <TableRow {...getRowProps({ row })}>
102
+ {row.cells.map((cell) => (
103
+ <TableCell key={cell.id}>{cell.value}</TableCell>
104
+ ))}
105
+ </TableRow>
106
+ ))}
107
+ </TableBody>
108
+ </Table>
109
+ </TableContainer>
110
+ );
111
+ }}
112
+ </DataTable>
113
+ );
114
+ };
115
+
116
+ export default FormsTable;
117
+ export { FormsTable };
@@ -0,0 +1,3 @@
1
+ import FormsTable from './FormsTable';
2
+
3
+ export default FormsTable;
@@ -0,0 +1,19 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+
4
+ .toolbarWrapper {
5
+ position: relative;
6
+ display: flex;
7
+ height: layout.$spacing-09;
8
+ justify-content: flex-end;
9
+ }
10
+
11
+ .tableToolbar {
12
+ width: 20%;
13
+ min-width: 12.5rem;
14
+ }
15
+
16
+ .inactiveLink {
17
+ color: colors.$gray-40;
18
+ cursor: not-allowed;
19
+ }
@@ -0,0 +1,3 @@
1
+ import FormsPage from './FormsPage';
2
+
3
+ export default FormsPage;
@@ -0,0 +1,9 @@
1
+ @use '@carbon/layout';
2
+
3
+ .mainContent {
4
+ padding: layout.$spacing-07;
5
+ }
6
+
7
+ .pageTitle {
8
+ margin-bottom: layout.$spacing-06;
9
+ }
@@ -0,0 +1,26 @@
1
+ import { ExtensionSlot } from '@openmrs/esm-framework';
2
+ import React from 'react';
3
+ import GroupDisplayHeader from './group-display-header';
4
+ import styles from './styles.scss';
5
+ import { GroupFormWorkflowProvider } from '../context/GroupFormWorkflowContext';
6
+ import GroupSearchHeader from './group-search-header';
7
+ import SessionMetaWorkspace from './SessionMetaWorkspace';
8
+ import GroupSessionWorkspace from './GroupSessionWorkspace';
9
+
10
+ const GroupFormEntryWorkflow = () => {
11
+ return (
12
+ <GroupFormWorkflowProvider>
13
+ <div className={styles.breadcrumbsContainer}>
14
+ <ExtensionSlot name="breadcrumbs-slot" />
15
+ </div>
16
+ <GroupSearchHeader />
17
+ <GroupDisplayHeader />
18
+ <div className={styles.workspaceWrapper}>
19
+ <SessionMetaWorkspace />
20
+ <GroupSessionWorkspace />
21
+ </div>
22
+ </GroupFormWorkflowProvider>
23
+ );
24
+ };
25
+
26
+ export default GroupFormEntryWorkflow;
@@ -0,0 +1,207 @@
1
+ import { getGlobalStore, useConfig, useSession, useStore } from '@openmrs/esm-framework';
2
+ import { Button } from '@carbon/react';
3
+ import React, { useCallback, useContext, useEffect, useState } from 'react';
4
+ import PatientCard from '../patient-card/PatientCard';
5
+ import styles from './styles.scss';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { v4 as uuid } from 'uuid';
8
+ import GroupFormWorkflowContext from '../context/GroupFormWorkflowContext';
9
+ import FormBootstrap from '../FormBootstrap';
10
+ import CompleteModal from '../CompleteModal';
11
+ import CancelModal from '../CancelModal';
12
+
13
+ const formStore = getGlobalStore('ampath-form-state');
14
+
15
+ const WorkflowNavigationButtons = () => {
16
+ const context = useContext(GroupFormWorkflowContext);
17
+ const { activeFormUuid, submitForNext, patientUuids, activePatientUuid, workflowState } = context;
18
+ const store = useStore(formStore);
19
+ const formState = store[activeFormUuid];
20
+ const navigationDisabled =
21
+ (formState !== 'ready' || workflowState !== 'EDIT_FORM') && formState !== 'readyWithValidationErrors';
22
+ const [cancelModalOpen, setCancelModalOpen] = useState(false);
23
+ const [completeModalOpen, setCompleteModalOpen] = useState(false);
24
+ const { t } = useTranslation();
25
+
26
+ const isLastPatient = activePatientUuid === patientUuids[patientUuids.length - 1];
27
+
28
+ const handleClickNext = () => {
29
+ if (workflowState === 'EDIT_FORM' || formState === 'readyWithValidationErrors') {
30
+ submitForNext();
31
+ }
32
+ };
33
+
34
+ return (
35
+ <>
36
+ <div className={styles.rightPanelActionButtons}>
37
+ <Button kind="primary" onClick={handleClickNext} disabled={navigationDisabled}>
38
+ {isLastPatient ? t('saveForm', 'Save Form') : t('nextPatient', 'Next patient')}
39
+ </Button>
40
+ <Button kind="secondary" onClick={() => setCompleteModalOpen(true)}>
41
+ {t('saveAndComplete', 'Save & Complete')}
42
+ </Button>
43
+ <Button kind="tertiary" onClick={() => setCancelModalOpen(true)}>
44
+ {t('cancel', 'Cancel')}
45
+ </Button>
46
+ </div>
47
+ <CancelModal open={cancelModalOpen} setOpen={setCancelModalOpen} context={context} />
48
+ <CompleteModal open={completeModalOpen} setOpen={setCompleteModalOpen} context={context} validateFirst={false} />
49
+ </>
50
+ );
51
+ };
52
+
53
+ const GroupSessionWorkspace = () => {
54
+ const { groupSessionConcepts } = useConfig();
55
+ const { t } = useTranslation();
56
+ const {
57
+ patientUuids,
58
+ activePatientUuid,
59
+ encounters,
60
+ activeEncounterUuid,
61
+ activeVisitUuid,
62
+ activeFormUuid,
63
+ activeGroupUuid,
64
+ activeGroupName,
65
+ activeSessionUuid,
66
+ saveEncounter,
67
+ activeSessionMeta,
68
+ groupVisitTypeUuid,
69
+ updateVisitUuid,
70
+ submitForNext,
71
+ workflowState,
72
+ } = useContext(GroupFormWorkflowContext);
73
+
74
+ const { sessionLocation } = useSession();
75
+
76
+ useEffect(() => {
77
+ if (activeVisitUuid) {
78
+ updateVisitUuid(activeVisitUuid);
79
+ }
80
+ }, [updateVisitUuid, activeVisitUuid, activePatientUuid]);
81
+
82
+ // If there's no active visit, trigger the creation of a new one
83
+ const handleEncounterCreate = useCallback(
84
+ (payload) => {
85
+ // Create a visit with the same date as the encounter being saved
86
+ const obsTime = new Date(activeSessionMeta.sessionDate);
87
+ payload.obs.forEach((item, index) => {
88
+ payload.obs[index] = {
89
+ ...item,
90
+ groupMembers: item.groupMembers?.map((mem) => ({
91
+ ...mem,
92
+ obsDatetime: obsTime.toISOString(),
93
+ })),
94
+ obsDatetime: obsTime.toISOString(),
95
+ };
96
+ });
97
+ const visitUuid = activeVisitUuid ? activeVisitUuid : uuid();
98
+ if (!activeVisitUuid) {
99
+ Object.entries(groupSessionConcepts).forEach(([field, uuid]) => {
100
+ if (activeSessionMeta?.[field] != null) {
101
+ payload.obs.push({
102
+ concept: uuid,
103
+ value: activeSessionMeta[field],
104
+ });
105
+ }
106
+ });
107
+
108
+ const otherIdentifiers = [
109
+ { concept: groupSessionConcepts.cohortId, value: activeGroupUuid },
110
+ { concept: groupSessionConcepts.cohortName, value: activeGroupName },
111
+ {
112
+ concept: groupSessionConcepts.sessionUuid,
113
+ value: activeSessionUuid,
114
+ },
115
+ ];
116
+ payload.obs.push(...otherIdentifiers);
117
+ // If this is a newly created encounter and visit, add session concepts to encounter payload.
118
+ const visitInfo = {
119
+ startDatetime: activeSessionMeta.sessionDate,
120
+ stopDatetime: activeSessionMeta.sessionDate,
121
+ uuid: visitUuid,
122
+ patient: {
123
+ uuid: activePatientUuid,
124
+ },
125
+ location: {
126
+ uuid: sessionLocation?.uuid,
127
+ },
128
+ visitType: {
129
+ uuid: groupVisitTypeUuid,
130
+ },
131
+ };
132
+ payload.visit = visitInfo;
133
+ updateVisitUuid(visitUuid);
134
+ }
135
+ payload.location = sessionLocation?.uuid;
136
+ payload.encounterDatetime = obsTime.toISOString();
137
+ },
138
+ [
139
+ activeSessionMeta,
140
+ activeVisitUuid,
141
+ sessionLocation?.uuid,
142
+ groupSessionConcepts,
143
+ activeGroupUuid,
144
+ activeGroupName,
145
+ activeSessionUuid,
146
+ activePatientUuid,
147
+ groupVisitTypeUuid,
148
+ updateVisitUuid,
149
+ ],
150
+ );
151
+
152
+ // Once form has been posted, save the new encounter uuid so we can edit it later
153
+ const handlePostResponse = useCallback(
154
+ (encounter) => {
155
+ if (encounter && encounter.uuid) {
156
+ saveEncounter(encounter.uuid);
157
+ }
158
+ },
159
+ [saveEncounter],
160
+ );
161
+
162
+ const switchPatient = useCallback(
163
+ (patientUuid) => {
164
+ submitForNext(patientUuid);
165
+ },
166
+ [submitForNext],
167
+ );
168
+
169
+ if (workflowState === 'NEW_GROUP_SESSION') return null;
170
+
171
+ return (
172
+ <div className={styles.workspace}>
173
+ <div className={styles.formMainContent}>
174
+ <div className={styles.formContainer}>
175
+ <FormBootstrap
176
+ patientUuid={activePatientUuid}
177
+ encounterUuid={activeEncounterUuid}
178
+ {...{
179
+ formUuid: activeFormUuid,
180
+ handlePostResponse,
181
+ handleEncounterCreate,
182
+ }}
183
+ />
184
+ </div>
185
+ <div className={styles.rightPanel}>
186
+ <h4>{t('formsFilled', 'Forms filled')}</h4>
187
+ <div className={styles.patientCardsSection}>
188
+ {patientUuids?.map((patientUuid) => (
189
+ <PatientCard
190
+ key={patientUuid}
191
+ {...{
192
+ patientUuid,
193
+ activePatientUuid,
194
+ editEncounter: switchPatient,
195
+ encounters,
196
+ }}
197
+ />
198
+ ))}
199
+ </div>
200
+ <WorkflowNavigationButtons />
201
+ </div>
202
+ </div>
203
+ </div>
204
+ );
205
+ };
206
+
207
+ export default GroupSessionWorkspace;
@@ -0,0 +1,154 @@
1
+ import { Layer, Tile, TextInput, TextArea, DatePicker, DatePickerInput } from '@carbon/react';
2
+ import React, { useContext } from 'react';
3
+ import { useConfig } from '@openmrs/esm-framework';
4
+ import { useParams } from 'react-router-dom';
5
+ import styles from './styles.scss';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Controller, useFormContext } from 'react-hook-form';
8
+ import { AttendanceTable } from './attendance-table';
9
+ import GroupFormWorkflowContext from '../context/GroupFormWorkflowContext';
10
+ import useGetPatients from '../hooks/useGetPatients';
11
+ import ConfigurableQuestionsSection from './configurable-questions/ConfigurableQuestionsSection';
12
+ import useSpecificQuestions from '../hooks/useForm';
13
+
14
+ interface ParamTypes {
15
+ formUuid?: string;
16
+ }
17
+
18
+ const SessionDetailsForm = () => {
19
+ const { specificQuestions } = useConfig();
20
+ const { formUuid } = useParams() as ParamTypes;
21
+ const { questions } = useSpecificQuestions(formUuid, specificQuestions);
22
+
23
+ const { t } = useTranslation();
24
+ const {
25
+ register,
26
+ formState: { errors },
27
+ control,
28
+ } = useFormContext();
29
+
30
+ const { activeGroupMembers } = useContext(GroupFormWorkflowContext);
31
+ const { patients, isLoading } = useGetPatients(activeGroupMembers);
32
+
33
+ return (
34
+ <div>
35
+ {!isLoading && (
36
+ <div className={styles.formSection}>
37
+ <h4>{t('sessionDetails', '1. Session details')}</h4>
38
+ <div>
39
+ <p>{t('allFieldsRequired', 'All fields are required unless marked optional')}</p>
40
+ </div>
41
+ <Layer>
42
+ <Tile className={styles.formSectionTile}>
43
+ <Layer>
44
+ <div
45
+ style={{
46
+ display: 'flex',
47
+ flexDirection: 'column',
48
+ rowGap: '1.5rem',
49
+ }}
50
+ >
51
+ <TextInput
52
+ id="text"
53
+ type="text"
54
+ labelText={t('sessionName', 'Session Name')}
55
+ {...register('sessionName', { required: true })}
56
+ invalid={errors.sessionName}
57
+ invalidText={t('requiredField', 'This field is required')}
58
+ />
59
+ <TextInput
60
+ id="text"
61
+ type="text"
62
+ labelText={t('practitionerName', 'Practitioner Name')}
63
+ {...register('practitionerName', { required: true })}
64
+ invalid={errors.practitionerName}
65
+ invalidText={t('requiredField', 'This field is required')}
66
+ />
67
+ <Controller
68
+ name="sessionDate"
69
+ control={control}
70
+ rules={{ required: true }}
71
+ render={({ field }) => (
72
+ <DatePicker datePickerType="single" size="md" maxDate={new Date()} {...field}>
73
+ <DatePickerInput
74
+ id="session-date"
75
+ labelText={t('sessionDate', 'Session Date')}
76
+ placeholder="mm/dd/yyyy"
77
+ size="md"
78
+ invalid={errors.sessionDate}
79
+ invalidText={t('requiredField', 'This field is required')}
80
+ />
81
+ </DatePicker>
82
+ )}
83
+ />
84
+ <TextArea
85
+ id="text"
86
+ type="text"
87
+ labelText={t('sessionNotes', 'Session Notes')}
88
+ {...register('sessionNotes', { required: true })}
89
+ invalid={errors.sessionNotes}
90
+ invalidText={t('requiredField', 'This field is required')}
91
+ />
92
+ </div>
93
+ </Layer>
94
+ </Tile>
95
+ </Layer>
96
+ <h4>{t('sessionParticipants', '2. Session participants')}</h4>
97
+ <div>
98
+ <p>
99
+ {t(
100
+ 'markAbsentPatients',
101
+ 'The patients in this group. Patients that are not present in the session should be marked as absent.',
102
+ )}
103
+ </p>
104
+ </div>
105
+ <Layer>
106
+ <Tile className={styles.formSectionTile}>
107
+ <Layer>
108
+ <div
109
+ style={{
110
+ display: 'flex',
111
+ flexDirection: 'column',
112
+ rowGap: '1.5rem',
113
+ }}
114
+ >
115
+ <AttendanceTable patients={patients} />
116
+ </div>
117
+ </Layer>
118
+ </Tile>
119
+ </Layer>
120
+ {questions?.length > 0 ? (
121
+ <>
122
+ <h4>{t('sessionSpecificDetails', '3. Specific details')}</h4>
123
+ <div>
124
+ <p>
125
+ {t(
126
+ 'sessionSpecificDetailsDescription',
127
+ 'They will be mapped to form responses for all patients as pre-filled data.',
128
+ )}
129
+ </p>
130
+ </div>
131
+ <Layer>
132
+ <Tile className={styles.formSectionTile}>
133
+ <Layer>
134
+ <div
135
+ style={{
136
+ display: 'flex',
137
+ flexDirection: 'column',
138
+ rowGap: '1.5rem',
139
+ }}
140
+ >
141
+ <ConfigurableQuestionsSection register={register} specificQuestions={questions} />
142
+ </div>
143
+ </Layer>
144
+ </Tile>
145
+ </Layer>
146
+ </>
147
+ ) : null}
148
+ </div>
149
+ )}
150
+ </div>
151
+ );
152
+ };
153
+
154
+ export default SessionDetailsForm;