@palladium-ethiopia/esm-admin-app 5.4.2-pre.100
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 +15 -0
- package/README.md +12 -0
- package/dist/117.js +1 -0
- package/dist/152.js +1 -0
- package/dist/152.js.map +1 -0
- package/dist/209.js +1 -0
- package/dist/209.js.map +1 -0
- package/dist/41.js +1 -0
- package/dist/41.js.map +1 -0
- package/dist/442.js +1 -0
- package/dist/442.js.map +1 -0
- package/dist/466.js +1 -0
- package/dist/466.js.map +1 -0
- package/dist/555.js +1 -0
- package/dist/555.js.map +1 -0
- package/dist/61.js +1 -0
- package/dist/61.js.map +1 -0
- package/dist/672.js +15 -0
- package/dist/672.js.map +1 -0
- package/dist/689.js +1 -0
- package/dist/689.js.map +1 -0
- package/dist/710.js +1 -0
- package/dist/710.js.map +1 -0
- package/dist/712.js +1 -0
- package/dist/712.js.map +1 -0
- package/dist/771.js +1 -0
- package/dist/771.js.map +1 -0
- package/dist/789.js +1 -0
- package/dist/789.js.map +1 -0
- package/dist/806.js +1 -0
- package/dist/826.js +1 -0
- package/dist/826.js.map +1 -0
- package/dist/914.js +27 -0
- package/dist/914.js.map +1 -0
- package/dist/926.js +17 -0
- package/dist/926.js.map +1 -0
- package/dist/ethiopia-esm-admin-app.js +6 -0
- package/dist/ethiopia-esm-admin-app.js.buildmanifest.json +556 -0
- package/dist/ethiopia-esm-admin-app.js.map +1 -0
- package/dist/main.js +32 -0
- package/dist/main.js.map +1 -0
- package/dist/routes.json +1 -0
- package/jest.config.js +8 -0
- package/package.json +52 -0
- package/rspack.config.js +1 -0
- package/src/components/confirm-modal/confirmation-operation-modal.component.tsx +43 -0
- package/src/components/confirm-modal/confirmation-operation.test.tsx +69 -0
- package/src/components/dashboard/dashboard.component.tsx +131 -0
- package/src/components/dashboard/dashboard.scss +38 -0
- package/src/components/dashboard/etl-dashboard.component.tsx +11 -0
- package/src/components/empty-state/empty-state-log.components.tsx +20 -0
- package/src/components/empty-state/empty-state-log.scss +28 -0
- package/src/components/empty-state/empty-state-log.test.tsx +24 -0
- package/src/components/facility-setup/card.component.tsx +16 -0
- package/src/components/facility-setup/facility-info.component.tsx +142 -0
- package/src/components/facility-setup/facility-info.scss +87 -0
- package/src/components/facility-setup/facility-setup.component.tsx +21 -0
- package/src/components/facility-setup/facility-setup.resource.tsx +7 -0
- package/src/components/facility-setup/facility-setup.scss +38 -0
- package/src/components/facility-setup/header/header.component.tsx +23 -0
- package/src/components/facility-setup/header/header.scss +19 -0
- package/src/components/header/header-illustration.component.tsx +13 -0
- package/src/components/header/header.component.tsx +28 -0
- package/src/components/header/header.scss +19 -0
- package/src/components/hook/healthWorkerAdapter.ts +213 -0
- package/src/components/hook/useFacilityInfo.tsx +37 -0
- package/src/components/hook/useSystemRoleSetting.tsx +33 -0
- package/src/components/locations/auto-suggest/autosuggest.component.tsx +149 -0
- package/src/components/locations/auto-suggest/autosuggest.scss +61 -0
- package/src/components/locations/auto-suggest/location-autosuggest.component.tsx +94 -0
- package/src/components/locations/auto-suggest/location-autosuggest.scss +48 -0
- package/src/components/locations/common/results-tile.component.tsx +45 -0
- package/src/components/locations/common/results-tile.scss +86 -0
- package/src/components/locations/forms/add-location/add-location.workspace.scss +34 -0
- package/src/components/locations/forms/add-location/add-location.workspace.tsx +200 -0
- package/src/components/locations/forms/search-location/search-location.workspace.scss +79 -0
- package/src/components/locations/forms/search-location/search-location.workspace.tsx +215 -0
- package/src/components/locations/header/header.component.tsx +48 -0
- package/src/components/locations/header/header.scss +58 -0
- package/src/components/locations/helpers/index.ts +16 -0
- package/src/components/locations/home/home-locations.component.tsx +18 -0
- package/src/components/locations/home/home-locations.scss +8 -0
- package/src/components/locations/hooks/UseFacilityLocations.ts +12 -0
- package/src/components/locations/hooks/useLocation.ts +18 -0
- package/src/components/locations/hooks/useLocationTags.ts +15 -0
- package/src/components/locations/tables/locations-table.component.tsx +243 -0
- package/src/components/locations/tables/locations-table.resource.ts +26 -0
- package/src/components/locations/tables/locations-table.scss +115 -0
- package/src/components/locations/types/index.ts +120 -0
- package/src/components/locations/utils/index.ts +5 -0
- package/src/components/logs-table/operation-log-resource.ts +41 -0
- package/src/components/logs-table/operation-log-table.component.tsx +120 -0
- package/src/components/logs-table/operation-log.scss +10 -0
- package/src/components/logs-table/operation-log.test.tsx +47 -0
- package/src/components/modal/hwr-confirmation.modal.scss +21 -0
- package/src/components/modal/hwr-confirmation.modal.tsx +170 -0
- package/src/components/modal/hwr-empty.modal.component.tsx +54 -0
- package/src/components/modal/hwr-sync.modal.scss +30 -0
- package/src/components/modal/hwr-sync.modal.tsx +209 -0
- package/src/components/modal/hwr-sync.resource.ts +23 -0
- package/src/components/provider-banner/provider-banner.component.tsx +106 -0
- package/src/components/provider-banner/provider-banner.module.scss +51 -0
- package/src/components/provider-banner/provider-banner.resource.ts +29 -0
- package/src/components/side-menu/left-panel.scss +42 -0
- package/src/components/side-menu/left-pannel.component.tsx +22 -0
- package/src/components/users/header/header.scss +90 -0
- package/src/components/users/header/user-management-header.component.tsx +42 -0
- package/src/components/users/manage-users/hooks/useProviderAttributeMapping.ts +110 -0
- package/src/components/users/manage-users/hooks/useUserFormSteps.ts +119 -0
- package/src/components/users/manage-users/hooks/useUserFormSubmission.ts +264 -0
- package/src/components/users/manage-users/hooks/useUserManagementForm.ts +122 -0
- package/src/components/users/manage-users/manage-user-role-scope/user-role-scope-list/user-role-scope-list.component.tsx +177 -0
- package/src/components/users/manage-users/manage-user-role-scope/user-role-scope-workspace/user-role-fields.scss +117 -0
- package/src/components/users/manage-users/manage-user-role-scope/user-role-scope-workspace/user-role-scope-fields.component.tsx +290 -0
- package/src/components/users/manage-users/manage-user-role-scope/user-role-scope-workspace/user-role-scope.workspace.tsx +316 -0
- package/src/components/users/manage-users/manage-user-role-scope/user-role-scope-workspace/userRoleScopeFormSchema.tsx +43 -0
- package/src/components/users/manage-users/manage-user.component.tsx +19 -0
- package/src/components/users/manage-users/manage-user.scss +31 -0
- package/src/components/users/manage-users/provider-autosuggest.component.tsx +117 -0
- package/src/components/users/manage-users/provider-search.resource.ts +34 -0
- package/src/components/users/manage-users/sections/demographic-section.component.tsx +156 -0
- package/src/components/users/manage-users/sections/login-section.component.tsx +88 -0
- package/src/components/users/manage-users/sections/provider-section.component.tsx +270 -0
- package/src/components/users/manage-users/sections/roles-section.component.tsx +88 -0
- package/src/components/users/manage-users/user-details/user-detail.scss +75 -0
- package/src/components/users/manage-users/user-details/user-details.component.tsx +182 -0
- package/src/components/users/manage-users/user-list/user-list.component.tsx +378 -0
- package/src/components/users/manage-users/user-list/user-list.resource.ts +30 -0
- package/src/components/users/manage-users/user-list/user-list.scss +37 -0
- package/src/components/users/manage-users/user-management.constants.ts +20 -0
- package/src/components/users/manage-users/user-management.utils.ts +100 -0
- package/src/components/users/manage-users/user-management.workspace.scss +172 -0
- package/src/components/users/manage-users/user-management.workspace.tsx +334 -0
- package/src/components/users/userManagementFormSchema.tsx +179 -0
- package/src/config-schema.ts +142 -0
- package/src/constants.ts +50 -0
- package/src/declarations.d.ts +2 -0
- package/src/index.ts +55 -0
- package/src/left-pannel-link.component.tsx +40 -0
- package/src/root.component.tsx +39 -0
- package/src/root.scss +12 -0
- package/src/routes.json +100 -0
- package/src/setup-tests.ts +1 -0
- package/src/types/index.ts +385 -0
- package/src/user-management.resources.ts +232 -0
- package/src/utils/utils.ts +20 -0
- package/translations/am.json +159 -0
- package/translations/en.json +159 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import React, { useRef } from 'react';
|
|
2
|
+
import styles from './user-detail.scss';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { Tag, Accordion, AccordionItem, ContainedList, ContainedListItem } from '@carbon/react';
|
|
5
|
+
import { PatientPhoto } from '@openmrs/esm-framework';
|
|
6
|
+
import dayjs from 'dayjs';
|
|
7
|
+
import classNames from 'classnames';
|
|
8
|
+
import capitalize from 'lodash/capitalize';
|
|
9
|
+
import { type ProviderResponse, type UserResponse } from '../../../../types';
|
|
10
|
+
|
|
11
|
+
interface UserDetailsProps {
|
|
12
|
+
provider: ProviderResponse;
|
|
13
|
+
user: UserResponse;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ProviderAttribute {
|
|
17
|
+
value: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface ProviderAttributes {
|
|
21
|
+
licenseAttr?: ProviderAttribute;
|
|
22
|
+
nationalID?: ProviderAttribute;
|
|
23
|
+
dateAttr?: ProviderAttribute;
|
|
24
|
+
phoneNumber?: ProviderAttribute;
|
|
25
|
+
qualification?: ProviderAttribute;
|
|
26
|
+
registrationNumber?: ProviderAttribute;
|
|
27
|
+
emailAddress?: ProviderAttribute;
|
|
28
|
+
passportNumber: ProviderAttribute;
|
|
29
|
+
providerUniqueIdentifier: ProviderAttribute;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const UserDetails: React.FC<UserDetailsProps> = ({ provider, user }) => {
|
|
33
|
+
const { t } = useTranslation();
|
|
34
|
+
const patientBannerRef = useRef(null);
|
|
35
|
+
|
|
36
|
+
const attributeMap = {
|
|
37
|
+
licenseAttr: 'Practising License Number',
|
|
38
|
+
nationalID: 'Provider National Id Number',
|
|
39
|
+
dateAttr: 'License Expiry Date',
|
|
40
|
+
phoneNumber: 'Provider Telephone',
|
|
41
|
+
qualification: 'Provider Qualification',
|
|
42
|
+
registrationNumber: 'License Body',
|
|
43
|
+
emailAddress: 'Provider Address',
|
|
44
|
+
passportNumber: 'Provider passport number',
|
|
45
|
+
providerUniqueIdentifier: 'Provider unique identifier',
|
|
46
|
+
providerExternalIdAttributeTypeUuid: 'ExternalId',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const attributes: ProviderAttributes = Object.entries(attributeMap).reduce((acc, [key, display]) => {
|
|
50
|
+
const attr = provider?.attributes?.find((attr) => attr.attributeType.display === display);
|
|
51
|
+
if (attr) {
|
|
52
|
+
acc[key as keyof ProviderAttributes] = { value: attr.value };
|
|
53
|
+
}
|
|
54
|
+
return acc;
|
|
55
|
+
}, {} as ProviderAttributes);
|
|
56
|
+
|
|
57
|
+
const {
|
|
58
|
+
licenseAttr,
|
|
59
|
+
nationalID,
|
|
60
|
+
dateAttr,
|
|
61
|
+
phoneNumber,
|
|
62
|
+
qualification,
|
|
63
|
+
registrationNumber,
|
|
64
|
+
emailAddress,
|
|
65
|
+
passportNumber,
|
|
66
|
+
providerUniqueIdentifier,
|
|
67
|
+
} = attributes;
|
|
68
|
+
|
|
69
|
+
const formattedExpiryDate = dateAttr?.value ? dayjs(dateAttr.value).format('YYYY-MM-DD') : null;
|
|
70
|
+
const today = dayjs();
|
|
71
|
+
const expiryDate = dateAttr?.value ? dayjs(dateAttr.value) : null;
|
|
72
|
+
const daysUntilExpiry = expiryDate ? expiryDate.diff(today, 'day') : null;
|
|
73
|
+
|
|
74
|
+
const getLicenseStatusTag = () => {
|
|
75
|
+
if (!licenseAttr?.value) {
|
|
76
|
+
return <Tag type="red">{t('unlicensed', 'Unlicensed')}</Tag>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (daysUntilExpiry < 0) {
|
|
80
|
+
return <Tag type="red">{t('licenseExpired', 'License has expired')}</Tag>;
|
|
81
|
+
} else if (daysUntilExpiry <= 3) {
|
|
82
|
+
return (
|
|
83
|
+
<>
|
|
84
|
+
<Tag type="cyan">{t('licenseExpiringSoon', 'License is expiring soon')}</Tag>
|
|
85
|
+
</>
|
|
86
|
+
);
|
|
87
|
+
} else {
|
|
88
|
+
return <Tag type="green">{t('active', 'Active')}</Tag>;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
<div className={styles.providerDetailsContainer}>
|
|
94
|
+
<header aria-label="patient banner" role="banner" ref={patientBannerRef}>
|
|
95
|
+
<div className={styles.patientBanner}>
|
|
96
|
+
<div className={styles.patientAvatar} role="img">
|
|
97
|
+
<PatientPhoto patientUuid={provider?.uuid} patientName={provider?.person?.display} />
|
|
98
|
+
</div>
|
|
99
|
+
<div className={styles.patientInfo}>
|
|
100
|
+
<div className={classNames(styles.row, styles.patientNameRow)}>
|
|
101
|
+
<div className={styles.flexRow}>
|
|
102
|
+
<span className={styles.patientName}>{provider?.person?.display} </span> ·
|
|
103
|
+
<span className={styles.gender}>
|
|
104
|
+
{provider?.person?.gender === 'M' ? 'Male' : provider?.person?.gender === 'F' ? 'Female' : ''}{' '}
|
|
105
|
+
·{' '}
|
|
106
|
+
</span>
|
|
107
|
+
<span className={styles.statusTag}>{getLicenseStatusTag()}</span>
|
|
108
|
+
<span className={styles.statusTag}>
|
|
109
|
+
{qualification?.value && <Tag type="cyan">{capitalize(qualification?.value)}</Tag>}
|
|
110
|
+
</span>
|
|
111
|
+
<span className={styles.statusTag}>
|
|
112
|
+
{providerUniqueIdentifier?.value && (
|
|
113
|
+
<Tag type="cyan">{capitalize(providerUniqueIdentifier?.value)}</Tag>
|
|
114
|
+
)}
|
|
115
|
+
</span>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div className={classNames(styles.row, styles.patientNameRow)}>
|
|
120
|
+
<div className={styles.flexRow}>
|
|
121
|
+
<span className={styles.spanField}>
|
|
122
|
+
{t('phoneNumber', 'Phone number')}: {phoneNumber?.value ? phoneNumber.value : '--'}
|
|
123
|
+
</span>
|
|
124
|
+
<span className={styles.middot}>· </span>
|
|
125
|
+
|
|
126
|
+
<span className={styles.spanField}>
|
|
127
|
+
{t('emailAddress', 'Email address')}: {emailAddress?.value ? emailAddress.value : '--'}
|
|
128
|
+
</span>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
<div className={classNames(styles.row, styles.patientNameRow)}>
|
|
132
|
+
<div className={styles.flexRow}>
|
|
133
|
+
<span className={styles.spanField}>
|
|
134
|
+
{t('nationalId', 'National ID')}: {nationalID?.value ? nationalID.value : '--'}
|
|
135
|
+
</span>
|
|
136
|
+
<span className={styles.middot}>· </span>
|
|
137
|
+
|
|
138
|
+
<span className={styles.spanField}>
|
|
139
|
+
{t('licenseNumber', 'License number')}: {licenseAttr?.value ? licenseAttr.value : '--'}
|
|
140
|
+
</span>
|
|
141
|
+
<span className={styles.middot}>· </span>
|
|
142
|
+
<span className={styles.spanField}>
|
|
143
|
+
{t('registrationNumber', 'Registration number')}:{' '}
|
|
144
|
+
{registrationNumber?.value ? registrationNumber.value : '--'}
|
|
145
|
+
</span>
|
|
146
|
+
<span className={styles.middot}>· </span>
|
|
147
|
+
|
|
148
|
+
<span className={styles.spanField}>
|
|
149
|
+
{t('passportNumber', 'Passport number')}: {passportNumber?.value ? passportNumber.value : '--'}
|
|
150
|
+
</span>
|
|
151
|
+
<span className={styles.middot}>· </span>
|
|
152
|
+
|
|
153
|
+
<span className={styles.spanField}>
|
|
154
|
+
{t('licenseExpiryDate', 'License expiry date')}: {formattedExpiryDate ? formattedExpiryDate : '--'}
|
|
155
|
+
</span>
|
|
156
|
+
</div>
|
|
157
|
+
<div className={classNames(styles.row, styles.patientNameRow, styles.viewRoles)}>
|
|
158
|
+
<Accordion>
|
|
159
|
+
<AccordionItem title={t('viewRoles', 'View roles')}>
|
|
160
|
+
{user?.roles?.map((role, i) => (
|
|
161
|
+
<ContainedListItem key={i}>
|
|
162
|
+
<div className={styles.roleContainer}>
|
|
163
|
+
<strong>{role.display}</strong>
|
|
164
|
+
<p className={styles.roleDescription}>
|
|
165
|
+
{role.description || t('noDescriptionAvailable', 'No description available')}
|
|
166
|
+
</p>
|
|
167
|
+
</div>
|
|
168
|
+
</ContainedListItem>
|
|
169
|
+
))}
|
|
170
|
+
</AccordionItem>
|
|
171
|
+
</Accordion>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
<br />
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</header>
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export default UserDetails;
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import React, { useMemo, useState, useCallback } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
DataTable,
|
|
5
|
+
DataTableSkeleton,
|
|
6
|
+
Dropdown,
|
|
7
|
+
OverflowMenu,
|
|
8
|
+
OverflowMenuItem,
|
|
9
|
+
Pagination,
|
|
10
|
+
Search,
|
|
11
|
+
Table,
|
|
12
|
+
TableBody,
|
|
13
|
+
TableCell,
|
|
14
|
+
TableContainer,
|
|
15
|
+
TableExpandedRow,
|
|
16
|
+
TableExpandHeader,
|
|
17
|
+
TableExpandRow,
|
|
18
|
+
TableHead,
|
|
19
|
+
TableHeader,
|
|
20
|
+
TableRow,
|
|
21
|
+
} from '@carbon/react';
|
|
22
|
+
import { UserFollow } from '@carbon/react/icons';
|
|
23
|
+
import {
|
|
24
|
+
ErrorState,
|
|
25
|
+
useConfig,
|
|
26
|
+
useLayoutType,
|
|
27
|
+
launchWorkspace,
|
|
28
|
+
WorkspaceContainer,
|
|
29
|
+
isDesktop,
|
|
30
|
+
usePagination,
|
|
31
|
+
} from '@openmrs/esm-framework';
|
|
32
|
+
import { CardHeader, usePaginationInfo } from '@openmrs/esm-patient-common-lib';
|
|
33
|
+
import { useTranslation } from 'react-i18next';
|
|
34
|
+
import dayjs from 'dayjs';
|
|
35
|
+
import EmptyState from '../../../empty-state/empty-state-log.components';
|
|
36
|
+
import { useSystemUserRoleConfigSetting } from '../../../hook/useSystemRoleSetting';
|
|
37
|
+
import { useProvider, useUsers } from './user-list.resource';
|
|
38
|
+
import styles from './user-list.scss';
|
|
39
|
+
import { ConfigObject } from '../../../../config-schema';
|
|
40
|
+
import { formatDateTime } from '../../../../utils/utils';
|
|
41
|
+
import UserDetails from '../user-details/user-details.component';
|
|
42
|
+
import upperCase from 'lodash-es/upperCase';
|
|
43
|
+
|
|
44
|
+
type FilterType = 'allUsers' | 'activeLicensed' | 'expiredLicensed' | 'licensedExpiringSoon' | 'unlicensed';
|
|
45
|
+
|
|
46
|
+
const getCardTitle = (filterName: FilterType): string => {
|
|
47
|
+
const filterMap: Record<FilterType, string> = {
|
|
48
|
+
allUsers: 'List of all users',
|
|
49
|
+
activeLicensed: 'List of active licensed users',
|
|
50
|
+
expiredLicensed: 'List of expired licensed users',
|
|
51
|
+
licensedExpiringSoon: 'List of licensed expiring soon users',
|
|
52
|
+
unlicensed: 'List of unlicensed users',
|
|
53
|
+
};
|
|
54
|
+
return filterMap[filterName] || 'List of users';
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const UserList: React.FC = () => {
|
|
58
|
+
const { t } = useTranslation();
|
|
59
|
+
const layout = useLayoutType();
|
|
60
|
+
const responsiveSize = isDesktop(layout) ? 'sm' : 'lg';
|
|
61
|
+
const { users, isLoading: isLoadingUsers, error: usersError } = useUsers();
|
|
62
|
+
const { provider, isLoading, error: providerError } = useProvider();
|
|
63
|
+
const {
|
|
64
|
+
licenseNumberUuid,
|
|
65
|
+
licenseExpiryDateUuid,
|
|
66
|
+
providerNationalIdUuid,
|
|
67
|
+
providerUniqueIdentifierAttributeTypeUuid,
|
|
68
|
+
} = useConfig<ConfigObject>();
|
|
69
|
+
|
|
70
|
+
const [pageSize, setPageSize] = useState(10);
|
|
71
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
72
|
+
const [selectedFilter, setSelectedFilter] = useState<FilterType>('allUsers');
|
|
73
|
+
|
|
74
|
+
const { error } = useSystemUserRoleConfigSetting();
|
|
75
|
+
|
|
76
|
+
const isActiveLicensed = useCallback(
|
|
77
|
+
(provider) => {
|
|
78
|
+
const licenseAttr = provider.attributes.find((attr) => attr.attributeType.uuid === licenseNumberUuid);
|
|
79
|
+
const expiryAttr = provider.attributes.find((attr) => attr.attributeType.uuid === licenseExpiryDateUuid);
|
|
80
|
+
const licenseExpiryDate = expiryAttr ? dayjs(expiryAttr.value) : null;
|
|
81
|
+
return licenseAttr && licenseExpiryDate && licenseExpiryDate.isAfter(dayjs());
|
|
82
|
+
},
|
|
83
|
+
[licenseNumberUuid, licenseExpiryDateUuid],
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const isExpiredLicensed = useCallback(
|
|
87
|
+
(provider) => {
|
|
88
|
+
const expiryAttr = provider.attributes.find((attr) => attr.attributeType.uuid === licenseExpiryDateUuid);
|
|
89
|
+
const licenseExpiryDate = expiryAttr ? dayjs(expiryAttr.value) : null;
|
|
90
|
+
return licenseExpiryDate && licenseExpiryDate.isBefore(dayjs());
|
|
91
|
+
},
|
|
92
|
+
[licenseExpiryDateUuid],
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const isExpiringSoon = useCallback(
|
|
96
|
+
(provider) => {
|
|
97
|
+
const expiryAttr = provider.attributes.find((attr) => attr.attributeType.uuid === licenseExpiryDateUuid);
|
|
98
|
+
const licenseExpiryDate = expiryAttr ? dayjs(expiryAttr.value) : null;
|
|
99
|
+
const today = dayjs();
|
|
100
|
+
return (
|
|
101
|
+
licenseExpiryDate &&
|
|
102
|
+
licenseExpiryDate.isAfter(today) &&
|
|
103
|
+
licenseExpiryDate.diff(today, 'day') > 0 &&
|
|
104
|
+
licenseExpiryDate.diff(today, 'day') <= 3
|
|
105
|
+
);
|
|
106
|
+
},
|
|
107
|
+
[licenseExpiryDateUuid],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const isUnlicensed = useCallback(
|
|
111
|
+
(provider) => {
|
|
112
|
+
const nationalId = provider.attributes.find((attr) => attr.attributeType.uuid === providerNationalIdUuid);
|
|
113
|
+
const licenseNumber = provider.attributes.find((attr) => attr.attributeType.uuid === licenseNumberUuid);
|
|
114
|
+
return !nationalId && !licenseNumber;
|
|
115
|
+
},
|
|
116
|
+
[providerNationalIdUuid, licenseNumberUuid],
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const filteredUsers = useMemo(() => {
|
|
120
|
+
let filtered = users;
|
|
121
|
+
|
|
122
|
+
if (searchQuery) {
|
|
123
|
+
filtered = filtered.filter((user) => user.person.display.toLowerCase().includes(searchQuery.toLowerCase()));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
switch (selectedFilter) {
|
|
127
|
+
case 'activeLicensed':
|
|
128
|
+
filtered = filtered.filter((user) => {
|
|
129
|
+
const userProvider = provider.find((p) => p.person?.uuid === user.person.uuid);
|
|
130
|
+
return userProvider && isActiveLicensed(userProvider);
|
|
131
|
+
});
|
|
132
|
+
break;
|
|
133
|
+
case 'expiredLicensed':
|
|
134
|
+
filtered = filtered.filter((user) => {
|
|
135
|
+
const userProvider = provider.find((p) => p.person?.uuid === user.person.uuid);
|
|
136
|
+
return userProvider && isExpiredLicensed(userProvider);
|
|
137
|
+
});
|
|
138
|
+
break;
|
|
139
|
+
case 'licensedExpiringSoon':
|
|
140
|
+
filtered = filtered.filter((user) => {
|
|
141
|
+
const userProvider = provider.find((p) => p.person?.uuid === user.person.uuid);
|
|
142
|
+
return userProvider && isExpiringSoon(userProvider);
|
|
143
|
+
});
|
|
144
|
+
break;
|
|
145
|
+
case 'unlicensed':
|
|
146
|
+
filtered = filtered.filter((user) => {
|
|
147
|
+
const userProvider = provider.find((p) => p.person?.uuid === user.person.uuid);
|
|
148
|
+
return userProvider && isUnlicensed(userProvider);
|
|
149
|
+
});
|
|
150
|
+
break;
|
|
151
|
+
default:
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return filtered;
|
|
156
|
+
}, [users, searchQuery, selectedFilter, provider, isActiveLicensed, isExpiredLicensed, isExpiringSoon, isUnlicensed]);
|
|
157
|
+
|
|
158
|
+
const { paginated, goTo, results, currentPage } = usePagination(filteredUsers, pageSize);
|
|
159
|
+
const { pageSizes } = usePaginationInfo(pageSize, filteredUsers.length, currentPage, results?.length);
|
|
160
|
+
|
|
161
|
+
if (isLoadingUsers || isLoading) {
|
|
162
|
+
return (
|
|
163
|
+
<div className={styles.Container}>
|
|
164
|
+
<DataTableSkeleton />
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (error || usersError || providerError) {
|
|
170
|
+
return <ErrorState error={error || usersError} headerTitle={t('usersManagement', 'Users management')} />;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (users.length === 0) {
|
|
174
|
+
return <EmptyState subTitle={t('noUsersAvailable', 'No users available')} />;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const headerData = [
|
|
178
|
+
{
|
|
179
|
+
key: 'systemId',
|
|
180
|
+
header: t('systemId', 'System ID'),
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
key: 'names',
|
|
184
|
+
header: t('names', 'Names'),
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
key: 'licenseNumber',
|
|
188
|
+
header: t('licenseNumber', 'License Number'),
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
key: 'providerUniqueIdentifier',
|
|
192
|
+
header: t('providerUniqueIdentifier', 'Provider Unique Identifier'),
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
key: 'licenseExpiryDate',
|
|
196
|
+
header: t('licenseExpiryDate', 'License Expiry Date'),
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
key: 'actions',
|
|
200
|
+
header: t('actions', 'Actions'),
|
|
201
|
+
},
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
const rowData = results?.map((user) => {
|
|
205
|
+
const userProvider = provider.find((p) => p.person?.uuid === user.person.uuid);
|
|
206
|
+
|
|
207
|
+
const licenseNumberAttribute = userProvider?.attributes.find(
|
|
208
|
+
(attr) => attr?.attributeType?.uuid === licenseNumberUuid,
|
|
209
|
+
);
|
|
210
|
+
const licenseExpiryDateAttribute = userProvider?.attributes.find(
|
|
211
|
+
(attr) => attr?.attributeType?.uuid === licenseExpiryDateUuid,
|
|
212
|
+
);
|
|
213
|
+
const licenseNumber = licenseNumberAttribute ? licenseNumberAttribute.value : '--';
|
|
214
|
+
const licenseExpiryDate = licenseExpiryDateAttribute ? licenseExpiryDateAttribute.value : '--';
|
|
215
|
+
|
|
216
|
+
const providerUniqueIdentifierAttribute = userProvider?.attributes.find(
|
|
217
|
+
(attr) => attr?.attributeType?.uuid === providerUniqueIdentifierAttributeTypeUuid,
|
|
218
|
+
);
|
|
219
|
+
const providerUniqueIdentifier = providerUniqueIdentifierAttribute ? providerUniqueIdentifierAttribute.value : '--';
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
id: user.uuid,
|
|
223
|
+
systemId: user.systemId,
|
|
224
|
+
names: upperCase(user.person.display),
|
|
225
|
+
licenseNumber: licenseNumber,
|
|
226
|
+
licenseExpiryDate: formatDateTime(new Date(licenseExpiryDate)),
|
|
227
|
+
providerUniqueIdentifier: providerUniqueIdentifier,
|
|
228
|
+
userProvider,
|
|
229
|
+
user,
|
|
230
|
+
actions: (
|
|
231
|
+
<OverflowMenu className={styles.btnSet} flipped={true} aria-label="user-management-menu">
|
|
232
|
+
<OverflowMenuItem
|
|
233
|
+
className={styles.btn}
|
|
234
|
+
onClick={() => {
|
|
235
|
+
const selectedUser = users.find((u) => u.uuid === user.uuid);
|
|
236
|
+
if (selectedUser) {
|
|
237
|
+
launchWorkspace('manage-user-workspace', {
|
|
238
|
+
workspaceTitle: t('editUser', 'Edit User'),
|
|
239
|
+
initialUserValue: user,
|
|
240
|
+
initialProvider: userProvider,
|
|
241
|
+
});
|
|
242
|
+
} else {
|
|
243
|
+
console.error('User not found:', user.uuid);
|
|
244
|
+
}
|
|
245
|
+
}}
|
|
246
|
+
itemText={t('editUser', 'Edit user')}
|
|
247
|
+
/>
|
|
248
|
+
</OverflowMenu>
|
|
249
|
+
),
|
|
250
|
+
};
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
<>
|
|
255
|
+
<div>
|
|
256
|
+
<div className={styles.Container}>
|
|
257
|
+
<div className={styles.buttonContainer}>
|
|
258
|
+
<p className={styles.filterByText}>{t('filterBy', 'Filter by:')}</p>
|
|
259
|
+
<Dropdown
|
|
260
|
+
hideLabel={true}
|
|
261
|
+
titleText={t('filter', 'Filter')}
|
|
262
|
+
id="filter-dropdown"
|
|
263
|
+
label={t('filter', 'Filter')}
|
|
264
|
+
className={styles.dropDownFilter}
|
|
265
|
+
items={[
|
|
266
|
+
{ id: 'allUsers', label: t('All users', 'All users') },
|
|
267
|
+
{ id: 'activeLicensed', label: t('Active Licensed', 'Active Licensed') },
|
|
268
|
+
{ id: 'expiredLicensed', label: t('Expired Licensed', 'Expired Licensed') },
|
|
269
|
+
{ id: 'licensedExpiringSoon', label: t('Licensed expiring soon', 'Licensed expiring soon') },
|
|
270
|
+
{ id: 'unlicensed', label: t('Unlicensed', 'Unlicensed') },
|
|
271
|
+
]}
|
|
272
|
+
itemToString={(item) => (item ? item.label : '')}
|
|
273
|
+
onChange={({ selectedItem }) => setSelectedFilter(selectedItem.id as FilterType)}
|
|
274
|
+
selectedItem={{ id: selectedFilter, label: t(selectedFilter, selectedFilter) }}
|
|
275
|
+
size={isDesktop(layout) ? 'lg' : 'sm'}
|
|
276
|
+
/>
|
|
277
|
+
<Button
|
|
278
|
+
onClick={() => launchWorkspace('manage-user-workspace', { workspaceTitle: t('addUser', 'Add user') })}
|
|
279
|
+
className={styles.userManagementModeButton}
|
|
280
|
+
renderIcon={UserFollow}
|
|
281
|
+
size={isDesktop(layout) ? 'lg' : 'sm'}
|
|
282
|
+
kind="primary">
|
|
283
|
+
{t('addUser', 'Add User')}
|
|
284
|
+
</Button>
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
<div className={styles.providerContainer}>
|
|
288
|
+
<CardHeader
|
|
289
|
+
title={`${t(getCardTitle(selectedFilter), { count: filteredUsers.length })} (${filteredUsers.length})`}>
|
|
290
|
+
{''}
|
|
291
|
+
</CardHeader>
|
|
292
|
+
<Search
|
|
293
|
+
labelText=""
|
|
294
|
+
placeholder={t('searchForUsers', 'Search for user')}
|
|
295
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
296
|
+
size={isDesktop(layout) ? 'sm' : 'lg'}
|
|
297
|
+
value={searchQuery}
|
|
298
|
+
/>
|
|
299
|
+
|
|
300
|
+
{filteredUsers.length === 0 ? (
|
|
301
|
+
<EmptyState subTitle={t('noMatchingUsers', 'No matching users found')} />
|
|
302
|
+
) : (
|
|
303
|
+
<>
|
|
304
|
+
<DataTable isSortable rows={rowData} headers={headerData} size={responsiveSize} useZebraStyles>
|
|
305
|
+
{({
|
|
306
|
+
rows,
|
|
307
|
+
headers,
|
|
308
|
+
getExpandHeaderProps,
|
|
309
|
+
getTableProps,
|
|
310
|
+
getTableContainerProps,
|
|
311
|
+
getHeaderProps,
|
|
312
|
+
getRowProps,
|
|
313
|
+
}) => (
|
|
314
|
+
<TableContainer {...getTableContainerProps()}>
|
|
315
|
+
<Table className={styles.table} {...getTableProps()} aria-label="Provider list">
|
|
316
|
+
<TableHead>
|
|
317
|
+
<TableRow>
|
|
318
|
+
<TableExpandHeader enableToggle {...getExpandHeaderProps()} />
|
|
319
|
+
{headers.map((header, i) => (
|
|
320
|
+
<TableHeader key={i} {...getHeaderProps({ header })}>
|
|
321
|
+
{header.header}
|
|
322
|
+
</TableHeader>
|
|
323
|
+
))}
|
|
324
|
+
</TableRow>
|
|
325
|
+
</TableHead>
|
|
326
|
+
<TableBody>
|
|
327
|
+
{rows.map((row, i) => (
|
|
328
|
+
<React.Fragment key={row.id}>
|
|
329
|
+
<TableExpandRow {...getRowProps({ row })}>
|
|
330
|
+
{row.cells.map((cell) => (
|
|
331
|
+
<TableCell key={cell.id}>{cell.value}</TableCell>
|
|
332
|
+
))}
|
|
333
|
+
</TableExpandRow>
|
|
334
|
+
{row && row.isExpanded ? (
|
|
335
|
+
<TableExpandedRow className={styles.expandedRow} colSpan={headers.length + 1}>
|
|
336
|
+
<div className={styles.container} key={i}>
|
|
337
|
+
<UserDetails provider={rowData[i].userProvider} user={rowData[i].user} />
|
|
338
|
+
</div>
|
|
339
|
+
</TableExpandedRow>
|
|
340
|
+
) : (
|
|
341
|
+
<TableExpandedRow className={styles.hiddenRow} colSpan={headers.length + 2} />
|
|
342
|
+
)}
|
|
343
|
+
</React.Fragment>
|
|
344
|
+
))}
|
|
345
|
+
</TableBody>
|
|
346
|
+
</Table>
|
|
347
|
+
</TableContainer>
|
|
348
|
+
)}
|
|
349
|
+
</DataTable>
|
|
350
|
+
{paginated && (
|
|
351
|
+
<Pagination
|
|
352
|
+
forwardText={t('nextPage', 'Next page')}
|
|
353
|
+
backwardText={t('previousPage', 'Previous page')}
|
|
354
|
+
page={currentPage}
|
|
355
|
+
pageSize={pageSize}
|
|
356
|
+
pageSizes={pageSizes}
|
|
357
|
+
totalItems={filteredUsers.length}
|
|
358
|
+
className={styles.pagination}
|
|
359
|
+
size={responsiveSize}
|
|
360
|
+
onChange={({ page: newPage, pageSize }) => {
|
|
361
|
+
if (newPage !== currentPage) {
|
|
362
|
+
goTo(newPage);
|
|
363
|
+
}
|
|
364
|
+
setPageSize(pageSize);
|
|
365
|
+
}}
|
|
366
|
+
/>
|
|
367
|
+
)}
|
|
368
|
+
</>
|
|
369
|
+
)}
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
<WorkspaceContainer overlay contextKey="admin" />
|
|
374
|
+
</>
|
|
375
|
+
);
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
export default UserList;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
2
|
+
import { type UserResponse, type ProviderResponse } from '../../../../types';
|
|
3
|
+
import useSWR from 'swr';
|
|
4
|
+
|
|
5
|
+
export function useProvider() {
|
|
6
|
+
const customRepresentation =
|
|
7
|
+
'custom:(uuid,display,person:(uuid,display,gender),identifier,attributes:(uuid,display,attributeType:(uuid,display),value:(uuid,display))';
|
|
8
|
+
|
|
9
|
+
const encodedRepresentation = encodeURIComponent(customRepresentation);
|
|
10
|
+
const url = `${restBaseUrl}/provider?v=${encodedRepresentation}`;
|
|
11
|
+
const { data, error, isLoading } = useSWR<{ data: { results: ProviderResponse[] } }>(url, openmrsFetch);
|
|
12
|
+
const provider = data?.data?.results || [];
|
|
13
|
+
|
|
14
|
+
return { provider, error, isLoading };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const useUsers = () => {
|
|
18
|
+
const customRepresentation =
|
|
19
|
+
'custom:(uuid,display,username,systemId,person:(uuid,display,gender),roles:(uuid,display,description))';
|
|
20
|
+
const encodedRepresentation = encodeURIComponent(customRepresentation);
|
|
21
|
+
const url = `${restBaseUrl}/user?v=${encodedRepresentation}`;
|
|
22
|
+
const { data, isLoading, error, mutate } = useSWR<FetchResponse<{ results: UserResponse[] }>>(url, openmrsFetch);
|
|
23
|
+
const users = data?.data?.results || [];
|
|
24
|
+
return {
|
|
25
|
+
users,
|
|
26
|
+
isLoading,
|
|
27
|
+
mutate,
|
|
28
|
+
error,
|
|
29
|
+
};
|
|
30
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
@use '@carbon/layout';
|
|
2
|
+
@use '@carbon/colors';
|
|
3
|
+
@use '@carbon/type';
|
|
4
|
+
|
|
5
|
+
.buttonContainer {
|
|
6
|
+
display: flex;
|
|
7
|
+
justify-content: flex-end;
|
|
8
|
+
align-items: center;
|
|
9
|
+
gap: layout.$spacing-02;
|
|
10
|
+
margin-bottom: layout.$spacing-05;
|
|
11
|
+
|
|
12
|
+
.filterByText {
|
|
13
|
+
margin: 0;
|
|
14
|
+
display: flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
padding-right: layout.$spacing-03;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
.dropDownFilter {
|
|
20
|
+
margin-right: layout.$spacing-03;
|
|
21
|
+
min-width: 205px;
|
|
22
|
+
text-align: start;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.widgetCard {
|
|
26
|
+
background-color: white;
|
|
27
|
+
}
|
|
28
|
+
.Container {
|
|
29
|
+
margin: layout.$spacing-05;
|
|
30
|
+
}
|
|
31
|
+
.providerContainer {
|
|
32
|
+
background-color: white;
|
|
33
|
+
border: 1px solid colors.$gray-20;
|
|
34
|
+
border-bottom: none;
|
|
35
|
+
width: 100%;
|
|
36
|
+
margin: 0 auto;
|
|
37
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ROLE_CATEGORIES } from '../../../constants';
|
|
2
|
+
|
|
3
|
+
export const FORM_STEP_IDS = ['demographic', 'login', 'provider', 'roles'] as const;
|
|
4
|
+
|
|
5
|
+
export const FORM_STEP_KEYS: Record<(typeof FORM_STEP_IDS)[number], { key: string; fallback: string }> = {
|
|
6
|
+
demographic: { key: 'demographicInformation', fallback: 'Demographic Info' },
|
|
7
|
+
login: { key: 'loginInformation', fallback: 'Login Info' },
|
|
8
|
+
provider: { key: 'providerAccount', fallback: 'Provider Account' },
|
|
9
|
+
roles: { key: 'roles', fallback: 'Roles Info' },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const UPDATABLE_PROVIDER_ATTRIBUTE_KEYS = [
|
|
13
|
+
'licenseBody',
|
|
14
|
+
'providerNationalId',
|
|
15
|
+
'licenseNumber',
|
|
16
|
+
'passportNumber',
|
|
17
|
+
'providerUniqueIdentifier',
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
export const EXCLUDED_ROLE_CATEGORY = ROLE_CATEGORIES.CORE_INVENTORY;
|