@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.
Files changed (133) hide show
  1. package/.eslintrc.js +10 -0
  2. package/.husky/pre-push +1 -6
  3. package/.tx/config +9 -0
  4. package/.yarn/plugins/@yarnpkg/plugin-version.cjs +550 -0
  5. package/.yarn/versions/c1451405.yml +0 -0
  6. package/README.md +39 -12
  7. package/dist/openmrs-esm-fast-data-entry-app.js +1 -1
  8. package/docs/config-icrc-forms.png +0 -0
  9. package/docs/config-other-forms.png +0 -0
  10. package/docs/configuring-form-categories.md +77 -0
  11. package/docs/fde-workflow.mov +0 -0
  12. package/docs/form-workflow-state-diagram.png +0 -0
  13. package/jest.config.json +20 -18
  14. package/package.json +99 -106
  15. package/src/FormBootstrap.tsx +151 -0
  16. package/src/Root.tsx +14 -3
  17. package/src/add-group-modal/AddGroupModal.tsx +262 -0
  18. package/src/add-group-modal/styles.scss +45 -0
  19. package/src/config-schema.ts +63 -31
  20. package/src/context/FormWorkflowContext.tsx +114 -0
  21. package/src/context/FormWorkflowReducer.ts +277 -0
  22. package/src/context/GroupFormWorkflowContext.tsx +143 -0
  23. package/src/context/GroupFormWorkflowReducer.ts +296 -0
  24. package/src/empty-state/EmptyDataIllustration.tsx +51 -0
  25. package/src/empty-state/EmptyState.tsx +33 -0
  26. package/src/empty-state/styles.scss +55 -0
  27. package/src/form-entry-workflow/FormEntryWorkflow.tsx +230 -0
  28. package/src/form-entry-workflow/form-review-card/FormReviewCard.tsx +50 -0
  29. package/src/form-entry-workflow/form-review-card/index.ts +3 -0
  30. package/src/form-entry-workflow/form-review-card/styles.scss +39 -0
  31. package/src/form-entry-workflow/index.ts +3 -0
  32. package/src/form-entry-workflow/patient-banner/PatientBanner.test.tsx +9 -0
  33. package/src/form-entry-workflow/patient-banner/PatientBanner.tsx +86 -0
  34. package/src/form-entry-workflow/patient-banner/index.ts +3 -0
  35. package/src/form-entry-workflow/patient-banner/styles.scss +45 -0
  36. package/src/form-entry-workflow/patient-search-header/PatientSearchHeader.tsx +63 -0
  37. package/src/form-entry-workflow/patient-search-header/index.ts +3 -0
  38. package/src/form-entry-workflow/patient-search-header/styles.scss +22 -0
  39. package/src/form-entry-workflow/styles.scss +64 -0
  40. package/src/form-entry-workflow/workflow-review/WorkflowReview.tsx +35 -0
  41. package/src/form-entry-workflow/workflow-review/index.ts +3 -0
  42. package/src/form-entry-workflow/workflow-review/styles.scss +34 -0
  43. package/src/forms-app-menu-link.tsx +3 -2
  44. package/src/forms-page/FormsPage.tsx +129 -0
  45. package/src/forms-page/forms-table/FormsTable.tsx +131 -0
  46. package/src/forms-page/forms-table/index.ts +3 -0
  47. package/src/forms-page/forms-table/styles.scss +20 -0
  48. package/src/forms-page/index.ts +3 -0
  49. package/src/forms-page/styles.scss +11 -0
  50. package/src/group-form-entry-workflow/GroupFormEntryWorkflow.tsx +413 -0
  51. package/src/group-form-entry-workflow/group-display-header/GroupDisplayHeader.test.tsx +9 -0
  52. package/src/group-form-entry-workflow/group-display-header/GroupDisplayHeader.tsx +71 -0
  53. package/src/group-form-entry-workflow/group-display-header/index.ts +3 -0
  54. package/src/group-form-entry-workflow/group-display-header/styles.scss +60 -0
  55. package/src/group-form-entry-workflow/group-search/CompactGroupResults.tsx +139 -0
  56. package/src/group-form-entry-workflow/group-search/CompactGroupSearch.tsx +68 -0
  57. package/src/group-form-entry-workflow/group-search/GroupSearch.tsx +150 -0
  58. package/src/group-form-entry-workflow/group-search/compact-group-result.scss +64 -0
  59. package/src/group-form-entry-workflow/group-search/compact-group-search.scss +35 -0
  60. package/src/group-form-entry-workflow/group-search/group-search.scss +96 -0
  61. package/src/group-form-entry-workflow/group-search-header/GroupSearchHeader.tsx +49 -0
  62. package/src/group-form-entry-workflow/group-search-header/index.ts +3 -0
  63. package/src/group-form-entry-workflow/group-search-header/styles.scss +20 -0
  64. package/src/group-form-entry-workflow/index.ts +3 -0
  65. package/src/group-form-entry-workflow/styles.scss +96 -0
  66. package/src/hooks/index.ts +7 -0
  67. package/src/hooks/useFormState.ts +23 -0
  68. package/src/hooks/useGetAllForms.ts +45 -0
  69. package/src/hooks/useGetEncounter.ts +21 -0
  70. package/src/hooks/useGetPatient.ts +23 -0
  71. package/src/hooks/useKeyPress.ts +31 -0
  72. package/src/hooks/usePostEndpoint.ts +70 -0
  73. package/src/hooks/useSearchEndpoint.ts +120 -0
  74. package/src/index.ts +20 -4
  75. package/src/patient-card/PatientCard.tsx +67 -0
  76. package/src/patient-card/index.ts +3 -0
  77. package/src/patient-card/styles.scss +45 -0
  78. package/translations/en.json +54 -4
  79. package/tsconfig.json +26 -23
  80. package/.eslintrc +0 -4
  81. package/.github/workflows/node.js.yml +0 -79
  82. package/.husky/pre-commit +0 -6
  83. package/dist/24.js +0 -3
  84. package/dist/24.js.LICENSE.txt +0 -16
  85. package/dist/24.js.map +0 -1
  86. package/dist/294.js +0 -3
  87. package/dist/294.js.LICENSE.txt +0 -14
  88. package/dist/294.js.map +0 -1
  89. package/dist/296.js +0 -2
  90. package/dist/296.js.map +0 -1
  91. package/dist/299.js +0 -2
  92. package/dist/299.js.map +0 -1
  93. package/dist/382.js +0 -3
  94. package/dist/382.js.LICENSE.txt +0 -8
  95. package/dist/382.js.map +0 -1
  96. package/dist/415.js +0 -2
  97. package/dist/415.js.map +0 -1
  98. package/dist/574.js +0 -1
  99. package/dist/595.js +0 -3
  100. package/dist/595.js.LICENSE.txt +0 -1
  101. package/dist/595.js.map +0 -1
  102. package/dist/69.js +0 -2
  103. package/dist/69.js.map +0 -1
  104. package/dist/735.js +0 -3
  105. package/dist/735.js.LICENSE.txt +0 -29
  106. package/dist/735.js.map +0 -1
  107. package/dist/777.js +0 -2
  108. package/dist/777.js.map +0 -1
  109. package/dist/860.js +0 -2
  110. package/dist/860.js.map +0 -1
  111. package/dist/906.js +0 -2
  112. package/dist/906.js.map +0 -1
  113. package/dist/openmrs-esm-fast-data-entry-app.js.buildmanifest.json +0 -369
  114. package/dist/openmrs-esm-fast-data-entry-app.js.map +0 -1
  115. package/dist/openmrs-esm-fast-data-entry-app.old +0 -2
  116. package/src/boxes/extensions/blue-box.tsx +0 -15
  117. package/src/boxes/extensions/box.scss +0 -23
  118. package/src/boxes/extensions/brand-box.tsx +0 -15
  119. package/src/boxes/extensions/red-box.tsx +0 -15
  120. package/src/boxes/slot/boxes.css +0 -23
  121. package/src/boxes/slot/boxes.tsx +0 -19
  122. package/src/forms/FormsRoot.tsx +0 -32
  123. package/src/forms/FormsTable.tsx +0 -64
  124. package/src/forms/mockData.ts +0 -43
  125. package/src/greeter/greeter.css +0 -4
  126. package/src/greeter/greeter.test.tsx +0 -29
  127. package/src/greeter/greeter.tsx +0 -25
  128. package/src/hello.css +0 -3
  129. package/src/hello.test.tsx +0 -45
  130. package/src/hello.tsx +0 -30
  131. package/src/patient-getter/patient-getter.resource.ts +0 -31
  132. package/src/patient-getter/patient-getter.test.tsx +0 -28
  133. 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,3 @@
1
+ import FormReviewCard from "./FormReviewCard";
2
+
3
+ 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,3 @@
1
+ import FormEntryWorkflow from "./FormEntryWorkflow";
2
+
3
+ export default FormEntryWorkflow;
@@ -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>&middot;</span>
22
+ <span>
23
+ <SkeletonText width="1rem" lineCount={1} />
24
+ </span>
25
+ <span>&middot;</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>&middot;</span>
71
+ <span>{age(patient.birthDate)}</span>
72
+ <span>&middot;</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,3 @@
1
+ import PatientBanner from "./PatientBanner";
2
+
3
+ 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;
@@ -0,0 +1,3 @@
1
+ import PatientSearchHeader from "./PatientSearchHeader";
2
+
3
+ export default PatientSearchHeader;