@openmrs/esm-fast-data-entry-app 1.0.0-pre.9 → 1.0.1-pre.15
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.
- package/.eslintrc.js +10 -0
- package/.husky/pre-push +1 -6
- package/.tx/config +9 -0
- package/.yarn/plugins/@yarnpkg/plugin-version.cjs +550 -0
- package/.yarn/versions/c1451405.yml +0 -0
- package/README.md +39 -12
- package/dist/openmrs-esm-fast-data-entry-app.js +1 -1
- package/docs/config-icrc-forms.png +0 -0
- package/docs/config-other-forms.png +0 -0
- package/docs/configuring-form-categories.md +77 -0
- package/docs/fde-workflow.mov +0 -0
- package/docs/form-workflow-state-diagram.png +0 -0
- package/jest.config.json +20 -18
- package/package.json +99 -106
- package/src/FormBootstrap.tsx +151 -0
- package/src/Root.tsx +14 -3
- package/src/add-group-modal/AddGroupModal.tsx +262 -0
- package/src/add-group-modal/styles.scss +45 -0
- package/src/config-schema.ts +63 -31
- package/src/context/FormWorkflowContext.tsx +114 -0
- package/src/context/FormWorkflowReducer.ts +277 -0
- package/src/context/GroupFormWorkflowContext.tsx +143 -0
- package/src/context/GroupFormWorkflowReducer.ts +296 -0
- package/src/empty-state/EmptyDataIllustration.tsx +51 -0
- package/src/empty-state/EmptyState.tsx +33 -0
- package/src/empty-state/styles.scss +55 -0
- package/src/form-entry-workflow/FormEntryWorkflow.tsx +230 -0
- package/src/form-entry-workflow/form-review-card/FormReviewCard.tsx +50 -0
- package/src/form-entry-workflow/form-review-card/index.ts +3 -0
- package/src/form-entry-workflow/form-review-card/styles.scss +39 -0
- package/src/form-entry-workflow/index.ts +3 -0
- package/src/form-entry-workflow/patient-banner/PatientBanner.test.tsx +9 -0
- package/src/form-entry-workflow/patient-banner/PatientBanner.tsx +86 -0
- package/src/form-entry-workflow/patient-banner/index.ts +3 -0
- package/src/form-entry-workflow/patient-banner/styles.scss +45 -0
- package/src/form-entry-workflow/patient-search-header/PatientSearchHeader.tsx +63 -0
- package/src/form-entry-workflow/patient-search-header/index.ts +3 -0
- package/src/form-entry-workflow/patient-search-header/styles.scss +22 -0
- package/src/form-entry-workflow/styles.scss +64 -0
- package/src/form-entry-workflow/workflow-review/WorkflowReview.tsx +35 -0
- package/src/form-entry-workflow/workflow-review/index.ts +3 -0
- package/src/form-entry-workflow/workflow-review/styles.scss +34 -0
- package/src/forms-app-menu-link.tsx +3 -2
- package/src/forms-page/FormsPage.tsx +129 -0
- package/src/forms-page/forms-table/FormsTable.tsx +131 -0
- package/src/forms-page/forms-table/index.ts +3 -0
- package/src/forms-page/forms-table/styles.scss +20 -0
- package/src/forms-page/index.ts +3 -0
- package/src/forms-page/styles.scss +11 -0
- package/src/group-form-entry-workflow/GroupFormEntryWorkflow.tsx +413 -0
- package/src/group-form-entry-workflow/group-display-header/GroupDisplayHeader.test.tsx +9 -0
- package/src/group-form-entry-workflow/group-display-header/GroupDisplayHeader.tsx +71 -0
- package/src/group-form-entry-workflow/group-display-header/index.ts +3 -0
- package/src/group-form-entry-workflow/group-display-header/styles.scss +60 -0
- package/src/group-form-entry-workflow/group-search/CompactGroupResults.tsx +139 -0
- package/src/group-form-entry-workflow/group-search/CompactGroupSearch.tsx +68 -0
- package/src/group-form-entry-workflow/group-search/GroupSearch.tsx +150 -0
- package/src/group-form-entry-workflow/group-search/compact-group-result.scss +64 -0
- package/src/group-form-entry-workflow/group-search/compact-group-search.scss +35 -0
- package/src/group-form-entry-workflow/group-search/group-search.scss +96 -0
- package/src/group-form-entry-workflow/group-search-header/GroupSearchHeader.tsx +49 -0
- package/src/group-form-entry-workflow/group-search-header/index.ts +3 -0
- package/src/group-form-entry-workflow/group-search-header/styles.scss +20 -0
- package/src/group-form-entry-workflow/index.ts +3 -0
- package/src/group-form-entry-workflow/styles.scss +96 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/useFormState.ts +23 -0
- package/src/hooks/useGetAllForms.ts +45 -0
- package/src/hooks/useGetEncounter.ts +21 -0
- package/src/hooks/useGetPatient.ts +23 -0
- package/src/hooks/useKeyPress.ts +31 -0
- package/src/hooks/usePostEndpoint.ts +70 -0
- package/src/hooks/useSearchEndpoint.ts +120 -0
- package/src/index.ts +20 -4
- package/src/patient-card/PatientCard.tsx +67 -0
- package/src/patient-card/index.ts +3 -0
- package/src/patient-card/styles.scss +45 -0
- package/translations/en.json +54 -4
- package/tsconfig.json +26 -23
- package/.eslintrc +0 -4
- package/.github/workflows/node.js.yml +0 -79
- package/.husky/pre-commit +0 -6
- package/dist/24.js +0 -3
- package/dist/24.js.LICENSE.txt +0 -16
- package/dist/24.js.map +0 -1
- package/dist/294.js +0 -3
- package/dist/294.js.LICENSE.txt +0 -14
- package/dist/294.js.map +0 -1
- package/dist/296.js +0 -2
- package/dist/296.js.map +0 -1
- package/dist/299.js +0 -2
- package/dist/299.js.map +0 -1
- package/dist/382.js +0 -3
- package/dist/382.js.LICENSE.txt +0 -8
- package/dist/382.js.map +0 -1
- package/dist/415.js +0 -2
- package/dist/415.js.map +0 -1
- package/dist/574.js +0 -1
- package/dist/595.js +0 -3
- package/dist/595.js.LICENSE.txt +0 -1
- package/dist/595.js.map +0 -1
- package/dist/69.js +0 -2
- package/dist/69.js.map +0 -1
- package/dist/735.js +0 -3
- package/dist/735.js.LICENSE.txt +0 -29
- package/dist/735.js.map +0 -1
- package/dist/777.js +0 -2
- package/dist/777.js.map +0 -1
- package/dist/860.js +0 -2
- package/dist/860.js.map +0 -1
- package/dist/906.js +0 -2
- package/dist/906.js.map +0 -1
- package/dist/openmrs-esm-fast-data-entry-app.js.buildmanifest.json +0 -369
- package/dist/openmrs-esm-fast-data-entry-app.js.map +0 -1
- package/dist/openmrs-esm-fast-data-entry-app.old +0 -2
- package/src/boxes/extensions/blue-box.tsx +0 -15
- package/src/boxes/extensions/box.scss +0 -23
- package/src/boxes/extensions/brand-box.tsx +0 -15
- package/src/boxes/extensions/red-box.tsx +0 -15
- package/src/boxes/slot/boxes.css +0 -23
- package/src/boxes/slot/boxes.tsx +0 -19
- package/src/forms/FormsRoot.tsx +0 -32
- package/src/forms/FormsTable.tsx +0 -64
- package/src/forms/mockData.ts +0 -43
- package/src/greeter/greeter.css +0 -4
- package/src/greeter/greeter.test.tsx +0 -29
- package/src/greeter/greeter.tsx +0 -25
- package/src/hello.css +0 -3
- package/src/hello.test.tsx +0 -45
- package/src/hello.tsx +0 -30
- package/src/patient-getter/patient-getter.resource.ts +0 -31
- package/src/patient-getter/patient-getter.test.tsx +0 -28
- package/src/patient-getter/patient-getter.tsx +0 -28
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ExtensionSlot,
|
|
3
|
+
getGlobalStore,
|
|
4
|
+
useStore,
|
|
5
|
+
} from "@openmrs/esm-framework";
|
|
6
|
+
import {
|
|
7
|
+
Button,
|
|
8
|
+
ComposedModal,
|
|
9
|
+
ModalBody,
|
|
10
|
+
ModalFooter,
|
|
11
|
+
ModalHeader,
|
|
12
|
+
Layer,
|
|
13
|
+
Tile,
|
|
14
|
+
TextInput,
|
|
15
|
+
TextArea,
|
|
16
|
+
DatePicker,
|
|
17
|
+
DatePickerInput,
|
|
18
|
+
} from "@carbon/react";
|
|
19
|
+
import React, { useContext, useEffect, useState } from "react";
|
|
20
|
+
import { useNavigate } from "react-router-dom";
|
|
21
|
+
import PatientCard from "../patient-card/PatientCard";
|
|
22
|
+
import GroupDisplayHeader from "./group-display-header";
|
|
23
|
+
import styles from "./styles.scss";
|
|
24
|
+
import { useTranslation } from "react-i18next";
|
|
25
|
+
import GroupFormWorkflowContext, {
|
|
26
|
+
GroupFormWorkflowProvider,
|
|
27
|
+
} from "../context/GroupFormWorkflowContext";
|
|
28
|
+
import GroupSearchHeader from "./group-search-header";
|
|
29
|
+
import {
|
|
30
|
+
Controller,
|
|
31
|
+
FormProvider,
|
|
32
|
+
useForm,
|
|
33
|
+
useFormContext,
|
|
34
|
+
} from "react-hook-form";
|
|
35
|
+
import FormBootstrap from "../FormBootstrap";
|
|
36
|
+
|
|
37
|
+
const formStore = getGlobalStore("ampath-form-state");
|
|
38
|
+
|
|
39
|
+
const CancelModal = ({ open, setOpen }) => {
|
|
40
|
+
const { destroySession, closeSession } = useContext(GroupFormWorkflowContext);
|
|
41
|
+
const { t } = useTranslation();
|
|
42
|
+
const navigate = useNavigate();
|
|
43
|
+
|
|
44
|
+
const discard = async () => {
|
|
45
|
+
await destroySession();
|
|
46
|
+
setOpen(false);
|
|
47
|
+
navigate("../");
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const saveAndClose = async () => {
|
|
51
|
+
await closeSession();
|
|
52
|
+
setOpen(false);
|
|
53
|
+
navigate("../");
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<ComposedModal open={open}>
|
|
58
|
+
<ModalHeader>{t("areYouSure", "Are you sure?")}</ModalHeader>
|
|
59
|
+
<ModalBody>
|
|
60
|
+
{t(
|
|
61
|
+
"cancelExplanation",
|
|
62
|
+
"You will lose any unsaved changes on the current form. Do you want to discard the current session?"
|
|
63
|
+
)}
|
|
64
|
+
</ModalBody>
|
|
65
|
+
<ModalFooter>
|
|
66
|
+
<Button kind="secondary" onClick={() => setOpen(false)}>
|
|
67
|
+
{t("cancel", "Cancel")}
|
|
68
|
+
</Button>
|
|
69
|
+
<Button kind="danger" onClick={discard}>
|
|
70
|
+
{t("discard", "Discard")}
|
|
71
|
+
</Button>
|
|
72
|
+
<Button kind="primary" onClick={saveAndClose}>
|
|
73
|
+
{t("saveSession", "Save Session")}
|
|
74
|
+
</Button>
|
|
75
|
+
</ModalFooter>
|
|
76
|
+
</ComposedModal>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const CompleteModal = ({ open, setOpen }) => {
|
|
81
|
+
const { submitForComplete } = useContext(GroupFormWorkflowContext);
|
|
82
|
+
const { t } = useTranslation();
|
|
83
|
+
|
|
84
|
+
const completeSession = () => {
|
|
85
|
+
submitForComplete();
|
|
86
|
+
setOpen(false);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<ComposedModal open={open}>
|
|
91
|
+
<ModalHeader>{t("areYouSure", "Are you sure?")}</ModalHeader>
|
|
92
|
+
<ModalBody>
|
|
93
|
+
{t(
|
|
94
|
+
"saveExplanation",
|
|
95
|
+
"Do you want to save the current form and exit the workflow?"
|
|
96
|
+
)}
|
|
97
|
+
</ModalBody>
|
|
98
|
+
<ModalFooter>
|
|
99
|
+
<Button kind="secondary" onClick={() => setOpen(false)}>
|
|
100
|
+
{t("cancel", "Cancel")}
|
|
101
|
+
</Button>
|
|
102
|
+
<Button kind="primary" onClick={completeSession}>
|
|
103
|
+
{t("complete", "Complete")}
|
|
104
|
+
</Button>
|
|
105
|
+
</ModalFooter>
|
|
106
|
+
</ComposedModal>
|
|
107
|
+
);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const NewGroupWorkflowButtons = () => {
|
|
111
|
+
const { t } = useTranslation();
|
|
112
|
+
const { workflowState } = useContext(GroupFormWorkflowContext);
|
|
113
|
+
const [cancelModalOpen, setCancelModalOpen] = useState(false);
|
|
114
|
+
if (workflowState !== "NEW_GROUP_SESSION") return null;
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<>
|
|
118
|
+
<div className={styles.rightPanelActionButtons}>
|
|
119
|
+
<Button kind="secondary" type="submit">
|
|
120
|
+
{t("createNewSession", "Create New Session")}
|
|
121
|
+
</Button>
|
|
122
|
+
<Button
|
|
123
|
+
kind="tertiary"
|
|
124
|
+
onClick={() => {
|
|
125
|
+
setCancelModalOpen(true);
|
|
126
|
+
}}
|
|
127
|
+
>
|
|
128
|
+
{t("cancel", "Cancel")}
|
|
129
|
+
</Button>
|
|
130
|
+
</div>
|
|
131
|
+
<CancelModal open={cancelModalOpen} setOpen={setCancelModalOpen} />
|
|
132
|
+
</>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const WorkflowNavigationButtons = () => {
|
|
137
|
+
const { activeFormUuid, submitForNext, patientUuids, activePatientUuid } =
|
|
138
|
+
useContext(GroupFormWorkflowContext);
|
|
139
|
+
const store = useStore(formStore);
|
|
140
|
+
const formState = store[activeFormUuid];
|
|
141
|
+
const navigationDisabled = formState !== "ready";
|
|
142
|
+
const [cancelModalOpen, setCancelModalOpen] = useState(false);
|
|
143
|
+
const [completeModalOpen, setCompleteModalOpen] = useState(false);
|
|
144
|
+
const { t } = useTranslation();
|
|
145
|
+
|
|
146
|
+
const isLastPatient =
|
|
147
|
+
activePatientUuid === patientUuids[patientUuids.length - 1];
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<>
|
|
151
|
+
<div className={styles.rightPanelActionButtons}>
|
|
152
|
+
<Button
|
|
153
|
+
kind="primary"
|
|
154
|
+
onClick={() => submitForNext()}
|
|
155
|
+
disabled={navigationDisabled}
|
|
156
|
+
>
|
|
157
|
+
{isLastPatient
|
|
158
|
+
? t("saveForm", "Save Form")
|
|
159
|
+
: t("nextPatient", "Next Patient")}
|
|
160
|
+
</Button>
|
|
161
|
+
<Button kind="secondary" onClick={() => setCompleteModalOpen(true)}>
|
|
162
|
+
{t("saveAndComplete", "Save & Complete")}
|
|
163
|
+
</Button>
|
|
164
|
+
<Button kind="tertiary" onClick={() => setCancelModalOpen(true)}>
|
|
165
|
+
{t("cancel", "Cancel")}
|
|
166
|
+
</Button>
|
|
167
|
+
</div>
|
|
168
|
+
<CancelModal open={cancelModalOpen} setOpen={setCancelModalOpen} />
|
|
169
|
+
<CompleteModal open={completeModalOpen} setOpen={setCompleteModalOpen} />
|
|
170
|
+
</>
|
|
171
|
+
);
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const SessionDetails = () => {
|
|
175
|
+
const { t } = useTranslation();
|
|
176
|
+
const {
|
|
177
|
+
register,
|
|
178
|
+
formState: { errors },
|
|
179
|
+
control,
|
|
180
|
+
} = useFormContext();
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<div className={styles.formSection}>
|
|
184
|
+
<h4>{t("sessionDetails", "Session details")}</h4>
|
|
185
|
+
<div>
|
|
186
|
+
<p>
|
|
187
|
+
{t(
|
|
188
|
+
"allFieldsRequired",
|
|
189
|
+
"All fields are required unless marked optional"
|
|
190
|
+
)}
|
|
191
|
+
</p>
|
|
192
|
+
</div>
|
|
193
|
+
<Layer>
|
|
194
|
+
<Tile className={styles.formSectionTile}>
|
|
195
|
+
<Layer>
|
|
196
|
+
<div
|
|
197
|
+
style={{
|
|
198
|
+
display: "flex",
|
|
199
|
+
flexDirection: "column",
|
|
200
|
+
rowGap: "1.5rem",
|
|
201
|
+
}}
|
|
202
|
+
>
|
|
203
|
+
<TextInput
|
|
204
|
+
id="text"
|
|
205
|
+
type="text"
|
|
206
|
+
labelText={t("sessionName", "Session Name")}
|
|
207
|
+
{...register("sessionName", { required: true })}
|
|
208
|
+
invalid={errors.sessionName}
|
|
209
|
+
invalidText={"This field is required"}
|
|
210
|
+
/>
|
|
211
|
+
<TextInput
|
|
212
|
+
id="text"
|
|
213
|
+
type="text"
|
|
214
|
+
labelText={t("practitionerName", "Practitioner Name")}
|
|
215
|
+
{...register("practitionerName", { required: true })}
|
|
216
|
+
invalid={errors.practitionerName}
|
|
217
|
+
invalidText={"This field is required"}
|
|
218
|
+
/>
|
|
219
|
+
<Controller
|
|
220
|
+
name="sessionDate"
|
|
221
|
+
control={control}
|
|
222
|
+
rules={{ required: true }}
|
|
223
|
+
render={({ field }) => (
|
|
224
|
+
<DatePicker
|
|
225
|
+
datePickerType="single"
|
|
226
|
+
size="md"
|
|
227
|
+
maxDate={new Date()}
|
|
228
|
+
{...field}
|
|
229
|
+
>
|
|
230
|
+
<DatePickerInput
|
|
231
|
+
id="session-date"
|
|
232
|
+
labelText={t("sessionDate", "Session Date")}
|
|
233
|
+
placeholder="mm/dd/yyyy"
|
|
234
|
+
size="md"
|
|
235
|
+
invalid={errors.sessionDate}
|
|
236
|
+
invalidText={"This field is required"}
|
|
237
|
+
/>
|
|
238
|
+
</DatePicker>
|
|
239
|
+
)}
|
|
240
|
+
/>
|
|
241
|
+
<TextArea
|
|
242
|
+
id="text"
|
|
243
|
+
type="text"
|
|
244
|
+
labelText={t("sessionNotes", "Session Notes")}
|
|
245
|
+
{...register("sessionNotes", { required: true })}
|
|
246
|
+
invalid={errors.sessionNotes}
|
|
247
|
+
invalidText={"This field is required"}
|
|
248
|
+
/>
|
|
249
|
+
</div>
|
|
250
|
+
</Layer>
|
|
251
|
+
</Tile>
|
|
252
|
+
</Layer>
|
|
253
|
+
</div>
|
|
254
|
+
);
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const GroupIdField = () => {
|
|
258
|
+
const { t } = useTranslation();
|
|
259
|
+
const {
|
|
260
|
+
register,
|
|
261
|
+
formState: { errors },
|
|
262
|
+
setValue,
|
|
263
|
+
} = useFormContext();
|
|
264
|
+
const { activeGroupUuid } = useContext(GroupFormWorkflowContext);
|
|
265
|
+
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
if (activeGroupUuid) setValue("groupUuid", activeGroupUuid);
|
|
268
|
+
}, [activeGroupUuid, setValue]);
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<>
|
|
272
|
+
<input
|
|
273
|
+
hidden
|
|
274
|
+
{...register("groupUuid", {
|
|
275
|
+
value: activeGroupUuid,
|
|
276
|
+
required: t("chooseGroupError", "Please choose a group."),
|
|
277
|
+
})}
|
|
278
|
+
/>
|
|
279
|
+
{errors.groupUuid && !activeGroupUuid && (
|
|
280
|
+
<div className={styles.formError}>
|
|
281
|
+
{errors.groupUuid.message as string}
|
|
282
|
+
</div>
|
|
283
|
+
)}
|
|
284
|
+
</>
|
|
285
|
+
);
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
const SessionMetaWorkspace = () => {
|
|
289
|
+
const { t } = useTranslation();
|
|
290
|
+
const { setSessionMeta } = useContext(GroupFormWorkflowContext);
|
|
291
|
+
const methods = useForm();
|
|
292
|
+
|
|
293
|
+
const onSubmit = (data) => {
|
|
294
|
+
const { sessionDate, ...rest } = data;
|
|
295
|
+
setSessionMeta({ ...rest, sessionDate: sessionDate[0] });
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
return (
|
|
299
|
+
<FormProvider {...methods}>
|
|
300
|
+
<form onSubmit={methods.handleSubmit(onSubmit)}>
|
|
301
|
+
<div className={styles.workspace}>
|
|
302
|
+
<div className={styles.formMainContent}>
|
|
303
|
+
<div className={styles.formContainer}>
|
|
304
|
+
<SessionDetails />
|
|
305
|
+
</div>
|
|
306
|
+
<div className={styles.rightPanel}>
|
|
307
|
+
<h4>{t("newGroupSession", "New Group Session")}</h4>
|
|
308
|
+
<GroupIdField />
|
|
309
|
+
<hr style={{ width: "100%" }} />
|
|
310
|
+
<NewGroupWorkflowButtons />
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
</form>
|
|
315
|
+
</FormProvider>
|
|
316
|
+
);
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const GroupSessionWorkspace = () => {
|
|
320
|
+
const { t } = useTranslation();
|
|
321
|
+
const {
|
|
322
|
+
patientUuids,
|
|
323
|
+
activePatientUuid,
|
|
324
|
+
editEncounter,
|
|
325
|
+
encounters,
|
|
326
|
+
activeEncounterUuid,
|
|
327
|
+
activeFormUuid,
|
|
328
|
+
saveEncounter,
|
|
329
|
+
// activeSessionMeta,
|
|
330
|
+
} = useContext(GroupFormWorkflowContext);
|
|
331
|
+
|
|
332
|
+
// const handleEncounterCreate = (payload: Record<string, unknown>) => {
|
|
333
|
+
// console.log("payload", payload);
|
|
334
|
+
// Object.entries(activeSessionMeta).forEach((key, value) => {
|
|
335
|
+
// payload[key as unknown as string] = value;
|
|
336
|
+
// });
|
|
337
|
+
// };
|
|
338
|
+
|
|
339
|
+
const handlePostResponse = (encounter) => {
|
|
340
|
+
if (encounter && encounter.uuid) {
|
|
341
|
+
saveEncounter(encounter.uuid);
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
return (
|
|
346
|
+
<div className={styles.workspace}>
|
|
347
|
+
<div className={styles.formMainContent}>
|
|
348
|
+
<div className={styles.formContainer}>
|
|
349
|
+
<FormBootstrap
|
|
350
|
+
patientUuid={activePatientUuid}
|
|
351
|
+
encounterUuid={activeEncounterUuid}
|
|
352
|
+
{...{
|
|
353
|
+
formUuid: activeFormUuid,
|
|
354
|
+
handlePostResponse,
|
|
355
|
+
// handleEncounterCreate,
|
|
356
|
+
}}
|
|
357
|
+
/>
|
|
358
|
+
</div>
|
|
359
|
+
<div className={styles.rightPanel}>
|
|
360
|
+
<h4>{t("formsFilled", "Forms filled")}</h4>
|
|
361
|
+
<div className={styles.patientCardsSection}>
|
|
362
|
+
{patientUuids?.map((patientUuid) => (
|
|
363
|
+
<PatientCard
|
|
364
|
+
key={patientUuid}
|
|
365
|
+
{...{
|
|
366
|
+
patientUuid,
|
|
367
|
+
activePatientUuid,
|
|
368
|
+
editEncounter,
|
|
369
|
+
encounters,
|
|
370
|
+
}}
|
|
371
|
+
/>
|
|
372
|
+
))}
|
|
373
|
+
</div>
|
|
374
|
+
<WorkflowNavigationButtons />
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
);
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const GroupFormEntryWorkflow = () => {
|
|
382
|
+
const { workflowState } = useContext(GroupFormWorkflowContext);
|
|
383
|
+
|
|
384
|
+
return (
|
|
385
|
+
<>
|
|
386
|
+
<div className={styles.breadcrumbsContainer}>
|
|
387
|
+
<ExtensionSlot extensionSlotName="breadcrumbs-slot" />
|
|
388
|
+
</div>
|
|
389
|
+
<GroupSearchHeader />
|
|
390
|
+
<GroupDisplayHeader />
|
|
391
|
+
{workflowState === "NEW_GROUP_SESSION" && (
|
|
392
|
+
<div className={styles.workspaceWrapper}>
|
|
393
|
+
<SessionMetaWorkspace />
|
|
394
|
+
</div>
|
|
395
|
+
)}
|
|
396
|
+
{["EDIT_FORM"].includes(workflowState) && (
|
|
397
|
+
<div className={styles.workspaceWrapper}>
|
|
398
|
+
<GroupSessionWorkspace />
|
|
399
|
+
</div>
|
|
400
|
+
)}
|
|
401
|
+
</>
|
|
402
|
+
);
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const GroupFormEntryWorkflowWrapper = () => {
|
|
406
|
+
return (
|
|
407
|
+
<GroupFormWorkflowProvider>
|
|
408
|
+
<GroupFormEntryWorkflow />
|
|
409
|
+
</GroupFormWorkflowProvider>
|
|
410
|
+
);
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
export default GroupFormEntryWorkflowWrapper;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import GroupDisplayHeader from "./GroupDisplayHeader";
|
|
4
|
+
|
|
5
|
+
describe("PatientBanner", () => {
|
|
6
|
+
it("renders placeholder information when no data is present", () => {
|
|
7
|
+
render(<GroupDisplayHeader />);
|
|
8
|
+
});
|
|
9
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React, { useContext } from "react";
|
|
2
|
+
import { Button } from "@carbon/react";
|
|
3
|
+
import { Events, Close } from "@carbon/react/icons";
|
|
4
|
+
import styles from "./styles.scss";
|
|
5
|
+
import { useTranslation } from "react-i18next";
|
|
6
|
+
import GroupFormWorkflowContext from "../../context/GroupFormWorkflowContext";
|
|
7
|
+
import { navigate } from "@openmrs/esm-framework";
|
|
8
|
+
|
|
9
|
+
const GroupDisplayHeader = () => {
|
|
10
|
+
const {
|
|
11
|
+
activeGroupName,
|
|
12
|
+
activeGroupUuid,
|
|
13
|
+
patientUuids,
|
|
14
|
+
activeSessionMeta,
|
|
15
|
+
unsetGroup,
|
|
16
|
+
destroySession,
|
|
17
|
+
} = useContext(GroupFormWorkflowContext);
|
|
18
|
+
const { t } = useTranslation();
|
|
19
|
+
|
|
20
|
+
if (!activeGroupUuid) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className={styles.container}>
|
|
26
|
+
<div className={styles.groupAvatar} role="img">
|
|
27
|
+
<Events size={48} />
|
|
28
|
+
</div>
|
|
29
|
+
<div className={styles.groupInfoContent}>
|
|
30
|
+
<div className={styles.groupInfoRow}>
|
|
31
|
+
<span className={styles.groupName}>{activeGroupName}</span>
|
|
32
|
+
</div>
|
|
33
|
+
<div className={styles.groupInfoRow}>
|
|
34
|
+
<span>
|
|
35
|
+
{patientUuids.length} {t("members", "members")}
|
|
36
|
+
</span>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
{activeSessionMeta?.sessionNotes && (
|
|
40
|
+
<div className={styles.groupMeataContent}>
|
|
41
|
+
<div className={`${styles.groupInfoRow} ${styles.sessionNotesLabel}`}>
|
|
42
|
+
{t("sessionNotes", "Session Notes")}
|
|
43
|
+
</div>
|
|
44
|
+
<div className={styles.groupInfoRow}>
|
|
45
|
+
{activeSessionMeta.sessionNotes}
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
)}
|
|
49
|
+
<span style={{ flexGrow: 1 }} />
|
|
50
|
+
<span>
|
|
51
|
+
<Button kind="ghost" onClick={() => unsetGroup()}>
|
|
52
|
+
{t("changeGroup", "Choose a different group")} <Close size={20} />
|
|
53
|
+
</Button>
|
|
54
|
+
</span>
|
|
55
|
+
<span>
|
|
56
|
+
<Button
|
|
57
|
+
kind="ghost"
|
|
58
|
+
onClick={() => {
|
|
59
|
+
destroySession();
|
|
60
|
+
// eslint-disable-next-line
|
|
61
|
+
navigate({ to: "${openmrsSpaBase}/forms" });
|
|
62
|
+
}}
|
|
63
|
+
>
|
|
64
|
+
{t("cancel", "Cancel")} <Close size={20} />
|
|
65
|
+
</Button>
|
|
66
|
+
</span>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export default GroupDisplayHeader;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
@use '@carbon/styles/scss/spacing';
|
|
2
|
+
@use '@carbon/styles/scss/type';
|
|
3
|
+
@import '~@openmrs/esm-styleguide/src/vars';
|
|
4
|
+
|
|
5
|
+
.container {
|
|
6
|
+
height: spacing.$spacing-11;
|
|
7
|
+
display: flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
background-color: $ui-02;
|
|
10
|
+
border-top: 0.0125rem solid $ui-03;
|
|
11
|
+
border-bottom: 0.0125rem solid $ui-03;
|
|
12
|
+
padding: 0 spacing.$spacing-05;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.photoPlaceholder {
|
|
16
|
+
height: spacing.$spacing-09;
|
|
17
|
+
width: spacing.$spacing-09;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.groupAvatar {
|
|
21
|
+
width: spacing.$spacing-09;
|
|
22
|
+
height: spacing.$spacing-09;
|
|
23
|
+
margin: spacing.$spacing-03 spacing.$spacing-05 spacing.$spacing-03 0;
|
|
24
|
+
border-radius: 1px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.groupName {
|
|
28
|
+
@include type.type-style('heading-03');
|
|
29
|
+
font-weight: 600;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.groupInfoContent {
|
|
33
|
+
margin-left: spacing.$spacing-05;
|
|
34
|
+
margin-right: spacing.$spacing-10;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.groupInfoRow {
|
|
38
|
+
display: flex;
|
|
39
|
+
align-items: center;
|
|
40
|
+
& > button {
|
|
41
|
+
min-height: spacing.$spacing-07;
|
|
42
|
+
}
|
|
43
|
+
@include type.type-style('body-compact-02');
|
|
44
|
+
color: $text-02;
|
|
45
|
+
column-gap: 0.8rem;
|
|
46
|
+
|
|
47
|
+
}
|
|
48
|
+
.sessionNotesLabel {
|
|
49
|
+
@include type.type-style('label-01');
|
|
50
|
+
margin-bottom: spacing.$spacing-01
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.groupEditBtn {
|
|
54
|
+
color: $ui-05;
|
|
55
|
+
margin: spacing.$spacing-03;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.groupMetaContent {
|
|
59
|
+
flex-grow: 1;
|
|
60
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import React, { useEffect, useReducer, useRef } from "react";
|
|
2
|
+
import { SkeletonIcon, SkeletonText } from "@carbon/react";
|
|
3
|
+
import { Events } from "@carbon/react/icons";
|
|
4
|
+
import styles from "./compact-group-result.scss";
|
|
5
|
+
import { useTranslation } from "react-i18next";
|
|
6
|
+
import useKeyPress from "../../hooks/useKeyPress";
|
|
7
|
+
|
|
8
|
+
const reducer = (state, action) => {
|
|
9
|
+
switch (action.type) {
|
|
10
|
+
case "arrowUp":
|
|
11
|
+
return { selectedIndex: Math.max(state.selectedIndex - 1, 0) };
|
|
12
|
+
case "arrowDown":
|
|
13
|
+
return {
|
|
14
|
+
selectedIndex: Math.min(state.selectedIndex + 1, action.listLength - 1),
|
|
15
|
+
};
|
|
16
|
+
case "select":
|
|
17
|
+
return { selectedIndex: action.payload };
|
|
18
|
+
default:
|
|
19
|
+
return state;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const scrollingOptions = {
|
|
24
|
+
behavior: "smooth",
|
|
25
|
+
block: "nearest",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const ResultItem = ({
|
|
29
|
+
index,
|
|
30
|
+
selectGroupAction,
|
|
31
|
+
group,
|
|
32
|
+
dispatch,
|
|
33
|
+
state,
|
|
34
|
+
totalGroups,
|
|
35
|
+
lastRef,
|
|
36
|
+
}) => {
|
|
37
|
+
const ref = useRef(null);
|
|
38
|
+
const { t } = useTranslation();
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (state.selectedIndex === totalGroups - 1) {
|
|
42
|
+
lastRef.current.scrollIntoView(scrollingOptions);
|
|
43
|
+
} else if (state.selectedIndex === index) {
|
|
44
|
+
ref.current.scrollIntoView(scrollingOptions);
|
|
45
|
+
}
|
|
46
|
+
}, [state, index, totalGroups, lastRef]);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
onClick={() => {
|
|
51
|
+
dispatch({ type: "select", payload: index });
|
|
52
|
+
selectGroupAction(group);
|
|
53
|
+
}}
|
|
54
|
+
className={`${styles.patientSearchResult} ${
|
|
55
|
+
index === state.selectedIndex && styles.patientSearchResultSelected
|
|
56
|
+
}`}
|
|
57
|
+
role="button"
|
|
58
|
+
aria-pressed={index === state.selectedIndex}
|
|
59
|
+
tabIndex={0}
|
|
60
|
+
ref={ref}
|
|
61
|
+
>
|
|
62
|
+
<div className={styles.patientAvatar} role="img">
|
|
63
|
+
<Events size={24} />
|
|
64
|
+
</div>
|
|
65
|
+
<div>
|
|
66
|
+
<h2 className={styles.patientName}>{group.name}</h2>
|
|
67
|
+
<p className={styles.demographics}>
|
|
68
|
+
{group.cohortMembers?.length ?? 0} {t("members", "members")}
|
|
69
|
+
<span className={styles.middot}>·</span> {group.description}
|
|
70
|
+
</p>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const CompactGroupResults = ({ groups, selectGroupAction, lastRef }) => {
|
|
77
|
+
const arrowUpPressed = useKeyPress("ArrowUp");
|
|
78
|
+
const arrowDownPressed = useKeyPress("ArrowDown");
|
|
79
|
+
const enterPressed = useKeyPress("Enter");
|
|
80
|
+
|
|
81
|
+
const [state, dispatch] = useReducer(reducer, { selectedIndex: 0 });
|
|
82
|
+
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
if (arrowUpPressed) {
|
|
85
|
+
dispatch({ type: "arrowUp" });
|
|
86
|
+
}
|
|
87
|
+
}, [arrowUpPressed]);
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (arrowDownPressed) {
|
|
91
|
+
dispatch({ type: "arrowDown", listLength: groups.length });
|
|
92
|
+
}
|
|
93
|
+
}, [arrowDownPressed, groups.length]);
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (enterPressed && groups.length) {
|
|
97
|
+
selectGroupAction(groups[state.selectedIndex]);
|
|
98
|
+
}
|
|
99
|
+
}, [enterPressed, selectGroupAction, groups, state.selectedIndex]);
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<>
|
|
103
|
+
{groups.map((group, index) => (
|
|
104
|
+
<ResultItem
|
|
105
|
+
key={index}
|
|
106
|
+
totalGroups={groups.length}
|
|
107
|
+
{...{ lastRef, index, selectGroupAction, group, dispatch, state }}
|
|
108
|
+
/>
|
|
109
|
+
))}
|
|
110
|
+
</>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const SearchResultSkeleton = () => {
|
|
115
|
+
return (
|
|
116
|
+
<div className={styles.patientSearchResult}>
|
|
117
|
+
<div className={styles.patientAvatar} role="img">
|
|
118
|
+
<SkeletonIcon
|
|
119
|
+
style={{
|
|
120
|
+
height: "3rem",
|
|
121
|
+
width: "3rem",
|
|
122
|
+
}}
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
<div>
|
|
126
|
+
<h2 className={styles.patientName}>
|
|
127
|
+
<SkeletonText />
|
|
128
|
+
</h2>
|
|
129
|
+
<span className={styles.demographics}>
|
|
130
|
+
<SkeletonIcon /> <span className={styles.middot}>·</span>{" "}
|
|
131
|
+
<SkeletonIcon /> <span className={styles.middot}>·</span>{" "}
|
|
132
|
+
<SkeletonIcon />
|
|
133
|
+
</span>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export default CompactGroupResults;
|