@openmrs/esm-fast-data-entry-app 1.0.0-pre.9 → 1.0.1-pre.101

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 (186) hide show
  1. package/README.md +58 -12
  2. package/dist/153.js +1 -0
  3. package/dist/153.js.map +1 -0
  4. package/dist/233.js +2 -0
  5. package/dist/{382.js.LICENSE.txt → 233.js.LICENSE.txt} +3 -2
  6. package/dist/233.js.map +1 -0
  7. package/dist/262.js +1 -0
  8. package/dist/262.js.map +1 -0
  9. package/dist/279.js +1 -0
  10. package/dist/279.js.map +1 -0
  11. package/dist/294.js +1 -2
  12. package/dist/294.js.LICENSE.txt +2 -7
  13. package/dist/294.js.map +1 -1
  14. package/dist/327.js +1 -0
  15. package/dist/327.js.map +1 -0
  16. package/dist/409.js +2 -0
  17. package/dist/409.js.LICENSE.txt +27 -0
  18. package/dist/409.js.map +1 -0
  19. package/dist/415.js +1 -2
  20. package/dist/415.js.map +1 -1
  21. package/dist/559.js +1 -0
  22. package/dist/559.js.map +1 -0
  23. package/dist/574.js +1 -1
  24. package/dist/651.js +1 -0
  25. package/dist/651.js.map +1 -0
  26. package/dist/706.js +1 -0
  27. package/dist/706.js.map +1 -0
  28. package/dist/757.js +1 -0
  29. package/dist/800.js +2 -0
  30. package/dist/800.js.LICENSE.txt +5 -0
  31. package/dist/800.js.map +1 -0
  32. package/dist/820.js +1 -0
  33. package/dist/820.js.map +1 -0
  34. package/dist/883.js +1 -0
  35. package/dist/883.js.map +1 -0
  36. package/dist/889.js +1 -0
  37. package/dist/889.js.map +1 -0
  38. package/dist/897.js +2 -0
  39. package/dist/897.js.LICENSE.txt +21 -0
  40. package/dist/897.js.map +1 -0
  41. package/dist/92.js +1 -0
  42. package/dist/92.js.map +1 -0
  43. package/dist/935.js +2 -0
  44. package/dist/{735.js.LICENSE.txt → 935.js.LICENSE.txt} +6 -16
  45. package/dist/935.js.map +1 -0
  46. package/dist/959.js +1 -0
  47. package/dist/959.js.map +1 -0
  48. package/dist/main.js +1 -0
  49. package/dist/main.js.map +1 -0
  50. package/dist/openmrs-esm-fast-data-entry-app.js +1 -1
  51. package/dist/openmrs-esm-fast-data-entry-app.js.buildmanifest.json +374 -89
  52. package/dist/openmrs-esm-fast-data-entry-app.js.map +1 -1
  53. package/dist/routes.json +1 -0
  54. package/docs/config-icrc-forms.png +0 -0
  55. package/docs/config-other-forms.png +0 -0
  56. package/docs/configuring-form-categories.md +77 -0
  57. package/docs/fde-workflow.mov +0 -0
  58. package/docs/form-workflow-state-diagram.png +0 -0
  59. package/jest.config.json +21 -18
  60. package/package.json +100 -106
  61. package/src/CancelModal.tsx +48 -0
  62. package/src/CompleteModal.tsx +46 -0
  63. package/src/FormBootstrap.tsx +166 -0
  64. package/src/Root.tsx +14 -3
  65. package/src/add-group-modal/AddGroupModal.tsx +288 -0
  66. package/src/add-group-modal/styles.scss +45 -0
  67. package/src/config-schema.ts +85 -31
  68. package/src/context/FormWorkflowContext.tsx +126 -0
  69. package/src/context/FormWorkflowReducer.ts +287 -0
  70. package/src/context/GroupFormWorkflowContext.tsx +176 -0
  71. package/src/context/GroupFormWorkflowReducer.ts +430 -0
  72. package/src/empty-state/EmptyDataIllustration.tsx +51 -0
  73. package/src/empty-state/EmptyState.tsx +33 -0
  74. package/src/empty-state/styles.scss +55 -0
  75. package/src/form-entry-workflow/FormEntryWorkflow.tsx +196 -0
  76. package/src/form-entry-workflow/form-review-card/FormReviewCard.tsx +50 -0
  77. package/src/form-entry-workflow/form-review-card/index.ts +3 -0
  78. package/src/form-entry-workflow/form-review-card/styles.scss +39 -0
  79. package/src/form-entry-workflow/index.ts +3 -0
  80. package/src/form-entry-workflow/patient-banner/PatientBanner.test.tsx +9 -0
  81. package/src/form-entry-workflow/patient-banner/PatientBanner.tsx +86 -0
  82. package/src/form-entry-workflow/patient-banner/index.ts +3 -0
  83. package/src/form-entry-workflow/patient-banner/styles.scss +45 -0
  84. package/src/form-entry-workflow/patient-search-header/PatientSearchHeader.tsx +63 -0
  85. package/src/form-entry-workflow/patient-search-header/index.ts +3 -0
  86. package/src/form-entry-workflow/patient-search-header/styles.scss +22 -0
  87. package/src/form-entry-workflow/styles.scss +65 -0
  88. package/src/form-entry-workflow/workflow-review/WorkflowReview.tsx +35 -0
  89. package/src/form-entry-workflow/workflow-review/index.ts +3 -0
  90. package/src/form-entry-workflow/workflow-review/styles.scss +34 -0
  91. package/src/forms-app-menu-link.tsx +3 -2
  92. package/src/forms-page/FormsPage.tsx +134 -0
  93. package/src/forms-page/forms-table/FormsTable.tsx +137 -0
  94. package/src/forms-page/forms-table/index.ts +3 -0
  95. package/src/forms-page/forms-table/styles.scss +20 -0
  96. package/src/forms-page/index.ts +3 -0
  97. package/src/forms-page/styles.scss +11 -0
  98. package/src/group-form-entry-workflow/GroupFormEntryWorkflow.tsx +26 -0
  99. package/src/group-form-entry-workflow/GroupSessionWorkspace.tsx +247 -0
  100. package/src/group-form-entry-workflow/SessionDetailsForm.tsx +131 -0
  101. package/src/group-form-entry-workflow/SessionMetaWorkspace.tsx +107 -0
  102. package/src/group-form-entry-workflow/attendance-table/AttendanceTable.tsx +144 -0
  103. package/src/group-form-entry-workflow/attendance-table/index.ts +1 -0
  104. package/src/group-form-entry-workflow/group-display-header/GroupDisplayHeader.test.tsx +9 -0
  105. package/src/group-form-entry-workflow/group-display-header/GroupDisplayHeader.tsx +63 -0
  106. package/src/group-form-entry-workflow/group-display-header/index.ts +3 -0
  107. package/src/group-form-entry-workflow/group-display-header/styles.scss +60 -0
  108. package/src/group-form-entry-workflow/group-search/CompactGroupResults.tsx +139 -0
  109. package/src/group-form-entry-workflow/group-search/CompactGroupSearch.tsx +68 -0
  110. package/src/group-form-entry-workflow/group-search/GroupSearch.tsx +150 -0
  111. package/src/group-form-entry-workflow/group-search/compact-group-result.scss +64 -0
  112. package/src/group-form-entry-workflow/group-search/compact-group-search.scss +35 -0
  113. package/src/group-form-entry-workflow/group-search/group-search.scss +96 -0
  114. package/src/group-form-entry-workflow/group-search-header/GroupSearchHeader.tsx +73 -0
  115. package/src/group-form-entry-workflow/group-search-header/index.ts +3 -0
  116. package/src/group-form-entry-workflow/group-search-header/styles.scss +20 -0
  117. package/src/group-form-entry-workflow/index.ts +3 -0
  118. package/src/group-form-entry-workflow/styles.scss +97 -0
  119. package/src/hooks/index.ts +7 -0
  120. package/src/hooks/useFormState.ts +23 -0
  121. package/src/hooks/useGetAllForms.ts +45 -0
  122. package/src/hooks/useGetEncounter.ts +21 -0
  123. package/src/hooks/useGetPatient.ts +23 -0
  124. package/src/hooks/useGetPatients.ts +34 -0
  125. package/src/hooks/useGetSystemSetting.ts +38 -0
  126. package/src/hooks/useKeyPress.ts +31 -0
  127. package/src/hooks/usePostEndpoint.ts +76 -0
  128. package/src/hooks/useSearchEndpoint.ts +120 -0
  129. package/src/hooks/useStartVisit.ts +92 -0
  130. package/src/index.ts +26 -62
  131. package/src/patient-card/PatientCard.tsx +67 -0
  132. package/src/patient-card/index.ts +3 -0
  133. package/src/patient-card/styles.scss +46 -0
  134. package/src/routes.json +24 -0
  135. package/tools/i18next-parser.config.js +93 -0
  136. package/translations/en.json +69 -4
  137. package/translations/fr.json +50 -0
  138. package/tsconfig.json +26 -23
  139. package/.editorconfig +0 -12
  140. package/.eslintignore +0 -2
  141. package/.eslintrc +0 -4
  142. package/.github/workflows/node.js.yml +0 -79
  143. package/.husky/pre-commit +0 -6
  144. package/.husky/pre-push +0 -6
  145. package/.prettierignore +0 -14
  146. package/dist/24.js +0 -3
  147. package/dist/24.js.LICENSE.txt +0 -16
  148. package/dist/24.js.map +0 -1
  149. package/dist/296.js +0 -2
  150. package/dist/296.js.map +0 -1
  151. package/dist/299.js +0 -2
  152. package/dist/299.js.map +0 -1
  153. package/dist/382.js +0 -3
  154. package/dist/382.js.map +0 -1
  155. package/dist/595.js +0 -3
  156. package/dist/595.js.LICENSE.txt +0 -1
  157. package/dist/595.js.map +0 -1
  158. package/dist/69.js +0 -2
  159. package/dist/69.js.map +0 -1
  160. package/dist/735.js +0 -3
  161. package/dist/735.js.map +0 -1
  162. package/dist/777.js +0 -2
  163. package/dist/777.js.map +0 -1
  164. package/dist/860.js +0 -2
  165. package/dist/860.js.map +0 -1
  166. package/dist/906.js +0 -2
  167. package/dist/906.js.map +0 -1
  168. package/dist/openmrs-esm-fast-data-entry-app.old +0 -2
  169. package/src/boxes/extensions/blue-box.tsx +0 -15
  170. package/src/boxes/extensions/box.scss +0 -23
  171. package/src/boxes/extensions/brand-box.tsx +0 -15
  172. package/src/boxes/extensions/red-box.tsx +0 -15
  173. package/src/boxes/slot/boxes.css +0 -23
  174. package/src/boxes/slot/boxes.tsx +0 -19
  175. package/src/forms/FormsRoot.tsx +0 -32
  176. package/src/forms/FormsTable.tsx +0 -64
  177. package/src/forms/mockData.ts +0 -43
  178. package/src/greeter/greeter.css +0 -4
  179. package/src/greeter/greeter.test.tsx +0 -29
  180. package/src/greeter/greeter.tsx +0 -25
  181. package/src/hello.css +0 -3
  182. package/src/hello.test.tsx +0 -45
  183. package/src/hello.tsx +0 -30
  184. package/src/patient-getter/patient-getter.resource.ts +0 -31
  185. package/src/patient-getter/patient-getter.test.tsx +0 -28
  186. package/src/patient-getter/patient-getter.tsx +0 -28
@@ -0,0 +1,134 @@
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 { 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 {
10
+ fdeWorkflowStorageName,
11
+ fdeWorkflowStorageVersion,
12
+ } from "../context/FormWorkflowReducer";
13
+ import {
14
+ fdeGroupWorkflowStorageName,
15
+ fdeGroupWorkflowStorageVersion,
16
+ } from "../context/GroupFormWorkflowReducer";
17
+
18
+ // helper function useful for debugging
19
+ // given a list of forms, it will organize into permissions
20
+ // and list which forms are associated with that permission
21
+ export const getFormPermissions = (forms) => {
22
+ const output = {};
23
+ forms?.forEach(
24
+ (form) =>
25
+ (output[form.encounterType.editPrivilege.display] = [
26
+ ...(output[form.encounterType.editPrivilege.display] || []),
27
+ form.display,
28
+ ])
29
+ );
30
+ return output;
31
+ };
32
+
33
+ // Function adds `id` field to rows so they will be accepted by DataTable
34
+ // "display" is prefered for display name if present, otherwise fall back on "name'"
35
+ const prepareRowsForTable = (rawFormData) => {
36
+ if (rawFormData) {
37
+ return rawFormData?.map((form) => ({
38
+ ...form,
39
+ id: form.uuid,
40
+ display: form.display || form.name,
41
+ }));
42
+ }
43
+ return null;
44
+ };
45
+
46
+ const FormsPage = () => {
47
+ const config = useConfig() as Config;
48
+ const { t } = useTranslation();
49
+ const { formCategories, formCategoriesToShow } = config;
50
+ const { forms, isLoading, error } = useGetAllForms();
51
+ const cleanRows = prepareRowsForTable(forms);
52
+ const { user } = useSession();
53
+ const savedFormsData = localStorage.getItem(
54
+ fdeWorkflowStorageName + ":" + user?.uuid
55
+ );
56
+ const savedGroupFormsData = localStorage.getItem(
57
+ fdeGroupWorkflowStorageName + ":" + user?.uuid
58
+ );
59
+ const activeForms = [];
60
+ const activeGroupForms = [];
61
+
62
+ if (
63
+ savedFormsData &&
64
+ JSON.parse(savedFormsData)?.["_storageVersion"] ===
65
+ fdeWorkflowStorageVersion
66
+ ) {
67
+ Object.entries(JSON.parse(savedFormsData).forms).forEach(
68
+ ([formUuid, form]: [string, { [key: string]: unknown }]) => {
69
+ if (form.workflowState) activeForms.push(formUuid);
70
+ }
71
+ );
72
+ }
73
+ if (
74
+ savedGroupFormsData &&
75
+ JSON.parse(savedGroupFormsData)?.["_storageVersion"] ===
76
+ fdeGroupWorkflowStorageVersion
77
+ ) {
78
+ Object.entries(JSON.parse(savedGroupFormsData).forms).forEach(
79
+ ([formUuid, form]: [string, { [key: string]: unknown }]) => {
80
+ if (form.workflowState) activeGroupForms.push(formUuid);
81
+ }
82
+ );
83
+ }
84
+
85
+ const categoryRows = formCategoriesToShow.map((name) => {
86
+ const category = formCategories.find((category) => category.name === name);
87
+ let rows = [];
88
+ if (category && cleanRows && cleanRows.length) {
89
+ const uuids = category.forms?.map((form) => form.formUUID);
90
+ rows = cleanRows.filter((row) => uuids.includes(row.uuid));
91
+ }
92
+ return { ...{ name, rows } };
93
+ });
94
+
95
+ return (
96
+ <div className={styles.mainContent}>
97
+ <h3 className={styles.pageTitle}>
98
+ {t("fastDataEntry", "Fast Data Entry")}
99
+ </h3>
100
+ <Tabs>
101
+ <TabList>
102
+ <Tab label={t("allForms", "All Forms")}>
103
+ {`${t("allForms", "All Forms")} (${
104
+ cleanRows ? cleanRows?.length : "??"
105
+ })`}
106
+ </Tab>
107
+ {categoryRows?.map((category, index) => (
108
+ <Tab label={category.name} key={index}>
109
+ {`${category.name} (${category.rows.length})`}
110
+ </Tab>
111
+ ))}
112
+ </TabList>
113
+ <TabPanels>
114
+ <TabPanel>
115
+ <FormsTable
116
+ rows={cleanRows}
117
+ {...{ error, isLoading, activeForms, activeGroupForms }}
118
+ />
119
+ </TabPanel>
120
+ {categoryRows?.map((category, index) => (
121
+ <TabPanel key={index}>
122
+ <FormsTable
123
+ rows={category.rows}
124
+ {...{ error, isLoading, activeForms, activeGroupForms }}
125
+ />
126
+ </TabPanel>
127
+ ))}
128
+ </TabPanels>
129
+ </Tabs>
130
+ </div>
131
+ );
132
+ };
133
+
134
+ export default FormsPage;
@@ -0,0 +1,137 @@
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 = ({
23
+ rows,
24
+ error,
25
+ isLoading,
26
+ activeForms,
27
+ activeGroupForms,
28
+ }) => {
29
+ const { t } = useTranslation();
30
+
31
+ const tableHeaders = [
32
+ {
33
+ key: "display",
34
+ header: t("formName", "Form Name"),
35
+ isSortable: true,
36
+ },
37
+ {
38
+ key: "actions",
39
+ header: t("actions", "Actions"),
40
+ },
41
+ {
42
+ key: "actions2",
43
+ header: "",
44
+ },
45
+ ];
46
+
47
+ const augmentedRows = rows?.map((row) => ({
48
+ ...row,
49
+ actions: (
50
+ <Link to={`form/${row.uuid}`}>
51
+ {activeForms.includes(row.uuid)
52
+ ? t("resumeSession", "Resume Session")
53
+ : t("fillForm", "Fill Form")}
54
+ </Link>
55
+ ),
56
+ actions2: (
57
+ <Link to={`groupform/${row.uuid}`}>
58
+ {activeGroupForms.includes(row.uuid)
59
+ ? t("resumeGroupSession", "Resume Group Session")
60
+ : t("startGroupSession", "Start Group Session")}
61
+ </Link>
62
+ ),
63
+ }));
64
+
65
+ if (isLoading) return <DataTableSkeleton />;
66
+ if (error) {
67
+ return (
68
+ <ErrorState
69
+ headerTitle={t("errorLoadingData", "Error Loading Data")}
70
+ error={error}
71
+ />
72
+ );
73
+ }
74
+ if (augmentedRows.length === 0) {
75
+ return (
76
+ <EmptyState
77
+ headerTitle={t("noFormsFound", "No Forms To Show")}
78
+ displayText={t(
79
+ "noFormsFoundMessage",
80
+ "No forms could be found for this category. Please double check the form concept uuids and access permissions."
81
+ )}
82
+ />
83
+ );
84
+ }
85
+ return (
86
+ <DataTable rows={augmentedRows} headers={tableHeaders}>
87
+ {({
88
+ rows,
89
+ headers,
90
+ getTableProps,
91
+ getHeaderProps,
92
+ getRowProps,
93
+ onInputChange,
94
+ }) => {
95
+ return (
96
+ <TableContainer>
97
+ <div className={styles.toolbarWrapper}>
98
+ <TableToolbar className={styles.tableToolbar}>
99
+ <TableToolbarContent>
100
+ <TableToolbarSearch onChange={onInputChange} />
101
+ </TableToolbarContent>
102
+ </TableToolbar>
103
+ </div>
104
+ <Table {...getTableProps()}>
105
+ <TableHead>
106
+ <TableRow>
107
+ {headers.map((header) => (
108
+ <TableHeader
109
+ {...getHeaderProps({
110
+ header,
111
+ isSortable: header.isSortable,
112
+ })}
113
+ >
114
+ {header.header}
115
+ </TableHeader>
116
+ ))}
117
+ </TableRow>
118
+ </TableHead>
119
+ <TableBody>
120
+ {rows?.map((row) => (
121
+ <TableRow {...getRowProps({ row })}>
122
+ {row.cells.map((cell) => (
123
+ <TableCell key={cell.id}>{cell.value}</TableCell>
124
+ ))}
125
+ </TableRow>
126
+ ))}
127
+ </TableBody>
128
+ </Table>
129
+ </TableContainer>
130
+ );
131
+ }}
132
+ </DataTable>
133
+ );
134
+ };
135
+
136
+ export default FormsTable;
137
+ export { FormsTable };
@@ -0,0 +1,3 @@
1
+ import FormsTable from "./FormsTable";
2
+
3
+ export default FormsTable;
@@ -0,0 +1,20 @@
1
+ @import "~@openmrs/esm-styleguide/src/vars";
2
+ @import "~carbon-components/src/globals/scss/vars";
3
+ @import "~carbon-components/src/globals/scss/mixins";
4
+
5
+ .toolbarWrapper {
6
+ position: relative;
7
+ display: flex;
8
+ height: $spacing-09;
9
+ justify-content: flex-end;
10
+ }
11
+
12
+ .tableToolbar {
13
+ width: 20%;
14
+ min-width: 12.5rem;
15
+ }
16
+
17
+ .inactiveLink {
18
+ color: $carbon--gray-40;
19
+ cursor: not-allowed;
20
+ }
@@ -0,0 +1,3 @@
1
+ import FormsPage from "./FormsPage";
2
+
3
+ export default FormsPage;
@@ -0,0 +1,11 @@
1
+ @use '@carbon/styles/scss/spacing';
2
+ // @use '@carbon/styles/scss/type';
3
+ // @import '~@openmrs/esm-styleguide/src/vars';
4
+
5
+ .mainContent {
6
+ padding: spacing.$spacing-07;
7
+ }
8
+
9
+ .pageTitle {
10
+ margin-bottom: spacing.$spacing-06;
11
+ }
@@ -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 extensionSlotName="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,247 @@
1
+ import {
2
+ getGlobalStore,
3
+ useConfig,
4
+ useSession,
5
+ useStore,
6
+ } from "@openmrs/esm-framework";
7
+ import { Button } from "@carbon/react";
8
+ import React, { useCallback, useContext, useEffect, useState } from "react";
9
+ import PatientCard from "../patient-card/PatientCard";
10
+ import styles from "./styles.scss";
11
+ import { useTranslation } from "react-i18next";
12
+ import GroupFormWorkflowContext from "../context/GroupFormWorkflowContext";
13
+ import FormBootstrap from "../FormBootstrap";
14
+ import useStartVisit from "../hooks/useStartVisit";
15
+ import CompleteModal from "../CompleteModal";
16
+ import CancelModal from "../CancelModal";
17
+
18
+ const formStore = getGlobalStore("ampath-form-state");
19
+
20
+ const WorkflowNavigationButtons = () => {
21
+ const context = useContext(GroupFormWorkflowContext);
22
+ const {
23
+ activeFormUuid,
24
+ submitForNext,
25
+ patientUuids,
26
+ activePatientUuid,
27
+ workflowState,
28
+ } = context;
29
+ const store = useStore(formStore);
30
+ const formState = store[activeFormUuid];
31
+ const navigationDisabled =
32
+ (formState !== "ready" || workflowState !== "EDIT_FORM") &&
33
+ formState !== "readyWithValidationErrors";
34
+ const [cancelModalOpen, setCancelModalOpen] = useState(false);
35
+ const [completeModalOpen, setCompleteModalOpen] = useState(false);
36
+ const { t } = useTranslation();
37
+
38
+ const isLastPatient =
39
+ activePatientUuid === patientUuids[patientUuids.length - 1];
40
+
41
+ const handleClickNext = () => {
42
+ if (
43
+ workflowState === "EDIT_FORM" ||
44
+ formState === "readyWithValidationErrors"
45
+ ) {
46
+ submitForNext();
47
+ }
48
+ };
49
+
50
+ return (
51
+ <>
52
+ <div className={styles.rightPanelActionButtons}>
53
+ <Button
54
+ kind="primary"
55
+ onClick={handleClickNext}
56
+ disabled={navigationDisabled}
57
+ >
58
+ {isLastPatient
59
+ ? t("saveForm", "Save Form")
60
+ : t("nextPatient", "Next Patient")}
61
+ </Button>
62
+ <Button kind="secondary" onClick={() => setCompleteModalOpen(true)}>
63
+ {t("saveAndComplete", "Save & Complete")}
64
+ </Button>
65
+ <Button kind="tertiary" onClick={() => setCancelModalOpen(true)}>
66
+ {t("cancel", "Cancel")}
67
+ </Button>
68
+ </div>
69
+ <CancelModal
70
+ open={cancelModalOpen}
71
+ setOpen={setCancelModalOpen}
72
+ context={context}
73
+ />
74
+ <CompleteModal
75
+ open={completeModalOpen}
76
+ setOpen={setCompleteModalOpen}
77
+ context={context}
78
+ validateFirst={false}
79
+ />
80
+ </>
81
+ );
82
+ };
83
+
84
+ const GroupSessionWorkspace = () => {
85
+ const { groupSessionConcepts } = useConfig();
86
+ const { t } = useTranslation();
87
+ const {
88
+ patientUuids,
89
+ activePatientUuid,
90
+ encounters,
91
+ activeEncounterUuid,
92
+ activeVisitUuid,
93
+ activeFormUuid,
94
+ saveEncounter,
95
+ activeSessionMeta,
96
+ groupVisitTypeUuid,
97
+ updateVisitUuid,
98
+ submitForNext,
99
+ workflowState,
100
+ } = useContext(GroupFormWorkflowContext);
101
+
102
+ const { sessionLocation } = useSession();
103
+ const [encounter, setEncounter] = useState(null);
104
+ const [visit, setVisit] = useState(null);
105
+
106
+ const {
107
+ saveVisit,
108
+ updateEncounter,
109
+ success: visitSaveSuccess,
110
+ } = useStartVisit({
111
+ showSuccessNotification: false,
112
+ showErrorNotification: true,
113
+ });
114
+
115
+ // 0. user clicks "next patient" in WorkflowNavigationButtons
116
+ // which triggers submitForNext() if workflowState === "EDIT_FORM"
117
+
118
+ // 1. save the new visit uuid and start form submission
119
+ useEffect(() => {
120
+ if (
121
+ visitSaveSuccess &&
122
+ visitSaveSuccess.data.patient.uuid === activePatientUuid
123
+ ) {
124
+ setVisit(visitSaveSuccess.data);
125
+ // Update visit UUID on workflow
126
+ updateVisitUuid(visitSaveSuccess.data.uuid);
127
+ }
128
+ }, [
129
+ visitSaveSuccess,
130
+ updateVisitUuid,
131
+ activeVisitUuid,
132
+ activePatientUuid,
133
+ visit,
134
+ setVisit,
135
+ ]);
136
+
137
+ // 2. If there's no active visit, trigger the creation of a new one
138
+ const handleEncounterCreate = useCallback(
139
+ (payload) => {
140
+ // Create a visit with the same date as the encounter being saved
141
+ if (!activeVisitUuid) {
142
+ saveVisit({
143
+ patientUuid: activePatientUuid,
144
+ startDatetime: activeSessionMeta.sessionDate,
145
+ stopDatetime: activeSessionMeta.sessionDate,
146
+ visitType: groupVisitTypeUuid,
147
+ location: sessionLocation?.uuid,
148
+ });
149
+ }
150
+ const obsTime = new Date(activeSessionMeta.sessionDate);
151
+ payload.obs.forEach((item, index) => {
152
+ payload.obs[index] = {
153
+ ...item,
154
+ groupMembers: item.groupMembers?.map((mem) => ({
155
+ ...mem,
156
+ obsDatetime: obsTime.toISOString(),
157
+ })),
158
+ obsDatetime: obsTime.toISOString(),
159
+ };
160
+ });
161
+ // If this is a newly created encounter and visit, add session concepts to encounter payload.
162
+ if (!activeVisitUuid) {
163
+ Object.entries(groupSessionConcepts).forEach(([field, uuid]) => {
164
+ payload.obs.push({
165
+ concept: uuid,
166
+ value: activeSessionMeta?.[field],
167
+ });
168
+ });
169
+ }
170
+ payload.location = sessionLocation?.uuid;
171
+ payload.encounterDatetime = obsTime.toISOString();
172
+ },
173
+ [
174
+ activePatientUuid,
175
+ activeVisitUuid,
176
+ activeSessionMeta,
177
+ groupSessionConcepts,
178
+ groupVisitTypeUuid,
179
+ saveVisit,
180
+ sessionLocation,
181
+ ]
182
+ );
183
+
184
+ // 3. Update encounter so that it belongs to the created visit
185
+ useEffect(() => {
186
+ if (encounter && visit && encounter.patient?.uuid === visit.patient?.uuid) {
187
+ updateEncounter({ uuid: encounter.uuid, visit: visit.uuid });
188
+ }
189
+ }, [encounter, updateEncounter, visit]);
190
+
191
+ // 4. Once form has been posted, save the new encounter uuid so we can edit it later
192
+ const handlePostResponse = useCallback(
193
+ (encounter) => {
194
+ if (encounter && encounter.uuid) {
195
+ saveEncounter(encounter.uuid);
196
+ setEncounter(encounter);
197
+ }
198
+ },
199
+ [saveEncounter]
200
+ );
201
+
202
+ const switchPatient = useCallback(
203
+ (patientUuid) => {
204
+ submitForNext(patientUuid);
205
+ },
206
+ [submitForNext]
207
+ );
208
+
209
+ if (workflowState === "NEW_GROUP_SESSION") return null;
210
+
211
+ return (
212
+ <div className={styles.workspace}>
213
+ <div className={styles.formMainContent}>
214
+ <div className={styles.formContainer}>
215
+ <FormBootstrap
216
+ patientUuid={activePatientUuid}
217
+ encounterUuid={activeEncounterUuid}
218
+ {...{
219
+ formUuid: activeFormUuid,
220
+ handlePostResponse,
221
+ handleEncounterCreate,
222
+ }}
223
+ />
224
+ </div>
225
+ <div className={styles.rightPanel}>
226
+ <h4>{t("formsFilled", "Forms filled")}</h4>
227
+ <div className={styles.patientCardsSection}>
228
+ {patientUuids?.map((patientUuid) => (
229
+ <PatientCard
230
+ key={patientUuid}
231
+ {...{
232
+ patientUuid,
233
+ activePatientUuid,
234
+ editEncounter: switchPatient,
235
+ encounters,
236
+ }}
237
+ />
238
+ ))}
239
+ </div>
240
+ <WorkflowNavigationButtons />
241
+ </div>
242
+ </div>
243
+ </div>
244
+ );
245
+ };
246
+
247
+ export default GroupSessionWorkspace;