@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,134 @@
|
|
|
1
|
+
import { useConfig, useSession } from "@openmrs/esm-framework";
|
|
2
|
+
import { Tab, Tabs, TabList, TabPanels, TabPanel } from "@carbon/react";
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { Config } from "../config-schema";
|
|
5
|
+
import { useGetAllForms } from "../hooks";
|
|
6
|
+
import FormsTable from "./forms-table";
|
|
7
|
+
import styles from "./styles.scss";
|
|
8
|
+
import { useTranslation } from "react-i18next";
|
|
9
|
+
import {
|
|
10
|
+
fdeWorkflowStorageName,
|
|
11
|
+
fdeWorkflowStorageVersion,
|
|
12
|
+
} from "../context/FormWorkflowReducer";
|
|
13
|
+
import {
|
|
14
|
+
fdeGroupWorkflowStorageName,
|
|
15
|
+
fdeGroupWorkflowStorageVersion,
|
|
16
|
+
} from "../context/GroupFormWorkflowReducer";
|
|
17
|
+
|
|
18
|
+
// helper function useful for debugging
|
|
19
|
+
// given a list of forms, it will organize into permissions
|
|
20
|
+
// and list which forms are associated with that permission
|
|
21
|
+
export const getFormPermissions = (forms) => {
|
|
22
|
+
const output = {};
|
|
23
|
+
forms?.forEach(
|
|
24
|
+
(form) =>
|
|
25
|
+
(output[form.encounterType.editPrivilege.display] = [
|
|
26
|
+
...(output[form.encounterType.editPrivilege.display] || []),
|
|
27
|
+
form.display,
|
|
28
|
+
])
|
|
29
|
+
);
|
|
30
|
+
return output;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Function adds `id` field to rows so they will be accepted by DataTable
|
|
34
|
+
// "display" is prefered for display name if present, otherwise fall back on "name'"
|
|
35
|
+
const prepareRowsForTable = (rawFormData) => {
|
|
36
|
+
if (rawFormData) {
|
|
37
|
+
return rawFormData?.map((form) => ({
|
|
38
|
+
...form,
|
|
39
|
+
id: form.uuid,
|
|
40
|
+
display: form.display || form.name,
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const FormsPage = () => {
|
|
47
|
+
const config = useConfig() as Config;
|
|
48
|
+
const { t } = useTranslation();
|
|
49
|
+
const { formCategories, formCategoriesToShow } = config;
|
|
50
|
+
const { forms, isLoading, error } = useGetAllForms();
|
|
51
|
+
const cleanRows = prepareRowsForTable(forms);
|
|
52
|
+
const { user } = useSession();
|
|
53
|
+
const savedFormsData = localStorage.getItem(
|
|
54
|
+
fdeWorkflowStorageName + ":" + user?.uuid
|
|
55
|
+
);
|
|
56
|
+
const savedGroupFormsData = localStorage.getItem(
|
|
57
|
+
fdeGroupWorkflowStorageName + ":" + user?.uuid
|
|
58
|
+
);
|
|
59
|
+
const activeForms = [];
|
|
60
|
+
const activeGroupForms = [];
|
|
61
|
+
|
|
62
|
+
if (
|
|
63
|
+
savedFormsData &&
|
|
64
|
+
JSON.parse(savedFormsData)?.["_storageVersion"] ===
|
|
65
|
+
fdeWorkflowStorageVersion
|
|
66
|
+
) {
|
|
67
|
+
Object.entries(JSON.parse(savedFormsData).forms).forEach(
|
|
68
|
+
([formUuid, form]: [string, { [key: string]: unknown }]) => {
|
|
69
|
+
if (form.workflowState) activeForms.push(formUuid);
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
if (
|
|
74
|
+
savedGroupFormsData &&
|
|
75
|
+
JSON.parse(savedGroupFormsData)?.["_storageVersion"] ===
|
|
76
|
+
fdeGroupWorkflowStorageVersion
|
|
77
|
+
) {
|
|
78
|
+
Object.entries(JSON.parse(savedGroupFormsData).forms).forEach(
|
|
79
|
+
([formUuid, form]: [string, { [key: string]: unknown }]) => {
|
|
80
|
+
if (form.workflowState) activeGroupForms.push(formUuid);
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const categoryRows = formCategoriesToShow.map((name) => {
|
|
86
|
+
const category = formCategories.find((category) => category.name === name);
|
|
87
|
+
let rows = [];
|
|
88
|
+
if (category && cleanRows && cleanRows.length) {
|
|
89
|
+
const uuids = category.forms?.map((form) => form.formUUID);
|
|
90
|
+
rows = cleanRows.filter((row) => uuids.includes(row.uuid));
|
|
91
|
+
}
|
|
92
|
+
return { ...{ name, rows } };
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className={styles.mainContent}>
|
|
97
|
+
<h3 className={styles.pageTitle}>
|
|
98
|
+
{t("fastDataEntry", "Fast Data Entry")}
|
|
99
|
+
</h3>
|
|
100
|
+
<Tabs>
|
|
101
|
+
<TabList>
|
|
102
|
+
<Tab label={t("allForms", "All Forms")}>
|
|
103
|
+
{`${t("allForms", "All Forms")} (${
|
|
104
|
+
cleanRows ? cleanRows?.length : "??"
|
|
105
|
+
})`}
|
|
106
|
+
</Tab>
|
|
107
|
+
{categoryRows?.map((category, index) => (
|
|
108
|
+
<Tab label={category.name} key={index}>
|
|
109
|
+
{`${category.name} (${category.rows.length})`}
|
|
110
|
+
</Tab>
|
|
111
|
+
))}
|
|
112
|
+
</TabList>
|
|
113
|
+
<TabPanels>
|
|
114
|
+
<TabPanel>
|
|
115
|
+
<FormsTable
|
|
116
|
+
rows={cleanRows}
|
|
117
|
+
{...{ error, isLoading, activeForms, activeGroupForms }}
|
|
118
|
+
/>
|
|
119
|
+
</TabPanel>
|
|
120
|
+
{categoryRows?.map((category, index) => (
|
|
121
|
+
<TabPanel key={index}>
|
|
122
|
+
<FormsTable
|
|
123
|
+
rows={category.rows}
|
|
124
|
+
{...{ error, isLoading, activeForms, activeGroupForms }}
|
|
125
|
+
/>
|
|
126
|
+
</TabPanel>
|
|
127
|
+
))}
|
|
128
|
+
</TabPanels>
|
|
129
|
+
</Tabs>
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export default FormsPage;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { ErrorState } from "@openmrs/esm-framework";
|
|
2
|
+
import {
|
|
3
|
+
DataTable,
|
|
4
|
+
DataTableSkeleton,
|
|
5
|
+
Table,
|
|
6
|
+
TableBody,
|
|
7
|
+
TableCell,
|
|
8
|
+
TableContainer,
|
|
9
|
+
TableHead,
|
|
10
|
+
TableHeader,
|
|
11
|
+
TableRow,
|
|
12
|
+
TableToolbar,
|
|
13
|
+
TableToolbarContent,
|
|
14
|
+
TableToolbarSearch,
|
|
15
|
+
} from "@carbon/react";
|
|
16
|
+
import React from "react";
|
|
17
|
+
import { useTranslation } from "react-i18next";
|
|
18
|
+
import { Link } from "react-router-dom";
|
|
19
|
+
import EmptyState from "../../empty-state/EmptyState";
|
|
20
|
+
import styles from "./styles.scss";
|
|
21
|
+
|
|
22
|
+
const FormsTable = ({
|
|
23
|
+
rows,
|
|
24
|
+
error,
|
|
25
|
+
isLoading,
|
|
26
|
+
activeForms,
|
|
27
|
+
activeGroupForms,
|
|
28
|
+
}) => {
|
|
29
|
+
const { t } = useTranslation();
|
|
30
|
+
|
|
31
|
+
const tableHeaders = [
|
|
32
|
+
{
|
|
33
|
+
key: "display",
|
|
34
|
+
header: t("formName", "Form Name"),
|
|
35
|
+
isSortable: true,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
key: "actions",
|
|
39
|
+
header: t("actions", "Actions"),
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
key: "actions2",
|
|
43
|
+
header: "",
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
const augmentedRows = rows?.map((row) => ({
|
|
48
|
+
...row,
|
|
49
|
+
actions: (
|
|
50
|
+
<Link to={`form/${row.uuid}`}>
|
|
51
|
+
{activeForms.includes(row.uuid)
|
|
52
|
+
? t("resumeSession", "Resume Session")
|
|
53
|
+
: t("fillForm", "Fill Form")}
|
|
54
|
+
</Link>
|
|
55
|
+
),
|
|
56
|
+
actions2: (
|
|
57
|
+
<Link to={`groupform/${row.uuid}`}>
|
|
58
|
+
{activeGroupForms.includes(row.uuid)
|
|
59
|
+
? t("resumeGroupSession", "Resume Group Session")
|
|
60
|
+
: t("startGroupSession", "Start Group Session")}
|
|
61
|
+
</Link>
|
|
62
|
+
),
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
if (isLoading) return <DataTableSkeleton />;
|
|
66
|
+
if (error) {
|
|
67
|
+
return (
|
|
68
|
+
<ErrorState
|
|
69
|
+
headerTitle={t("errorLoadingData", "Error Loading Data")}
|
|
70
|
+
error={error}
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
if (augmentedRows.length === 0) {
|
|
75
|
+
return (
|
|
76
|
+
<EmptyState
|
|
77
|
+
headerTitle={t("noFormsFound", "No Forms To Show")}
|
|
78
|
+
displayText={t(
|
|
79
|
+
"noFormsFoundMessage",
|
|
80
|
+
"No forms could be found for this category. Please double check the form concept uuids and access permissions."
|
|
81
|
+
)}
|
|
82
|
+
/>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
return (
|
|
86
|
+
<DataTable rows={augmentedRows} headers={tableHeaders}>
|
|
87
|
+
{({
|
|
88
|
+
rows,
|
|
89
|
+
headers,
|
|
90
|
+
getTableProps,
|
|
91
|
+
getHeaderProps,
|
|
92
|
+
getRowProps,
|
|
93
|
+
onInputChange,
|
|
94
|
+
}) => {
|
|
95
|
+
return (
|
|
96
|
+
<TableContainer>
|
|
97
|
+
<div className={styles.toolbarWrapper}>
|
|
98
|
+
<TableToolbar className={styles.tableToolbar}>
|
|
99
|
+
<TableToolbarContent>
|
|
100
|
+
<TableToolbarSearch onChange={onInputChange} />
|
|
101
|
+
</TableToolbarContent>
|
|
102
|
+
</TableToolbar>
|
|
103
|
+
</div>
|
|
104
|
+
<Table {...getTableProps()}>
|
|
105
|
+
<TableHead>
|
|
106
|
+
<TableRow>
|
|
107
|
+
{headers.map((header) => (
|
|
108
|
+
<TableHeader
|
|
109
|
+
{...getHeaderProps({
|
|
110
|
+
header,
|
|
111
|
+
isSortable: header.isSortable,
|
|
112
|
+
})}
|
|
113
|
+
>
|
|
114
|
+
{header.header}
|
|
115
|
+
</TableHeader>
|
|
116
|
+
))}
|
|
117
|
+
</TableRow>
|
|
118
|
+
</TableHead>
|
|
119
|
+
<TableBody>
|
|
120
|
+
{rows?.map((row) => (
|
|
121
|
+
<TableRow {...getRowProps({ row })}>
|
|
122
|
+
{row.cells.map((cell) => (
|
|
123
|
+
<TableCell key={cell.id}>{cell.value}</TableCell>
|
|
124
|
+
))}
|
|
125
|
+
</TableRow>
|
|
126
|
+
))}
|
|
127
|
+
</TableBody>
|
|
128
|
+
</Table>
|
|
129
|
+
</TableContainer>
|
|
130
|
+
);
|
|
131
|
+
}}
|
|
132
|
+
</DataTable>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export default FormsTable;
|
|
137
|
+
export { FormsTable };
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
.toolbarWrapper {
|
|
6
|
+
position: relative;
|
|
7
|
+
display: flex;
|
|
8
|
+
height: $spacing-09;
|
|
9
|
+
justify-content: flex-end;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.tableToolbar {
|
|
13
|
+
width: 20%;
|
|
14
|
+
min-width: 12.5rem;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.inactiveLink {
|
|
18
|
+
color: $carbon--gray-40;
|
|
19
|
+
cursor: not-allowed;
|
|
20
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ExtensionSlot } from "@openmrs/esm-framework";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import GroupDisplayHeader from "./group-display-header";
|
|
4
|
+
import styles from "./styles.scss";
|
|
5
|
+
import { GroupFormWorkflowProvider } from "../context/GroupFormWorkflowContext";
|
|
6
|
+
import GroupSearchHeader from "./group-search-header";
|
|
7
|
+
import SessionMetaWorkspace from "./SessionMetaWorkspace";
|
|
8
|
+
import GroupSessionWorkspace from "./GroupSessionWorkspace";
|
|
9
|
+
|
|
10
|
+
const GroupFormEntryWorkflow = () => {
|
|
11
|
+
return (
|
|
12
|
+
<GroupFormWorkflowProvider>
|
|
13
|
+
<div className={styles.breadcrumbsContainer}>
|
|
14
|
+
<ExtensionSlot extensionSlotName="breadcrumbs-slot" />
|
|
15
|
+
</div>
|
|
16
|
+
<GroupSearchHeader />
|
|
17
|
+
<GroupDisplayHeader />
|
|
18
|
+
<div className={styles.workspaceWrapper}>
|
|
19
|
+
<SessionMetaWorkspace />
|
|
20
|
+
<GroupSessionWorkspace />
|
|
21
|
+
</div>
|
|
22
|
+
</GroupFormWorkflowProvider>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default GroupFormEntryWorkflow;
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getGlobalStore,
|
|
3
|
+
useConfig,
|
|
4
|
+
useSession,
|
|
5
|
+
useStore,
|
|
6
|
+
} from "@openmrs/esm-framework";
|
|
7
|
+
import { Button } from "@carbon/react";
|
|
8
|
+
import React, { useCallback, useContext, useEffect, useState } from "react";
|
|
9
|
+
import PatientCard from "../patient-card/PatientCard";
|
|
10
|
+
import styles from "./styles.scss";
|
|
11
|
+
import { useTranslation } from "react-i18next";
|
|
12
|
+
import GroupFormWorkflowContext from "../context/GroupFormWorkflowContext";
|
|
13
|
+
import FormBootstrap from "../FormBootstrap";
|
|
14
|
+
import useStartVisit from "../hooks/useStartVisit";
|
|
15
|
+
import CompleteModal from "../CompleteModal";
|
|
16
|
+
import CancelModal from "../CancelModal";
|
|
17
|
+
|
|
18
|
+
const formStore = getGlobalStore("ampath-form-state");
|
|
19
|
+
|
|
20
|
+
const WorkflowNavigationButtons = () => {
|
|
21
|
+
const context = useContext(GroupFormWorkflowContext);
|
|
22
|
+
const {
|
|
23
|
+
activeFormUuid,
|
|
24
|
+
submitForNext,
|
|
25
|
+
patientUuids,
|
|
26
|
+
activePatientUuid,
|
|
27
|
+
workflowState,
|
|
28
|
+
} = context;
|
|
29
|
+
const store = useStore(formStore);
|
|
30
|
+
const formState = store[activeFormUuid];
|
|
31
|
+
const navigationDisabled =
|
|
32
|
+
(formState !== "ready" || workflowState !== "EDIT_FORM") &&
|
|
33
|
+
formState !== "readyWithValidationErrors";
|
|
34
|
+
const [cancelModalOpen, setCancelModalOpen] = useState(false);
|
|
35
|
+
const [completeModalOpen, setCompleteModalOpen] = useState(false);
|
|
36
|
+
const { t } = useTranslation();
|
|
37
|
+
|
|
38
|
+
const isLastPatient =
|
|
39
|
+
activePatientUuid === patientUuids[patientUuids.length - 1];
|
|
40
|
+
|
|
41
|
+
const handleClickNext = () => {
|
|
42
|
+
if (
|
|
43
|
+
workflowState === "EDIT_FORM" ||
|
|
44
|
+
formState === "readyWithValidationErrors"
|
|
45
|
+
) {
|
|
46
|
+
submitForNext();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<>
|
|
52
|
+
<div className={styles.rightPanelActionButtons}>
|
|
53
|
+
<Button
|
|
54
|
+
kind="primary"
|
|
55
|
+
onClick={handleClickNext}
|
|
56
|
+
disabled={navigationDisabled}
|
|
57
|
+
>
|
|
58
|
+
{isLastPatient
|
|
59
|
+
? t("saveForm", "Save Form")
|
|
60
|
+
: t("nextPatient", "Next Patient")}
|
|
61
|
+
</Button>
|
|
62
|
+
<Button kind="secondary" onClick={() => setCompleteModalOpen(true)}>
|
|
63
|
+
{t("saveAndComplete", "Save & Complete")}
|
|
64
|
+
</Button>
|
|
65
|
+
<Button kind="tertiary" onClick={() => setCancelModalOpen(true)}>
|
|
66
|
+
{t("cancel", "Cancel")}
|
|
67
|
+
</Button>
|
|
68
|
+
</div>
|
|
69
|
+
<CancelModal
|
|
70
|
+
open={cancelModalOpen}
|
|
71
|
+
setOpen={setCancelModalOpen}
|
|
72
|
+
context={context}
|
|
73
|
+
/>
|
|
74
|
+
<CompleteModal
|
|
75
|
+
open={completeModalOpen}
|
|
76
|
+
setOpen={setCompleteModalOpen}
|
|
77
|
+
context={context}
|
|
78
|
+
validateFirst={false}
|
|
79
|
+
/>
|
|
80
|
+
</>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const GroupSessionWorkspace = () => {
|
|
85
|
+
const { groupSessionConcepts } = useConfig();
|
|
86
|
+
const { t } = useTranslation();
|
|
87
|
+
const {
|
|
88
|
+
patientUuids,
|
|
89
|
+
activePatientUuid,
|
|
90
|
+
encounters,
|
|
91
|
+
activeEncounterUuid,
|
|
92
|
+
activeVisitUuid,
|
|
93
|
+
activeFormUuid,
|
|
94
|
+
saveEncounter,
|
|
95
|
+
activeSessionMeta,
|
|
96
|
+
groupVisitTypeUuid,
|
|
97
|
+
updateVisitUuid,
|
|
98
|
+
submitForNext,
|
|
99
|
+
workflowState,
|
|
100
|
+
} = useContext(GroupFormWorkflowContext);
|
|
101
|
+
|
|
102
|
+
const { sessionLocation } = useSession();
|
|
103
|
+
const [encounter, setEncounter] = useState(null);
|
|
104
|
+
const [visit, setVisit] = useState(null);
|
|
105
|
+
|
|
106
|
+
const {
|
|
107
|
+
saveVisit,
|
|
108
|
+
updateEncounter,
|
|
109
|
+
success: visitSaveSuccess,
|
|
110
|
+
} = useStartVisit({
|
|
111
|
+
showSuccessNotification: false,
|
|
112
|
+
showErrorNotification: true,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// 0. user clicks "next patient" in WorkflowNavigationButtons
|
|
116
|
+
// which triggers submitForNext() if workflowState === "EDIT_FORM"
|
|
117
|
+
|
|
118
|
+
// 1. save the new visit uuid and start form submission
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
if (
|
|
121
|
+
visitSaveSuccess &&
|
|
122
|
+
visitSaveSuccess.data.patient.uuid === activePatientUuid
|
|
123
|
+
) {
|
|
124
|
+
setVisit(visitSaveSuccess.data);
|
|
125
|
+
// Update visit UUID on workflow
|
|
126
|
+
updateVisitUuid(visitSaveSuccess.data.uuid);
|
|
127
|
+
}
|
|
128
|
+
}, [
|
|
129
|
+
visitSaveSuccess,
|
|
130
|
+
updateVisitUuid,
|
|
131
|
+
activeVisitUuid,
|
|
132
|
+
activePatientUuid,
|
|
133
|
+
visit,
|
|
134
|
+
setVisit,
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
// 2. If there's no active visit, trigger the creation of a new one
|
|
138
|
+
const handleEncounterCreate = useCallback(
|
|
139
|
+
(payload) => {
|
|
140
|
+
// Create a visit with the same date as the encounter being saved
|
|
141
|
+
if (!activeVisitUuid) {
|
|
142
|
+
saveVisit({
|
|
143
|
+
patientUuid: activePatientUuid,
|
|
144
|
+
startDatetime: activeSessionMeta.sessionDate,
|
|
145
|
+
stopDatetime: activeSessionMeta.sessionDate,
|
|
146
|
+
visitType: groupVisitTypeUuid,
|
|
147
|
+
location: sessionLocation?.uuid,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
const obsTime = new Date(activeSessionMeta.sessionDate);
|
|
151
|
+
payload.obs.forEach((item, index) => {
|
|
152
|
+
payload.obs[index] = {
|
|
153
|
+
...item,
|
|
154
|
+
groupMembers: item.groupMembers?.map((mem) => ({
|
|
155
|
+
...mem,
|
|
156
|
+
obsDatetime: obsTime.toISOString(),
|
|
157
|
+
})),
|
|
158
|
+
obsDatetime: obsTime.toISOString(),
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
// If this is a newly created encounter and visit, add session concepts to encounter payload.
|
|
162
|
+
if (!activeVisitUuid) {
|
|
163
|
+
Object.entries(groupSessionConcepts).forEach(([field, uuid]) => {
|
|
164
|
+
payload.obs.push({
|
|
165
|
+
concept: uuid,
|
|
166
|
+
value: activeSessionMeta?.[field],
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
payload.location = sessionLocation?.uuid;
|
|
171
|
+
payload.encounterDatetime = obsTime.toISOString();
|
|
172
|
+
},
|
|
173
|
+
[
|
|
174
|
+
activePatientUuid,
|
|
175
|
+
activeVisitUuid,
|
|
176
|
+
activeSessionMeta,
|
|
177
|
+
groupSessionConcepts,
|
|
178
|
+
groupVisitTypeUuid,
|
|
179
|
+
saveVisit,
|
|
180
|
+
sessionLocation,
|
|
181
|
+
]
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// 3. Update encounter so that it belongs to the created visit
|
|
185
|
+
useEffect(() => {
|
|
186
|
+
if (encounter && visit && encounter.patient?.uuid === visit.patient?.uuid) {
|
|
187
|
+
updateEncounter({ uuid: encounter.uuid, visit: visit.uuid });
|
|
188
|
+
}
|
|
189
|
+
}, [encounter, updateEncounter, visit]);
|
|
190
|
+
|
|
191
|
+
// 4. Once form has been posted, save the new encounter uuid so we can edit it later
|
|
192
|
+
const handlePostResponse = useCallback(
|
|
193
|
+
(encounter) => {
|
|
194
|
+
if (encounter && encounter.uuid) {
|
|
195
|
+
saveEncounter(encounter.uuid);
|
|
196
|
+
setEncounter(encounter);
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
[saveEncounter]
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
const switchPatient = useCallback(
|
|
203
|
+
(patientUuid) => {
|
|
204
|
+
submitForNext(patientUuid);
|
|
205
|
+
},
|
|
206
|
+
[submitForNext]
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
if (workflowState === "NEW_GROUP_SESSION") return null;
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<div className={styles.workspace}>
|
|
213
|
+
<div className={styles.formMainContent}>
|
|
214
|
+
<div className={styles.formContainer}>
|
|
215
|
+
<FormBootstrap
|
|
216
|
+
patientUuid={activePatientUuid}
|
|
217
|
+
encounterUuid={activeEncounterUuid}
|
|
218
|
+
{...{
|
|
219
|
+
formUuid: activeFormUuid,
|
|
220
|
+
handlePostResponse,
|
|
221
|
+
handleEncounterCreate,
|
|
222
|
+
}}
|
|
223
|
+
/>
|
|
224
|
+
</div>
|
|
225
|
+
<div className={styles.rightPanel}>
|
|
226
|
+
<h4>{t("formsFilled", "Forms filled")}</h4>
|
|
227
|
+
<div className={styles.patientCardsSection}>
|
|
228
|
+
{patientUuids?.map((patientUuid) => (
|
|
229
|
+
<PatientCard
|
|
230
|
+
key={patientUuid}
|
|
231
|
+
{...{
|
|
232
|
+
patientUuid,
|
|
233
|
+
activePatientUuid,
|
|
234
|
+
editEncounter: switchPatient,
|
|
235
|
+
encounters,
|
|
236
|
+
}}
|
|
237
|
+
/>
|
|
238
|
+
))}
|
|
239
|
+
</div>
|
|
240
|
+
<WorkflowNavigationButtons />
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
export default GroupSessionWorkspace;
|