@openmrs/esm-fast-data-entry-app 1.0.0-pre.11 → 1.0.0-pre.21
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/dist/132.js +1 -0
- package/dist/147.js +2 -0
- package/dist/{502.js.LICENSE.txt → 147.js.LICENSE.txt} +0 -30
- package/dist/247.js +1 -0
- package/dist/294.js +1 -2
- package/dist/536.js +2 -0
- package/dist/{382.js.LICENSE.txt → 536.js.LICENSE.txt} +0 -0
- package/dist/595.js +1 -2
- package/dist/595.js.LICENSE.txt +2 -0
- package/dist/634.js +2 -0
- package/dist/634.js.LICENSE.txt +5 -0
- package/dist/776.js +1 -0
- package/dist/804.js +1 -0
- package/dist/852.js +1 -0
- package/dist/906.js +1 -2
- package/dist/935.js +2 -0
- package/dist/935.js.LICENSE.txt +23 -0
- package/dist/947.js +1 -0
- package/dist/openmrs-esm-fast-data-entry-app.js +1 -1
- package/dist/openmrs-esm-fast-data-entry-app.js.buildmanifest.json +142 -124
- package/dist/openmrs-esm-fast-data-entry-app.old +1 -2
- package/package.json +29 -33
- package/src/FormEntry.tsx +42 -0
- package/src/Loader.tsx +16 -0
- package/src/Root.tsx +7 -3
- package/src/empty-state/EmptyDataIllustration.tsx +51 -0
- package/src/empty-state/EmptyState.tsx +29 -0
- package/src/empty-state/styles.scss +55 -0
- package/src/forms/FormWorkflow.tsx +135 -0
- package/src/forms/FormsRoot.tsx +12 -10
- package/src/forms/FormsTable.tsx +22 -5
- package/src/forms/PatientCard.tsx +37 -0
- package/src/forms/PatientInfo.test.tsx +9 -0
- package/src/forms/PatientInfo.tsx +140 -0
- package/src/forms/patient-info.scss +59 -0
- package/src/forms/styles.scss +13 -0
- package/src/forms/useGetPatient.ts +23 -0
- package/src/index.ts +13 -1
- package/src/loader.scss +9 -0
- package/tsconfig.json +26 -23
- package/.husky/pre-commit +0 -6
- package/.husky/pre-push +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.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/415.js +0 -2
- package/dist/415.js.map +0 -1
- package/dist/502.js +0 -3
- package/dist/502.js.map +0 -1
- package/dist/595.js.map +0 -1
- package/dist/69.js +0 -2
- package/dist/69.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.map +0 -1
- package/dist/openmrs-esm-fast-data-entry-app.js.map +0 -1
- 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,42 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ExtensionSlot } from "@openmrs/esm-framework";
|
|
3
|
+
import useGetPatient from "./forms/useGetPatient";
|
|
4
|
+
interface FormParams {
|
|
5
|
+
formUuid: string;
|
|
6
|
+
patientUuid: string;
|
|
7
|
+
visitUuid?: string;
|
|
8
|
+
visitTypeUuid?: string;
|
|
9
|
+
encounterUuid?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const FormEntry = ({
|
|
13
|
+
formUuid,
|
|
14
|
+
patientUuid,
|
|
15
|
+
visitUuid,
|
|
16
|
+
visitTypeUuid,
|
|
17
|
+
encounterUuid,
|
|
18
|
+
}: FormParams) => {
|
|
19
|
+
const patient = useGetPatient(patientUuid);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div>
|
|
23
|
+
{formUuid && patientUuid && patient && (
|
|
24
|
+
<ExtensionSlot
|
|
25
|
+
extensionSlotName="form-widget-slot"
|
|
26
|
+
state={{
|
|
27
|
+
view: "form",
|
|
28
|
+
formUuid,
|
|
29
|
+
visitUuid: visitUuid ?? "",
|
|
30
|
+
visitTypeUuid: visitTypeUuid ?? "",
|
|
31
|
+
patientUuid,
|
|
32
|
+
patient,
|
|
33
|
+
encounterUuid: encounterUuid ?? "",
|
|
34
|
+
closeWorkspace: () => {},
|
|
35
|
+
}}
|
|
36
|
+
/>
|
|
37
|
+
)}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default FormEntry;
|
package/src/Loader.tsx
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import styles from "./loader.scss";
|
|
3
|
+
import { InlineLoading } from "carbon-components-react";
|
|
4
|
+
import { useTranslation } from "react-i18next";
|
|
5
|
+
|
|
6
|
+
const Loader: React.FC = () => {
|
|
7
|
+
const { t } = useTranslation();
|
|
8
|
+
return (
|
|
9
|
+
<InlineLoading
|
|
10
|
+
className={styles.loading}
|
|
11
|
+
description={`${t("loading", "Loading")} ...`}
|
|
12
|
+
/>
|
|
13
|
+
);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default Loader;
|
package/src/Root.tsx
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import { BrowserRouter, Route } from "react-router-dom";
|
|
2
|
+
import { BrowserRouter, Route, Switch } from "react-router-dom";
|
|
3
3
|
import { appPath } from "./constant";
|
|
4
|
-
|
|
4
|
+
const FormsRoot = React.lazy(() => import("./forms/FormsRoot"));
|
|
5
|
+
const FormWorkflow = React.lazy(() => import("./forms/FormWorkflow"));
|
|
5
6
|
|
|
6
7
|
const Root = () => {
|
|
7
8
|
return (
|
|
8
9
|
<main>
|
|
9
10
|
<BrowserRouter basename={appPath}>
|
|
10
|
-
<
|
|
11
|
+
<Switch>
|
|
12
|
+
<Route exact path="/" children={<FormsRoot />} />
|
|
13
|
+
<Route path="/:formUuid?" children={<FormWorkflow />} />
|
|
14
|
+
</Switch>
|
|
11
15
|
</BrowserRouter>
|
|
12
16
|
</main>
|
|
13
17
|
);
|
|
@@ -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,29 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Tile } from "carbon-components-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
|
+
<Tile light className={styles.tile}>
|
|
19
|
+
<div className={isTablet ? styles.tabletHeading : styles.desktopHeading}>
|
|
20
|
+
<h4>{headerTitle}</h4>
|
|
21
|
+
</div>
|
|
22
|
+
<EmptyDataIllustration />
|
|
23
|
+
<p className={styles.content}>{displayText}</p>
|
|
24
|
+
</Tile>
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default EmptyState;
|
|
29
|
+
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,135 @@
|
|
|
1
|
+
import { Add20, Close20 } from "@carbon/icons-react";
|
|
2
|
+
import { ExtensionSlot } from "@openmrs/esm-framework";
|
|
3
|
+
import { Button } from "carbon-components-react";
|
|
4
|
+
import React, { useState } from "react";
|
|
5
|
+
import { Link, useParams } from "react-router-dom";
|
|
6
|
+
import FormEntry from "../FormEntry";
|
|
7
|
+
import PatientCard from "./PatientCard";
|
|
8
|
+
import PatientInfo from "./PatientInfo";
|
|
9
|
+
import styles from "./styles.scss";
|
|
10
|
+
|
|
11
|
+
const PatientSearchHeader = ({
|
|
12
|
+
patientUuids,
|
|
13
|
+
setPatientUuids,
|
|
14
|
+
setActivePatientUuid,
|
|
15
|
+
}) => {
|
|
16
|
+
const handleSelectPatient = (uuid) => {
|
|
17
|
+
setPatientUuids([...patientUuids, uuid]), setActivePatientUuid(uuid);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div
|
|
22
|
+
style={{
|
|
23
|
+
display: "flex",
|
|
24
|
+
backgroundColor: "white",
|
|
25
|
+
padding: "2rem 1rem",
|
|
26
|
+
}}
|
|
27
|
+
>
|
|
28
|
+
<span style={{ padding: "1rem" }}>Next patient:</span>
|
|
29
|
+
<span style={{ minWidth: "35rem" }}>
|
|
30
|
+
<ExtensionSlot
|
|
31
|
+
extensionSlotName="patient-search-bar-slot"
|
|
32
|
+
state={{
|
|
33
|
+
selectPatientAction: handleSelectPatient,
|
|
34
|
+
buttonProps: {
|
|
35
|
+
kind: "primary",
|
|
36
|
+
},
|
|
37
|
+
}}
|
|
38
|
+
/>
|
|
39
|
+
</span>
|
|
40
|
+
<span style={{ padding: "1rem" }}>or</span>
|
|
41
|
+
<span>
|
|
42
|
+
<Button disabled>
|
|
43
|
+
Create new patient <Add20 />
|
|
44
|
+
</Button>
|
|
45
|
+
</span>
|
|
46
|
+
<span style={{ flexGrow: 1 }} />
|
|
47
|
+
<span>
|
|
48
|
+
<Button kind="ghost" href={`${window.spaBase}/forms`}>
|
|
49
|
+
Cancel <Close20 />
|
|
50
|
+
</Button>
|
|
51
|
+
</span>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
interface ParamTypes {
|
|
57
|
+
formUuid: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const FormWorkflow = () => {
|
|
61
|
+
const { formUuid } = useParams() as ParamTypes;
|
|
62
|
+
const [patientUuids, setPatientUuids] = useState([]);
|
|
63
|
+
const [activePatientUuid, setActivePatientUuid] = useState(null);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<div>
|
|
67
|
+
<div className={styles.breadcrumbsContainer}>
|
|
68
|
+
<ExtensionSlot extensionSlotName="breadcrumbs-slot" />
|
|
69
|
+
</div>
|
|
70
|
+
{!activePatientUuid && (
|
|
71
|
+
<PatientSearchHeader
|
|
72
|
+
{...{ patientUuids, setPatientUuids, setActivePatientUuid }}
|
|
73
|
+
/>
|
|
74
|
+
)}
|
|
75
|
+
{activePatientUuid && <PatientInfo patientUuid={activePatientUuid} />}
|
|
76
|
+
<div style={{ display: "flex", justifyContent: "center" }}>
|
|
77
|
+
<div style={{ width: "1100px" }}>
|
|
78
|
+
{!patientUuids.length && (
|
|
79
|
+
<div style={{ margin: "2rem", textAlign: "center" }}>
|
|
80
|
+
Please select a patient first
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
{!!patientUuids.length && (
|
|
84
|
+
<div className={styles.formContainer}>
|
|
85
|
+
<div style={{ flexGrow: 1 }}>
|
|
86
|
+
<FormEntry
|
|
87
|
+
patientUuid={activePatientUuid}
|
|
88
|
+
{...{
|
|
89
|
+
formUuid,
|
|
90
|
+
}}
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
<div style={{ width: "13rem", textAlign: "left" }}>
|
|
94
|
+
<h4>Forms filled</h4>
|
|
95
|
+
<div
|
|
96
|
+
style={{
|
|
97
|
+
margin: "1rem 0",
|
|
98
|
+
borderBottom: "1px solid #f4f4f4",
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
{patientUuids.map((patientUuid) => (
|
|
102
|
+
<PatientCard patientUuid={patientUuid} key={patientUuid} />
|
|
103
|
+
))}
|
|
104
|
+
</div>
|
|
105
|
+
<div
|
|
106
|
+
style={{
|
|
107
|
+
display: "flex",
|
|
108
|
+
flexDirection: "column",
|
|
109
|
+
rowGap: "0.5rem",
|
|
110
|
+
}}
|
|
111
|
+
>
|
|
112
|
+
<Button
|
|
113
|
+
kind="primary"
|
|
114
|
+
onClick={() => setActivePatientUuid(null)}
|
|
115
|
+
style={{ width: "100%" }}
|
|
116
|
+
>
|
|
117
|
+
Next Patient
|
|
118
|
+
</Button>
|
|
119
|
+
<Button kind="secondary" disabled style={{ width: "100%" }}>
|
|
120
|
+
Review & Save
|
|
121
|
+
</Button>
|
|
122
|
+
<Button kind="tertiary" disabled style={{ width: "100%" }}>
|
|
123
|
+
Cancel
|
|
124
|
+
</Button>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export default FormWorkflow;
|
package/src/forms/FormsRoot.tsx
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { openmrsFetch, useConfig } from "@openmrs/esm-framework";
|
|
2
2
|
import { Tab, Tabs } from "carbon-components-react";
|
|
3
3
|
import React from "react";
|
|
4
|
-
import { from } from "rxjs";
|
|
5
4
|
import useSWR from "swr";
|
|
6
5
|
import { Config } from "../config-schema";
|
|
7
6
|
import FormsTable from "./FormsTable";
|
|
8
|
-
import { tableData } from "./mockData";
|
|
9
7
|
|
|
10
8
|
export const customFormRepresentation =
|
|
11
9
|
"(uuid,name,display,encounterType:(uuid,name,viewPrivilege,editPrivilege),version,published,retired,resources:(uuid,name,dataType,valueReference))";
|
|
@@ -16,8 +14,7 @@ export const formEncounterUrlPoc = `/ws/rest/v1/form?v=custom:${customFormRepres
|
|
|
16
14
|
export function useFormEncounters(cachedOfflineFormsOnly = false) {
|
|
17
15
|
const showHtmlFormEntryForms = true;
|
|
18
16
|
const url = showHtmlFormEntryForms ? formEncounterUrl : formEncounterUrlPoc;
|
|
19
|
-
|
|
20
|
-
return useSWR([url, cachedOfflineFormsOnly], async () => {
|
|
17
|
+
const { data, error } = useSWR([url, cachedOfflineFormsOnly], async () => {
|
|
21
18
|
const res = await openmrsFetch(url);
|
|
22
19
|
// show published forms and hide component forms
|
|
23
20
|
const forms =
|
|
@@ -27,6 +24,12 @@ export function useFormEncounters(cachedOfflineFormsOnly = false) {
|
|
|
27
24
|
|
|
28
25
|
return forms;
|
|
29
26
|
});
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
forms: data,
|
|
30
|
+
isLoading: !error && !data,
|
|
31
|
+
error,
|
|
32
|
+
};
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
const cleanForms = (rawFormData) => {
|
|
@@ -36,14 +39,13 @@ const cleanForms = (rawFormData) => {
|
|
|
36
39
|
id: form.uuid,
|
|
37
40
|
}));
|
|
38
41
|
}
|
|
42
|
+
return null;
|
|
39
43
|
};
|
|
40
44
|
|
|
41
|
-
const FormsRoot = (
|
|
42
|
-
const { tab } = match?.params;
|
|
45
|
+
const FormsRoot = () => {
|
|
43
46
|
const config = useConfig() as Config;
|
|
44
47
|
const { formCategories, formCategoriesToShow } = config;
|
|
45
|
-
const {
|
|
46
|
-
const { rows: mockRows } = tableData;
|
|
48
|
+
const { forms, isLoading, error } = useFormEncounters();
|
|
47
49
|
const cleanRows = cleanForms(forms);
|
|
48
50
|
|
|
49
51
|
const categoryRows = formCategoriesToShow.map((name) => {
|
|
@@ -61,11 +63,11 @@ const FormsRoot = ({ match }) => {
|
|
|
61
63
|
<h3 style={{ marginBottom: "1.5rem" }}>Forms</h3>
|
|
62
64
|
<Tabs type="container">
|
|
63
65
|
<Tab label="All Forms">
|
|
64
|
-
<FormsTable rows={cleanRows} />
|
|
66
|
+
<FormsTable rows={cleanRows} {...{ error, isLoading }} />
|
|
65
67
|
</Tab>
|
|
66
68
|
{categoryRows?.map((category) => (
|
|
67
69
|
<Tab label={category.name}>
|
|
68
|
-
<FormsTable rows={category.rows} />
|
|
70
|
+
<FormsTable rows={category.rows} {...{ error, isLoading }} />
|
|
69
71
|
</Tab>
|
|
70
72
|
))}
|
|
71
73
|
</Tabs>
|
package/src/forms/FormsTable.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ErrorState } from "@openmrs/esm-framework";
|
|
1
2
|
import {
|
|
2
3
|
DataTable,
|
|
3
4
|
DataTableSkeleton,
|
|
@@ -13,7 +14,8 @@ import {
|
|
|
13
14
|
TableToolbarSearch,
|
|
14
15
|
} from "carbon-components-react";
|
|
15
16
|
import React from "react";
|
|
16
|
-
import {
|
|
17
|
+
import { Link } from "react-router-dom";
|
|
18
|
+
import EmptyState from "../empty-state/EmptyState";
|
|
17
19
|
|
|
18
20
|
const formsHeader = [
|
|
19
21
|
{
|
|
@@ -26,13 +28,28 @@ const formsHeader = [
|
|
|
26
28
|
},
|
|
27
29
|
];
|
|
28
30
|
|
|
29
|
-
const FormsTable = ({ rows }) => {
|
|
31
|
+
const FormsTable = ({ rows, error, isLoading }) => {
|
|
30
32
|
const augmenteRows = rows?.map((row) => ({
|
|
31
33
|
...row,
|
|
32
|
-
actions:
|
|
34
|
+
actions: <Link to={row.uuid}>Fill Form</Link>,
|
|
33
35
|
}));
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
|
|
37
|
+
if (isLoading) return <DataTableSkeleton />;
|
|
38
|
+
if (error) {
|
|
39
|
+
return (
|
|
40
|
+
<EmptyState
|
|
41
|
+
headerTitle="Error Loading Data"
|
|
42
|
+
displayText={`Something went wrong loading data from the server. "${error?.response?.status}: ${error?.response?.statusText}"`}
|
|
43
|
+
/>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
if (augmenteRows.length === 0) {
|
|
47
|
+
return (
|
|
48
|
+
<EmptyState
|
|
49
|
+
headerTitle="No Forms To Show"
|
|
50
|
+
displayText="No forms could be found for this category. Please double check the form concept uuids and access permissions."
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
36
53
|
}
|
|
37
54
|
return (
|
|
38
55
|
<DataTable rows={augmenteRows} headers={formsHeader} isSortable>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { SkeletonText } from "carbon-components-react";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import useGetPatient from "./useGetPatient";
|
|
4
|
+
|
|
5
|
+
const CardContainer = ({ children }) => {
|
|
6
|
+
return <div style={{ padding: "1rem" }}>{children}</div>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const PatientCard = ({ patientUuid }) => {
|
|
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
|
+
|
|
15
|
+
if (!patient) {
|
|
16
|
+
return (
|
|
17
|
+
<CardContainer>
|
|
18
|
+
<SkeletonText style={{ maxWidth: "8rem" }} />
|
|
19
|
+
</CardContainer>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<CardContainer>
|
|
25
|
+
<div
|
|
26
|
+
style={{ fontWeight: 300, fontSize: "0.8rem", lineHeight: "0.9rem" }}
|
|
27
|
+
>
|
|
28
|
+
{identifier}
|
|
29
|
+
</div>
|
|
30
|
+
<div style={{ fontWeight: "bold" }}>
|
|
31
|
+
{givenName} {familyName}
|
|
32
|
+
</div>
|
|
33
|
+
</CardContainer>
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export default PatientCard;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render } from "@testing-library/react";
|
|
3
|
+
import PatientInfo from "./PatientInfo";
|
|
4
|
+
|
|
5
|
+
describe("PatientInfo", () => {
|
|
6
|
+
it("renders placeholder information when no data is present", () => {
|
|
7
|
+
render(<PatientInfo patientUuid={null} />);
|
|
8
|
+
});
|
|
9
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import {
|
|
2
|
+
age,
|
|
3
|
+
ExtensionSlot,
|
|
4
|
+
formatDate,
|
|
5
|
+
parseDate,
|
|
6
|
+
} from "@openmrs/esm-framework";
|
|
7
|
+
import {
|
|
8
|
+
Button,
|
|
9
|
+
SkeletonPlaceholder,
|
|
10
|
+
SkeletonText,
|
|
11
|
+
} from "carbon-components-react";
|
|
12
|
+
import React, { useState } from "react";
|
|
13
|
+
import styles from "./patient-info.scss";
|
|
14
|
+
import ChevronDown16 from "@carbon/icons-react/es/chevron--down/16";
|
|
15
|
+
import ChevronUp16 from "@carbon/icons-react/es/chevron--up/16";
|
|
16
|
+
import { useTranslation } from "react-i18next";
|
|
17
|
+
import useGetPatient from "./useGetPatient";
|
|
18
|
+
interface PatientInfoProps {
|
|
19
|
+
patientUuid: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const SkeletonPatientInfo = () => {
|
|
23
|
+
return (
|
|
24
|
+
<div className={styles.container}>
|
|
25
|
+
<div className={styles.patientInfoContainer}>
|
|
26
|
+
<SkeletonPlaceholder />
|
|
27
|
+
<div className={styles.patientInfoContent}>
|
|
28
|
+
<div className={styles.patientInfoRow}>
|
|
29
|
+
<span className={styles.patientName}>
|
|
30
|
+
<SkeletonText />
|
|
31
|
+
</span>
|
|
32
|
+
</div>
|
|
33
|
+
<div className={styles.patientInfoRow}>
|
|
34
|
+
<div className={styles.demographics}>
|
|
35
|
+
<span>
|
|
36
|
+
<SkeletonText />
|
|
37
|
+
</span>
|
|
38
|
+
<span>
|
|
39
|
+
<SkeletonText />
|
|
40
|
+
</span>
|
|
41
|
+
<span>
|
|
42
|
+
<SkeletonText />
|
|
43
|
+
</span>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<div className={styles.patientInfoRow}>
|
|
47
|
+
<span className={styles.identifier}>
|
|
48
|
+
<SkeletonText />
|
|
49
|
+
</span>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const PatientInfo: React.FC<PatientInfoProps> = ({ patientUuid }) => {
|
|
58
|
+
const patient = useGetPatient(patientUuid);
|
|
59
|
+
const { t } = useTranslation();
|
|
60
|
+
const [showContactDetails, setShowContactDetails] = useState<boolean>(false);
|
|
61
|
+
const patientName = `${patient?.name?.[0].given?.join(" ")} ${
|
|
62
|
+
patient?.name?.[0]?.family
|
|
63
|
+
}`;
|
|
64
|
+
|
|
65
|
+
const patientPhotoSlotState = React.useMemo(
|
|
66
|
+
() => ({ patientUuid: patient?.id, patientName }),
|
|
67
|
+
[patient?.id, patientName]
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const toggleShowMore = (e: React.MouseEvent) => {
|
|
71
|
+
e.stopPropagation();
|
|
72
|
+
setShowContactDetails((prevState) => !prevState);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (!patient) {
|
|
76
|
+
return <SkeletonPatientInfo />;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div className={styles.container}>
|
|
81
|
+
<div
|
|
82
|
+
className={`${
|
|
83
|
+
showContactDetails
|
|
84
|
+
? styles.activePatientInfoContainer
|
|
85
|
+
: styles.patientInfoContainer
|
|
86
|
+
}`}
|
|
87
|
+
>
|
|
88
|
+
<ExtensionSlot
|
|
89
|
+
extensionSlotName="patient-photo-slot"
|
|
90
|
+
state={patientPhotoSlotState}
|
|
91
|
+
/>
|
|
92
|
+
<div className={styles.patientInfoContent}>
|
|
93
|
+
<div className={styles.patientInfoRow}>
|
|
94
|
+
<span className={styles.patientName}>{patientName}</span>
|
|
95
|
+
</div>
|
|
96
|
+
<div className={styles.patientInfoRow}>
|
|
97
|
+
<div className={styles.demographics}>
|
|
98
|
+
<span>
|
|
99
|
+
{(patient.gender ?? t("unknown", "Unknown")).replace(
|
|
100
|
+
/^\w/,
|
|
101
|
+
(c) => c.toUpperCase()
|
|
102
|
+
)}{" "}
|
|
103
|
+
·{" "}
|
|
104
|
+
</span>
|
|
105
|
+
<span>{age(patient.birthDate)} · </span>
|
|
106
|
+
<span>
|
|
107
|
+
{formatDate(parseDate(patient.birthDate), {
|
|
108
|
+
mode: "wide",
|
|
109
|
+
time: false,
|
|
110
|
+
})}
|
|
111
|
+
</span>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
<div className={styles.patientInfoRow}>
|
|
115
|
+
<span className={styles.identifier}>
|
|
116
|
+
{patient.identifier.length
|
|
117
|
+
? patient.identifier
|
|
118
|
+
.map((identifier) => identifier.value)
|
|
119
|
+
.join(", ")
|
|
120
|
+
: "--"}
|
|
121
|
+
</span>
|
|
122
|
+
<Button
|
|
123
|
+
kind="ghost"
|
|
124
|
+
renderIcon={showContactDetails ? ChevronUp16 : ChevronDown16}
|
|
125
|
+
iconDescription="Toggle contact details"
|
|
126
|
+
onClick={(e) => toggleShowMore(e)}
|
|
127
|
+
disabled
|
|
128
|
+
>
|
|
129
|
+
{showContactDetails
|
|
130
|
+
? t("showLess", "Show less")
|
|
131
|
+
: t("showAllDetails", "Show all details")}
|
|
132
|
+
</Button>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export default PatientInfo;
|