@openmrs/esm-fast-data-entry-app 1.0.0-pre.53 → 1.0.0-pre.59

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.
@@ -1,76 +1,274 @@
1
+ import { navigate } from "@openmrs/esm-framework";
2
+ import { initialWorkflowState } from "./FormWorkflowContext";
3
+
4
+ export const fdeWorkflowStorageVersion = "1.0.12";
5
+ export const fdeWorkflowStorageName = "openmrs:fastDataEntryWorkflowState";
6
+ const persistData = (data) => {
7
+ localStorage.setItem(fdeWorkflowStorageName, JSON.stringify(data));
8
+ };
9
+
10
+ const initialFormState = {
11
+ workflowState: "NEW_PATIENT",
12
+ activePatientUuid: null,
13
+ activeEncounterUuid: null,
14
+ patientUuids: [],
15
+ encounters: {},
16
+ };
17
+
1
18
  const reducer = (state, action) => {
2
19
  switch (action.type) {
3
- case "ADD_PATIENT":
4
- return {
5
- ...state,
6
- patientUuids: [...state.patientUuids, action.patientUuid],
7
- activePatientUuid: action.patientUuid,
8
- activeEncounterUuid: null,
9
- workflowState: "EDIT_FORM",
10
- };
11
- case "OPEN_PATIENT_SEARCH":
12
- // this will need to be updated once AMPATH hook is available
13
- return {
20
+ case "INITIALIZE_WORKFLOW_STATE": {
21
+ const savedData = localStorage.getItem(fdeWorkflowStorageName);
22
+ const savedDataObject = savedData ? JSON.parse(savedData) : {};
23
+ let newState: { [key: string]: unknown } = {};
24
+ const newPatient = action.newPatientUuid
25
+ ? {
26
+ activePatientUuid: action.newPatientUuid,
27
+ workflowState: "EDIT_FORM",
28
+ }
29
+ : {};
30
+
31
+ if (
32
+ savedData &&
33
+ savedDataObject["_storageVersion"] === fdeWorkflowStorageVersion
34
+ ) {
35
+ // there is localStorage data and it is still valid
36
+ newState = {
37
+ ...savedDataObject,
38
+ activeFormUuid: action.activeFormUuid,
39
+ forms: {
40
+ ...savedDataObject.forms,
41
+ // initialize this particular form if it hasn't been created already
42
+ [action.activeFormUuid]: {
43
+ ...initialFormState,
44
+ ...savedDataObject.forms[action.activeFormUuid],
45
+ // if we receive activePatientUuid from a query parameter use that one
46
+ ...newPatient,
47
+ patientUuids:
48
+ savedDataObject.forms[action.activeFormUuid]?.patientUuids ||
49
+ initialFormState.patientUuids,
50
+ },
51
+ },
52
+ };
53
+ if (
54
+ action.newPatientUuid &&
55
+ !newState.forms[action.activeFormUuid].patientUuids.includes(
56
+ action.newPatientUuid
57
+ )
58
+ ) {
59
+ newState.forms[action.activeFormUuid].patientUuids.push(
60
+ action.newPatientUuid
61
+ );
62
+ }
63
+ } else {
64
+ // no localStorage data, or we should void it
65
+ newState = {
66
+ ...initialWorkflowState,
67
+ _storageVersion: fdeWorkflowStorageVersion,
68
+ forms: {
69
+ [action.activeFormUuid]: initialFormState,
70
+ },
71
+ activeFormUuid: action.activeFormUuid,
72
+ };
73
+ }
74
+ persistData(newState);
75
+ return { ...newState };
76
+ }
77
+ case "ADD_PATIENT": {
78
+ const newState = {
14
79
  ...state,
15
- activePatientUuid: null,
16
- activeEncounterUuid: null,
17
- workflowState: "NEW_PATIENT",
80
+ forms: {
81
+ ...state.forms,
82
+ [state.activeFormUuid]: {
83
+ ...state.forms[state.activeFormUuid],
84
+ patientUuids: [
85
+ ...state.forms[state.activeFormUuid].patientUuids,
86
+ action.patientUuid,
87
+ ],
88
+ activePatientUuid: action.patientUuid,
89
+ activeEncounterUuid: null,
90
+ workflowState: "EDIT_FORM",
91
+ },
92
+ },
18
93
  };
19
- case "SAVE_ENCOUNTER":
20
- return {
94
+ persistData(newState);
95
+ return newState;
96
+ }
97
+ case "OPEN_PATIENT_SEARCH": {
98
+ const newState = {
21
99
  ...state,
22
- encounters: {
23
- ...state.encounters,
24
- [state.activePatientUuid]: action.encounterUuid,
100
+ forms: {
101
+ ...state.forms,
102
+ [state.activeFormUuid]: {
103
+ ...state.forms[state.activeFormUuid],
104
+ activePatientUuid: null,
105
+ activeEncounterUuid: null,
106
+ workflowState: "NEW_PATIENT",
107
+ },
25
108
  },
26
- activePatientUuid: null,
27
- activeEncounterUuid: null,
28
- workflowState:
29
- state.workflowState === "SUBMIT_FOR_NEXT"
30
- ? "NEW_PATIENT"
31
- : state.workflowState === "SUBMIT_FOR_REVIEW"
32
- ? "REVIEW"
33
- : state.workflowState,
34
109
  };
35
- case "EDIT_ENCOUNTER":
36
- return {
110
+ // the persist here is optional...
111
+ persistData(newState);
112
+ return newState;
113
+ }
114
+ case "SAVE_ENCOUNTER": {
115
+ if (
116
+ state.forms[state.activeFormUuid].workflowState ===
117
+ "SUBMIT_FOR_COMPLETE"
118
+ ) {
119
+ const { [state.activeFormUuid]: activeForm, ...formRest } = state.forms;
120
+ const newState = {
121
+ ...state,
122
+ forms: formRest,
123
+ activeFormUuid: null,
124
+ };
125
+ persistData(newState);
126
+ // eslint-disable-next-line
127
+ navigate({ to: "${openmrsSpaBase}/forms" });
128
+ return newState;
129
+ } else {
130
+ const newState = {
131
+ ...state,
132
+ forms: {
133
+ ...state.forms,
134
+ [state.activeFormUuid]: {
135
+ ...state.forms[state.activeFormUuid],
136
+ encounters: {
137
+ ...state.forms[state.activeFormUuid].encounters,
138
+ [state.forms[state.activeFormUuid].activePatientUuid]:
139
+ action.encounterUuid,
140
+ },
141
+ activePatientUuid: null,
142
+ activeEncounterUuid: null,
143
+ workflowState:
144
+ state.forms[state.activeFormUuid].workflowState ===
145
+ "SUBMIT_FOR_NEXT"
146
+ ? "NEW_PATIENT"
147
+ : state.forms[state.activeFormUuid].workflowState ===
148
+ "SUBMIT_FOR_REVIEW"
149
+ ? "REVIEW"
150
+ : state.forms[state.activeFormUuid].workflowState,
151
+ },
152
+ },
153
+ };
154
+ persistData(newState);
155
+ return newState;
156
+ }
157
+ }
158
+ case "EDIT_ENCOUNTER": {
159
+ const newState = {
37
160
  ...state,
38
- activeEncounterUuid: state.encounters[action.patientUuid],
39
- activePatientUuid: action.patientUuid,
40
- workflowState: "EDIT_FORM",
161
+ forms: {
162
+ ...state.forms,
163
+ [state.activeFormUuid]: {
164
+ ...state.forms[state.activeFormUuid],
165
+ activeEncounterUuid:
166
+ state.forms[state.activeFormUuid].encounters[action.patientUuid],
167
+ activePatientUuid: action.patientUuid,
168
+ workflowState: "EDIT_FORM",
169
+ },
170
+ },
41
171
  };
172
+ persistData(newState);
173
+ return newState;
174
+ }
42
175
  case "SUBMIT_FOR_NEXT":
176
+ // this state should not be persisted
43
177
  window.dispatchEvent(
44
178
  new CustomEvent("ampath-form-action", {
45
- detail: { formUuid: state.formUuid, action: "onSubmit" },
179
+ detail: {
180
+ formUuid: state.activeFormUuid,
181
+ patientUuid: state.forms[state.activeFormUuid].activePatientUuid,
182
+ action: "onSubmit",
183
+ },
46
184
  })
47
185
  );
48
186
  return {
49
187
  ...state,
50
- workflowState: "SUBMIT_FOR_NEXT",
188
+ forms: {
189
+ ...state.forms,
190
+ [state.activeFormUuid]: {
191
+ ...state.forms[state.activeFormUuid],
192
+ workflowState: "SUBMIT_FOR_NEXT",
193
+ },
194
+ },
51
195
  };
52
196
  case "SUBMIT_FOR_REVIEW":
197
+ // this state should not be persisted
53
198
  window.dispatchEvent(
54
199
  new CustomEvent("ampath-form-action", {
55
- detail: { formUuid: state.formUuid, action: "onSubmit" },
200
+ detail: {
201
+ formUuid: state.activeFormUuid,
202
+ patientUuid: state.forms[state.activeFormUuid].activePatientUuid,
203
+ action: "onSubmit",
204
+ },
56
205
  })
57
206
  );
58
207
  return {
59
208
  ...state,
60
- workflowState: "SUBMIT_FOR_REVIEW",
209
+ forms: {
210
+ ...state.forms,
211
+ [state.activeFormUuid]: {
212
+ ...state.forms[state.activeFormUuid],
213
+ workflowState: "SUBMIT_FOR_REVIEW",
214
+ },
215
+ },
61
216
  };
62
- case "UPDATE_FORM_UUID":
217
+ case "SUBMIT_FOR_COMPLETE":
218
+ // this state should not be persisted
219
+ window.dispatchEvent(
220
+ new CustomEvent("ampath-form-action", {
221
+ detail: {
222
+ formUuid: state.activeFormUuid,
223
+ patientUuid: state.forms[state.activeFormUuid].activePatientUuid,
224
+ action: "onSubmit",
225
+ },
226
+ })
227
+ );
63
228
  return {
64
229
  ...state,
65
- formUuid: action.formUuid,
230
+ forms: {
231
+ ...state.forms,
232
+ [state.activeFormUuid]: {
233
+ ...state.forms[state.activeFormUuid],
234
+ workflowState: "SUBMIT_FOR_COMPLETE",
235
+ },
236
+ },
66
237
  };
67
- case "GO_TO_REVIEW":
68
- return {
238
+ case "GO_TO_REVIEW": {
239
+ const newState = {
240
+ ...state,
241
+ forms: {
242
+ ...state.forms,
243
+ [state.activeFormUuid]: {
244
+ ...state.forms[state.activeFormUuid],
245
+ activeEncounterUuid: null,
246
+ activePatientUuid: null,
247
+ workflowState: "REVIEW",
248
+ },
249
+ },
250
+ };
251
+ persistData(newState);
252
+ return newState;
253
+ }
254
+ case "DESTROY_SESSION": {
255
+ const { [state.activeFormUuid]: activeForm, ...formRest } = state.forms;
256
+ const newState = {
257
+ ...state,
258
+ forms: formRest,
259
+ activeFormUuid: null,
260
+ };
261
+ persistData(newState);
262
+ return newState;
263
+ }
264
+ case "CLOSE_SESSION": {
265
+ const newState = {
69
266
  ...state,
70
- activeEncounterUuid: null,
71
- activePatientUuid: null,
72
- workflowState: "REVIEW",
267
+ activeFormUuid: null,
73
268
  };
269
+ persistData(newState);
270
+ return newState;
271
+ }
74
272
  default:
75
273
  return state;
76
274
  }
@@ -3,8 +3,14 @@ import {
3
3
  getGlobalStore,
4
4
  useStore,
5
5
  } from "@openmrs/esm-framework";
6
- import { Button } from "carbon-components-react";
7
- import React, { useContext } from "react";
6
+ import {
7
+ Button,
8
+ ComposedModal,
9
+ ModalBody,
10
+ ModalFooter,
11
+ ModalHeader,
12
+ } from "carbon-components-react";
13
+ import React, { useContext, useState } from "react";
8
14
  import { useHistory } from "react-router-dom";
9
15
  import FormBootstrap from "../FormBootstrap";
10
16
  import PatientCard from "../patient-card/PatientCard";
@@ -19,48 +25,116 @@ import WorkflowReview from "../workflow-review";
19
25
 
20
26
  const formStore = getGlobalStore("ampath-form-state");
21
27
 
22
- const WorkflowNavigationButtons = () => {
23
- const {
24
- formUuid,
25
- submitForReview,
26
- submitForNext,
27
- workflowState,
28
- goToReview,
29
- } = useContext(FormWorkflowContext);
28
+ const CancelModal = ({ open, setOpen }) => {
29
+ const { destroySession, closeSession } = useContext(FormWorkflowContext);
30
+ const { t } = useTranslation();
30
31
  const history = useHistory();
32
+
33
+ const discard = () => {
34
+ destroySession();
35
+ setOpen(false);
36
+ history.push("/");
37
+ };
38
+
39
+ const saveAndClose = () => {
40
+ closeSession();
41
+ setOpen(false);
42
+ history.push("/");
43
+ };
44
+
45
+ return (
46
+ <ComposedModal open={open}>
47
+ <ModalHeader>{t("areYouSure", "Are you sure?")}</ModalHeader>
48
+ <ModalBody>
49
+ {t(
50
+ "cancelExplanation",
51
+ "You will lose any unsaved changes on the current form. Do you want to discard the current session?"
52
+ )}
53
+ </ModalBody>
54
+ <ModalFooter>
55
+ <Button kind="secondary" onClick={() => setOpen(false)}>
56
+ {t("cancel", "Cancel")}
57
+ </Button>
58
+ <Button kind="danger" onClick={discard}>
59
+ {t("discard", "Discard")}
60
+ </Button>
61
+ <Button kind="primary" onClick={saveAndClose}>
62
+ {t("saveSession", "Save Session")}
63
+ </Button>
64
+ </ModalFooter>
65
+ </ComposedModal>
66
+ );
67
+ };
68
+
69
+ const CompleteModal = ({ open, setOpen }) => {
70
+ const { submitForComplete } = useContext(FormWorkflowContext);
71
+ const { t } = useTranslation();
72
+
73
+ const completeSession = () => {
74
+ submitForComplete();
75
+ setOpen(false);
76
+ };
77
+
78
+ return (
79
+ <ComposedModal open={open}>
80
+ <ModalHeader>{t("areYouSure", "Are you sure?")}</ModalHeader>
81
+ <ModalBody>
82
+ {t(
83
+ "saveExplanation",
84
+ "Do you want to save the current form and exit the workflow?"
85
+ )}
86
+ </ModalBody>
87
+ <ModalFooter>
88
+ <Button kind="secondary" onClick={() => setOpen(false)}>
89
+ {t("cancel", "Cancel")}
90
+ </Button>
91
+ <Button kind="primary" onClick={completeSession}>
92
+ {t("complete", "Complete")}
93
+ </Button>
94
+ </ModalFooter>
95
+ </ComposedModal>
96
+ );
97
+ };
98
+
99
+ const WorkflowNavigationButtons = () => {
100
+ const { activeFormUuid, submitForNext, workflowState, destroySession } =
101
+ useContext(FormWorkflowContext);
31
102
  const store = useStore(formStore);
32
- const formState = store[formUuid];
103
+ const formState = store[activeFormUuid];
33
104
  const navigationDisabled = formState !== "ready";
105
+ const [cancelModalOpen, setCancelModalOpen] = useState(false);
106
+ const [completeModalOpen, setCompleteModalOpen] = useState(false);
34
107
  const { t } = useTranslation();
35
108
 
109
+ if (!workflowState) return null;
110
+
36
111
  return (
37
- <div className={styles.rightPanelActionButtons}>
38
- <Button
39
- kind="primary"
40
- onClick={() => submitForNext()}
41
- disabled={navigationDisabled || workflowState === "NEW_PATIENT"}
42
- >
43
- {t("nextPatient", "Next Patient")}
44
- </Button>
45
- <Button
46
- kind="secondary"
47
- disabled={navigationDisabled}
48
- onClick={
49
- workflowState === "NEW_PATIENT"
50
- ? () => goToReview()
51
- : () => submitForReview()
52
- }
53
- >
54
- {t("reviewSave", "Review & Save")}
55
- </Button>
56
- <Button
57
- kind="tertiary"
58
- onClick={() => history.push("/")}
59
- disabled={navigationDisabled}
60
- >
61
- {t("cancel", "Cancel")}
62
- </Button>
63
- </div>
112
+ <>
113
+ <div className={styles.rightPanelActionButtons}>
114
+ <Button
115
+ kind="primary"
116
+ onClick={() => submitForNext()}
117
+ disabled={navigationDisabled || workflowState === "NEW_PATIENT"}
118
+ >
119
+ {t("nextPatient", "Next Patient")}
120
+ </Button>
121
+ <Button
122
+ kind="secondary"
123
+ onClick={
124
+ workflowState === "NEW_PATIENT"
125
+ ? () => destroySession()
126
+ : () => setCompleteModalOpen(true)
127
+ }
128
+ >
129
+ {t("saveAndComplete", "Save & Complete")}
130
+ </Button>
131
+ <Button kind="tertiary" onClick={() => setCancelModalOpen(true)}>
132
+ {t("cancel", "Cancel")}
133
+ </Button>
134
+ </div>
135
+ <CancelModal open={cancelModalOpen} setOpen={setCancelModalOpen} />
136
+ <CompleteModal open={completeModalOpen} setOpen={setCompleteModalOpen} />
137
+ </>
64
138
  );
65
139
  };
66
140
 
@@ -70,7 +144,7 @@ const FormWorkspace = () => {
70
144
  activePatientUuid,
71
145
  activeEncounterUuid,
72
146
  saveEncounter,
73
- formUuid,
147
+ activeFormUuid,
74
148
  } = useContext(FormWorkflowContext);
75
149
  const { t } = useTranslation();
76
150
 
@@ -94,7 +168,7 @@ const FormWorkspace = () => {
94
168
  patientUuid={activePatientUuid}
95
169
  encounterUuid={activeEncounterUuid}
96
170
  {...{
97
- formUuid,
171
+ formUuid: activeFormUuid,
98
172
  handlePostResponse,
99
173
  }}
100
174
  />
@@ -59,8 +59,5 @@
59
59
  & button {
60
60
  width: 100%;
61
61
  text-decoration: "none";
62
- & :hover {
63
- background-color: $carbon--gray-30;
64
- }
65
62
  }
66
63
  }
@@ -6,6 +6,10 @@ import { useGetAllForms } from "../hooks";
6
6
  import FormsTable from "../forms-table";
7
7
  import styles from "./styles.scss";
8
8
  import { useTranslation } from "react-i18next";
9
+ import {
10
+ fdeWorkflowStorageName,
11
+ fdeWorkflowStorageVersion,
12
+ } from "../context/FormWorkflowReducer";
9
13
 
10
14
  // helper function useful for debugging
11
15
  // given a list of forms, it will organize into permissions
@@ -41,6 +45,18 @@ const FormsPage = () => {
41
45
  const { formCategories, formCategoriesToShow } = config;
42
46
  const { forms, isLoading, error } = useGetAllForms();
43
47
  const cleanRows = prepareRowsForTable(forms);
48
+ const savedData = localStorage.getItem(fdeWorkflowStorageName);
49
+ const activeForms = [];
50
+ if (
51
+ savedData &&
52
+ JSON.parse(savedData)?.["_storageVersion"] === fdeWorkflowStorageVersion
53
+ ) {
54
+ Object.entries(JSON.parse(savedData).forms).forEach(
55
+ ([formUuid, form]: [string, { [key: string]: unknown }]) => {
56
+ if (form.workflowState) activeForms.push(formUuid);
57
+ }
58
+ );
59
+ }
44
60
 
45
61
  const categoryRows = formCategoriesToShow.map((name) => {
46
62
  const category = formCategories.find((category) => category.name === name);
@@ -61,11 +77,14 @@ const FormsPage = () => {
61
77
  cleanRows ? cleanRows?.length : "??"
62
78
  })`}
63
79
  >
64
- <FormsTable rows={cleanRows} {...{ error, isLoading }} />
80
+ <FormsTable rows={cleanRows} {...{ error, isLoading, activeForms }} />
65
81
  </Tab>
66
82
  {categoryRows?.map((category, index) => (
67
83
  <Tab label={`${category.name} (${category.rows.length})`} key={index}>
68
- <FormsTable rows={category.rows} {...{ error, isLoading }} />
84
+ <FormsTable
85
+ rows={category.rows}
86
+ {...{ error, isLoading, activeForms }}
87
+ />
69
88
  </Tab>
70
89
  ))}
71
90
  </Tabs>
@@ -19,7 +19,7 @@ import { Link } from "react-router-dom";
19
19
  import EmptyState from "../empty-state/EmptyState";
20
20
  import styles from "./styles.scss";
21
21
 
22
- const FormsTable = ({ rows, error, isLoading }) => {
22
+ const FormsTable = ({ rows, error, isLoading, activeForms }) => {
23
23
  const { t } = useTranslation();
24
24
 
25
25
  const formsHeader = [
@@ -39,7 +39,13 @@ const FormsTable = ({ rows, error, isLoading }) => {
39
39
 
40
40
  const augmenteRows = rows?.map((row) => ({
41
41
  ...row,
42
- actions: <Link to={row.uuid}>{t("fillForm", "Fill Form")}</Link>,
42
+ actions: (
43
+ <Link to={row.uuid}>
44
+ {activeForms.includes(row.uuid)
45
+ ? t("resumeSession", "Resume Session")
46
+ : t("fillForm", "Fill Form")}
47
+ </Link>
48
+ ),
43
49
  actions2: (
44
50
  <Link to="#" className={styles.inactiveLink}>
45
51
  {t("startGroupSession", "Start Group Session")}
@@ -1,3 +1,4 @@
1
+ import { CheckmarkOutline16, WarningAlt16 } from "@carbon/icons-react";
1
2
  import { SkeletonText } from "carbon-components-react";
2
3
  import React, { useContext } from "react";
3
4
  import FormWorkflowContext from "../context/FormWorkflowContext";
@@ -18,7 +19,8 @@ const CardContainer = ({ onClick = () => undefined, active, children }) => {
18
19
  };
19
20
 
20
21
  const PatientCard = ({ patientUuid }) => {
21
- const { activePatientUuid, editEncounter } = useContext(FormWorkflowContext);
22
+ const { activePatientUuid, editEncounter, encounters } =
23
+ useContext(FormWorkflowContext);
22
24
  const patient = useGetPatient(patientUuid);
23
25
  const givenName = patient?.name?.[0]?.given?.[0];
24
26
  const familyName = patient?.name?.[0]?.family;
@@ -39,13 +41,22 @@ const PatientCard = ({ patientUuid }) => {
39
41
  onClick={active ? () => undefined : () => editEncounter(patientUuid)}
40
42
  active={active}
41
43
  >
42
- <div className={styles.identifier}>{identifier}</div>
43
- <div
44
- className={`${styles.displayName} ${
45
- active && styles.activeDisplayName
46
- }`}
47
- >
48
- {givenName} {familyName}
44
+ <div className={styles.patientInfo}>
45
+ <div className={styles.identifier}>{identifier}</div>
46
+ <div
47
+ className={`${styles.displayName} ${
48
+ active && styles.activeDisplayName
49
+ }`}
50
+ >
51
+ {givenName} {familyName}
52
+ </div>
53
+ </div>
54
+ <div>
55
+ {patientUuid in encounters ? (
56
+ <CheckmarkOutline16 className={styles.statusSuccess} />
57
+ ) : (
58
+ <WarningAlt16 className={styles.statusWarning} />
59
+ )}
49
60
  </div>
50
61
  </CardContainer>
51
62
  );
@@ -4,6 +4,7 @@
4
4
 
5
5
  .cardContainer {
6
6
  padding: $spacing-05;
7
+ display: flex;
7
8
  }
8
9
 
9
10
  .skeletonText {
@@ -29,3 +30,15 @@
29
30
  background-color: $carbon--gray-40;
30
31
  }
31
32
  }
33
+
34
+ .patientInfo {
35
+ flex-grow: 1;
36
+ }
37
+
38
+ .statusSuccess {
39
+ fill: $support-02;
40
+ }
41
+
42
+ .statusWarning {
43
+ fill: $support-03;
44
+ }