@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,54 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Button } from '@carbon/react';
|
|
4
|
+
import { HWR_API_NO_CREDENTIALS, RESOURCE_NOT_FOUND, UNKNOWN } from '../../constants';
|
|
5
|
+
|
|
6
|
+
interface HWREmptyModalProps {
|
|
7
|
+
close: () => void;
|
|
8
|
+
errorCode?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const HWREmptyModal: React.FC<HWREmptyModalProps> = ({ close, errorCode }) => {
|
|
12
|
+
const { t } = useTranslation();
|
|
13
|
+
|
|
14
|
+
const errorMessages = {
|
|
15
|
+
[RESOURCE_NOT_FOUND]: t(
|
|
16
|
+
'ResourceNotFound',
|
|
17
|
+
'The Health Work Registry is not reachable, kindly confirm your internet connectivity and try again. Do you want to continue to create an account',
|
|
18
|
+
),
|
|
19
|
+
[HWR_API_NO_CREDENTIALS]: t(
|
|
20
|
+
'noHwrApi',
|
|
21
|
+
'Health Care Worker Registry API credentials not configured, Kindly contact system admin. Do you want to continue to create an account',
|
|
22
|
+
),
|
|
23
|
+
[UNKNOWN]: t(
|
|
24
|
+
'unknownError',
|
|
25
|
+
'An error occurred while searching Health Worker Registry, kindly contact system admin. Do you want to continue to create an account',
|
|
26
|
+
),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const defaultMessage = t(
|
|
30
|
+
'HealthworkerNotFound',
|
|
31
|
+
'The health worker records could not be found in Health Worker registry, do you want to continue to create an account',
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const message = errorMessages[errorCode] || defaultMessage;
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<>
|
|
38
|
+
<div className="cds--modal-header">
|
|
39
|
+
<h3 className="cds--modal-header__heading">{t('healthWorkerRegistryEmpty', 'Create an Account')}</h3>
|
|
40
|
+
</div>
|
|
41
|
+
<div className="cds--modal-content">
|
|
42
|
+
<p>{message}</p>
|
|
43
|
+
</div>
|
|
44
|
+
<div className="cds--modal-footer">
|
|
45
|
+
<Button kind="secondary" onClick={close}>
|
|
46
|
+
{t('cancel', 'Cancel')}
|
|
47
|
+
</Button>
|
|
48
|
+
<Button onClick={close}>{t('continue', 'Continue to registration')}</Button>
|
|
49
|
+
</div>
|
|
50
|
+
</>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export default HWREmptyModal;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
@use '@carbon/layout';
|
|
2
|
+
@use '@carbon/type';
|
|
3
|
+
@use '@carbon/colors';
|
|
4
|
+
|
|
5
|
+
.modalContainer {
|
|
6
|
+
display: flex;
|
|
7
|
+
justify-content: center;
|
|
8
|
+
gap: layout.$spacing-05;
|
|
9
|
+
margin-top: layout.$spacing-03;
|
|
10
|
+
margin-bottom: layout.$spacing-10;
|
|
11
|
+
height: auto;
|
|
12
|
+
align-items: center;
|
|
13
|
+
width: 100%;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.identifierTypeColumn {
|
|
17
|
+
margin-top: layout.$spacing-05;
|
|
18
|
+
margin-bottom: layout.$spacing-10;
|
|
19
|
+
flex-grow: 1;
|
|
20
|
+
min-width: 250px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.identifierTypeHeader {
|
|
24
|
+
color: colors.$gray-70;
|
|
25
|
+
display: inline-block;
|
|
26
|
+
font-size: layout.$spacing-04;
|
|
27
|
+
line-height: layout.$spacing-05;
|
|
28
|
+
margin-bottom: layout.$spacing-03;
|
|
29
|
+
vertical-align: baseline;
|
|
30
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Button, Column, Search, ComboBox, InlineLoading } from '@carbon/react';
|
|
4
|
+
import styles from './hwr-sync.modal.scss';
|
|
5
|
+
import { useConfig, showSnackbar, formatDate, parseDate, showToast, restBaseUrl } from '@openmrs/esm-framework';
|
|
6
|
+
import { mutate } from 'swr';
|
|
7
|
+
import { CustomHIEPractitionerResponse, type PractitionerResponse, type ProviderResponse } from '../../types';
|
|
8
|
+
import { ConfigObject } from '../../config-schema';
|
|
9
|
+
import { searchHealthCareWork, HealthWorkerAdapter } from '../hook/healthWorkerAdapter';
|
|
10
|
+
import { createProviderAttribute, updateProviderAttributes } from './hwr-sync.resource';
|
|
11
|
+
|
|
12
|
+
interface HWRSyncModalProps {
|
|
13
|
+
close: () => void;
|
|
14
|
+
provider: ProviderResponse;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const HWRSyncModal: React.FC<HWRSyncModalProps> = ({ close, provider }) => {
|
|
18
|
+
const { t } = useTranslation();
|
|
19
|
+
const [syncLoading, setSyncLoading] = useState(false);
|
|
20
|
+
|
|
21
|
+
const config = useConfig<ConfigObject>();
|
|
22
|
+
const {
|
|
23
|
+
providerNationalIdUuid,
|
|
24
|
+
licenseBodyUuid,
|
|
25
|
+
licenseExpiryDateUuid,
|
|
26
|
+
passportNumberUuid,
|
|
27
|
+
licenseNumberUuid,
|
|
28
|
+
identifierTypes,
|
|
29
|
+
phoneNumberUuid,
|
|
30
|
+
qualificationUuid,
|
|
31
|
+
providerAddressUuid,
|
|
32
|
+
providerHieFhirReference,
|
|
33
|
+
providerUniqueIdentifierAttributeTypeUuid,
|
|
34
|
+
regulatorOptions,
|
|
35
|
+
} = config;
|
|
36
|
+
|
|
37
|
+
const attributeMapping = {
|
|
38
|
+
[identifierTypes[0]?.key]:
|
|
39
|
+
provider.attributes.find((attr) => attr.attributeType.uuid === providerNationalIdUuid)?.value || '--',
|
|
40
|
+
[identifierTypes[1]?.key]:
|
|
41
|
+
provider.attributes.find((attr) => attr.attributeType.uuid === licenseBodyUuid)?.value || '--',
|
|
42
|
+
[identifierTypes[2]?.key]:
|
|
43
|
+
provider.attributes.find((attr) => attr.attributeType.uuid === passportNumberUuid)?.value || '--',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const [searchHWR, setSearchHWR] = useState({
|
|
47
|
+
identifierType: identifierTypes[0]?.key,
|
|
48
|
+
identifier: attributeMapping[identifierTypes[0]?.key],
|
|
49
|
+
regulator: regulatorOptions[0]?.key,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const handleIdentifierTypeChange = (selectedItem: { key: string; name: string } | null) => {
|
|
53
|
+
const selectedKey = selectedItem?.key ?? '';
|
|
54
|
+
setSearchHWR((prev) => ({
|
|
55
|
+
...prev,
|
|
56
|
+
identifierType: selectedKey,
|
|
57
|
+
identifier: attributeMapping[selectedKey] || '',
|
|
58
|
+
}));
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handleRegulatorChange = (selectedItem: { key: string; name: string } | null) => {
|
|
62
|
+
const selectedKey = selectedItem?.key ?? '';
|
|
63
|
+
setSearchHWR((prev) => ({
|
|
64
|
+
...prev,
|
|
65
|
+
regulator: selectedKey,
|
|
66
|
+
}));
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const isSearchDisabled = () => !searchHWR.identifier;
|
|
70
|
+
|
|
71
|
+
const handleSync = async () => {
|
|
72
|
+
try {
|
|
73
|
+
setSyncLoading(true);
|
|
74
|
+
const unifiedResponse = await searchHealthCareWork(
|
|
75
|
+
searchHWR.identifierType,
|
|
76
|
+
searchHWR.identifier,
|
|
77
|
+
searchHWR.regulator,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const normalizedData = HealthWorkerAdapter.normalize(unifiedResponse);
|
|
81
|
+
|
|
82
|
+
if (!normalizedData) {
|
|
83
|
+
throw new Error(t('noResults', 'No results found'));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const updatableAttributes = [
|
|
87
|
+
{ attributeType: licenseNumberUuid, value: normalizedData.licenseNumber },
|
|
88
|
+
{ attributeType: licenseBodyUuid, value: normalizedData.registrationId },
|
|
89
|
+
{
|
|
90
|
+
attributeType: licenseExpiryDateUuid,
|
|
91
|
+
value: normalizedData.licenseEndDate ? parseDate(normalizedData.licenseEndDate) : null,
|
|
92
|
+
},
|
|
93
|
+
{ attributeType: phoneNumberUuid, value: normalizedData.phoneNumber },
|
|
94
|
+
{ attributeType: qualificationUuid, value: normalizedData.qualification },
|
|
95
|
+
{
|
|
96
|
+
attributeType: providerHieFhirReference,
|
|
97
|
+
value: JSON.stringify({
|
|
98
|
+
...unifiedResponse.data,
|
|
99
|
+
fhirFormat: unifiedResponse.fhirFormat,
|
|
100
|
+
searchParameters: {
|
|
101
|
+
regulator: searchHWR.regulator,
|
|
102
|
+
identifierType: searchHWR.identifierType,
|
|
103
|
+
},
|
|
104
|
+
}),
|
|
105
|
+
},
|
|
106
|
+
{ attributeType: providerAddressUuid, value: normalizedData.email },
|
|
107
|
+
{ attributeType: providerNationalIdUuid, value: normalizedData.nationalId },
|
|
108
|
+
{
|
|
109
|
+
attributeType: providerUniqueIdentifierAttributeTypeUuid,
|
|
110
|
+
value: normalizedData.providerUniqueIdentifier,
|
|
111
|
+
},
|
|
112
|
+
].filter((attr) => attr.value !== undefined && attr.value !== null && attr.value !== '');
|
|
113
|
+
|
|
114
|
+
await Promise.all(
|
|
115
|
+
updatableAttributes.map((attr) => {
|
|
116
|
+
const existingAttribute = provider.attributes.find(
|
|
117
|
+
(at) => at.attributeType.uuid === attr.attributeType,
|
|
118
|
+
)?.uuid;
|
|
119
|
+
|
|
120
|
+
const payload = {
|
|
121
|
+
attributeType: attr.attributeType,
|
|
122
|
+
value: attr.value,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
if (!existingAttribute) {
|
|
126
|
+
return createProviderAttribute(payload, provider.uuid);
|
|
127
|
+
}
|
|
128
|
+
return updateProviderAttributes(payload, provider.uuid, existingAttribute);
|
|
129
|
+
}),
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
mutate((key) => typeof key === 'string' && key.startsWith(`${restBaseUrl}/provider`));
|
|
133
|
+
showSnackbar({
|
|
134
|
+
title: 'Success',
|
|
135
|
+
kind: 'success',
|
|
136
|
+
subtitle: t('syncMessage', 'user details synced successfully'),
|
|
137
|
+
});
|
|
138
|
+
close();
|
|
139
|
+
} catch (err) {
|
|
140
|
+
showToast({
|
|
141
|
+
critical: false,
|
|
142
|
+
kind: 'error',
|
|
143
|
+
description: t('errorSyncMsg', `Failed to sync the account with ${searchHWR.identifier}. ${err}`),
|
|
144
|
+
title: t('hwrERROR', 'Sync Failed'),
|
|
145
|
+
});
|
|
146
|
+
} finally {
|
|
147
|
+
setSyncLoading(false);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<>
|
|
153
|
+
<div className="cds--modal-header">
|
|
154
|
+
<h3 className="cds--modal-header__heading">{t('healthWorkerRegistry', 'Health worker registry')}</h3>
|
|
155
|
+
</div>
|
|
156
|
+
<div className="cds--modal-content">
|
|
157
|
+
<p>{t('healthWorkerSync', 'Health worker information to be synced with the registry.')}</p>
|
|
158
|
+
<div className={styles.modalContainer}>
|
|
159
|
+
<Column className={styles.identifierTypeColumn}>
|
|
160
|
+
<ComboBox
|
|
161
|
+
onChange={({ selectedItem }) => handleIdentifierTypeChange(selectedItem)}
|
|
162
|
+
id="formIdentifierType"
|
|
163
|
+
titleText={t('identificationType', 'Identification Type')}
|
|
164
|
+
placeholder={t('chooseIdentifierType', 'Choose identifier type')}
|
|
165
|
+
initialSelectedItem={identifierTypes.find((item) => item.key === searchHWR.identifierType)}
|
|
166
|
+
items={identifierTypes}
|
|
167
|
+
itemToString={(item) => (item ? item.name : '')}
|
|
168
|
+
className={styles.ComboBox}
|
|
169
|
+
/>
|
|
170
|
+
</Column>
|
|
171
|
+
<Column className={styles.identifierTypeColumn}>
|
|
172
|
+
<ComboBox
|
|
173
|
+
onChange={({ selectedItem }) => handleRegulatorChange(selectedItem)}
|
|
174
|
+
id="regulatorOptions"
|
|
175
|
+
titleText={t('regulator', 'Regulator')}
|
|
176
|
+
placeholder={t('chooseRegulator', 'Choose regulator')}
|
|
177
|
+
initialSelectedItem={regulatorOptions.find((item) => item.key === searchHWR.regulator)}
|
|
178
|
+
items={regulatorOptions}
|
|
179
|
+
itemToString={(item) => (item ? item.name : '')}
|
|
180
|
+
className={styles.ComboBox}
|
|
181
|
+
/>
|
|
182
|
+
</Column>
|
|
183
|
+
<Column className={styles.identifierTypeColumn}>
|
|
184
|
+
<span className={styles.identifierTypeHeader}>{t('identifierNumber', 'Identifier number*')}</span>
|
|
185
|
+
<Search
|
|
186
|
+
labelText={t('enterIdentifierNumber', 'Enter identifier number')}
|
|
187
|
+
className={styles.formSearch}
|
|
188
|
+
value={searchHWR.identifier}
|
|
189
|
+
placeholder={t('enterIdentifierNumber', 'Enter identifier number')}
|
|
190
|
+
id="formSearchHealthWorkers"
|
|
191
|
+
disabled={isSearchDisabled()}
|
|
192
|
+
onChange={(value) => setSearchHWR({ ...searchHWR, identifier: value.target.value })}
|
|
193
|
+
/>
|
|
194
|
+
</Column>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
<div className="cds--modal-footer">
|
|
198
|
+
<Button kind="secondary" onClick={close}>
|
|
199
|
+
{t('cancel', 'Cancel')}
|
|
200
|
+
</Button>
|
|
201
|
+
<Button disabled={isSearchDisabled() || syncLoading} onClick={handleSync}>
|
|
202
|
+
{syncLoading ? <InlineLoading status="active" description={t('syncing', 'Syncing...')} /> : t('sync', 'Sync')}
|
|
203
|
+
</Button>
|
|
204
|
+
</div>
|
|
205
|
+
</>
|
|
206
|
+
);
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export default HWRSyncModal;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
2
|
+
|
|
3
|
+
export const createProviderAttribute = (payload, providerUuid: string) => {
|
|
4
|
+
const url = `${restBaseUrl}/provider/${providerUuid}/attribute`;
|
|
5
|
+
return openmrsFetch(url, {
|
|
6
|
+
method: 'POST',
|
|
7
|
+
body: JSON.stringify(payload),
|
|
8
|
+
headers: {
|
|
9
|
+
'Content-Type': 'application/json',
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const updateProviderAttributes = (payload, providerUuid: string, attributeUuid: string) => {
|
|
15
|
+
const url = `${restBaseUrl}/provider/${providerUuid}/attribute/${attributeUuid}`;
|
|
16
|
+
return openmrsFetch(url, {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
body: JSON.stringify(payload),
|
|
19
|
+
headers: {
|
|
20
|
+
'Content-Type': 'application/json',
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { useSession } from '@openmrs/esm-framework';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { type Attribute, useProviderAttributes } from './provider-banner.resource';
|
|
4
|
+
import styles from './provider-banner.module.scss';
|
|
5
|
+
import { InlineLoading, Tag } from '@carbon/react';
|
|
6
|
+
import { useTranslation } from 'react-i18next';
|
|
7
|
+
import upperCase from 'lodash-es/upperCase';
|
|
8
|
+
|
|
9
|
+
const ProviderBannerTag: React.FC = () => {
|
|
10
|
+
const { currentProvider } = useSession();
|
|
11
|
+
const { t } = useTranslation();
|
|
12
|
+
const currentProviderUuid = currentProvider?.uuid;
|
|
13
|
+
const { isLoading, error, providerAttributes } = useProviderAttributes(currentProviderUuid);
|
|
14
|
+
|
|
15
|
+
const getAttributeValue = (attributes: Array<Attribute>, displayName: string): string => {
|
|
16
|
+
const attribute = attributes?.find((attr) => attr.attributeType.display === displayName);
|
|
17
|
+
return attribute?.value || '';
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const getLicenseStatus = (expiryDate: string) => {
|
|
21
|
+
if (!expiryDate || expiryDate === '0000-00-00') {
|
|
22
|
+
return { status: 'unknown', message: t('unlicensed', 'Unlicensed'), tagType: 'magenta' as const };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const today = new Date();
|
|
26
|
+
const expiry = new Date(expiryDate);
|
|
27
|
+
const timeDiff = expiry.getTime() - today.getTime();
|
|
28
|
+
const daysDiff = Math.ceil(timeDiff / (1000 * 3600 * 24));
|
|
29
|
+
|
|
30
|
+
if (daysDiff < 0) {
|
|
31
|
+
return { status: 'expired', message: t('licenseExpired', 'License Expired'), tagType: 'red' as const };
|
|
32
|
+
} else if (daysDiff <= 30) {
|
|
33
|
+
return { status: 'warning', message: t('expiresSoon', 'Expires Soon'), tagType: 'blue' as const };
|
|
34
|
+
} else if (daysDiff <= 90) {
|
|
35
|
+
return { status: 'caution', message: t('expiresIn3Months', 'Expires in 3 months'), tagType: 'teal' as const };
|
|
36
|
+
} else {
|
|
37
|
+
return { status: 'valid', message: t('validLicense', 'Valid License'), tagType: 'green' as const };
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const formatDate = (dateString: string): string => {
|
|
42
|
+
if (!dateString || dateString === '000-000-00') {
|
|
43
|
+
return '0000-00-00';
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const date = new Date(dateString);
|
|
47
|
+
return date.toISOString().split('T')[0];
|
|
48
|
+
} catch {
|
|
49
|
+
return '0000-00-00';
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (isLoading) {
|
|
54
|
+
return (
|
|
55
|
+
<div className={styles.loading}>
|
|
56
|
+
<InlineLoading description={t('loadingState', 'loading' + '...')} />
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const name = upperCase(providerAttributes?.person?.display) || 'NONE';
|
|
62
|
+
const hwi = getAttributeValue(providerAttributes?.attributes, 'Provider unique identifier') || 'NONE';
|
|
63
|
+
const licenseExpiry = getAttributeValue(providerAttributes?.attributes, 'License Expiry Date');
|
|
64
|
+
const formattedExpiry = formatDate(licenseExpiry) || '0000-00-00';
|
|
65
|
+
const shouldShowLicenseExpiry = !!licenseExpiry;
|
|
66
|
+
const licenseStatus = getLicenseStatus(licenseExpiry);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className={styles.providerBanner}>
|
|
70
|
+
<div className={styles.bannerContent}>
|
|
71
|
+
<div className={styles.divider} />
|
|
72
|
+
<div className={styles.infoItem}>
|
|
73
|
+
<span className={styles.label}>{t('identifierProvider', 'Identifier:')}</span>
|
|
74
|
+
<span className={`${styles.value} ${styles.hwiValue}`}>{hwi}</span>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<div className={styles.divider} />
|
|
78
|
+
|
|
79
|
+
<>
|
|
80
|
+
<div className={styles.infoItem}>
|
|
81
|
+
<span className={styles.label}>{t('nameProvider', 'Name:')}</span>
|
|
82
|
+
<span className={styles.value}>{name}</span>
|
|
83
|
+
</div>
|
|
84
|
+
<div className={styles.divider} />
|
|
85
|
+
<div className={styles.infoItem}>
|
|
86
|
+
<span className={styles.label}>{t('licenseProvider', 'License:')}</span>
|
|
87
|
+
|
|
88
|
+
{shouldShowLicenseExpiry && (
|
|
89
|
+
<>
|
|
90
|
+
<span className={styles.value}>{formattedExpiry}</span>
|
|
91
|
+
</>
|
|
92
|
+
)}
|
|
93
|
+
|
|
94
|
+
<span className={styles.statusIndicator}>
|
|
95
|
+
<Tag size="md" type={licenseStatus.tagType}>
|
|
96
|
+
{licenseStatus.message}
|
|
97
|
+
</Tag>
|
|
98
|
+
</span>
|
|
99
|
+
</div>
|
|
100
|
+
</>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export default ProviderBannerTag;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
@use '@carbon/colors';
|
|
2
|
+
@use '@carbon/layout';
|
|
3
|
+
@use '@carbon/type';
|
|
4
|
+
|
|
5
|
+
.providerBanner {
|
|
6
|
+
position: relative;
|
|
7
|
+
padding: layout.$spacing-03 layout.$spacing-05;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.bannerContent {
|
|
11
|
+
display: flex;
|
|
12
|
+
align-items: center;
|
|
13
|
+
flex-wrap: wrap;
|
|
14
|
+
@include type.type-style('caption-01');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.infoItem {
|
|
18
|
+
display: flex;
|
|
19
|
+
align-items: center;
|
|
20
|
+
gap: 6px;
|
|
21
|
+
flex-wrap: wrap;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.divider {
|
|
25
|
+
width: 1px;
|
|
26
|
+
height: layout.$spacing-06;
|
|
27
|
+
background-color: rgba(244, 244, 244, 0.4);
|
|
28
|
+
margin-inline-start: layout.$spacing-04;
|
|
29
|
+
}
|
|
30
|
+
.label {
|
|
31
|
+
color: colors.$gray-90;
|
|
32
|
+
content: '';
|
|
33
|
+
margin-left: layout.$spacing-01;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.value {
|
|
37
|
+
color: colors.$gray-90;
|
|
38
|
+
font-weight: 600;
|
|
39
|
+
font-size: 1rem;
|
|
40
|
+
white-space: nowrap;
|
|
41
|
+
@include type.type-style('caption-01');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.hwiValue {
|
|
45
|
+
font-size: 0.875rem;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.loading {
|
|
49
|
+
text-align: center;
|
|
50
|
+
padding: layout.$spacing-03 0;
|
|
51
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
2
|
+
import useSWR from 'swr';
|
|
3
|
+
|
|
4
|
+
export interface ProviderAttributesResponse {
|
|
5
|
+
person: Person;
|
|
6
|
+
attributes: Array<Attribute>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface Person {
|
|
10
|
+
display: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface Attribute {
|
|
14
|
+
attributeType: AttributeType;
|
|
15
|
+
value: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AttributeType {
|
|
19
|
+
display: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const useProviderAttributes = (uuid: string) => {
|
|
23
|
+
const customRepresentation = 'custom:(person:(display),attributes:(attributeType:(display),value))';
|
|
24
|
+
const url = `${restBaseUrl}/provider/${uuid}?v=${customRepresentation}`;
|
|
25
|
+
|
|
26
|
+
const { isLoading, error, data } = useSWR<FetchResponse<ProviderAttributesResponse>>(url, openmrsFetch);
|
|
27
|
+
const providerAttributes = data?.data;
|
|
28
|
+
return { isLoading, error, providerAttributes };
|
|
29
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
@use '@carbon/colors';
|
|
2
|
+
@use '@carbon/layout';
|
|
3
|
+
@use '@carbon/type';
|
|
4
|
+
|
|
5
|
+
.leftPanel {
|
|
6
|
+
border-right: 1px solid colors.$gray-20;
|
|
7
|
+
padding-top: layout.$spacing-05;
|
|
8
|
+
|
|
9
|
+
// Left nav menu item
|
|
10
|
+
:global(.cds--side-nav__link) {
|
|
11
|
+
color: colors.$gray-70;
|
|
12
|
+
@include type.type-style('heading-compact-01');
|
|
13
|
+
|
|
14
|
+
&:focus,
|
|
15
|
+
&:hover {
|
|
16
|
+
background-color: colors.$gray-10-hover;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Active menu item
|
|
20
|
+
&:global(.active-left-nav-link) {
|
|
21
|
+
color: colors.$gray-100;
|
|
22
|
+
border-left: layout.$spacing-02 solid var(--brand-01);
|
|
23
|
+
outline: none;
|
|
24
|
+
padding: 0 layout.$spacing-04;
|
|
25
|
+
background-color: colors.$gray-20;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* Desktop */
|
|
31
|
+
:global(.omrs-breakpoint-gt-tablet) {
|
|
32
|
+
:global(.cds--side-nav__link) {
|
|
33
|
+
height: layout.$spacing-07;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Tablet */
|
|
38
|
+
:global(.omrs-breakpoint-lt-desktop) {
|
|
39
|
+
:global(.cds--side-nav__link) {
|
|
40
|
+
height: layout.$spacing-09;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { SideNav } from '@carbon/react';
|
|
4
|
+
import { attach, ExtensionSlot, isDesktop, useLayoutType } from '@openmrs/esm-framework';
|
|
5
|
+
import styles from './left-panel.scss';
|
|
6
|
+
|
|
7
|
+
attach('nav-menu-slot', 'admin-left-panel');
|
|
8
|
+
|
|
9
|
+
const LeftPanel: React.FC = () => {
|
|
10
|
+
const { t } = useTranslation();
|
|
11
|
+
const layout = useLayoutType();
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
isDesktop(layout) && (
|
|
15
|
+
<SideNav aria-label={t('adminLeftPannel', 'Admin left panel')} className={styles.leftPanel} expanded>
|
|
16
|
+
<ExtensionSlot name="admin-left-panel-slot" />
|
|
17
|
+
</SideNav>
|
|
18
|
+
)
|
|
19
|
+
);
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default LeftPanel;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
@use '@carbon/layout';
|
|
2
|
+
@use '@carbon/type';
|
|
3
|
+
@use '@openmrs/esm-styleguide/src/vars' as *;
|
|
4
|
+
|
|
5
|
+
.header {
|
|
6
|
+
@include type.type-style('body-compact-02');
|
|
7
|
+
color: $text-02;
|
|
8
|
+
height: layout.$spacing-12;
|
|
9
|
+
background-color: $ui-02;
|
|
10
|
+
border-bottom: 1px solid $ui-03;
|
|
11
|
+
display: flex;
|
|
12
|
+
justify-content: space-between;
|
|
13
|
+
padding: layout.$spacing-05;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.leftJustifiedItems {
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: row;
|
|
19
|
+
align-items: center;
|
|
20
|
+
cursor: pointer;
|
|
21
|
+
align-items: center;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.rightJustifiedItems {
|
|
25
|
+
@include type.type-style('body-compact-02');
|
|
26
|
+
color: $text-02;
|
|
27
|
+
display: flex;
|
|
28
|
+
flex-direction: column;
|
|
29
|
+
justify-content: space-between;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.pageName {
|
|
33
|
+
@include type.type-style('heading-04');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.pageLabels {
|
|
37
|
+
margin: layout.$spacing-05;
|
|
38
|
+
|
|
39
|
+
p:first-of-type {
|
|
40
|
+
margin-bottom: layout.$spacing-02;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.dateAndLocation {
|
|
45
|
+
display: flex;
|
|
46
|
+
justify-content: flex-end;
|
|
47
|
+
align-items: center;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.userContainer {
|
|
51
|
+
display: flex;
|
|
52
|
+
justify-content: flex-end;
|
|
53
|
+
gap: layout.$spacing-05;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.value {
|
|
57
|
+
margin-left: layout.$spacing-02;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.middot {
|
|
61
|
+
margin: 0 layout.$spacing-03;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.view {
|
|
65
|
+
@include type.type-style('label-01');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Overriding styles for RTL support
|
|
69
|
+
html[dir='rtl'] {
|
|
70
|
+
.date-and-location {
|
|
71
|
+
& > svg {
|
|
72
|
+
order: -1;
|
|
73
|
+
}
|
|
74
|
+
& > span:nth-child(2) {
|
|
75
|
+
order: -2;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.userIcon {
|
|
81
|
+
fill: $ui-05;
|
|
82
|
+
margin: layout.$spacing-01;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.svgContainer svg {
|
|
86
|
+
width: layout.$spacing-10;
|
|
87
|
+
height: layout.$spacing-10;
|
|
88
|
+
margin-right: layout.$spacing-06;
|
|
89
|
+
fill: var(--brand-03);
|
|
90
|
+
}
|