@openmrs/esm-fast-data-entry-app 1.0.0-pre.9 → 1.0.0
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/__mocks__/react-i18next.js +9 -14
- package/dist/101.js +1 -0
- package/dist/101.js.map +1 -0
- package/dist/132.js +1 -0
- package/dist/188.js +1 -0
- package/dist/188.js.map +1 -0
- package/dist/197.js +1 -0
- package/dist/219.js +1 -0
- package/dist/219.js.map +1 -0
- package/dist/221.js +1 -0
- package/dist/221.js.map +1 -0
- package/dist/259.js +1 -0
- package/dist/259.js.map +1 -0
- package/dist/29.js +2 -0
- package/dist/29.js.LICENSE.txt +3 -0
- package/dist/29.js.map +1 -0
- package/dist/300.js +1 -0
- package/dist/326.js +1 -0
- package/dist/326.js.map +1 -0
- package/dist/335.js +1 -0
- package/dist/367.js +1 -0
- package/dist/367.js.map +1 -0
- package/dist/480.js +1 -0
- package/dist/540.js +2 -0
- package/dist/{382.js.LICENSE.txt → 540.js.LICENSE.txt} +3 -2
- package/dist/540.js.map +1 -0
- package/dist/55.js +1 -0
- package/dist/564.js +1 -0
- package/dist/564.js.map +1 -0
- package/dist/602.js +1 -0
- package/dist/602.js.map +1 -0
- package/dist/626.js +2 -0
- package/dist/{294.js.LICENSE.txt → 626.js.LICENSE.txt} +3 -8
- package/dist/626.js.map +1 -0
- package/dist/652.js +1 -0
- package/dist/685.js +1 -0
- package/dist/685.js.map +1 -0
- package/dist/773.js +2 -0
- package/dist/773.js.LICENSE.txt +32 -0
- package/dist/773.js.map +1 -0
- package/dist/893.js +1 -0
- package/dist/893.js.map +1 -0
- package/dist/91.js +1 -0
- package/dist/91.js.map +1 -0
- package/dist/941.js +2 -0
- package/dist/941.js.LICENSE.txt +30 -0
- package/dist/941.js.map +1 -0
- package/dist/961.js +2 -0
- package/dist/{735.js.LICENSE.txt → 961.js.LICENSE.txt} +6 -16
- package/dist/961.js.map +1 -0
- package/dist/99.js +1 -0
- package/dist/99.js.map +1 -0
- package/dist/991.js +1 -0
- package/dist/991.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 +537 -95
- 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 +101 -106
- package/prettier.config.js +8 -0
- package/src/CancelModal.tsx +42 -0
- package/src/CompleteModal.tsx +35 -0
- package/src/FormBootstrap.tsx +179 -0
- package/src/Root.tsx +11 -5
- package/src/add-group-modal/AddGroupModal.tsx +249 -0
- package/src/add-group-modal/styles.scss +49 -0
- package/src/config-schema.ts +124 -31
- package/src/constant.ts +1 -1
- package/src/context/FormWorkflowContext.tsx +113 -0
- package/src/context/FormWorkflowReducer.ts +263 -0
- package/src/context/GroupFormWorkflowContext.tsx +155 -0
- package/src/context/GroupFormWorkflowReducer.ts +405 -0
- package/src/declarations.d.ts +4 -0
- package/src/empty-state/EmptyDataIllustration.tsx +39 -0
- package/src/empty-state/EmptyState.tsx +28 -0
- package/src/empty-state/styles.scss +55 -0
- package/src/form-entry-workflow/FormEntryWorkflow.tsx +184 -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 +37 -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 +73 -0
- package/src/form-entry-workflow/patient-banner/index.ts +3 -0
- package/src/form-entry-workflow/patient-banner/styles.scss +44 -0
- package/src/form-entry-workflow/patient-search-header/PatientSearchHeader.tsx +54 -0
- package/src/form-entry-workflow/patient-search-header/index.ts +3 -0
- package/src/form-entry-workflow/patient-search-header/styles.scss +25 -0
- package/src/form-entry-workflow/styles.scss +63 -0
- package/src/form-entry-workflow/workflow-review/WorkflowReview.tsx +37 -0
- package/src/form-entry-workflow/workflow-review/index.ts +3 -0
- package/src/form-entry-workflow/workflow-review/styles.scss +30 -0
- package/src/forms-app-menu-link.tsx +6 -7
- package/src/forms-page/FormsPage.tsx +106 -0
- package/src/forms-page/forms-table/FormsTable.tsx +117 -0
- package/src/forms-page/forms-table/index.ts +3 -0
- package/src/forms-page/forms-table/styles.scss +19 -0
- package/src/forms-page/index.ts +3 -0
- package/src/forms-page/styles.scss +9 -0
- package/src/group-form-entry-workflow/GroupFormEntryWorkflow.tsx +26 -0
- package/src/group-form-entry-workflow/GroupSessionWorkspace.tsx +207 -0
- package/src/group-form-entry-workflow/SessionDetailsForm.tsx +154 -0
- package/src/group-form-entry-workflow/SessionMetaWorkspace.tsx +99 -0
- package/src/group-form-entry-workflow/attendance-table/AttendanceTable.tsx +130 -0
- package/src/group-form-entry-workflow/attendance-table/index.ts +1 -0
- package/src/group-form-entry-workflow/configurable-questions/ConfigurableQuestionsSection.tsx +41 -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 +55 -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 +128 -0
- package/src/group-form-entry-workflow/group-search/CompactGroupSearch.tsx +66 -0
- package/src/group-form-entry-workflow/group-search/GroupSearch.tsx +134 -0
- package/src/group-form-entry-workflow/group-search/compact-group-result.scss +63 -0
- package/src/group-form-entry-workflow/group-search/compact-group-search.scss +34 -0
- package/src/group-form-entry-workflow/group-search/group-search.scss +93 -0
- package/src/group-form-entry-workflow/group-search-header/GroupSearchHeader.tsx +72 -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 +94 -0
- package/src/hooks/index.ts +8 -0
- package/src/hooks/useForm.ts +56 -0
- package/src/hooks/useFormState.ts +23 -0
- package/src/hooks/useGetAllForms.ts +37 -0
- package/src/hooks/useGetEncounter.ts +21 -0
- package/src/hooks/useGetPatient.ts +23 -0
- package/src/hooks/useGetPatients.ts +32 -0
- package/src/hooks/useGetSystemSetting.ts +36 -0
- package/src/hooks/useKeyPress.ts +31 -0
- package/src/hooks/usePostEndpoint.ts +76 -0
- package/src/hooks/useSearchEndpoint.ts +103 -0
- package/src/hooks/useStartVisit.ts +82 -0
- package/src/index.ts +18 -66
- package/src/patient-card/PatientCard.tsx +55 -0
- package/src/patient-card/index.ts +3 -0
- package/src/patient-card/styles.scss +44 -0
- package/src/routes.json +24 -0
- package/src/setup-tests.ts +1 -1
- package/src/types.ts +20 -0
- package/tools/i18next-parser.config.js +93 -0
- package/translations/am.json +75 -0
- package/translations/ar.json +75 -0
- package/translations/en.json +75 -4
- package/translations/es.json +75 -0
- package/translations/fr.json +75 -0
- package/translations/he.json +75 -0
- package/translations/km.json +75 -0
- package/tsconfig.json +26 -23
- package/turbo.json +18 -0
- package/webpack.config.js +1 -1
- 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/294.js +0 -3
- 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/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.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/declarations.d.tsx +0 -2
- 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,106 @@
|
|
|
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 { type 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 { fdeWorkflowStorageName, fdeWorkflowStorageVersion } from '../context/FormWorkflowReducer';
|
|
10
|
+
import { fdeGroupWorkflowStorageName, fdeGroupWorkflowStorageVersion } from '../context/GroupFormWorkflowReducer';
|
|
11
|
+
|
|
12
|
+
// helper function useful for debugging
|
|
13
|
+
// given a list of forms, it will organize into permissions
|
|
14
|
+
// and list which forms are associated with that permission
|
|
15
|
+
export const getFormPermissions = (forms) => {
|
|
16
|
+
const output = {};
|
|
17
|
+
forms?.forEach(
|
|
18
|
+
(form) =>
|
|
19
|
+
(output[form.encounterType.editPrivilege.display] = [
|
|
20
|
+
...(output[form.encounterType.editPrivilege.display] || []),
|
|
21
|
+
form.display,
|
|
22
|
+
]),
|
|
23
|
+
);
|
|
24
|
+
return output;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Function adds `id` field to rows so they will be accepted by DataTable
|
|
28
|
+
// "display" is prefered for display name if present, otherwise fall back on "name'"
|
|
29
|
+
const prepareRowsForTable = (rawFormData) => {
|
|
30
|
+
if (rawFormData) {
|
|
31
|
+
return rawFormData?.map((form) => ({
|
|
32
|
+
...form,
|
|
33
|
+
id: form.uuid,
|
|
34
|
+
display: form.display || form.name,
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const FormsPage = () => {
|
|
41
|
+
const config = useConfig();
|
|
42
|
+
const { t } = useTranslation();
|
|
43
|
+
const { formCategories, formCategoriesToShow } = config;
|
|
44
|
+
const { forms, isLoading, error } = useGetAllForms();
|
|
45
|
+
const cleanRows = prepareRowsForTable(forms);
|
|
46
|
+
const { user } = useSession();
|
|
47
|
+
const savedFormsData = localStorage.getItem(fdeWorkflowStorageName + ':' + user?.uuid);
|
|
48
|
+
const savedGroupFormsData = localStorage.getItem(fdeGroupWorkflowStorageName + ':' + user?.uuid);
|
|
49
|
+
const activeForms = [];
|
|
50
|
+
const activeGroupForms = [];
|
|
51
|
+
|
|
52
|
+
if (savedFormsData && JSON.parse(savedFormsData)?.['_storageVersion'] === fdeWorkflowStorageVersion) {
|
|
53
|
+
Object.entries(JSON.parse(savedFormsData).forms).forEach(
|
|
54
|
+
([formUuid, form]: [string, { [key: string]: unknown }]) => {
|
|
55
|
+
if (form.workflowState) activeForms.push(formUuid);
|
|
56
|
+
},
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
if (savedGroupFormsData && JSON.parse(savedGroupFormsData)?.['_storageVersion'] === fdeGroupWorkflowStorageVersion) {
|
|
60
|
+
Object.entries(JSON.parse(savedGroupFormsData).forms).forEach(
|
|
61
|
+
([formUuid, form]: [string, { [key: string]: unknown }]) => {
|
|
62
|
+
if (form.workflowState) activeGroupForms.push(formUuid);
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const categoryRows = formCategoriesToShow.map((name) => {
|
|
68
|
+
const category = formCategories.find((category) => category.name === name);
|
|
69
|
+
let rows = [];
|
|
70
|
+
if (category && cleanRows && cleanRows.length) {
|
|
71
|
+
const uuids = category.forms?.map((form) => form.formUUID);
|
|
72
|
+
rows = cleanRows.filter((row) => uuids.includes(row.uuid));
|
|
73
|
+
}
|
|
74
|
+
return { ...{ name, rows } };
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div className={styles.mainContent}>
|
|
79
|
+
<h3 className={styles.pageTitle}>{t('fastDataEntry', 'Fast Data Entry')}</h3>
|
|
80
|
+
<Tabs>
|
|
81
|
+
<TabList>
|
|
82
|
+
<Tab label={t('allForms', 'All Forms')}>
|
|
83
|
+
{`${t('allForms', 'All Forms')} (${cleanRows ? cleanRows?.length : '??'})`}
|
|
84
|
+
</Tab>
|
|
85
|
+
{categoryRows?.map((category, index) => (
|
|
86
|
+
<Tab label={category.name} key={index}>
|
|
87
|
+
{`${category.name} (${category.rows.length})`}
|
|
88
|
+
</Tab>
|
|
89
|
+
))}
|
|
90
|
+
</TabList>
|
|
91
|
+
<TabPanels>
|
|
92
|
+
<TabPanel>
|
|
93
|
+
<FormsTable rows={cleanRows} {...{ error, isLoading, activeForms, activeGroupForms }} />
|
|
94
|
+
</TabPanel>
|
|
95
|
+
{categoryRows?.map((category, index) => (
|
|
96
|
+
<TabPanel key={index}>
|
|
97
|
+
<FormsTable rows={category.rows} {...{ error, isLoading, activeForms, activeGroupForms }} />
|
|
98
|
+
</TabPanel>
|
|
99
|
+
))}
|
|
100
|
+
</TabPanels>
|
|
101
|
+
</Tabs>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export default FormsPage;
|
|
@@ -0,0 +1,117 @@
|
|
|
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 = ({ rows, error, isLoading, activeForms, activeGroupForms }) => {
|
|
23
|
+
const { t } = useTranslation();
|
|
24
|
+
|
|
25
|
+
const tableHeaders = [
|
|
26
|
+
{
|
|
27
|
+
key: 'display',
|
|
28
|
+
header: t('formName', 'Form Name'),
|
|
29
|
+
isSortable: true,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
key: 'actions',
|
|
33
|
+
header: t('actions', 'Actions'),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
key: 'actions2',
|
|
37
|
+
header: '',
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const augmentedRows = rows?.map((row) => ({
|
|
42
|
+
...row,
|
|
43
|
+
actions: (
|
|
44
|
+
<Link to={`form/${row.uuid}`}>
|
|
45
|
+
{activeForms.includes(row.uuid) ? t('resumeSession', 'Resume Session') : t('fillForm', 'Fill Form')}
|
|
46
|
+
</Link>
|
|
47
|
+
),
|
|
48
|
+
actions2: (
|
|
49
|
+
<Link to={`groupform/${row.uuid}`}>
|
|
50
|
+
{activeGroupForms.includes(row.uuid)
|
|
51
|
+
? t('resumeGroupSession', 'Resume Group Session')
|
|
52
|
+
: t('startGroupSession', 'Start Group Session')}
|
|
53
|
+
</Link>
|
|
54
|
+
),
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
if (isLoading) return <DataTableSkeleton />;
|
|
58
|
+
if (error) {
|
|
59
|
+
return <ErrorState headerTitle={t('errorLoadingData', 'Error Loading Data')} error={error} />;
|
|
60
|
+
}
|
|
61
|
+
if (augmentedRows.length === 0) {
|
|
62
|
+
return (
|
|
63
|
+
<EmptyState
|
|
64
|
+
headerTitle={t('noFormsFound', 'No Forms To Show')}
|
|
65
|
+
displayText={t(
|
|
66
|
+
'noFormsFoundMessage',
|
|
67
|
+
'No forms could be found for this category. Please double check the form concept uuids and access permissions.',
|
|
68
|
+
)}
|
|
69
|
+
/>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return (
|
|
73
|
+
<DataTable rows={augmentedRows} headers={tableHeaders}>
|
|
74
|
+
{({ rows, headers, getTableProps, getHeaderProps, getRowProps, onInputChange }) => {
|
|
75
|
+
return (
|
|
76
|
+
<TableContainer>
|
|
77
|
+
<div className={styles.toolbarWrapper}>
|
|
78
|
+
<TableToolbar className={styles.tableToolbar}>
|
|
79
|
+
<TableToolbarContent>
|
|
80
|
+
<TableToolbarSearch onChange={onInputChange} />
|
|
81
|
+
</TableToolbarContent>
|
|
82
|
+
</TableToolbar>
|
|
83
|
+
</div>
|
|
84
|
+
<Table {...getTableProps()}>
|
|
85
|
+
<TableHead>
|
|
86
|
+
<TableRow>
|
|
87
|
+
{headers.map((header) => (
|
|
88
|
+
<TableHeader
|
|
89
|
+
{...getHeaderProps({
|
|
90
|
+
header,
|
|
91
|
+
isSortable: header.isSortable,
|
|
92
|
+
})}
|
|
93
|
+
>
|
|
94
|
+
{header.header}
|
|
95
|
+
</TableHeader>
|
|
96
|
+
))}
|
|
97
|
+
</TableRow>
|
|
98
|
+
</TableHead>
|
|
99
|
+
<TableBody>
|
|
100
|
+
{rows?.map((row) => (
|
|
101
|
+
<TableRow {...getRowProps({ row })}>
|
|
102
|
+
{row.cells.map((cell) => (
|
|
103
|
+
<TableCell key={cell.id}>{cell.value}</TableCell>
|
|
104
|
+
))}
|
|
105
|
+
</TableRow>
|
|
106
|
+
))}
|
|
107
|
+
</TableBody>
|
|
108
|
+
</Table>
|
|
109
|
+
</TableContainer>
|
|
110
|
+
);
|
|
111
|
+
}}
|
|
112
|
+
</DataTable>
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export default FormsTable;
|
|
117
|
+
export { FormsTable };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
@use '@carbon/colors';
|
|
2
|
+
@use '@carbon/layout';
|
|
3
|
+
|
|
4
|
+
.toolbarWrapper {
|
|
5
|
+
position: relative;
|
|
6
|
+
display: flex;
|
|
7
|
+
height: layout.$spacing-09;
|
|
8
|
+
justify-content: flex-end;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.tableToolbar {
|
|
12
|
+
width: 20%;
|
|
13
|
+
min-width: 12.5rem;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.inactiveLink {
|
|
17
|
+
color: colors.$gray-40;
|
|
18
|
+
cursor: not-allowed;
|
|
19
|
+
}
|
|
@@ -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 name="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,207 @@
|
|
|
1
|
+
import { getGlobalStore, useConfig, useSession, useStore } from '@openmrs/esm-framework';
|
|
2
|
+
import { Button } from '@carbon/react';
|
|
3
|
+
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
|
4
|
+
import PatientCard from '../patient-card/PatientCard';
|
|
5
|
+
import styles from './styles.scss';
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
import { v4 as uuid } from 'uuid';
|
|
8
|
+
import GroupFormWorkflowContext from '../context/GroupFormWorkflowContext';
|
|
9
|
+
import FormBootstrap from '../FormBootstrap';
|
|
10
|
+
import CompleteModal from '../CompleteModal';
|
|
11
|
+
import CancelModal from '../CancelModal';
|
|
12
|
+
|
|
13
|
+
const formStore = getGlobalStore('ampath-form-state');
|
|
14
|
+
|
|
15
|
+
const WorkflowNavigationButtons = () => {
|
|
16
|
+
const context = useContext(GroupFormWorkflowContext);
|
|
17
|
+
const { activeFormUuid, submitForNext, patientUuids, activePatientUuid, workflowState } = context;
|
|
18
|
+
const store = useStore(formStore);
|
|
19
|
+
const formState = store[activeFormUuid];
|
|
20
|
+
const navigationDisabled =
|
|
21
|
+
(formState !== 'ready' || workflowState !== 'EDIT_FORM') && formState !== 'readyWithValidationErrors';
|
|
22
|
+
const [cancelModalOpen, setCancelModalOpen] = useState(false);
|
|
23
|
+
const [completeModalOpen, setCompleteModalOpen] = useState(false);
|
|
24
|
+
const { t } = useTranslation();
|
|
25
|
+
|
|
26
|
+
const isLastPatient = activePatientUuid === patientUuids[patientUuids.length - 1];
|
|
27
|
+
|
|
28
|
+
const handleClickNext = () => {
|
|
29
|
+
if (workflowState === 'EDIT_FORM' || formState === 'readyWithValidationErrors') {
|
|
30
|
+
submitForNext();
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<>
|
|
36
|
+
<div className={styles.rightPanelActionButtons}>
|
|
37
|
+
<Button kind="primary" onClick={handleClickNext} disabled={navigationDisabled}>
|
|
38
|
+
{isLastPatient ? t('saveForm', 'Save Form') : t('nextPatient', 'Next patient')}
|
|
39
|
+
</Button>
|
|
40
|
+
<Button kind="secondary" onClick={() => setCompleteModalOpen(true)}>
|
|
41
|
+
{t('saveAndComplete', 'Save & Complete')}
|
|
42
|
+
</Button>
|
|
43
|
+
<Button kind="tertiary" onClick={() => setCancelModalOpen(true)}>
|
|
44
|
+
{t('cancel', 'Cancel')}
|
|
45
|
+
</Button>
|
|
46
|
+
</div>
|
|
47
|
+
<CancelModal open={cancelModalOpen} setOpen={setCancelModalOpen} context={context} />
|
|
48
|
+
<CompleteModal open={completeModalOpen} setOpen={setCompleteModalOpen} context={context} validateFirst={false} />
|
|
49
|
+
</>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const GroupSessionWorkspace = () => {
|
|
54
|
+
const { groupSessionConcepts } = useConfig();
|
|
55
|
+
const { t } = useTranslation();
|
|
56
|
+
const {
|
|
57
|
+
patientUuids,
|
|
58
|
+
activePatientUuid,
|
|
59
|
+
encounters,
|
|
60
|
+
activeEncounterUuid,
|
|
61
|
+
activeVisitUuid,
|
|
62
|
+
activeFormUuid,
|
|
63
|
+
activeGroupUuid,
|
|
64
|
+
activeGroupName,
|
|
65
|
+
activeSessionUuid,
|
|
66
|
+
saveEncounter,
|
|
67
|
+
activeSessionMeta,
|
|
68
|
+
groupVisitTypeUuid,
|
|
69
|
+
updateVisitUuid,
|
|
70
|
+
submitForNext,
|
|
71
|
+
workflowState,
|
|
72
|
+
} = useContext(GroupFormWorkflowContext);
|
|
73
|
+
|
|
74
|
+
const { sessionLocation } = useSession();
|
|
75
|
+
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
if (activeVisitUuid) {
|
|
78
|
+
updateVisitUuid(activeVisitUuid);
|
|
79
|
+
}
|
|
80
|
+
}, [updateVisitUuid, activeVisitUuid, activePatientUuid]);
|
|
81
|
+
|
|
82
|
+
// If there's no active visit, trigger the creation of a new one
|
|
83
|
+
const handleEncounterCreate = useCallback(
|
|
84
|
+
(payload) => {
|
|
85
|
+
// Create a visit with the same date as the encounter being saved
|
|
86
|
+
const obsTime = new Date(activeSessionMeta.sessionDate);
|
|
87
|
+
payload.obs.forEach((item, index) => {
|
|
88
|
+
payload.obs[index] = {
|
|
89
|
+
...item,
|
|
90
|
+
groupMembers: item.groupMembers?.map((mem) => ({
|
|
91
|
+
...mem,
|
|
92
|
+
obsDatetime: obsTime.toISOString(),
|
|
93
|
+
})),
|
|
94
|
+
obsDatetime: obsTime.toISOString(),
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
const visitUuid = activeVisitUuid ? activeVisitUuid : uuid();
|
|
98
|
+
if (!activeVisitUuid) {
|
|
99
|
+
Object.entries(groupSessionConcepts).forEach(([field, uuid]) => {
|
|
100
|
+
if (activeSessionMeta?.[field] != null) {
|
|
101
|
+
payload.obs.push({
|
|
102
|
+
concept: uuid,
|
|
103
|
+
value: activeSessionMeta[field],
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const otherIdentifiers = [
|
|
109
|
+
{ concept: groupSessionConcepts.cohortId, value: activeGroupUuid },
|
|
110
|
+
{ concept: groupSessionConcepts.cohortName, value: activeGroupName },
|
|
111
|
+
{
|
|
112
|
+
concept: groupSessionConcepts.sessionUuid,
|
|
113
|
+
value: activeSessionUuid,
|
|
114
|
+
},
|
|
115
|
+
];
|
|
116
|
+
payload.obs.push(...otherIdentifiers);
|
|
117
|
+
// If this is a newly created encounter and visit, add session concepts to encounter payload.
|
|
118
|
+
const visitInfo = {
|
|
119
|
+
startDatetime: activeSessionMeta.sessionDate,
|
|
120
|
+
stopDatetime: activeSessionMeta.sessionDate,
|
|
121
|
+
uuid: visitUuid,
|
|
122
|
+
patient: {
|
|
123
|
+
uuid: activePatientUuid,
|
|
124
|
+
},
|
|
125
|
+
location: {
|
|
126
|
+
uuid: sessionLocation?.uuid,
|
|
127
|
+
},
|
|
128
|
+
visitType: {
|
|
129
|
+
uuid: groupVisitTypeUuid,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
payload.visit = visitInfo;
|
|
133
|
+
updateVisitUuid(visitUuid);
|
|
134
|
+
}
|
|
135
|
+
payload.location = sessionLocation?.uuid;
|
|
136
|
+
payload.encounterDatetime = obsTime.toISOString();
|
|
137
|
+
},
|
|
138
|
+
[
|
|
139
|
+
activeSessionMeta,
|
|
140
|
+
activeVisitUuid,
|
|
141
|
+
sessionLocation?.uuid,
|
|
142
|
+
groupSessionConcepts,
|
|
143
|
+
activeGroupUuid,
|
|
144
|
+
activeGroupName,
|
|
145
|
+
activeSessionUuid,
|
|
146
|
+
activePatientUuid,
|
|
147
|
+
groupVisitTypeUuid,
|
|
148
|
+
updateVisitUuid,
|
|
149
|
+
],
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Once form has been posted, save the new encounter uuid so we can edit it later
|
|
153
|
+
const handlePostResponse = useCallback(
|
|
154
|
+
(encounter) => {
|
|
155
|
+
if (encounter && encounter.uuid) {
|
|
156
|
+
saveEncounter(encounter.uuid);
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
[saveEncounter],
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const switchPatient = useCallback(
|
|
163
|
+
(patientUuid) => {
|
|
164
|
+
submitForNext(patientUuid);
|
|
165
|
+
},
|
|
166
|
+
[submitForNext],
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
if (workflowState === 'NEW_GROUP_SESSION') return null;
|
|
170
|
+
|
|
171
|
+
return (
|
|
172
|
+
<div className={styles.workspace}>
|
|
173
|
+
<div className={styles.formMainContent}>
|
|
174
|
+
<div className={styles.formContainer}>
|
|
175
|
+
<FormBootstrap
|
|
176
|
+
patientUuid={activePatientUuid}
|
|
177
|
+
encounterUuid={activeEncounterUuid}
|
|
178
|
+
{...{
|
|
179
|
+
formUuid: activeFormUuid,
|
|
180
|
+
handlePostResponse,
|
|
181
|
+
handleEncounterCreate,
|
|
182
|
+
}}
|
|
183
|
+
/>
|
|
184
|
+
</div>
|
|
185
|
+
<div className={styles.rightPanel}>
|
|
186
|
+
<h4>{t('formsFilled', 'Forms filled')}</h4>
|
|
187
|
+
<div className={styles.patientCardsSection}>
|
|
188
|
+
{patientUuids?.map((patientUuid) => (
|
|
189
|
+
<PatientCard
|
|
190
|
+
key={patientUuid}
|
|
191
|
+
{...{
|
|
192
|
+
patientUuid,
|
|
193
|
+
activePatientUuid,
|
|
194
|
+
editEncounter: switchPatient,
|
|
195
|
+
encounters,
|
|
196
|
+
}}
|
|
197
|
+
/>
|
|
198
|
+
))}
|
|
199
|
+
</div>
|
|
200
|
+
<WorkflowNavigationButtons />
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
export default GroupSessionWorkspace;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { Layer, Tile, TextInput, TextArea, DatePicker, DatePickerInput } from '@carbon/react';
|
|
2
|
+
import React, { useContext } from 'react';
|
|
3
|
+
import { useConfig } from '@openmrs/esm-framework';
|
|
4
|
+
import { useParams } from 'react-router-dom';
|
|
5
|
+
import styles from './styles.scss';
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
import { Controller, useFormContext } from 'react-hook-form';
|
|
8
|
+
import { AttendanceTable } from './attendance-table';
|
|
9
|
+
import GroupFormWorkflowContext from '../context/GroupFormWorkflowContext';
|
|
10
|
+
import useGetPatients from '../hooks/useGetPatients';
|
|
11
|
+
import ConfigurableQuestionsSection from './configurable-questions/ConfigurableQuestionsSection';
|
|
12
|
+
import useSpecificQuestions from '../hooks/useForm';
|
|
13
|
+
|
|
14
|
+
interface ParamTypes {
|
|
15
|
+
formUuid?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const SessionDetailsForm = () => {
|
|
19
|
+
const { specificQuestions } = useConfig();
|
|
20
|
+
const { formUuid } = useParams() as ParamTypes;
|
|
21
|
+
const { questions } = useSpecificQuestions(formUuid, specificQuestions);
|
|
22
|
+
|
|
23
|
+
const { t } = useTranslation();
|
|
24
|
+
const {
|
|
25
|
+
register,
|
|
26
|
+
formState: { errors },
|
|
27
|
+
control,
|
|
28
|
+
} = useFormContext();
|
|
29
|
+
|
|
30
|
+
const { activeGroupMembers } = useContext(GroupFormWorkflowContext);
|
|
31
|
+
const { patients, isLoading } = useGetPatients(activeGroupMembers);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div>
|
|
35
|
+
{!isLoading && (
|
|
36
|
+
<div className={styles.formSection}>
|
|
37
|
+
<h4>{t('sessionDetails', '1. Session details')}</h4>
|
|
38
|
+
<div>
|
|
39
|
+
<p>{t('allFieldsRequired', 'All fields are required unless marked optional')}</p>
|
|
40
|
+
</div>
|
|
41
|
+
<Layer>
|
|
42
|
+
<Tile className={styles.formSectionTile}>
|
|
43
|
+
<Layer>
|
|
44
|
+
<div
|
|
45
|
+
style={{
|
|
46
|
+
display: 'flex',
|
|
47
|
+
flexDirection: 'column',
|
|
48
|
+
rowGap: '1.5rem',
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
<TextInput
|
|
52
|
+
id="text"
|
|
53
|
+
type="text"
|
|
54
|
+
labelText={t('sessionName', 'Session Name')}
|
|
55
|
+
{...register('sessionName', { required: true })}
|
|
56
|
+
invalid={errors.sessionName}
|
|
57
|
+
invalidText={t('requiredField', 'This field is required')}
|
|
58
|
+
/>
|
|
59
|
+
<TextInput
|
|
60
|
+
id="text"
|
|
61
|
+
type="text"
|
|
62
|
+
labelText={t('practitionerName', 'Practitioner Name')}
|
|
63
|
+
{...register('practitionerName', { required: true })}
|
|
64
|
+
invalid={errors.practitionerName}
|
|
65
|
+
invalidText={t('requiredField', 'This field is required')}
|
|
66
|
+
/>
|
|
67
|
+
<Controller
|
|
68
|
+
name="sessionDate"
|
|
69
|
+
control={control}
|
|
70
|
+
rules={{ required: true }}
|
|
71
|
+
render={({ field }) => (
|
|
72
|
+
<DatePicker datePickerType="single" size="md" maxDate={new Date()} {...field}>
|
|
73
|
+
<DatePickerInput
|
|
74
|
+
id="session-date"
|
|
75
|
+
labelText={t('sessionDate', 'Session Date')}
|
|
76
|
+
placeholder="mm/dd/yyyy"
|
|
77
|
+
size="md"
|
|
78
|
+
invalid={errors.sessionDate}
|
|
79
|
+
invalidText={t('requiredField', 'This field is required')}
|
|
80
|
+
/>
|
|
81
|
+
</DatePicker>
|
|
82
|
+
)}
|
|
83
|
+
/>
|
|
84
|
+
<TextArea
|
|
85
|
+
id="text"
|
|
86
|
+
type="text"
|
|
87
|
+
labelText={t('sessionNotes', 'Session Notes')}
|
|
88
|
+
{...register('sessionNotes', { required: true })}
|
|
89
|
+
invalid={errors.sessionNotes}
|
|
90
|
+
invalidText={t('requiredField', 'This field is required')}
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
</Layer>
|
|
94
|
+
</Tile>
|
|
95
|
+
</Layer>
|
|
96
|
+
<h4>{t('sessionParticipants', '2. Session participants')}</h4>
|
|
97
|
+
<div>
|
|
98
|
+
<p>
|
|
99
|
+
{t(
|
|
100
|
+
'markAbsentPatients',
|
|
101
|
+
'The patients in this group. Patients that are not present in the session should be marked as absent.',
|
|
102
|
+
)}
|
|
103
|
+
</p>
|
|
104
|
+
</div>
|
|
105
|
+
<Layer>
|
|
106
|
+
<Tile className={styles.formSectionTile}>
|
|
107
|
+
<Layer>
|
|
108
|
+
<div
|
|
109
|
+
style={{
|
|
110
|
+
display: 'flex',
|
|
111
|
+
flexDirection: 'column',
|
|
112
|
+
rowGap: '1.5rem',
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
<AttendanceTable patients={patients} />
|
|
116
|
+
</div>
|
|
117
|
+
</Layer>
|
|
118
|
+
</Tile>
|
|
119
|
+
</Layer>
|
|
120
|
+
{questions?.length > 0 ? (
|
|
121
|
+
<>
|
|
122
|
+
<h4>{t('sessionSpecificDetails', '3. Specific details')}</h4>
|
|
123
|
+
<div>
|
|
124
|
+
<p>
|
|
125
|
+
{t(
|
|
126
|
+
'sessionSpecificDetailsDescription',
|
|
127
|
+
'They will be mapped to form responses for all patients as pre-filled data.',
|
|
128
|
+
)}
|
|
129
|
+
</p>
|
|
130
|
+
</div>
|
|
131
|
+
<Layer>
|
|
132
|
+
<Tile className={styles.formSectionTile}>
|
|
133
|
+
<Layer>
|
|
134
|
+
<div
|
|
135
|
+
style={{
|
|
136
|
+
display: 'flex',
|
|
137
|
+
flexDirection: 'column',
|
|
138
|
+
rowGap: '1.5rem',
|
|
139
|
+
}}
|
|
140
|
+
>
|
|
141
|
+
<ConfigurableQuestionsSection register={register} specificQuestions={questions} />
|
|
142
|
+
</div>
|
|
143
|
+
</Layer>
|
|
144
|
+
</Tile>
|
|
145
|
+
</Layer>
|
|
146
|
+
</>
|
|
147
|
+
) : null}
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export default SessionDetailsForm;
|