@openmrs/esm-fast-data-entry-app 1.0.1-pre.87 → 1.0.1-pre.93

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,4 +1,10 @@
1
- import React, { useCallback, useContext, useEffect, useState } from "react";
1
+ import React, {
2
+ useCallback,
3
+ useContext,
4
+ useEffect,
5
+ useMemo,
6
+ useState,
7
+ } from "react";
2
8
  import {
3
9
  ComposedModal,
4
10
  Button,
@@ -7,9 +13,8 @@ import {
7
13
  ModalBody,
8
14
  TextInput,
9
15
  FormLabel,
10
- Loading,
11
16
  } from "@carbon/react";
12
- import { Add, TrashCan } from "@carbon/react/icons";
17
+ import { TrashCan } from "@carbon/react/icons";
13
18
  import { useTranslation } from "react-i18next";
14
19
  import { ExtensionSlot, showToast } from "@openmrs/esm-framework";
15
20
  import styles from "./styles.scss";
@@ -18,23 +23,49 @@ import { usePostCohort } from "../hooks";
18
23
 
19
24
  const MemExtension = React.memo(ExtensionSlot);
20
25
 
26
+ const buildPatientDisplay = (patient) => {
27
+ const givenName = patient?.name?.[0]?.given?.[0];
28
+ const familyName = patient?.name?.[0]?.family;
29
+ const identifier = patient?.identifier?.[0]?.value;
30
+
31
+ let display = identifier ? identifier + " - " : "";
32
+ display += (givenName || "") + " " + (familyName || "");
33
+ return display.replace(/\s+/g, " ");
34
+ };
35
+
21
36
  const PatientRow = ({ patient, removePatient }) => {
22
37
  const { t } = useTranslation();
38
+ const onClickHandler = useCallback(
39
+ () => removePatient(patient?.uuid),
40
+ [patient, removePatient]
41
+ );
42
+ const patientDisplay = useMemo(() => {
43
+ if (!patient) {
44
+ return "";
45
+ }
46
+
47
+ if (patient.display) {
48
+ return patient.display;
49
+ }
50
+
51
+ return buildPatientDisplay(patient);
52
+ }, [patient]);
53
+
23
54
  return (
24
- <li key={patient.uuid} className={styles.patientRow}>
55
+ <li key={patient?.uuid} className={styles.patientRow}>
25
56
  <span>
26
57
  <Button
27
58
  kind="tertiary"
28
59
  size="sm"
29
60
  hasIconOnly
30
- onClick={() => removePatient(patient.uuid)}
61
+ onClick={onClickHandler}
31
62
  renderIcon={TrashCan}
32
63
  tooltipAlignment="start"
33
64
  tooltipPosition="top"
34
65
  iconDescription={t("remove", "Remove")}
35
66
  />
36
67
  </span>
37
- <span className={styles.patientName}>{patient?.display}</span>
68
+ <span className={styles.patientName}>{patientDisplay}</span>
38
69
  </li>
39
70
  );
40
71
  };
@@ -108,18 +139,21 @@ const NewGroupForm = (props) => {
108
139
  );
109
140
  };
110
141
 
111
- const AddGroupModal = () => {
142
+ const AddGroupModal = ({
143
+ patients = undefined,
144
+ isCreate = undefined,
145
+ groupName = "",
146
+ cohortUuid = undefined,
147
+ isOpen,
148
+ handleCancel,
149
+ onPostSubmit,
150
+ }) => {
112
151
  const { setGroup } = useContext(GroupFormWorkflowContext);
113
152
  const { t } = useTranslation();
114
- const [open, setOpen] = useState(false);
115
153
  const [errors, setErrors] = useState({});
116
- const [name, setName] = useState("");
117
- const [patientList, setPatientList] = useState([]);
118
- const { post, result, isPosting, error } = usePostCohort();
119
-
120
- const handleCancel = () => {
121
- setOpen(false);
122
- };
154
+ const [name, setName] = useState(groupName);
155
+ const [patientList, setPatientList] = useState(patients || []);
156
+ const { post, result, error } = usePostCohort();
123
157
 
124
158
  const removePatient = useCallback(
125
159
  (patientUuid: string) =>
@@ -174,9 +208,13 @@ const AddGroupModal = () => {
174
208
  const handleSubmit = () => {
175
209
  if (validate()) {
176
210
  post({
211
+ uuid: cohortUuid,
177
212
  name: name,
178
213
  cohortMembers: patientList.map((p) => ({ patient: p.uuid })),
179
214
  });
215
+ if (onPostSubmit) {
216
+ onPostSubmit();
217
+ }
180
218
  }
181
219
  };
182
220
 
@@ -198,7 +236,7 @@ const AddGroupModal = () => {
198
236
  title: t("postError", "POST Error"),
199
237
  description:
200
238
  error.message ??
201
- t("unknownPostError", "An unknown error occured while saving data"),
239
+ t("unknownPostError", "An unknown error occurred while saving data"),
202
240
  });
203
241
  if (error.fieldErrors) {
204
242
  setErrors(
@@ -215,43 +253,31 @@ const AddGroupModal = () => {
215
253
 
216
254
  return (
217
255
  <div className={styles.modal}>
218
- <Button
219
- onClick={() => setOpen(true)}
220
- renderIcon={Add}
221
- iconDescription="Add"
222
- >
223
- {t("createNewGroup", "Create New Group")}
224
- </Button>
225
- <ComposedModal open={open} onClose={() => setOpen(false)}>
226
- <ModalHeader>{t("createNewGroup", "Create New Group")}</ModalHeader>
256
+ <ComposedModal open={isOpen} onClose={handleCancel}>
257
+ <ModalHeader>
258
+ {isCreate
259
+ ? t("createNewGroup", "Create New Group")
260
+ : t("editGroup", "Edit Group")}
261
+ </ModalHeader>
227
262
  <ModalBody>
228
- {result ? (
229
- <p>Group saved succesfully</p>
230
- ) : isPosting ? (
231
- <div className={styles.loading}>
232
- <Loading withOverlay={false} />
233
- <span>Saving new group...</span>
234
- </div>
235
- ) : (
236
- <NewGroupForm
237
- {...{
238
- name,
239
- setName,
240
- patientList,
241
- updatePatientList,
242
- errors,
243
- validate,
244
- removePatient,
245
- }}
246
- />
247
- )}
263
+ <NewGroupForm
264
+ {...{
265
+ name,
266
+ setName,
267
+ patientList,
268
+ updatePatientList,
269
+ errors,
270
+ validate,
271
+ removePatient,
272
+ }}
273
+ />
248
274
  </ModalBody>
249
275
  <ModalFooter>
250
- <Button kind="secondary" onClick={handleCancel} disabled={isPosting}>
276
+ <Button kind="secondary" onClick={handleCancel}>
251
277
  {t("cancel", "Cancel")}
252
278
  </Button>
253
- <Button kind="primary" onClick={handleSubmit} disabled={isPosting}>
254
- {t("createGroup", "Create Group")}
279
+ <Button kind="primary" onClick={handleSubmit}>
280
+ {isCreate ? t("createGroup", "Create Group") : t("save", "Save")}
255
281
  </Button>
256
282
  </ModalFooter>
257
283
  </ComposedModal>
@@ -6,11 +6,13 @@ import {
6
6
  DatePicker,
7
7
  DatePickerInput,
8
8
  } from "@carbon/react";
9
- import React from "react";
9
+ import React, { useContext } from "react";
10
10
  import styles from "./styles.scss";
11
11
  import { useTranslation } from "react-i18next";
12
12
  import { Controller, useFormContext } from "react-hook-form";
13
13
  import { AttendanceTable } from "./attendance-table";
14
+ import GroupFormWorkflowContext from "../context/GroupFormWorkflowContext";
15
+ import useGetPatients from "../hooks/useGetPatients";
14
16
 
15
17
  const SessionDetailsForm = () => {
16
18
  const { t } = useTranslation();
@@ -20,101 +22,108 @@ const SessionDetailsForm = () => {
20
22
  control,
21
23
  } = useFormContext();
22
24
 
25
+ const { patientUuids } = useContext(GroupFormWorkflowContext);
26
+ const { patients, isLoading } = useGetPatients(patientUuids);
27
+
23
28
  return (
24
- <div className={styles.formSection}>
25
- <h4>{t("sessionDetails", "1. Session details")}</h4>
26
- <div>
27
- <p>
28
- {t(
29
- "allFieldsRequired",
30
- "All fields are required unless marked optional"
31
- )}
32
- </p>
33
- </div>
34
- <Layer>
35
- <Tile className={styles.formSectionTile}>
29
+ <div>
30
+ {!isLoading && (
31
+ <div className={styles.formSection}>
32
+ <h4>{t("sessionDetails", "1. Session details")}</h4>
33
+ <div>
34
+ <p>
35
+ {t(
36
+ "allFieldsRequired",
37
+ "All fields are required unless marked optional"
38
+ )}
39
+ </p>
40
+ </div>
36
41
  <Layer>
37
- <div
38
- style={{
39
- display: "flex",
40
- flexDirection: "column",
41
- rowGap: "1.5rem",
42
- }}
43
- >
44
- <TextInput
45
- id="text"
46
- type="text"
47
- labelText={t("sessionName", "Session Name")}
48
- {...register("sessionName", { required: true })}
49
- invalid={errors.sessionName}
50
- invalidText={"This field is required"}
51
- />
52
- <TextInput
53
- id="text"
54
- type="text"
55
- labelText={t("practitionerName", "Practitioner Name")}
56
- {...register("practitionerName", { required: true })}
57
- invalid={errors.practitionerName}
58
- invalidText={"This field is required"}
59
- />
60
- <Controller
61
- name="sessionDate"
62
- control={control}
63
- rules={{ required: true }}
64
- render={({ field }) => (
65
- <DatePicker
66
- datePickerType="single"
67
- size="md"
68
- maxDate={new Date()}
69
- {...field}
70
- >
71
- <DatePickerInput
72
- id="session-date"
73
- labelText={t("sessionDate", "Session Date")}
74
- placeholder="mm/dd/yyyy"
75
- size="md"
76
- invalid={errors.sessionDate}
77
- invalidText={"This field is required"}
78
- />
79
- </DatePicker>
80
- )}
81
- />
82
- <TextArea
83
- id="text"
84
- type="text"
85
- labelText={t("sessionNotes", "Session Notes")}
86
- {...register("sessionNotes", { required: true })}
87
- invalid={errors.sessionNotes}
88
- invalidText={"This field is required"}
89
- />
90
- </div>
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={"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={"This field is required"}
66
+ />
67
+ <Controller
68
+ name="sessionDate"
69
+ control={control}
70
+ rules={{ required: true }}
71
+ render={({ field }) => (
72
+ <DatePicker
73
+ datePickerType="single"
74
+ size="md"
75
+ maxDate={new Date()}
76
+ {...field}
77
+ >
78
+ <DatePickerInput
79
+ id="session-date"
80
+ labelText={t("sessionDate", "Session Date")}
81
+ placeholder="mm/dd/yyyy"
82
+ size="md"
83
+ invalid={errors.sessionDate}
84
+ invalidText={"This field is required"}
85
+ />
86
+ </DatePicker>
87
+ )}
88
+ />
89
+ <TextArea
90
+ id="text"
91
+ type="text"
92
+ labelText={t("sessionNotes", "Session Notes")}
93
+ {...register("sessionNotes", { required: true })}
94
+ invalid={errors.sessionNotes}
95
+ invalidText={"This field is required"}
96
+ />
97
+ </div>
98
+ </Layer>
99
+ </Tile>
91
100
  </Layer>
92
- </Tile>
93
- </Layer>
94
- <h4>{t("sessionParticipants", "2. Session participants")}</h4>
95
- <div>
96
- <p>
97
- {t(
98
- "markAbsentPatients",
99
- "The patients in this group. Patients that are not present in the session should be marked as absent."
100
- )}
101
- </p>
102
- </div>
103
- <Layer>
104
- <Tile className={styles.formSectionTile}>
101
+ <h4>{t("sessionParticipants", "2. Session participants")}</h4>
102
+ <div>
103
+ <p>
104
+ {t(
105
+ "markAbsentPatients",
106
+ "The patients in this group. Patients that are not present in the session should be marked as absent."
107
+ )}
108
+ </p>
109
+ </div>
105
110
  <Layer>
106
- <div
107
- style={{
108
- display: "flex",
109
- flexDirection: "column",
110
- rowGap: "1.5rem",
111
- }}
112
- >
113
- <AttendanceTable />
114
- </div>
111
+ <Tile className={styles.formSectionTile}>
112
+ <Layer>
113
+ <div
114
+ style={{
115
+ display: "flex",
116
+ flexDirection: "column",
117
+ rowGap: "1.5rem",
118
+ }}
119
+ >
120
+ <AttendanceTable patients={patients} />
121
+ </div>
122
+ </Layer>
123
+ </Tile>
115
124
  </Layer>
116
- </Tile>
117
- </Layer>
125
+ </div>
126
+ )}
118
127
  </div>
119
128
  );
120
129
  };
@@ -1,4 +1,5 @@
1
- import React, { useContext } from "react";
1
+ import React, { useCallback, useContext, useMemo, useState } from "react";
2
+ import { Edit } from "@carbon/react/icons";
2
3
 
3
4
  import {
4
5
  Checkbox,
@@ -9,13 +10,13 @@ import {
9
10
  TableHeader,
10
11
  TableBody,
11
12
  TableCell,
13
+ Button,
12
14
  } from "@carbon/react";
13
15
  import { useTranslation } from "react-i18next";
14
16
  import GroupFormWorkflowContext from "../../context/GroupFormWorkflowContext";
15
- import { useGetPatient } from "../../hooks";
17
+ import AddGroupModal from "../../add-group-modal/AddGroupModal";
16
18
 
17
- const PatientRow = ({ patientUuid }) => {
18
- const patient = useGetPatient(patientUuid);
19
+ const PatientRow = ({ patient }) => {
19
20
  const { patientUuids, addPatientUuid, removePatientUuid } = useContext(
20
21
  GroupFormWorkflowContext
21
22
  );
@@ -25,9 +26,9 @@ const PatientRow = ({ patientUuid }) => {
25
26
 
26
27
  const handleOnChange = (e, { checked }) => {
27
28
  if (checked) {
28
- addPatientUuid(patientUuid);
29
+ addPatientUuid(patient.id);
29
30
  } else {
30
- removePatientUuid(patientUuid);
31
+ removePatientUuid(patient.id);
31
32
  }
32
33
  };
33
34
 
@@ -57,8 +58,8 @@ const PatientRow = ({ patientUuid }) => {
57
58
  <TableCell>{identifier}</TableCell>
58
59
  <TableCell>
59
60
  <Checkbox
60
- checked={patientUuids.includes(patientUuid)}
61
- labelText={patientUuid}
61
+ checked={patientUuids.includes(patient.id)}
62
+ labelText={patient.id}
62
63
  hideLabel
63
64
  id={`${identifier}-attendance-checkbox`}
64
65
  onChange={handleOnChange}
@@ -68,37 +69,75 @@ const PatientRow = ({ patientUuid }) => {
68
69
  );
69
70
  };
70
71
 
71
- const AttendanceTable = () => {
72
+ const AttendanceTable = ({ patients }) => {
72
73
  const { t } = useTranslation();
73
- const { activeGroupUuid, activeGroupMembers } = useContext(
74
+ const { activeGroupUuid, activeGroupName, activeGroupMembers } = useContext(
74
75
  GroupFormWorkflowContext
75
76
  );
76
77
 
78
+ const [isOpen, setOpen] = useState(false);
79
+
77
80
  const headers = [
78
81
  t("name", "Name"),
79
82
  t("identifier", "Patient ID"),
80
83
  t("patientIsPresent", "Patient is present"),
81
84
  ];
82
85
 
86
+ const handleCancel = useCallback(() => {
87
+ setOpen(false);
88
+ }, []);
89
+
90
+ const onPostSubmit = useCallback(() => {
91
+ setOpen(false);
92
+ }, []);
93
+
94
+ const newArr = useMemo(() => {
95
+ return activeGroupMembers.map(function (value) {
96
+ const patient = patients.find((patient) => patient.id === value);
97
+ return { uuid: value, ...patient };
98
+ });
99
+ }, [activeGroupMembers, patients]);
100
+
83
101
  if (!activeGroupUuid) {
84
102
  return <div>{t("selectGroupFirst", "Please select a group first")}</div>;
85
103
  }
86
104
 
87
105
  return (
88
- <Table>
89
- <TableHead>
90
- <TableRow>
91
- {headers.map((header, index) => (
92
- <TableHeader key={index}>{header}</TableHeader>
93
- ))}
94
- </TableRow>
95
- </TableHead>
96
- <TableBody>
97
- {activeGroupMembers.map((patientUuid, index) => (
98
- <PatientRow {...{ patientUuid }} key={index} />
99
- ))}
100
- </TableBody>
101
- </Table>
106
+ <div>
107
+ <span style={{ flexGrow: 1 }} />
108
+ <Button kind="ghost" onClick={() => setOpen(true)}>
109
+ {t("editGroup", "Edit Group")}&nbsp;
110
+ <Edit size={20} />
111
+ </Button>
112
+ <AddGroupModal
113
+ {...{
114
+ cohortUuid: activeGroupUuid,
115
+ patients: newArr,
116
+ isCreate: false,
117
+ groupName: activeGroupName,
118
+ isOpen: isOpen,
119
+ handleCancel: handleCancel,
120
+ onPostSubmit: onPostSubmit,
121
+ }}
122
+ />
123
+ <Table>
124
+ <TableHead>
125
+ <TableRow>
126
+ {headers.map((header, index) => (
127
+ <TableHeader key={index}>{header}</TableHeader>
128
+ ))}
129
+ </TableRow>
130
+ </TableHead>
131
+ <TableBody>
132
+ {activeGroupMembers.map((patientUuid, index) => {
133
+ const patient = patients.find(
134
+ (patient) => patient.id === patientUuid
135
+ );
136
+ return <PatientRow patient={patient} key={index} />;
137
+ })}
138
+ </TableBody>
139
+ </Table>
140
+ </div>
102
141
  );
103
142
  };
104
143
 
@@ -1,6 +1,6 @@
1
- import { Close } from "@carbon/react/icons";
1
+ import { Close, Add } from "@carbon/react/icons";
2
2
  import { Button } from "@carbon/react";
3
- import React, { useContext } from "react";
3
+ import React, { useCallback, useContext, useState } from "react";
4
4
  import GroupFormWorkflowContext from "../../context/GroupFormWorkflowContext";
5
5
  import styles from "./styles.scss";
6
6
  import { useTranslation } from "react-i18next";
@@ -12,10 +12,23 @@ const GroupSearchHeader = () => {
12
12
  const { activeGroupUuid, setGroup, destroySession } = useContext(
13
13
  GroupFormWorkflowContext
14
14
  );
15
+ const [isOpen, setOpen] = useState(false);
15
16
  const handleSelectGroup = (group) => {
16
17
  setGroup(group);
17
18
  };
18
19
 
20
+ const handleCancel = useCallback(() => {
21
+ setOpen(false);
22
+ }, []);
23
+
24
+ const onPostSubmit = useCallback(() => {
25
+ setOpen(false);
26
+ }, []);
27
+
28
+ const handleOpenClick = useCallback(() => {
29
+ setOpen(true);
30
+ }, []);
31
+
19
32
  if (activeGroupUuid) return null;
20
33
 
21
34
  return (
@@ -26,7 +39,21 @@ const GroupSearchHeader = () => {
26
39
  </span>
27
40
  <span className={styles.padded}>{t("or", "or")}</span>
28
41
  <span>
29
- <AddGroupModal />
42
+ <Button
43
+ onClick={handleOpenClick}
44
+ renderIcon={Add}
45
+ iconDescription="Add"
46
+ >
47
+ {t("createNewGroup", "Create New Group")}
48
+ </Button>
49
+ <AddGroupModal
50
+ {...{
51
+ isCreate: true,
52
+ isOpen: isOpen,
53
+ handleCancel: handleCancel,
54
+ onPostSubmit: onPostSubmit,
55
+ }}
56
+ />
30
57
  </span>
31
58
  <span style={{ flexGrow: 1 }} />
32
59
  <span>
@@ -0,0 +1,34 @@
1
+ import { fetchCurrentPatient } from "@openmrs/esm-framework";
2
+ import { useEffect, useState } from "react";
3
+
4
+ const useGetPatients = (patientUuids) => {
5
+ const [patients, setPatients] = useState([]);
6
+ const [isLoading, setIsLoading] = useState(true);
7
+
8
+ useEffect(() => {
9
+ if (!patientUuids || patientUuids.length === 0) {
10
+ setPatients([]);
11
+ setIsLoading(false);
12
+ } else {
13
+ getPatients(patientUuids);
14
+ }
15
+ }, [patientUuids]);
16
+
17
+ const getPatients = async (uuids) => {
18
+ try {
19
+ setIsLoading(true);
20
+ const results = await Promise.all(
21
+ uuids.map(async (uuid) => await fetchCurrentPatient(uuid))
22
+ );
23
+ setPatients(results);
24
+ setIsLoading(false);
25
+ } catch (error) {
26
+ console.error("Error fetching patients:", error);
27
+ setIsLoading(false);
28
+ }
29
+ };
30
+
31
+ return { patients, isLoading };
32
+ };
33
+
34
+ export default useGetPatients;
@@ -31,7 +31,13 @@ const usePostEndpoint = ({ endpointUrl }) => {
31
31
  const post = useCallback(
32
32
  async (data) => {
33
33
  setSubmissionInProgress(true);
34
- return openmrsFetch(endpointUrl, {
34
+
35
+ let path = endpointUrl;
36
+ if (data.uuid) {
37
+ path += "/" + data.uuid;
38
+ }
39
+
40
+ return openmrsFetch(path, {
35
41
  method: "POST",
36
42
  headers: {
37
43
  "Content-Type": "application/json",
@@ -14,6 +14,7 @@
14
14
  "createNewPatient": "Create new patient",
15
15
  "createNewSession": "Create New Session",
16
16
  "discard": "Discard",
17
+ "editGroup": "Edit Group",
17
18
  "error": "Error",
18
19
  "errorCopy": "Sorry, there was an error. You can try to reload this page, or contact the site administrator and quote the error code above.",
19
20
  "errorLoadingData": "Error Loading Data",
@@ -46,6 +47,7 @@
46
47
  "remove": "Remove",
47
48
  "resumeGroupSession": "Resume Group Session",
48
49
  "resumeSession": "Resume Session",
50
+ "save": "Save",
49
51
  "saveAndComplete": "Save & Complete",
50
52
  "saveExplanation": "Do you want to save the current form and exit the workflow?",
51
53
  "saveForm": "Save Form",
@@ -63,5 +65,5 @@
63
65
  "startGroupSession": "Start Group Session",
64
66
  "trySearchWithPatientUniqueID": "Try searching with the cohort's description",
65
67
  "unknown": "Unknown",
66
- "unknownPostError": "An unknown error occured while saving data"
68
+ "unknownPostError": "An unknown error occurred while saving data"
67
69
  }