@openmrs/esm-fast-data-entry-app 1.0.0-pre.9 → 1.0.1-pre.8
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/.yarn/plugins/@yarnpkg/plugin-version.cjs +550 -0
- package/.yarn/versions/7ee3eceb.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 +97 -106
- package/src/FormBootstrap.tsx +151 -0
- package/src/Root.tsx +14 -3
- package/src/add-group-modal/AddGroupModal.tsx +209 -0
- package/src/add-group-modal/styles.scss +35 -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 +141 -0
- package/src/context/GroupFormWorkflowReducer.ts +272 -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-banner/GroupBanner.test.tsx +9 -0
- package/src/group-form-entry-workflow/group-banner/GroupBanner.tsx +45 -0
- package/src/group-form-entry-workflow/group-banner/index.ts +3 -0
- package/src/group-form-entry-workflow/group-banner/styles.scss +60 -0
- package/src/group-form-entry-workflow/group-search/CompactGroupResults.tsx +106 -0
- package/src/group-form-entry-workflow/group-search/CompactGroupSearch.tsx +63 -0
- package/src/group-form-entry-workflow/group-search/GroupSearch.tsx +93 -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 +94 -0
- package/src/group-form-entry-workflow/group-search/mock-group-data.ts +79 -0
- package/src/group-form-entry-workflow/group-search/useGroupSearch.ts +14 -0
- package/src/group-form-entry-workflow/group-search-header/GroupSearchHeader.tsx +42 -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 +86 -0
- package/src/hooks/index.ts +6 -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/usePostCohort.ts +18 -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 +49 -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,51 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
export const EmptyDataIllustration = ({ width = "64", height = "64" }) => {
|
|
4
|
+
return (
|
|
5
|
+
<svg width={width} height={height} viewBox="0 0 64 64">
|
|
6
|
+
<title>Empty data illustration</title>
|
|
7
|
+
<g fill="none" fillRule="evenodd">
|
|
8
|
+
<path
|
|
9
|
+
d="M38.133 13.186H21.947c-.768.001-1.39.623-1.39 1.391V50.55l-.186.057-3.97 1.216a.743.743 0 01-.927-.493L3.664 12.751a.742.742 0 01.492-.926l6.118-1.874 17.738-5.43 6.119-1.873a.741.741 0 01.926.492L38.076 13l.057.186z"
|
|
10
|
+
fill="#F4F4F4"
|
|
11
|
+
/>
|
|
12
|
+
<path
|
|
13
|
+
d="M41.664 13L38.026 1.117A1.576 1.576 0 0036.056.07l-8.601 2.633-17.737 5.43-8.603 2.634a1.578 1.578 0 00-1.046 1.97l12.436 40.616a1.58 1.58 0 001.969 1.046l5.897-1.805.185-.057v-.194l-.185.057-5.952 1.822a1.393 1.393 0 01-1.737-.923L.247 12.682a1.39 1.39 0 01.923-1.738L9.772 8.31 27.51 2.881 36.112.247a1.393 1.393 0 011.737.923L41.47 13l.057.186h.193l-.057-.185z"
|
|
14
|
+
fill="#8D8D8D"
|
|
15
|
+
/>
|
|
16
|
+
<path
|
|
17
|
+
d="M11.378 11.855a.836.836 0 01-.798-.59L9.385 7.361a.835.835 0 01.554-1.042l16.318-4.996a.836.836 0 011.042.554l1.195 3.902a.836.836 0 01-.554 1.043l-16.318 4.995a.831.831 0 01-.244.037z"
|
|
18
|
+
fill="#C6C6C6"
|
|
19
|
+
/>
|
|
20
|
+
<circle fill="#C6C6C6" cx={17.636} cy={2.314} r={1.855} />
|
|
21
|
+
<circle
|
|
22
|
+
fill="#FFF"
|
|
23
|
+
fillRule="nonzero"
|
|
24
|
+
cx={17.636}
|
|
25
|
+
cy={2.314}
|
|
26
|
+
r={1.175}
|
|
27
|
+
/>
|
|
28
|
+
<path
|
|
29
|
+
d="M55.893 53.995H24.544a.79.79 0 01-.788-.789V15.644a.79.79 0 01.788-.788h31.349a.79.79 0 01.788.788v37.562a.79.79 0 01-.788.789z"
|
|
30
|
+
fill="#F4F4F4"
|
|
31
|
+
/>
|
|
32
|
+
<path
|
|
33
|
+
d="M41.47 13H21.948a1.579 1.579 0 00-1.576 1.577V52.4l.185-.057V14.577c.001-.768.623-1.39 1.391-1.39h19.581L41.471 13zm17.02 0H21.947a1.579 1.579 0 00-1.576 1.577v42.478c0 .87.706 1.576 1.576 1.577H58.49a1.579 1.579 0 001.576-1.577V14.577a1.579 1.579 0 00-1.576-1.576zm1.39 44.055c0 .768-.622 1.39-1.39 1.392H21.947c-.768-.001-1.39-.624-1.39-1.392V14.577c0-.768.622-1.39 1.39-1.39H58.49c.768 0 1.39.622 1.39 1.39v42.478z"
|
|
34
|
+
fill="#8D8D8D"
|
|
35
|
+
/>
|
|
36
|
+
<path
|
|
37
|
+
d="M48.751 17.082H31.686a.836.836 0 01-.835-.835v-4.081c0-.46.374-.834.835-.835H48.75c.461 0 .834.374.835.835v4.08c0 .462-.374.835-.835.836z"
|
|
38
|
+
fill="#C6C6C6"
|
|
39
|
+
/>
|
|
40
|
+
<circle fill="#C6C6C6" cx={40.218} cy={9.755} r={1.855} />
|
|
41
|
+
<circle
|
|
42
|
+
fill="#FFF"
|
|
43
|
+
fillRule="nonzero"
|
|
44
|
+
cx={40.218}
|
|
45
|
+
cy={9.755}
|
|
46
|
+
r={1.13}
|
|
47
|
+
/>
|
|
48
|
+
</g>
|
|
49
|
+
</svg>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Tile, Layer } from "@carbon/react";
|
|
3
|
+
import styles from "./styles.scss";
|
|
4
|
+
import { useLayoutType } from "@openmrs/esm-framework";
|
|
5
|
+
import { EmptyDataIllustration } from "./EmptyDataIllustration";
|
|
6
|
+
|
|
7
|
+
export interface EmptyStateProps {
|
|
8
|
+
headerTitle: string;
|
|
9
|
+
displayText: string;
|
|
10
|
+
}
|
|
11
|
+
const EmptyState: React.FC<EmptyStateProps> = ({
|
|
12
|
+
headerTitle,
|
|
13
|
+
displayText,
|
|
14
|
+
}) => {
|
|
15
|
+
const isTablet = useLayoutType() === "tablet";
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<Layer>
|
|
19
|
+
<Tile className={styles.tile}>
|
|
20
|
+
<div
|
|
21
|
+
className={isTablet ? styles.tabletHeading : styles.desktopHeading}
|
|
22
|
+
>
|
|
23
|
+
<h4>{headerTitle}</h4>
|
|
24
|
+
</div>
|
|
25
|
+
<EmptyDataIllustration />
|
|
26
|
+
<p className={styles.content}>{displayText}</p>
|
|
27
|
+
</Tile>
|
|
28
|
+
</Layer>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export default EmptyState;
|
|
33
|
+
export { EmptyState };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
@import "~@openmrs/esm-styleguide/src/vars";
|
|
2
|
+
@import "~carbon-components/src/globals/scss/vars";
|
|
3
|
+
@import "~carbon-components/src/globals/scss/mixins";
|
|
4
|
+
|
|
5
|
+
.action {
|
|
6
|
+
margin-bottom: $spacing-03;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.content {
|
|
10
|
+
@include carbon--type-style("productive-heading-01");
|
|
11
|
+
color: $text-02;
|
|
12
|
+
margin-top: $spacing-05;
|
|
13
|
+
margin-bottom: $spacing-03;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.desktopHeading {
|
|
17
|
+
h4 {
|
|
18
|
+
@include carbon--type-style('productive-heading-02');
|
|
19
|
+
color: $text-02;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.tabletHeading {
|
|
24
|
+
h4 {
|
|
25
|
+
@include carbon--type-style('productive-heading-03');
|
|
26
|
+
color: $text-02;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.desktopHeading, .tabletHeading {
|
|
31
|
+
text-align: left;
|
|
32
|
+
text-transform: capitalize;
|
|
33
|
+
margin-bottom: $spacing-05;
|
|
34
|
+
|
|
35
|
+
h4:after {
|
|
36
|
+
content: "";
|
|
37
|
+
display: block;
|
|
38
|
+
width: 2rem;
|
|
39
|
+
padding-top: 0.188rem;
|
|
40
|
+
border-bottom: 0.375rem solid var(--brand-03);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.heading:after {
|
|
45
|
+
content: "";
|
|
46
|
+
display: block;
|
|
47
|
+
width: 2rem;
|
|
48
|
+
padding-top: 0.188rem;
|
|
49
|
+
border-bottom: 0.375rem solid var(--brand-03);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.tile {
|
|
53
|
+
text-align: center;
|
|
54
|
+
border: 1px solid $ui-03;
|
|
55
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
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
|
+
} from "@carbon/react";
|
|
13
|
+
import React, { useContext, useState } from "react";
|
|
14
|
+
import { useNavigate } from "react-router-dom";
|
|
15
|
+
import FormBootstrap from "../FormBootstrap";
|
|
16
|
+
import PatientCard from "../patient-card/PatientCard";
|
|
17
|
+
import styles from "./styles.scss";
|
|
18
|
+
import PatientSearchHeader from "./patient-search-header";
|
|
19
|
+
import { useTranslation } from "react-i18next";
|
|
20
|
+
import FormWorkflowContext, {
|
|
21
|
+
FormWorkflowProvider,
|
|
22
|
+
} from "../context/FormWorkflowContext";
|
|
23
|
+
import WorkflowReview from "./workflow-review";
|
|
24
|
+
import PatientBanner from "./patient-banner";
|
|
25
|
+
|
|
26
|
+
const formStore = getGlobalStore("ampath-form-state");
|
|
27
|
+
|
|
28
|
+
const CancelModal = ({ open, setOpen }) => {
|
|
29
|
+
const { destroySession, closeSession } = useContext(FormWorkflowContext);
|
|
30
|
+
const { t } = useTranslation();
|
|
31
|
+
const navigate = useNavigate();
|
|
32
|
+
|
|
33
|
+
const discard = async () => {
|
|
34
|
+
await destroySession();
|
|
35
|
+
setOpen(false);
|
|
36
|
+
navigate("../");
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const saveAndClose = async () => {
|
|
40
|
+
await closeSession();
|
|
41
|
+
setOpen(false);
|
|
42
|
+
navigate("../");
|
|
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);
|
|
102
|
+
const store = useStore(formStore);
|
|
103
|
+
const formState = store[activeFormUuid];
|
|
104
|
+
const navigationDisabled = formState !== "ready";
|
|
105
|
+
const [cancelModalOpen, setCancelModalOpen] = useState(false);
|
|
106
|
+
const [completeModalOpen, setCompleteModalOpen] = useState(false);
|
|
107
|
+
const { t } = useTranslation();
|
|
108
|
+
|
|
109
|
+
if (!workflowState) return null;
|
|
110
|
+
|
|
111
|
+
return (
|
|
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
|
+
</>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const FormWorkspace = () => {
|
|
142
|
+
const {
|
|
143
|
+
patientUuids,
|
|
144
|
+
activePatientUuid,
|
|
145
|
+
activeEncounterUuid,
|
|
146
|
+
saveEncounter,
|
|
147
|
+
activeFormUuid,
|
|
148
|
+
editEncounter,
|
|
149
|
+
encounters,
|
|
150
|
+
} = useContext(FormWorkflowContext);
|
|
151
|
+
const { t } = useTranslation();
|
|
152
|
+
|
|
153
|
+
const handlePostResponse = (encounter) => {
|
|
154
|
+
if (encounter && encounter.uuid) {
|
|
155
|
+
saveEncounter(encounter.uuid);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
return (
|
|
160
|
+
<div className={styles.workspace}>
|
|
161
|
+
{!patientUuids.length && (
|
|
162
|
+
<div className={styles.selectPatientMessage}>
|
|
163
|
+
{t("selectPatientFirst", "Please select a patient first")}
|
|
164
|
+
</div>
|
|
165
|
+
)}
|
|
166
|
+
{!!patientUuids.length && (
|
|
167
|
+
<div className={styles.formMainContent}>
|
|
168
|
+
<div className={styles.formContainer}>
|
|
169
|
+
<FormBootstrap
|
|
170
|
+
patientUuid={activePatientUuid}
|
|
171
|
+
encounterUuid={activeEncounterUuid}
|
|
172
|
+
{...{
|
|
173
|
+
formUuid: activeFormUuid,
|
|
174
|
+
handlePostResponse,
|
|
175
|
+
}}
|
|
176
|
+
/>
|
|
177
|
+
</div>
|
|
178
|
+
<div className={styles.rightPanel}>
|
|
179
|
+
<h4>Forms filled</h4>
|
|
180
|
+
<div className={styles.patientCardsSection}>
|
|
181
|
+
{patientUuids.map((patientUuid) => (
|
|
182
|
+
<PatientCard
|
|
183
|
+
key={patientUuid}
|
|
184
|
+
{...{
|
|
185
|
+
patientUuid,
|
|
186
|
+
activePatientUuid,
|
|
187
|
+
editEncounter,
|
|
188
|
+
encounters,
|
|
189
|
+
}}
|
|
190
|
+
/>
|
|
191
|
+
))}
|
|
192
|
+
</div>
|
|
193
|
+
<WorkflowNavigationButtons />
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
197
|
+
</div>
|
|
198
|
+
);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const FormEntryWorkflow = () => {
|
|
202
|
+
const { workflowState } = useContext(FormWorkflowContext);
|
|
203
|
+
return (
|
|
204
|
+
<>
|
|
205
|
+
<div className={styles.breadcrumbsContainer}>
|
|
206
|
+
<ExtensionSlot extensionSlotName="breadcrumbs-slot" />
|
|
207
|
+
</div>
|
|
208
|
+
{workflowState === "REVIEW" && <WorkflowReview />}
|
|
209
|
+
{workflowState !== "REVIEW" && (
|
|
210
|
+
<>
|
|
211
|
+
<PatientSearchHeader />
|
|
212
|
+
<PatientBanner />
|
|
213
|
+
<div className={styles.workspaceWrapper}>
|
|
214
|
+
<FormWorkspace />
|
|
215
|
+
</div>
|
|
216
|
+
</>
|
|
217
|
+
)}
|
|
218
|
+
</>
|
|
219
|
+
);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const FormEntryWorkflowWrapper = () => {
|
|
223
|
+
return (
|
|
224
|
+
<FormWorkflowProvider>
|
|
225
|
+
<FormEntryWorkflow />
|
|
226
|
+
</FormWorkflowProvider>
|
|
227
|
+
);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
export default FormEntryWorkflowWrapper;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Accordion, AccordionItem, Button } from "@carbon/react";
|
|
2
|
+
import React, { useContext } from "react";
|
|
3
|
+
import { useTranslation } from "react-i18next";
|
|
4
|
+
import FormWorkflowContext from "../../context/FormWorkflowContext";
|
|
5
|
+
import { useGetPatient, useGetEncounter } from "../../hooks";
|
|
6
|
+
import styles from "./styles.scss";
|
|
7
|
+
|
|
8
|
+
const FormReviewCard = ({ patientUuid }) => {
|
|
9
|
+
const { encounters, editEncounter } = useContext(FormWorkflowContext);
|
|
10
|
+
const patient = useGetPatient(patientUuid);
|
|
11
|
+
const givenName = patient?.name?.[0]?.given?.[0];
|
|
12
|
+
const familyName = patient?.name?.[0]?.family;
|
|
13
|
+
const identifier = patient?.identifier?.[0]?.value;
|
|
14
|
+
const encounterUuid = encounters?.[patientUuid];
|
|
15
|
+
const { encounter } = useGetEncounter(encounterUuid);
|
|
16
|
+
const { t } = useTranslation();
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className={styles.formReviewCard}>
|
|
20
|
+
<Accordion align="start">
|
|
21
|
+
<AccordionItem
|
|
22
|
+
title={
|
|
23
|
+
<>
|
|
24
|
+
<span className={styles.identifier}>{identifier}</span>
|
|
25
|
+
<span className={styles.displayName}>
|
|
26
|
+
{givenName} {familyName}
|
|
27
|
+
</span>
|
|
28
|
+
</>
|
|
29
|
+
}
|
|
30
|
+
className={styles.accordionItem}
|
|
31
|
+
>
|
|
32
|
+
{encounter && encounter?.obs && encounter.obs?.length && (
|
|
33
|
+
<div className={styles.dataField}>
|
|
34
|
+
<ul>
|
|
35
|
+
{encounter.obs.map((obs, index) => (
|
|
36
|
+
<li key={index}>{obs.display}</li>
|
|
37
|
+
))}
|
|
38
|
+
</ul>
|
|
39
|
+
</div>
|
|
40
|
+
)}
|
|
41
|
+
<Button kind="primary" onClick={() => editEncounter(patientUuid)}>
|
|
42
|
+
{t("goToForm", "Go To Form")}
|
|
43
|
+
</Button>
|
|
44
|
+
</AccordionItem>
|
|
45
|
+
</Accordion>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export default FormReviewCard;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
@use '@carbon/styles/scss/spacing';
|
|
2
|
+
// @use '@carbon/colors';
|
|
3
|
+
@use '@carbon/styles/scss/type';
|
|
4
|
+
@import '~@openmrs/esm-styleguide/src/vars';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
.formReviewCard {
|
|
8
|
+
background-color: $ui-02;
|
|
9
|
+
padding: spacing.$spacing-02;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.formReviewCard :global(.cds--accordion) :global(.cds--accordion__item) {
|
|
13
|
+
border: none;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.formReviewCard :global(.cds--accordion__title) {
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: baseline;
|
|
19
|
+
column-gap: spacing.$spacing-05;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.formReviewCard :global(.cds--accordion__content) {
|
|
23
|
+
padding: spacing.$spacing-03;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.dataField {
|
|
27
|
+
@include type.type-style('code-02');
|
|
28
|
+
background-color: $ui-01;
|
|
29
|
+
padding: spacing.$spacing-03;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.displayName {
|
|
33
|
+
@include type.type-style('heading-02');
|
|
34
|
+
font-weight: bold;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.identifier {
|
|
38
|
+
@include type.type-style('body-compact-01')
|
|
39
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import PatientBanner from "./PatientBanner";
|
|
4
|
+
|
|
5
|
+
describe("PatientBanner", () => {
|
|
6
|
+
it("renders placeholder information when no data is present", () => {
|
|
7
|
+
render(<PatientBanner />);
|
|
8
|
+
});
|
|
9
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { age, ExtensionSlot } from "@openmrs/esm-framework";
|
|
2
|
+
import { SkeletonPlaceholder, SkeletonText } from "@carbon/react";
|
|
3
|
+
import React, { useContext } from "react";
|
|
4
|
+
import styles from "./styles.scss";
|
|
5
|
+
import { useTranslation } from "react-i18next";
|
|
6
|
+
import useGetPatient from "../../hooks/useGetPatient";
|
|
7
|
+
import FormWorkflowContext from "../../context/FormWorkflowContext";
|
|
8
|
+
|
|
9
|
+
const SkeletonPatientInfo = () => {
|
|
10
|
+
return (
|
|
11
|
+
<div className={styles.container}>
|
|
12
|
+
<SkeletonPlaceholder className={styles.photoPlaceholder} />
|
|
13
|
+
<div className={styles.patientInfoContent}>
|
|
14
|
+
<div className={styles.patientInfoRow}>
|
|
15
|
+
<SkeletonText width="7rem" lineCount={1} />
|
|
16
|
+
</div>
|
|
17
|
+
<div className={styles.patientInfoRow}>
|
|
18
|
+
<span>
|
|
19
|
+
<SkeletonText width="1rem" lineCount={1} />
|
|
20
|
+
</span>
|
|
21
|
+
<span>·</span>
|
|
22
|
+
<span>
|
|
23
|
+
<SkeletonText width="1rem" lineCount={1} />
|
|
24
|
+
</span>
|
|
25
|
+
<span>·</span>
|
|
26
|
+
<span>
|
|
27
|
+
<SkeletonText width="1rem" lineCount={1} />
|
|
28
|
+
</span>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const PatientBanner = () => {
|
|
36
|
+
const { activePatientUuid, workflowState } = useContext(FormWorkflowContext);
|
|
37
|
+
const patient = useGetPatient(activePatientUuid);
|
|
38
|
+
const { t } = useTranslation();
|
|
39
|
+
const patientName = `${patient?.name?.[0].given?.join(" ")} ${
|
|
40
|
+
patient?.name?.[0]?.family
|
|
41
|
+
}`;
|
|
42
|
+
|
|
43
|
+
const patientPhotoSlotState = React.useMemo(
|
|
44
|
+
() => ({ patientUuid: patient?.id, patientName, size: "small" }),
|
|
45
|
+
[patient?.id, patientName]
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
if (workflowState === "NEW_PATIENT") return null;
|
|
49
|
+
|
|
50
|
+
if (!patient) {
|
|
51
|
+
return <SkeletonPatientInfo />;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div className={styles.container}>
|
|
56
|
+
<ExtensionSlot
|
|
57
|
+
extensionSlotName="patient-photo-slot"
|
|
58
|
+
state={patientPhotoSlotState}
|
|
59
|
+
/>
|
|
60
|
+
<div className={styles.patientInfoContent}>
|
|
61
|
+
<div className={styles.patientInfoRow}>
|
|
62
|
+
<span className={styles.patientName}>{patientName}</span>
|
|
63
|
+
</div>
|
|
64
|
+
<div className={styles.patientInfoRow}>
|
|
65
|
+
<span>
|
|
66
|
+
{(patient.gender ?? t("unknown", "Unknown")).replace(/^\w/, (c) =>
|
|
67
|
+
c.toUpperCase()
|
|
68
|
+
)}
|
|
69
|
+
</span>
|
|
70
|
+
<span>·</span>
|
|
71
|
+
<span>{age(patient.birthDate)}</span>
|
|
72
|
+
<span>·</span>
|
|
73
|
+
<span>
|
|
74
|
+
{patient.identifier.length
|
|
75
|
+
? patient.identifier
|
|
76
|
+
.map((identifier) => identifier.value)
|
|
77
|
+
.join(", ")
|
|
78
|
+
: "--"}
|
|
79
|
+
</span>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export default PatientBanner;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
@use '@carbon/styles/scss/spacing';
|
|
2
|
+
// @use '@carbon/colors';
|
|
3
|
+
@use '@carbon/styles/scss/type';
|
|
4
|
+
@import '~@openmrs/esm-styleguide/src/vars';
|
|
5
|
+
|
|
6
|
+
.container {
|
|
7
|
+
height: spacing.$spacing-11;
|
|
8
|
+
display: flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
background-color: $ui-02;
|
|
11
|
+
border-top: 0.0125rem solid $ui-03;
|
|
12
|
+
border-bottom: 0.0125rem solid $ui-03;
|
|
13
|
+
padding: 0 spacing.$spacing-05;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.photoPlaceholder {
|
|
17
|
+
height: 48px;
|
|
18
|
+
width: 48px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.patientName {
|
|
22
|
+
@include type.type-style('heading-03');
|
|
23
|
+
font-weight: 600;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.patientInfoContent {
|
|
27
|
+
width: 100%;
|
|
28
|
+
margin-left: 1rem;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.patientInfoRow {
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
& > button {
|
|
35
|
+
min-height: 2rem;
|
|
36
|
+
}
|
|
37
|
+
@include type.type-style('body-compact-02');
|
|
38
|
+
color: $text-02;
|
|
39
|
+
column-gap: 0.8rem;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.patientEditBtn {
|
|
43
|
+
color: $ui-05;
|
|
44
|
+
margin: spacing.$spacing-03;
|
|
45
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Add, Close } from "@carbon/react/icons";
|
|
2
|
+
import {
|
|
3
|
+
ExtensionSlot,
|
|
4
|
+
interpolateUrl,
|
|
5
|
+
navigate,
|
|
6
|
+
} from "@openmrs/esm-framework";
|
|
7
|
+
import { Button } from "@carbon/react";
|
|
8
|
+
import React, { useContext } from "react";
|
|
9
|
+
import { Link } from "react-router-dom";
|
|
10
|
+
import FormWorkflowContext from "../../context/FormWorkflowContext";
|
|
11
|
+
import styles from "./styles.scss";
|
|
12
|
+
import { useTranslation } from "react-i18next";
|
|
13
|
+
|
|
14
|
+
const PatientSearchHeader = () => {
|
|
15
|
+
const { addPatient, workflowState, activeFormUuid } =
|
|
16
|
+
useContext(FormWorkflowContext);
|
|
17
|
+
const handleSelectPatient = (patient) => {
|
|
18
|
+
addPatient(patient.uuid);
|
|
19
|
+
};
|
|
20
|
+
const { t } = useTranslation();
|
|
21
|
+
|
|
22
|
+
if (workflowState !== "NEW_PATIENT") return null;
|
|
23
|
+
|
|
24
|
+
const afterUrl = encodeURIComponent(
|
|
25
|
+
`\${openmrsSpaBase}/forms/form/${activeFormUuid}?patientUuid=\${patientUuid}`
|
|
26
|
+
);
|
|
27
|
+
const patientRegistrationUrl = interpolateUrl(
|
|
28
|
+
`\${openmrsSpaBase}/patient-registration?afterUrl=${afterUrl}`
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className={styles.searchHeaderContainer}>
|
|
33
|
+
<span className={styles.padded}>{t("nextPatient", "Next patient")}:</span>
|
|
34
|
+
<span className={styles.searchBarWrapper}>
|
|
35
|
+
<ExtensionSlot
|
|
36
|
+
extensionSlotName="patient-search-bar-slot"
|
|
37
|
+
state={{
|
|
38
|
+
selectPatientAction: handleSelectPatient,
|
|
39
|
+
buttonProps: {
|
|
40
|
+
kind: "primary",
|
|
41
|
+
},
|
|
42
|
+
}}
|
|
43
|
+
/>
|
|
44
|
+
</span>
|
|
45
|
+
<span className={styles.padded}>{t("or", "or")}</span>
|
|
46
|
+
<span>
|
|
47
|
+
<Button onClick={() => navigate({ to: patientRegistrationUrl })}>
|
|
48
|
+
{t("createNewPatient", "Create new patient")} <Add size={20} />
|
|
49
|
+
</Button>
|
|
50
|
+
</span>
|
|
51
|
+
<span style={{ flexGrow: 1 }} />
|
|
52
|
+
<span>
|
|
53
|
+
<Link to="../">
|
|
54
|
+
<Button kind="ghost">
|
|
55
|
+
{t("cancel", "Cancel")} <Close size={20} />
|
|
56
|
+
</Button>
|
|
57
|
+
</Link>
|
|
58
|
+
</span>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export default PatientSearchHeader;
|