@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.
- package/README.md +58 -12
- package/dist/153.js +1 -0
- package/dist/153.js.map +1 -0
- package/dist/233.js +2 -0
- package/dist/{382.js.LICENSE.txt → 233.js.LICENSE.txt} +3 -2
- package/dist/233.js.map +1 -0
- package/dist/262.js +1 -0
- package/dist/262.js.map +1 -0
- package/dist/279.js +1 -0
- package/dist/279.js.map +1 -0
- package/dist/294.js +1 -2
- package/dist/294.js.LICENSE.txt +2 -7
- package/dist/294.js.map +1 -1
- package/dist/327.js +1 -0
- package/dist/327.js.map +1 -0
- package/dist/409.js +2 -0
- package/dist/409.js.LICENSE.txt +27 -0
- package/dist/409.js.map +1 -0
- package/dist/415.js +1 -2
- package/dist/415.js.map +1 -1
- package/dist/559.js +1 -0
- package/dist/559.js.map +1 -0
- package/dist/574.js +1 -1
- package/dist/651.js +1 -0
- package/dist/651.js.map +1 -0
- package/dist/706.js +1 -0
- package/dist/706.js.map +1 -0
- package/dist/757.js +1 -0
- package/dist/800.js +2 -0
- package/dist/800.js.LICENSE.txt +5 -0
- package/dist/800.js.map +1 -0
- package/dist/820.js +1 -0
- package/dist/820.js.map +1 -0
- package/dist/883.js +1 -0
- package/dist/883.js.map +1 -0
- package/dist/889.js +1 -0
- package/dist/889.js.map +1 -0
- package/dist/897.js +2 -0
- package/dist/897.js.LICENSE.txt +21 -0
- package/dist/897.js.map +1 -0
- package/dist/92.js +1 -0
- package/dist/92.js.map +1 -0
- package/dist/935.js +2 -0
- package/dist/{735.js.LICENSE.txt → 935.js.LICENSE.txt} +6 -16
- package/dist/935.js.map +1 -0
- package/dist/959.js +1 -0
- package/dist/959.js.map +1 -0
- package/dist/main.js +1 -0
- package/dist/main.js.map +1 -0
- package/dist/openmrs-esm-fast-data-entry-app.js +1 -1
- package/dist/openmrs-esm-fast-data-entry-app.js.buildmanifest.json +374 -89
- package/dist/openmrs-esm-fast-data-entry-app.js.map +1 -1
- package/dist/routes.json +1 -0
- 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 +21 -18
- package/package.json +100 -106
- package/src/CancelModal.tsx +48 -0
- package/src/CompleteModal.tsx +46 -0
- package/src/FormBootstrap.tsx +166 -0
- package/src/Root.tsx +14 -3
- package/src/add-group-modal/AddGroupModal.tsx +288 -0
- package/src/add-group-modal/styles.scss +45 -0
- package/src/config-schema.ts +85 -31
- package/src/context/FormWorkflowContext.tsx +126 -0
- package/src/context/FormWorkflowReducer.ts +287 -0
- package/src/context/GroupFormWorkflowContext.tsx +176 -0
- package/src/context/GroupFormWorkflowReducer.ts +430 -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 +196 -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 +65 -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 +134 -0
- package/src/forms-page/forms-table/FormsTable.tsx +137 -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 +26 -0
- package/src/group-form-entry-workflow/GroupSessionWorkspace.tsx +247 -0
- package/src/group-form-entry-workflow/SessionDetailsForm.tsx +131 -0
- package/src/group-form-entry-workflow/SessionMetaWorkspace.tsx +107 -0
- package/src/group-form-entry-workflow/attendance-table/AttendanceTable.tsx +144 -0
- package/src/group-form-entry-workflow/attendance-table/index.ts +1 -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 +63 -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 +73 -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 +97 -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/useGetPatients.ts +34 -0
- package/src/hooks/useGetSystemSetting.ts +38 -0
- package/src/hooks/useKeyPress.ts +31 -0
- package/src/hooks/usePostEndpoint.ts +76 -0
- package/src/hooks/useSearchEndpoint.ts +120 -0
- package/src/hooks/useStartVisit.ts +92 -0
- package/src/index.ts +26 -62
- package/src/patient-card/PatientCard.tsx +67 -0
- package/src/patient-card/index.ts +3 -0
- package/src/patient-card/styles.scss +46 -0
- package/src/routes.json +24 -0
- package/tools/i18next-parser.config.js +93 -0
- package/translations/en.json +69 -4
- package/translations/fr.json +50 -0
- package/tsconfig.json +26 -23
- package/.editorconfig +0 -12
- package/.eslintignore +0 -2
- package/.eslintrc +0 -4
- package/.github/workflows/node.js.yml +0 -79
- package/.husky/pre-commit +0 -6
- package/.husky/pre-push +0 -6
- package/.prettierignore +0 -14
- package/dist/24.js +0 -3
- package/dist/24.js.LICENSE.txt +0 -16
- package/dist/24.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.map +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.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.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,131 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Layer,
|
|
3
|
+
Tile,
|
|
4
|
+
TextInput,
|
|
5
|
+
TextArea,
|
|
6
|
+
DatePicker,
|
|
7
|
+
DatePickerInput,
|
|
8
|
+
} from "@carbon/react";
|
|
9
|
+
import React, { useContext } from "react";
|
|
10
|
+
import styles from "./styles.scss";
|
|
11
|
+
import { useTranslation } from "react-i18next";
|
|
12
|
+
import { Controller, useFormContext } from "react-hook-form";
|
|
13
|
+
import { AttendanceTable } from "./attendance-table";
|
|
14
|
+
import GroupFormWorkflowContext from "../context/GroupFormWorkflowContext";
|
|
15
|
+
import useGetPatients from "../hooks/useGetPatients";
|
|
16
|
+
|
|
17
|
+
const SessionDetailsForm = () => {
|
|
18
|
+
const { t } = useTranslation();
|
|
19
|
+
const {
|
|
20
|
+
register,
|
|
21
|
+
formState: { errors },
|
|
22
|
+
control,
|
|
23
|
+
} = useFormContext();
|
|
24
|
+
|
|
25
|
+
const { patientUuids } = useContext(GroupFormWorkflowContext);
|
|
26
|
+
const { patients, isLoading } = useGetPatients(patientUuids);
|
|
27
|
+
|
|
28
|
+
return (
|
|
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>
|
|
41
|
+
<Layer>
|
|
42
|
+
<Tile className={styles.formSectionTile}>
|
|
43
|
+
<Layer>
|
|
44
|
+
<div
|
|
45
|
+
style={{
|
|
46
|
+
display: "flex",
|
|
47
|
+
flexDirection: "column",
|
|
48
|
+
rowGap: "1.5rem",
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
<TextInput
|
|
52
|
+
id="text"
|
|
53
|
+
type="text"
|
|
54
|
+
labelText={t("sessionName", "Session Name")}
|
|
55
|
+
{...register("sessionName", { required: true })}
|
|
56
|
+
invalid={errors.sessionName}
|
|
57
|
+
invalidText={"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>
|
|
100
|
+
</Layer>
|
|
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>
|
|
110
|
+
<Layer>
|
|
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>
|
|
124
|
+
</Layer>
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export default SessionDetailsForm;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { Button } from "@carbon/react";
|
|
2
|
+
import React, { useContext, useEffect, useState } from "react";
|
|
3
|
+
import styles from "./styles.scss";
|
|
4
|
+
import { useTranslation } from "react-i18next";
|
|
5
|
+
import GroupFormWorkflowContext from "../context/GroupFormWorkflowContext";
|
|
6
|
+
import { FormProvider, useForm, useFormContext } from "react-hook-form";
|
|
7
|
+
import CancelModal from "../CancelModal";
|
|
8
|
+
import SessionDetailsForm from "./SessionDetailsForm";
|
|
9
|
+
|
|
10
|
+
const NewGroupWorkflowButtons = () => {
|
|
11
|
+
const { t } = useTranslation();
|
|
12
|
+
const context = useContext(GroupFormWorkflowContext);
|
|
13
|
+
const { workflowState, patientUuids } = context;
|
|
14
|
+
const [cancelModalOpen, setCancelModalOpen] = useState(false);
|
|
15
|
+
if (workflowState !== "NEW_GROUP_SESSION") return null;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<>
|
|
19
|
+
<div className={styles.rightPanelActionButtons}>
|
|
20
|
+
<Button kind="secondary" type="submit" disabled={!patientUuids.length}>
|
|
21
|
+
{t("createNewSession", "Create New Session")}
|
|
22
|
+
</Button>
|
|
23
|
+
<Button
|
|
24
|
+
kind="tertiary"
|
|
25
|
+
onClick={() => {
|
|
26
|
+
setCancelModalOpen(true);
|
|
27
|
+
}}
|
|
28
|
+
>
|
|
29
|
+
{t("cancel", "Cancel")}
|
|
30
|
+
</Button>
|
|
31
|
+
</div>
|
|
32
|
+
<CancelModal
|
|
33
|
+
open={cancelModalOpen}
|
|
34
|
+
setOpen={setCancelModalOpen}
|
|
35
|
+
context={context}
|
|
36
|
+
/>
|
|
37
|
+
</>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const GroupIdField = () => {
|
|
42
|
+
const { t } = useTranslation();
|
|
43
|
+
const {
|
|
44
|
+
register,
|
|
45
|
+
formState: { errors },
|
|
46
|
+
setValue,
|
|
47
|
+
} = useFormContext();
|
|
48
|
+
const { activeGroupUuid } = useContext(GroupFormWorkflowContext);
|
|
49
|
+
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (activeGroupUuid) setValue("groupUuid", activeGroupUuid);
|
|
52
|
+
}, [activeGroupUuid, setValue]);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<>
|
|
56
|
+
<input
|
|
57
|
+
hidden
|
|
58
|
+
{...register("groupUuid", {
|
|
59
|
+
value: activeGroupUuid,
|
|
60
|
+
required: t("chooseGroupError", "Please choose a group."),
|
|
61
|
+
})}
|
|
62
|
+
/>
|
|
63
|
+
{errors.groupUuid && !activeGroupUuid && (
|
|
64
|
+
<div className={styles.formError}>
|
|
65
|
+
{errors.groupUuid.message as string}
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
</>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const SessionMetaWorkspace = () => {
|
|
73
|
+
const { t } = useTranslation();
|
|
74
|
+
const { setSessionMeta, workflowState } = useContext(
|
|
75
|
+
GroupFormWorkflowContext
|
|
76
|
+
);
|
|
77
|
+
const methods = useForm();
|
|
78
|
+
|
|
79
|
+
const onSubmit = (data) => {
|
|
80
|
+
const { sessionDate, ...rest } = data;
|
|
81
|
+
setSessionMeta({ ...rest, sessionDate: sessionDate[0] });
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (workflowState !== "NEW_GROUP_SESSION") return null;
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<FormProvider {...methods}>
|
|
88
|
+
<form onSubmit={methods.handleSubmit(onSubmit)}>
|
|
89
|
+
<div className={styles.workspace}>
|
|
90
|
+
<div className={styles.formMainContent}>
|
|
91
|
+
<div className={styles.formContainer}>
|
|
92
|
+
<SessionDetailsForm />
|
|
93
|
+
</div>
|
|
94
|
+
<div className={styles.rightPanel}>
|
|
95
|
+
<h4>{t("newGroupSession", "New Group Session")}</h4>
|
|
96
|
+
<GroupIdField />
|
|
97
|
+
<hr style={{ width: "100%" }} />
|
|
98
|
+
<NewGroupWorkflowButtons />
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</form>
|
|
103
|
+
</FormProvider>
|
|
104
|
+
);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export default SessionMetaWorkspace;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import React, { useCallback, useContext, useMemo, useState } from "react";
|
|
2
|
+
import { Edit } from "@carbon/react/icons";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
Checkbox,
|
|
6
|
+
SkeletonText,
|
|
7
|
+
Table,
|
|
8
|
+
TableHead,
|
|
9
|
+
TableRow,
|
|
10
|
+
TableHeader,
|
|
11
|
+
TableBody,
|
|
12
|
+
TableCell,
|
|
13
|
+
Button,
|
|
14
|
+
} from "@carbon/react";
|
|
15
|
+
import { useTranslation } from "react-i18next";
|
|
16
|
+
import GroupFormWorkflowContext from "../../context/GroupFormWorkflowContext";
|
|
17
|
+
import AddGroupModal from "../../add-group-modal/AddGroupModal";
|
|
18
|
+
|
|
19
|
+
const PatientRow = ({ patient }) => {
|
|
20
|
+
const { patientUuids, addPatientUuid, removePatientUuid } = useContext(
|
|
21
|
+
GroupFormWorkflowContext
|
|
22
|
+
);
|
|
23
|
+
const givenName = patient?.name?.[0]?.given?.[0];
|
|
24
|
+
const familyName = patient?.name?.[0]?.family;
|
|
25
|
+
const identifier = patient?.identifier?.[0]?.value;
|
|
26
|
+
|
|
27
|
+
const handleOnChange = (e, { checked }) => {
|
|
28
|
+
if (checked) {
|
|
29
|
+
addPatientUuid(patient.id);
|
|
30
|
+
} else {
|
|
31
|
+
removePatientUuid(patient.id);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
if (!patient) {
|
|
36
|
+
return (
|
|
37
|
+
<TableRow>
|
|
38
|
+
<TableCell>
|
|
39
|
+
<SkeletonText />
|
|
40
|
+
</TableCell>
|
|
41
|
+
<TableCell>
|
|
42
|
+
<SkeletonText />
|
|
43
|
+
</TableCell>
|
|
44
|
+
<TableCell>
|
|
45
|
+
<Checkbox diabled />
|
|
46
|
+
</TableCell>
|
|
47
|
+
</TableRow>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<TableRow>
|
|
53
|
+
<TableCell>
|
|
54
|
+
{patient.display ||
|
|
55
|
+
patient.displayName ||
|
|
56
|
+
[givenName, familyName].join(" ")}
|
|
57
|
+
</TableCell>
|
|
58
|
+
<TableCell>{identifier}</TableCell>
|
|
59
|
+
<TableCell>
|
|
60
|
+
<Checkbox
|
|
61
|
+
checked={patientUuids.includes(patient.id)}
|
|
62
|
+
labelText={patient.id}
|
|
63
|
+
hideLabel
|
|
64
|
+
id={`${identifier}-attendance-checkbox`}
|
|
65
|
+
onChange={handleOnChange}
|
|
66
|
+
/>
|
|
67
|
+
</TableCell>
|
|
68
|
+
</TableRow>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const AttendanceTable = ({ patients }) => {
|
|
73
|
+
const { t } = useTranslation();
|
|
74
|
+
const { activeGroupUuid, activeGroupName, activeGroupMembers } = useContext(
|
|
75
|
+
GroupFormWorkflowContext
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const [isOpen, setOpen] = useState(false);
|
|
79
|
+
|
|
80
|
+
const headers = [
|
|
81
|
+
t("name", "Name"),
|
|
82
|
+
t("identifier", "Patient ID"),
|
|
83
|
+
t("patientIsPresent", "Patient is present"),
|
|
84
|
+
];
|
|
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
|
+
|
|
101
|
+
if (!activeGroupUuid) {
|
|
102
|
+
return <div>{t("selectGroupFirst", "Please select a group first")}</div>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<div>
|
|
107
|
+
<span style={{ flexGrow: 1 }} />
|
|
108
|
+
<Button kind="ghost" onClick={() => setOpen(true)}>
|
|
109
|
+
{t("editGroup", "Edit Group")}
|
|
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>
|
|
141
|
+
);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export default AttendanceTable;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as AttendanceTable } from "./AttendanceTable";
|
|
@@ -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,63 @@
|
|
|
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
|
+
|
|
8
|
+
const GroupDisplayHeader = () => {
|
|
9
|
+
const {
|
|
10
|
+
activeGroupName,
|
|
11
|
+
activeGroupUuid,
|
|
12
|
+
patientUuids,
|
|
13
|
+
activeSessionMeta,
|
|
14
|
+
unsetGroup,
|
|
15
|
+
destroySession,
|
|
16
|
+
} = useContext(GroupFormWorkflowContext);
|
|
17
|
+
const { t } = useTranslation();
|
|
18
|
+
|
|
19
|
+
if (!activeGroupUuid) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className={styles.container}>
|
|
25
|
+
<div className={styles.groupAvatar} role="img">
|
|
26
|
+
<Events size={48} />
|
|
27
|
+
</div>
|
|
28
|
+
<div className={styles.groupInfoContent}>
|
|
29
|
+
<div className={styles.groupInfoRow}>
|
|
30
|
+
<span className={styles.groupName}>{activeGroupName}</span>
|
|
31
|
+
</div>
|
|
32
|
+
<div className={styles.groupInfoRow}>
|
|
33
|
+
<span>
|
|
34
|
+
{patientUuids.length} {t("members", "members")}
|
|
35
|
+
</span>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
{activeSessionMeta?.sessionNotes && (
|
|
39
|
+
<div className={styles.groupMeataContent}>
|
|
40
|
+
<div className={`${styles.groupInfoRow} ${styles.sessionNotesLabel}`}>
|
|
41
|
+
{t("sessionNotes", "Session Notes")}
|
|
42
|
+
</div>
|
|
43
|
+
<div className={styles.groupInfoRow}>
|
|
44
|
+
{activeSessionMeta.sessionNotes}
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
)}
|
|
48
|
+
<span style={{ flexGrow: 1 }} />
|
|
49
|
+
<span>
|
|
50
|
+
<Button kind="ghost" onClick={() => unsetGroup()}>
|
|
51
|
+
{t("changeGroup", "Choose a different group")} <Close size={20} />
|
|
52
|
+
</Button>
|
|
53
|
+
</span>
|
|
54
|
+
<span>
|
|
55
|
+
<Button kind="ghost" onClick={() => destroySession()}>
|
|
56
|
+
{t("cancel", "Cancel")} <Close size={20} />
|
|
57
|
+
</Button>
|
|
58
|
+
</span>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
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;
|