@kenyaemr/esm-patient-list-management-app 7.0.2-pre.65
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/.turbo/turbo-build.log +41 -0
- package/dist/130.js +2 -0
- package/dist/130.js.LICENSE.txt +3 -0
- package/dist/130.js.map +1 -0
- package/dist/139.js +1 -0
- package/dist/139.js.map +1 -0
- package/dist/255.js +2 -0
- package/dist/255.js.LICENSE.txt +9 -0
- package/dist/255.js.map +1 -0
- package/dist/271.js +1 -0
- package/dist/319.js +1 -0
- package/dist/382.js +1 -0
- package/dist/382.js.map +1 -0
- package/dist/443.js +1 -0
- package/dist/443.js.map +1 -0
- package/dist/460.js +1 -0
- package/dist/548.js +1 -0
- package/dist/548.js.map +1 -0
- package/dist/574.js +1 -0
- package/dist/591.js +2 -0
- package/dist/591.js.LICENSE.txt +32 -0
- package/dist/591.js.map +1 -0
- package/dist/635.js +1 -0
- package/dist/635.js.map +1 -0
- package/dist/644.js +1 -0
- package/dist/729.js +1 -0
- package/dist/729.js.map +1 -0
- package/dist/757.js +1 -0
- package/dist/784.js +2 -0
- package/dist/784.js.LICENSE.txt +9 -0
- package/dist/784.js.map +1 -0
- package/dist/788.js +1 -0
- package/dist/807.js +1 -0
- package/dist/833.js +1 -0
- package/dist/930.js +2 -0
- package/dist/930.js.LICENSE.txt +35 -0
- package/dist/930.js.map +1 -0
- package/dist/kenyaemr-esm-patient-list-management-app.js +1 -0
- package/dist/kenyaemr-esm-patient-list-management-app.js.buildmanifest.json +580 -0
- package/dist/kenyaemr-esm-patient-list-management-app.js.map +1 -0
- package/dist/main.js +2 -0
- package/dist/main.js.LICENSE.txt +45 -0
- package/dist/main.js.map +1 -0
- package/dist/routes.json +1 -0
- package/jest.config.js +3 -0
- package/package.json +56 -0
- package/src/add-patient/add-patient.component.tsx +271 -0
- package/src/add-patient/add-patient.scss +51 -0
- package/src/add-patient-to-patient-list-menu-item.component.tsx +48 -0
- package/src/add-patient-to-patient-list-menu-item.test.tsx +33 -0
- package/src/api/api-remote.ts +211 -0
- package/src/api/hooks.ts +150 -0
- package/src/api/types.ts +102 -0
- package/src/config-schema.ts +25 -0
- package/src/constants.ts +5 -0
- package/src/create-edit-patient-list/create-edit-list.component.tsx +170 -0
- package/src/create-edit-patient-list/create-edit-patient-list.scss +31 -0
- package/src/createDashboardLink.component.tsx +40 -0
- package/src/dashboard.meta.ts +5 -0
- package/src/declarations.d.ts +5 -0
- package/src/empty-state/empty-data-illustration.component.tsx +42 -0
- package/src/empty-state/empty-state.component.tsx +41 -0
- package/src/empty-state/empty-state.scss +24 -0
- package/src/error-state/error-state.component.tsx +35 -0
- package/src/error-state/error-state.scss +50 -0
- package/src/header/header.component.tsx +51 -0
- package/src/header/header.scss +52 -0
- package/src/illo.component.tsx +25 -0
- package/src/index.ts +41 -0
- package/src/list-details/list-details.component.tsx +201 -0
- package/src/list-details/list-details.scss +47 -0
- package/src/list-details/list-details.test.tsx +112 -0
- package/src/list-details/patient-list-detail.test.tsx +105 -0
- package/src/list-details-table/list-details-table.component.tsx +402 -0
- package/src/list-details-table/list-details-table.scss +143 -0
- package/src/list-details-table/list-details-table.test.tsx +94 -0
- package/src/lists-dashboard/lists-dashboard.component.tsx +104 -0
- package/src/lists-dashboard/lists-dashboard.scss +110 -0
- package/src/lists-dashboard/lists-dashboard.test.tsx +129 -0
- package/src/lists-table/custom-pagination.component.tsx +43 -0
- package/src/lists-table/custom-pagination.scss +67 -0
- package/src/lists-table/lists-table.component.tsx +317 -0
- package/src/lists-table/lists-table.scss +100 -0
- package/src/lists-table/lists-table.test.tsx +189 -0
- package/src/lists-table/use-pagination-info.component.tsx +35 -0
- package/src/offline.ts +31 -0
- package/src/overlay.component.tsx +49 -0
- package/src/overlay.scss +89 -0
- package/src/overlay.test.tsx +60 -0
- package/src/root.component.tsx +19 -0
- package/src/routes.json +36 -0
- package/src/style.scss +46 -0
- package/translations/am.json +84 -0
- package/translations/ar.json +84 -0
- package/translations/en.json +84 -0
- package/translations/es.json +84 -0
- package/translations/fr.json +84 -0
- package/translations/he.json +84 -0
- package/translations/km.json +84 -0
- package/translations/zh.json +84 -0
- package/translations/zh_CN.json +84 -0
- package/tsconfig.json +5 -0
- package/webpack.config.js +1 -0
package/src/api/hooks.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import useSWR from 'swr';
|
|
3
|
+
import useSWRInfinite from 'swr/infinite';
|
|
4
|
+
import { openmrsFetch, type FetchResponse, useConfig, useSession } from '@openmrs/esm-framework';
|
|
5
|
+
import { cohortUrl, getAllPatientLists, getPatientListIdsForPatient, getPatientListMembers } from './api-remote';
|
|
6
|
+
import { type ConfigSchema } from '../config-schema';
|
|
7
|
+
import {
|
|
8
|
+
type CohortResponse,
|
|
9
|
+
type CohortType,
|
|
10
|
+
type OpenmrsCohort,
|
|
11
|
+
type OpenmrsCohortMember,
|
|
12
|
+
type PatientListFilter,
|
|
13
|
+
PatientListType,
|
|
14
|
+
} from './types';
|
|
15
|
+
|
|
16
|
+
interface PatientListResponse {
|
|
17
|
+
results: CohortResponse<OpenmrsCohort>;
|
|
18
|
+
links: Array<{ rel: 'prev' | 'next' }>;
|
|
19
|
+
totalCount: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function useAllPatientLists({ isStarred, type }: PatientListFilter) {
|
|
23
|
+
const custom = 'custom:(uuid,name,description,display,size,attributes,cohortType)';
|
|
24
|
+
const query: Array<[string, string]> = [
|
|
25
|
+
['v', custom],
|
|
26
|
+
['totalCount', 'true'],
|
|
27
|
+
];
|
|
28
|
+
const config: ConfigSchema = useConfig();
|
|
29
|
+
|
|
30
|
+
if (type === PatientListType.USER) {
|
|
31
|
+
query.push(['cohortType', config.myListCohortTypeUUID]);
|
|
32
|
+
} else if (type === PatientListType.SYSTEM) {
|
|
33
|
+
query.push(['cohortType', config.systemListCohortTypeUUID]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const params = query.map(([key, value]) => `${key}=${encodeURIComponent(value)}`).join('&');
|
|
37
|
+
|
|
38
|
+
const getUrl = (pageIndex, previousPageData: FetchResponse<PatientListResponse>) => {
|
|
39
|
+
if (pageIndex && !previousPageData?.data?.links?.some((link) => link.rel === 'next')) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let url = `${cohortUrl}/cohort?${params}`;
|
|
44
|
+
|
|
45
|
+
if (pageIndex) {
|
|
46
|
+
url += `&startIndex=${pageIndex * 50}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return url;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const {
|
|
53
|
+
data,
|
|
54
|
+
error,
|
|
55
|
+
mutate,
|
|
56
|
+
isValidating,
|
|
57
|
+
isLoading,
|
|
58
|
+
size: pageNumber,
|
|
59
|
+
setSize,
|
|
60
|
+
} = useSWRInfinite<FetchResponse<PatientListResponse>, Error>(getUrl, openmrsFetch);
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (data && data?.[pageNumber - 1]?.data?.links?.some((link) => link.rel === 'next')) {
|
|
64
|
+
setSize((currentSize) => currentSize + 1);
|
|
65
|
+
}
|
|
66
|
+
}, [data, pageNumber, setSize]);
|
|
67
|
+
|
|
68
|
+
const patientListsData = (data?.flatMap((res) => res?.data?.results ?? []) ?? []).map((cohort) => ({
|
|
69
|
+
id: cohort.uuid,
|
|
70
|
+
display: cohort.name,
|
|
71
|
+
description: cohort.description,
|
|
72
|
+
type: cohort.cohortType?.display,
|
|
73
|
+
size: cohort.size,
|
|
74
|
+
}));
|
|
75
|
+
const { user } = useSession();
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
patientLists: isStarred
|
|
79
|
+
? patientListsData.filter(({ id }) => user?.userProperties?.starredPatientLists?.includes(id))
|
|
80
|
+
: patientListsData,
|
|
81
|
+
isLoading,
|
|
82
|
+
isValidating,
|
|
83
|
+
error,
|
|
84
|
+
mutate,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function useAllPatientListMembers(patientListId: string) {
|
|
89
|
+
return useSWR(['patientListMembers', patientListId], () => getPatientListMembers(patientListId));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A hook for querying all local and remote patient lists that exist for a given user,
|
|
94
|
+
* but without those patient lists where a specific patient has already been added as a member.
|
|
95
|
+
*
|
|
96
|
+
* This is intended for displaying all lists to which a given patient can still be added.
|
|
97
|
+
*/
|
|
98
|
+
export function useAllPatientListsWhichDoNotIncludeGivenPatient(patientUuid: string) {
|
|
99
|
+
const config = useConfig() as ConfigSchema;
|
|
100
|
+
return useSWR(['patientListWithoutPatient', patientUuid], async () => {
|
|
101
|
+
const [allLists, listsIdsOfThisPatient] = await Promise.all([
|
|
102
|
+
getAllPatientLists({}, config?.myListCohortTypeUUID, config?.systemListCohortTypeUUID),
|
|
103
|
+
getPatientListIdsForPatient(patientUuid),
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
const listsWithoutPatient = allLists.filter((list) => !listsIdsOfThisPatient.includes(list.id));
|
|
107
|
+
return listsWithoutPatient;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function usePatientListDetails(patientListUuid: string) {
|
|
112
|
+
const url = `${cohortUrl}/cohort/${patientListUuid}?v=custom:(uuid,name,description,display,size,attributes,startDate,endDate,cohortType)`;
|
|
113
|
+
|
|
114
|
+
const { data, error, isLoading, mutate } = useSWR<FetchResponse<OpenmrsCohort>, Error>(
|
|
115
|
+
patientListUuid ? url : null,
|
|
116
|
+
openmrsFetch,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
listDetails: data?.data,
|
|
121
|
+
error,
|
|
122
|
+
isLoading,
|
|
123
|
+
mutateListDetails: mutate,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function usePatientListMembers(
|
|
128
|
+
patientListUuid: string,
|
|
129
|
+
searchQuery: string = '',
|
|
130
|
+
startIndex: number = 0,
|
|
131
|
+
pageSize: number = 10,
|
|
132
|
+
v: string = 'full',
|
|
133
|
+
) {
|
|
134
|
+
const { data, error, isLoading, mutate } = useSWR<FetchResponse<CohortResponse<OpenmrsCohortMember>>, Error>(
|
|
135
|
+
`${cohortUrl}/cohortmember?cohort=${patientListUuid}&startIndex=${startIndex}&limit=${pageSize}&v=${v}&q=${searchQuery}`,
|
|
136
|
+
openmrsFetch,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
listMembers: data?.data?.results ?? [],
|
|
141
|
+
isLoadingListMembers: isLoading,
|
|
142
|
+
error: error,
|
|
143
|
+
mutateListMembers: mutate,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function useCohortTypes() {
|
|
148
|
+
const swrResult = useSWR<FetchResponse<CohortResponse<CohortType>>, Error>(`${cohortUrl}/cohorttype`, openmrsFetch);
|
|
149
|
+
return { ...swrResult, data: swrResult?.data?.data?.results };
|
|
150
|
+
}
|
package/src/api/types.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { OpenmrsResource } from '@openmrs/esm-framework';
|
|
2
|
+
|
|
3
|
+
export enum PatientListType {
|
|
4
|
+
STARRED = 'Starred',
|
|
5
|
+
SYSTEM = 'System list',
|
|
6
|
+
USER = 'My list',
|
|
7
|
+
ALL = 'All',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface PatientList {
|
|
11
|
+
id: string;
|
|
12
|
+
display: string;
|
|
13
|
+
description: string;
|
|
14
|
+
type: string;
|
|
15
|
+
size: number;
|
|
16
|
+
options?: Array<PatientListOption>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface PatientListUpdate {
|
|
20
|
+
isStarred: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface PatientListFilter {
|
|
24
|
+
isStarred?: boolean;
|
|
25
|
+
name?: string;
|
|
26
|
+
type?: PatientListType;
|
|
27
|
+
label?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PatientListOption {
|
|
31
|
+
type: string;
|
|
32
|
+
name: string;
|
|
33
|
+
value: any;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface PatientListMember {
|
|
37
|
+
endDate: string | number | Date;
|
|
38
|
+
id: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface AddPatientData {
|
|
42
|
+
patient: string;
|
|
43
|
+
cohort: string;
|
|
44
|
+
startDate: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface OpenmrsCohort {
|
|
48
|
+
uuid: string;
|
|
49
|
+
resourceVersion: string;
|
|
50
|
+
name: string;
|
|
51
|
+
description: string;
|
|
52
|
+
attributes: Array<any>;
|
|
53
|
+
links: Array<any>;
|
|
54
|
+
location: Location | null;
|
|
55
|
+
groupCohort: boolean | null;
|
|
56
|
+
startDate: string | null;
|
|
57
|
+
endDate: string | null;
|
|
58
|
+
voidReason: string | null;
|
|
59
|
+
voided: boolean;
|
|
60
|
+
isStarred?: boolean;
|
|
61
|
+
type?: string;
|
|
62
|
+
size: number;
|
|
63
|
+
cohortType?: CohortType;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface OpenmrsCohortRef {
|
|
67
|
+
cohort: OpenmrsCohortMember;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface OpenmrsCohortMember {
|
|
71
|
+
attributes: Array<any>;
|
|
72
|
+
description: string;
|
|
73
|
+
endDate: string;
|
|
74
|
+
startDate: string;
|
|
75
|
+
name: string;
|
|
76
|
+
uuid: string;
|
|
77
|
+
patient: OpenmrsResource;
|
|
78
|
+
voided: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface CohortResponse<T> {
|
|
82
|
+
results: Array<T>;
|
|
83
|
+
error: any;
|
|
84
|
+
totalCount: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface NewCohortData {
|
|
88
|
+
name: string;
|
|
89
|
+
description: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface NewCohortDataPayload {
|
|
93
|
+
name: string;
|
|
94
|
+
description: string;
|
|
95
|
+
cohortType: string;
|
|
96
|
+
location: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface CohortType {
|
|
100
|
+
display: string;
|
|
101
|
+
uuid: string;
|
|
102
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Type } from '@openmrs/esm-framework';
|
|
2
|
+
|
|
3
|
+
export const configSchema = {
|
|
4
|
+
myListCohortTypeUUID: {
|
|
5
|
+
_type: Type.UUID,
|
|
6
|
+
_description: 'UUID of the `My List` cohort type',
|
|
7
|
+
_default: 'e71857cb-33af-4f2c-86ab-7223bcfa37ad',
|
|
8
|
+
},
|
|
9
|
+
systemListCohortTypeUUID: {
|
|
10
|
+
_type: Type.UUID,
|
|
11
|
+
_description: 'UUID of the `System List` cohort type',
|
|
12
|
+
_default: 'eee9970e-7ca0-4e8c-a280-c33e9d5f6a04',
|
|
13
|
+
},
|
|
14
|
+
patientListsToShow: {
|
|
15
|
+
_type: Type.Number,
|
|
16
|
+
_description: 'The default number of lists to show in the Lists dashboard table',
|
|
17
|
+
_default: 10,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export interface ConfigSchema {
|
|
22
|
+
myListCohortTypeUUID: string;
|
|
23
|
+
systemListCohortTypeUUID: string;
|
|
24
|
+
patientListsToShow: number;
|
|
25
|
+
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import React, { useCallback, type SyntheticEvent, useEffect, useId, useState } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Button, ButtonSet, Layer, TextArea, TextInput } from '@carbon/react';
|
|
4
|
+
import { useLayoutType, showSnackbar, useSession, useConfig } from '@openmrs/esm-framework';
|
|
5
|
+
import type { ConfigSchema } from '../config-schema';
|
|
6
|
+
import type { NewCohortData, OpenmrsCohort } from '../api/types';
|
|
7
|
+
import { createPatientList, editPatientList } from '../api/api-remote';
|
|
8
|
+
import Overlay from '../overlay.component';
|
|
9
|
+
import styles from './create-edit-patient-list.scss';
|
|
10
|
+
|
|
11
|
+
interface CreateEditPatientListProps {
|
|
12
|
+
close: () => void;
|
|
13
|
+
isEditing?: boolean;
|
|
14
|
+
patientListDetails?: OpenmrsCohort;
|
|
15
|
+
onSuccess?: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const CreateEditPatientList: React.FC<CreateEditPatientListProps> = ({
|
|
19
|
+
close,
|
|
20
|
+
isEditing = false,
|
|
21
|
+
patientListDetails = null,
|
|
22
|
+
onSuccess = () => {},
|
|
23
|
+
}) => {
|
|
24
|
+
const { t } = useTranslation();
|
|
25
|
+
const id = useId();
|
|
26
|
+
const config = useConfig() as ConfigSchema;
|
|
27
|
+
const isTablet = useLayoutType() === 'tablet';
|
|
28
|
+
const responsiveLevel = isTablet ? 1 : 0;
|
|
29
|
+
const session = useSession();
|
|
30
|
+
const { user } = session;
|
|
31
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
32
|
+
const [cohortDetails, setCohortDetails] = useState<NewCohortData>({
|
|
33
|
+
name: '',
|
|
34
|
+
description: '',
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
setCohortDetails({
|
|
39
|
+
name: patientListDetails?.name || '',
|
|
40
|
+
description: patientListDetails?.description || '',
|
|
41
|
+
});
|
|
42
|
+
}, [user, patientListDetails]);
|
|
43
|
+
|
|
44
|
+
const handleSubmit = useCallback(() => {
|
|
45
|
+
setIsSubmitting(true);
|
|
46
|
+
|
|
47
|
+
if (isEditing) {
|
|
48
|
+
editPatientList(patientListDetails.uuid, cohortDetails)
|
|
49
|
+
.then(() => {
|
|
50
|
+
showSnackbar({
|
|
51
|
+
title: t('updated', 'Updated'),
|
|
52
|
+
subtitle: t('listUpdated', 'List updated successfully'),
|
|
53
|
+
kind: 'success',
|
|
54
|
+
isLowContrast: true,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
onSuccess();
|
|
58
|
+
setIsSubmitting(false);
|
|
59
|
+
close();
|
|
60
|
+
})
|
|
61
|
+
.catch((error) => {
|
|
62
|
+
showSnackbar({
|
|
63
|
+
title: t('errorUpdatingList', 'Error updating list'),
|
|
64
|
+
subtitle: t('problemUpdatingList', 'There was a problem updating the list'),
|
|
65
|
+
kind: 'error',
|
|
66
|
+
});
|
|
67
|
+
setIsSubmitting(false);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!isEditing) {
|
|
72
|
+
createPatientList({
|
|
73
|
+
...cohortDetails,
|
|
74
|
+
cohortType: config?.myListCohortTypeUUID,
|
|
75
|
+
location: session?.sessionLocation?.uuid,
|
|
76
|
+
})
|
|
77
|
+
.then(() => {
|
|
78
|
+
showSnackbar({
|
|
79
|
+
title: t('created', 'Created'),
|
|
80
|
+
subtitle: `${t('listCreated', 'List created successfully')}`,
|
|
81
|
+
kind: 'success',
|
|
82
|
+
isLowContrast: true,
|
|
83
|
+
});
|
|
84
|
+
onSuccess();
|
|
85
|
+
setIsSubmitting(false);
|
|
86
|
+
close();
|
|
87
|
+
})
|
|
88
|
+
.catch((error) => {
|
|
89
|
+
showSnackbar({
|
|
90
|
+
title: t('errorCreatingList', 'Error creating list'),
|
|
91
|
+
subtitle: t('problemCreatingList', 'There was a problem creating the list'),
|
|
92
|
+
kind: 'error',
|
|
93
|
+
});
|
|
94
|
+
setIsSubmitting(false);
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}, [
|
|
98
|
+
close,
|
|
99
|
+
cohortDetails,
|
|
100
|
+
config?.myListCohortTypeUUID,
|
|
101
|
+
isEditing,
|
|
102
|
+
patientListDetails?.uuid,
|
|
103
|
+
onSuccess,
|
|
104
|
+
session.sessionLocation?.uuid,
|
|
105
|
+
t,
|
|
106
|
+
]);
|
|
107
|
+
|
|
108
|
+
const handleChange = useCallback(
|
|
109
|
+
({ currentTarget }: SyntheticEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
|
110
|
+
setCohortDetails((cohortDetails) => ({
|
|
111
|
+
...cohortDetails,
|
|
112
|
+
[currentTarget?.name]: currentTarget?.value,
|
|
113
|
+
}));
|
|
114
|
+
},
|
|
115
|
+
[setCohortDetails],
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<Overlay
|
|
120
|
+
buttonsGroup={
|
|
121
|
+
<ButtonSet className={styles.buttonsGroup}>
|
|
122
|
+
<Button className={styles.button} onClick={close} kind="secondary" size="xl">
|
|
123
|
+
{t('cancel', 'Cancel')}
|
|
124
|
+
</Button>
|
|
125
|
+
<Button onClick={handleSubmit} size="xl" disabled={isSubmitting}>
|
|
126
|
+
{isSubmitting
|
|
127
|
+
? t('submitting', 'Submitting')
|
|
128
|
+
: isEditing
|
|
129
|
+
? t('editList', 'Edit list')
|
|
130
|
+
: t('createList', 'Create list')}
|
|
131
|
+
</Button>
|
|
132
|
+
</ButtonSet>
|
|
133
|
+
}
|
|
134
|
+
close={close}
|
|
135
|
+
header={
|
|
136
|
+
isEditing ? t('editPatientListHeader', 'Edit patient list') : t('newPatientListHeader', 'New patient list')
|
|
137
|
+
}>
|
|
138
|
+
<h4 className={styles.header}>{t('configureList', 'Configure your patient list using the fields below')}</h4>
|
|
139
|
+
<div>
|
|
140
|
+
<Layer level={responsiveLevel}>
|
|
141
|
+
<TextInput
|
|
142
|
+
id={`${id}-input`}
|
|
143
|
+
labelText={t('newPatientListNameLabel', 'List name')}
|
|
144
|
+
name="name"
|
|
145
|
+
onChange={handleChange}
|
|
146
|
+
placeholder={t('listNamePlaceholder', 'e.g. Potential research participants')}
|
|
147
|
+
value={cohortDetails?.name}
|
|
148
|
+
/>
|
|
149
|
+
</Layer>
|
|
150
|
+
</div>
|
|
151
|
+
<div className={styles.input}>
|
|
152
|
+
<Layer level={responsiveLevel}>
|
|
153
|
+
<TextArea
|
|
154
|
+
id={`${id}-textarea`}
|
|
155
|
+
labelText={t('newPatientListDescriptionLabel', 'Describe the purpose of this list in a few words')}
|
|
156
|
+
name="description"
|
|
157
|
+
onChange={handleChange}
|
|
158
|
+
placeholder={t(
|
|
159
|
+
'listDescriptionPlaceholder',
|
|
160
|
+
'e.g. Patients with diagnosed asthma who may be willing to be a part of a university research study',
|
|
161
|
+
)}
|
|
162
|
+
value={cohortDetails?.description}
|
|
163
|
+
/>
|
|
164
|
+
</Layer>
|
|
165
|
+
</div>
|
|
166
|
+
</Overlay>
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export default CreateEditPatientList;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
@use '@carbon/styles/scss/spacing';
|
|
2
|
+
@use '@carbon/styles/scss/type';
|
|
3
|
+
@import '../style.scss';
|
|
4
|
+
|
|
5
|
+
.header {
|
|
6
|
+
@include type.type-style('heading-compact-02');
|
|
7
|
+
margin-bottom: spacing.$spacing-05;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.input {
|
|
11
|
+
margin-top: spacing.$spacing-07;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.button {
|
|
15
|
+
height: 4rem;
|
|
16
|
+
display: flex;
|
|
17
|
+
align-content: flex-start;
|
|
18
|
+
align-items: baseline;
|
|
19
|
+
min-width: 50%;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.buttonsGroup {
|
|
23
|
+
width: 100%;
|
|
24
|
+
display: grid;
|
|
25
|
+
grid-template-columns: 1fr 1fr;
|
|
26
|
+
|
|
27
|
+
button {
|
|
28
|
+
max-width: unset;
|
|
29
|
+
width: 50%;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import { ConfigurableLink } from '@openmrs/esm-framework';
|
|
4
|
+
import { BrowserRouter, useLocation } from 'react-router-dom';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
|
|
7
|
+
export interface DashboardLinkConfig {
|
|
8
|
+
name: string;
|
|
9
|
+
title: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function DashboardExtension({ dashboardLinkConfig }: { dashboardLinkConfig: DashboardLinkConfig }) {
|
|
13
|
+
//
|
|
14
|
+
const { t } = useTranslation();
|
|
15
|
+
const { name } = dashboardLinkConfig;
|
|
16
|
+
const location = useLocation();
|
|
17
|
+
const spaBasePath = `${window.spaBase}/home`;
|
|
18
|
+
|
|
19
|
+
const navLink = useMemo(() => {
|
|
20
|
+
const pathArray = location.pathname.split('/home');
|
|
21
|
+
const lastElement = pathArray[pathArray.length - 1];
|
|
22
|
+
return decodeURIComponent(lastElement);
|
|
23
|
+
}, [location.pathname]);
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<ConfigurableLink
|
|
27
|
+
className={classNames('cds--side-nav__link', {
|
|
28
|
+
'active-left-nav-link': navLink.match(name),
|
|
29
|
+
})}
|
|
30
|
+
to={`${spaBasePath}/${name}`}>
|
|
31
|
+
{t('patientLists', 'Patient lists')}
|
|
32
|
+
</ConfigurableLink>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const createDashboardLink = (dashboardLinkConfig: DashboardLinkConfig) => () => (
|
|
37
|
+
<BrowserRouter>
|
|
38
|
+
<DashboardExtension dashboardLinkConfig={dashboardLinkConfig} />
|
|
39
|
+
</BrowserRouter>
|
|
40
|
+
);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
|
|
4
|
+
export const EmptyDataIllustration = ({ width = '64', height = '64' }) => {
|
|
5
|
+
const { t } = useTranslation();
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<svg width={width} height={height} viewBox="0 0 64 64">
|
|
9
|
+
<title>{t('emptyStateIllustration', 'Empty state illustration')}</title>
|
|
10
|
+
<g fill="none" fillRule="evenodd">
|
|
11
|
+
<path
|
|
12
|
+
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"
|
|
13
|
+
fill="#F4F4F4"
|
|
14
|
+
/>
|
|
15
|
+
<path
|
|
16
|
+
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"
|
|
17
|
+
fill="#8D8D8D"
|
|
18
|
+
/>
|
|
19
|
+
<path
|
|
20
|
+
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"
|
|
21
|
+
fill="#C6C6C6"
|
|
22
|
+
/>
|
|
23
|
+
<circle fill="#C6C6C6" cx={17.636} cy={2.314} r={1.855} />
|
|
24
|
+
<circle fill="#FFF" fillRule="nonzero" cx={17.636} cy={2.314} r={1.175} />
|
|
25
|
+
<path
|
|
26
|
+
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"
|
|
27
|
+
fill="#F4F4F4"
|
|
28
|
+
/>
|
|
29
|
+
<path
|
|
30
|
+
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"
|
|
31
|
+
fill="#8D8D8D"
|
|
32
|
+
/>
|
|
33
|
+
<path
|
|
34
|
+
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"
|
|
35
|
+
fill="#C6C6C6"
|
|
36
|
+
/>
|
|
37
|
+
<circle fill="#C6C6C6" cx={40.218} cy={9.755} r={1.855} />
|
|
38
|
+
<circle fill="#FFF" fillRule="nonzero" cx={40.218} cy={9.755} r={1.13} />
|
|
39
|
+
</g>
|
|
40
|
+
</svg>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Layer, Button, Tile } from '@carbon/react';
|
|
4
|
+
import { Add } from '@carbon/react/icons';
|
|
5
|
+
import { EmptyDataIllustration } from './empty-data-illustration.component';
|
|
6
|
+
import styles from './empty-state.scss';
|
|
7
|
+
|
|
8
|
+
export interface EmptyStateProps {
|
|
9
|
+
listType: string;
|
|
10
|
+
launchForm?(): void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const EmptyState: React.FC<EmptyStateProps> = ({ listType, launchForm }) => {
|
|
14
|
+
const { t } = useTranslation();
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<Layer>
|
|
18
|
+
<Tile className={styles.tile}>
|
|
19
|
+
<div className={styles.illo}>
|
|
20
|
+
<EmptyDataIllustration />
|
|
21
|
+
</div>
|
|
22
|
+
<p className={styles.content}>
|
|
23
|
+
{t('emptyStateText', 'There are no {{listType}} patient lists to display', {
|
|
24
|
+
listType: listType.toLowerCase(),
|
|
25
|
+
})}
|
|
26
|
+
</p>
|
|
27
|
+
<p className={styles.action}>
|
|
28
|
+
{launchForm && (
|
|
29
|
+
<span>
|
|
30
|
+
<Button renderIcon={Add} kind="ghost" onClick={() => launchForm()}>
|
|
31
|
+
{t('createPatientList', 'Create patient list')}
|
|
32
|
+
</Button>
|
|
33
|
+
</span>
|
|
34
|
+
)}
|
|
35
|
+
</p>
|
|
36
|
+
</Tile>
|
|
37
|
+
</Layer>
|
|
38
|
+
);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default EmptyState;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
@use '@carbon/styles/scss/spacing';
|
|
2
|
+
@use '@carbon/styles/scss/type';
|
|
3
|
+
@import '~@openmrs/esm-styleguide/src/vars';
|
|
4
|
+
|
|
5
|
+
.tile {
|
|
6
|
+
text-align: center;
|
|
7
|
+
border: 1px solid $ui-03;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.illo {
|
|
11
|
+
margin-top: spacing.$spacing-05;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.content {
|
|
15
|
+
@include type.type-style('heading-compact-01');
|
|
16
|
+
color: $text-02;
|
|
17
|
+
margin-top: spacing.$spacing-05;
|
|
18
|
+
margin-bottom: spacing.$spacing-03;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.actionText {
|
|
22
|
+
display: flex;
|
|
23
|
+
align-items: center;
|
|
24
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Layer, Tile } from '@carbon/react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { useLayoutType } from '@openmrs/esm-framework';
|
|
5
|
+
import styles from './error-state.scss';
|
|
6
|
+
|
|
7
|
+
export interface ErrorStateProps {
|
|
8
|
+
error: any;
|
|
9
|
+
headerTitle: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const ErrorState: React.FC<ErrorStateProps> = ({ error, headerTitle }) => {
|
|
13
|
+
const { t } = useTranslation();
|
|
14
|
+
const isTablet = useLayoutType() === 'tablet';
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<Layer>
|
|
18
|
+
<Tile className={styles.tile}>
|
|
19
|
+
<div className={isTablet ? styles.tabletHeading : styles.desktopHeading}>
|
|
20
|
+
<h4>{headerTitle}</h4>
|
|
21
|
+
</div>
|
|
22
|
+
<p className={styles.errorMessage}>
|
|
23
|
+
{t('error', 'Error')} {`${error?.response?.status}: `}
|
|
24
|
+
{error?.response?.statusText}
|
|
25
|
+
</p>
|
|
26
|
+
<p className={styles.errorCopy}>
|
|
27
|
+
{t(
|
|
28
|
+
'errorCopy',
|
|
29
|
+
'Sorry, there was a problem displaying this information. You can try to reload this page, or contact the site administrator and quote the error code above.',
|
|
30
|
+
)}
|
|
31
|
+
</p>
|
|
32
|
+
</Tile>
|
|
33
|
+
</Layer>
|
|
34
|
+
);
|
|
35
|
+
};
|