@kenyaemr/esm-admin-app 5.4.2-pre.2269 → 5.4.2-pre.2272
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 -15
- package/dist/342.js +1 -0
- package/dist/342.js.map +1 -0
- package/dist/965.js +1 -0
- package/dist/965.js.map +1 -0
- package/dist/kenyaemr-esm-admin-app.js +1 -1
- package/dist/kenyaemr-esm-admin-app.js.buildmanifest.json +54 -54
- package/dist/kenyaemr-esm-admin-app.js.map +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +1 -1
- 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 +64 -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 +202 -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/index.json +35 -0
- package/src/components/locations/hooks/useLocation.ts +24 -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 +41 -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/index.ts +9 -0
- package/src/root.component.tsx +2 -0
- package/src/routes.json +27 -3
- package/dist/479.js +0 -1
- package/dist/479.js.map +0 -1
- package/dist/512.js +0 -1
- package/dist/512.js.map +0 -1
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import React, { useEffect, useMemo } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
type DefaultWorkspaceProps,
|
|
4
|
+
ResponsiveWrapper,
|
|
5
|
+
useLayoutType,
|
|
6
|
+
showSnackbar,
|
|
7
|
+
useConfig,
|
|
8
|
+
restBaseUrl,
|
|
9
|
+
} from '@openmrs/esm-framework';
|
|
10
|
+
import { useTranslation } from 'react-i18next';
|
|
11
|
+
import { Controller, useForm } from 'react-hook-form';
|
|
12
|
+
import {
|
|
13
|
+
ButtonSet,
|
|
14
|
+
Button,
|
|
15
|
+
InlineLoading,
|
|
16
|
+
TextInput,
|
|
17
|
+
FormGroup,
|
|
18
|
+
Stack,
|
|
19
|
+
Form,
|
|
20
|
+
FilterableMultiSelect,
|
|
21
|
+
} from '@carbon/react';
|
|
22
|
+
import classNames from 'classnames';
|
|
23
|
+
import { z } from 'zod';
|
|
24
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
25
|
+
import styles from './add-location.workspace.scss';
|
|
26
|
+
import { editLocation, saveLocation } from '../../hooks/useLocation';
|
|
27
|
+
import { LocationResponse, type AdmissionLocationResponse } from '../../types';
|
|
28
|
+
import { extractErrorMessagesFromResponse } from '../../helpers';
|
|
29
|
+
import { useLocationTags } from '../../hooks/useLocationTags';
|
|
30
|
+
import { mutate } from 'swr';
|
|
31
|
+
|
|
32
|
+
type AddLocationWorkspaceProps = DefaultWorkspaceProps & {
|
|
33
|
+
location?: LocationResponse;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const locationFormSchema = z.object({
|
|
37
|
+
name: z.string().min(1, { message: 'Location name is required' }),
|
|
38
|
+
tags: z
|
|
39
|
+
.object({
|
|
40
|
+
uuid: z.string().uuid(),
|
|
41
|
+
display: z.string(),
|
|
42
|
+
})
|
|
43
|
+
.array()
|
|
44
|
+
.nonempty('At least one tag is required'),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
type LocationFormType = z.infer<typeof locationFormSchema>;
|
|
48
|
+
|
|
49
|
+
const AddLocationWorkspace: React.FC<AddLocationWorkspaceProps> = ({
|
|
50
|
+
closeWorkspace,
|
|
51
|
+
closeWorkspaceWithSavedChanges,
|
|
52
|
+
promptBeforeClosing,
|
|
53
|
+
location,
|
|
54
|
+
}) => {
|
|
55
|
+
const { t } = useTranslation();
|
|
56
|
+
const isTablet = useLayoutType() === 'tablet';
|
|
57
|
+
const { locationTagList: Tags } = useLocationTags();
|
|
58
|
+
|
|
59
|
+
const hasLocationAttributes = useMemo(() => {
|
|
60
|
+
return location?.attributes && location.attributes.length > 0;
|
|
61
|
+
}, [location?.attributes]);
|
|
62
|
+
|
|
63
|
+
const handleMutation = () => {
|
|
64
|
+
const url = `${restBaseUrl}/location`;
|
|
65
|
+
mutate((key) => typeof key === 'string' && key.startsWith(url), undefined, { revalidate: true });
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const {
|
|
69
|
+
handleSubmit,
|
|
70
|
+
control,
|
|
71
|
+
getValues,
|
|
72
|
+
formState: { isSubmitting, isDirty, errors },
|
|
73
|
+
} = useForm<LocationFormType>({
|
|
74
|
+
resolver: zodResolver(locationFormSchema),
|
|
75
|
+
defaultValues: {
|
|
76
|
+
name: location?.name || '',
|
|
77
|
+
tags: location?.tags || [],
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const onSubmit = async (data: LocationFormType) => {
|
|
82
|
+
const formDataFormSubmission = getValues();
|
|
83
|
+
|
|
84
|
+
const locationTagsUuid = formDataFormSubmission?.tags?.map((tag) => tag.uuid) || [];
|
|
85
|
+
|
|
86
|
+
const locationPayload = {
|
|
87
|
+
name: formDataFormSubmission.name,
|
|
88
|
+
tags: locationTagsUuid,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
if (location?.uuid) {
|
|
93
|
+
await editLocation(location.uuid, locationPayload);
|
|
94
|
+
} else {
|
|
95
|
+
await saveLocation(locationPayload);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
showSnackbar({
|
|
99
|
+
title: t('success', 'Success'),
|
|
100
|
+
kind: 'success',
|
|
101
|
+
subtitle: location?.uuid
|
|
102
|
+
? t('locationUpdated', 'Location {{locationName}} was updated successfully.', {
|
|
103
|
+
locationName: data.name,
|
|
104
|
+
})
|
|
105
|
+
: t('locationCreated', 'Location {{locationName}} was created successfully.', {
|
|
106
|
+
locationName: data.name,
|
|
107
|
+
}),
|
|
108
|
+
});
|
|
109
|
+
handleMutation();
|
|
110
|
+
closeWorkspaceWithSavedChanges();
|
|
111
|
+
} catch (error: any) {
|
|
112
|
+
const errorMessages = extractErrorMessagesFromResponse(error);
|
|
113
|
+
showSnackbar({
|
|
114
|
+
title: t('error', 'Error'),
|
|
115
|
+
kind: 'error',
|
|
116
|
+
subtitle: errorMessages.join(', ') || t('locationSaveError', 'Error saving location'),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
promptBeforeClosing(() => isDirty);
|
|
123
|
+
}, [isDirty, promptBeforeClosing]);
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<Form onSubmit={handleSubmit(onSubmit)} className={styles.form}>
|
|
127
|
+
<div className={styles.formContainer}>
|
|
128
|
+
<Stack gap={3}>
|
|
129
|
+
<ResponsiveWrapper>
|
|
130
|
+
<FormGroup legendText="">
|
|
131
|
+
<Controller
|
|
132
|
+
control={control}
|
|
133
|
+
name="name"
|
|
134
|
+
render={({ field }) => (
|
|
135
|
+
<TextInput
|
|
136
|
+
id="locationName"
|
|
137
|
+
placeholder={t('locationPlaceholder', 'Add a location')}
|
|
138
|
+
labelText={t('locationName', 'Location Name')}
|
|
139
|
+
value={field.value}
|
|
140
|
+
onChange={field.onChange}
|
|
141
|
+
invalid={!!errors.name?.message}
|
|
142
|
+
invalidText={errors.name?.message}
|
|
143
|
+
disabled={hasLocationAttributes}
|
|
144
|
+
/>
|
|
145
|
+
)}
|
|
146
|
+
/>
|
|
147
|
+
</FormGroup>
|
|
148
|
+
</ResponsiveWrapper>
|
|
149
|
+
|
|
150
|
+
<ResponsiveWrapper>
|
|
151
|
+
<FormGroup legendText="">
|
|
152
|
+
<Controller
|
|
153
|
+
control={control}
|
|
154
|
+
name="tags"
|
|
155
|
+
render={({ field: { onChange, value, ref } }) => (
|
|
156
|
+
<FilterableMultiSelect
|
|
157
|
+
id="locationTags"
|
|
158
|
+
titleText={t('selectTags', 'Select tag(s)')}
|
|
159
|
+
placeholder={t('selectTagPlaceholder', 'Select a tag')}
|
|
160
|
+
items={Tags || []}
|
|
161
|
+
selectedItems={(value || []).map(
|
|
162
|
+
(selected) => Tags?.find((tag) => tag.uuid === selected.uuid) || selected,
|
|
163
|
+
)}
|
|
164
|
+
onChange={({ selectedItems }) => onChange(selectedItems || [])}
|
|
165
|
+
itemToString={(item) => (item && typeof item === 'object' ? item.display : '')}
|
|
166
|
+
selectionFeedback="top-after-reopen"
|
|
167
|
+
invalid={!!errors.tags?.message}
|
|
168
|
+
invalidText={errors.tags?.message}
|
|
169
|
+
disabled={!Tags?.length}
|
|
170
|
+
ref={ref}
|
|
171
|
+
/>
|
|
172
|
+
)}
|
|
173
|
+
/>
|
|
174
|
+
</FormGroup>
|
|
175
|
+
</ResponsiveWrapper>
|
|
176
|
+
</Stack>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<ButtonSet
|
|
180
|
+
className={classNames({
|
|
181
|
+
[styles.tablet]: isTablet,
|
|
182
|
+
[styles.desktop]: !isTablet,
|
|
183
|
+
})}>
|
|
184
|
+
<Button className={styles.buttonContainer} kind="secondary" onClick={() => closeWorkspace()}>
|
|
185
|
+
{t('cancel', 'Cancel')}
|
|
186
|
+
</Button>
|
|
187
|
+
<Button className={styles.buttonContainer} disabled={isSubmitting || !isDirty} kind="primary" type="submit">
|
|
188
|
+
{isSubmitting ? (
|
|
189
|
+
<span className={styles.inlineLoading}>
|
|
190
|
+
{t('submitting', 'Submitting' + '...')}
|
|
191
|
+
<InlineLoading status="active" iconDescription="Loading" />
|
|
192
|
+
</span>
|
|
193
|
+
) : (
|
|
194
|
+
t('saveAndClose', 'Save & close')
|
|
195
|
+
)}
|
|
196
|
+
</Button>
|
|
197
|
+
</ButtonSet>
|
|
198
|
+
</Form>
|
|
199
|
+
);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export default AddLocationWorkspace;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
@use '@carbon/colors';
|
|
2
|
+
@use '@carbon/layout';
|
|
3
|
+
@use '@carbon/type';
|
|
4
|
+
|
|
5
|
+
.form {
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
justify-content: space-between;
|
|
9
|
+
height: 100%;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.formContainer {
|
|
13
|
+
margin: layout.$spacing-05;
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
gap: layout.$spacing-05;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.tablet {
|
|
20
|
+
padding: layout.$spacing-06 layout.$spacing-05;
|
|
21
|
+
background-color: colors.$white;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.desktop {
|
|
25
|
+
padding: 0;
|
|
26
|
+
}
|
|
27
|
+
.buttonContainer {
|
|
28
|
+
max-width: 50%;
|
|
29
|
+
}
|
|
30
|
+
.inlineLoading {
|
|
31
|
+
display: flex;
|
|
32
|
+
align-items: center;
|
|
33
|
+
gap: layout.$spacing-03;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.tileContainer {
|
|
37
|
+
background-color: colors.$white-0;
|
|
38
|
+
border-top: 1px solid colors.$gray-20;
|
|
39
|
+
padding: layout.$spacing-09 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.tileNoContent {
|
|
43
|
+
margin: auto;
|
|
44
|
+
width: fit-content;
|
|
45
|
+
display: flex;
|
|
46
|
+
flex-direction: column;
|
|
47
|
+
align-items: center;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.tileContent {
|
|
51
|
+
display: flex;
|
|
52
|
+
flex-direction: row; // Changed from column to row
|
|
53
|
+
align-items: center; // Center items vertically
|
|
54
|
+
gap: layout.$spacing-05; // Add gap between pictogram and details
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.details {
|
|
58
|
+
display: flex;
|
|
59
|
+
flex-direction: column;
|
|
60
|
+
align-items: flex-start;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.content {
|
|
64
|
+
@include type.type-style('heading-compact-02');
|
|
65
|
+
color: colors.$gray-70;
|
|
66
|
+
margin-bottom: layout.$spacing-03;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.helper {
|
|
70
|
+
@include type.type-style('body-compact-01');
|
|
71
|
+
color: colors.$gray-70;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.illustrationPictogram {
|
|
75
|
+
width: layout.$spacing-07;
|
|
76
|
+
height: layout.$spacing-07;
|
|
77
|
+
fill: var(--brand-03);
|
|
78
|
+
flex-shrink: 0;
|
|
79
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { Button, ButtonSet, FilterableMultiSelect, Form, FormGroup, InlineLoading, Stack } from '@carbon/react';
|
|
2
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
3
|
+
import {
|
|
4
|
+
type DefaultWorkspaceProps,
|
|
5
|
+
ResponsiveWrapper,
|
|
6
|
+
restBaseUrl,
|
|
7
|
+
showSnackbar,
|
|
8
|
+
useLayoutType,
|
|
9
|
+
} from '@openmrs/esm-framework';
|
|
10
|
+
import classNames from 'classnames';
|
|
11
|
+
import React, { useEffect, useState } from 'react';
|
|
12
|
+
import { Controller, useForm } from 'react-hook-form';
|
|
13
|
+
import { useTranslation } from 'react-i18next';
|
|
14
|
+
import { mutate } from 'swr';
|
|
15
|
+
import { z } from 'zod';
|
|
16
|
+
import { LocationAutosuggest } from '../../auto-suggest/location-autosuggest.component';
|
|
17
|
+
import ResultsTile from '../../common/results-tile.component';
|
|
18
|
+
import { extractErrorMessagesFromResponse } from '../../helpers';
|
|
19
|
+
import { editLocation } from '../../hooks/useLocation';
|
|
20
|
+
import { useLocationTags } from '../../hooks/useLocationTags';
|
|
21
|
+
import { LocationResponse } from '../../types';
|
|
22
|
+
import styles from './search-location.workspace.scss';
|
|
23
|
+
|
|
24
|
+
type AddLocationWorkspaceProps = DefaultWorkspaceProps & {
|
|
25
|
+
location?: LocationResponse;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const locationFormSchema = z.object({
|
|
29
|
+
name: z.string().min(1, { message: 'Location name is required' }),
|
|
30
|
+
tags: z
|
|
31
|
+
.object({
|
|
32
|
+
uuid: z.string().uuid(),
|
|
33
|
+
display: z.string(),
|
|
34
|
+
})
|
|
35
|
+
.array(),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
type LocationFormType = z.infer<typeof locationFormSchema>;
|
|
39
|
+
|
|
40
|
+
const SearchLocationWorkspace: React.FC<AddLocationWorkspaceProps> = ({
|
|
41
|
+
closeWorkspace,
|
|
42
|
+
closeWorkspaceWithSavedChanges,
|
|
43
|
+
promptBeforeClosing,
|
|
44
|
+
location,
|
|
45
|
+
}) => {
|
|
46
|
+
const { t } = useTranslation();
|
|
47
|
+
const isTablet = useLayoutType() === 'tablet';
|
|
48
|
+
const { locationTagList: Tags } = useLocationTags();
|
|
49
|
+
const [selectedLocation, setSelectedLocation] = useState<LocationResponse | null>(null);
|
|
50
|
+
|
|
51
|
+
const handleMutation = () => {
|
|
52
|
+
const url = `${restBaseUrl}/location`;
|
|
53
|
+
mutate((key) => typeof key === 'string' && key.startsWith(url), undefined, { revalidate: true });
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const {
|
|
57
|
+
handleSubmit,
|
|
58
|
+
control,
|
|
59
|
+
getValues,
|
|
60
|
+
setValue,
|
|
61
|
+
reset,
|
|
62
|
+
formState: { isSubmitting, isDirty, errors },
|
|
63
|
+
} = useForm<LocationFormType>({
|
|
64
|
+
resolver: zodResolver(locationFormSchema),
|
|
65
|
+
defaultValues: {
|
|
66
|
+
name: location?.name || '',
|
|
67
|
+
tags: location?.tags || [],
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const onSubmit = async (data: LocationFormType) => {
|
|
72
|
+
try {
|
|
73
|
+
const locationUuid = selectedLocation?.uuid || location?.uuid;
|
|
74
|
+
|
|
75
|
+
const locationTagsUuid = data.tags.map((tag) => tag.uuid);
|
|
76
|
+
|
|
77
|
+
const locationPayload = {
|
|
78
|
+
tags: locationTagsUuid,
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
await editLocation(locationUuid, locationPayload);
|
|
82
|
+
|
|
83
|
+
showSnackbar({
|
|
84
|
+
title: t('success', 'Success'),
|
|
85
|
+
kind: 'success',
|
|
86
|
+
subtitle: t('locationUpdated', 'Location {{locationName}} was updated successfully.', {
|
|
87
|
+
locationName: selectedLocation?.name || location?.name || data.name,
|
|
88
|
+
}),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
handleMutation();
|
|
92
|
+
closeWorkspaceWithSavedChanges();
|
|
93
|
+
} catch (error: any) {
|
|
94
|
+
console.error('Error saving location:', error);
|
|
95
|
+
const errorMessages = extractErrorMessagesFromResponse(error);
|
|
96
|
+
showSnackbar({
|
|
97
|
+
title: t('error', 'Error'),
|
|
98
|
+
kind: 'error',
|
|
99
|
+
subtitle: errorMessages.join(', ') || t('locationSaveError', 'Error saving location'),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const handleLocationSelected = (locationUuid: string, locationData: LocationResponse) => {
|
|
105
|
+
setSelectedLocation(locationData);
|
|
106
|
+
|
|
107
|
+
setValue('name', locationData.name || '', { shouldDirty: true });
|
|
108
|
+
|
|
109
|
+
if (locationData?.tags && locationData.tags.length > 0) {
|
|
110
|
+
const formattedTags = locationData.tags.map((tag) => ({
|
|
111
|
+
uuid: tag.uuid,
|
|
112
|
+
display: tag.display || tag.name || '',
|
|
113
|
+
}));
|
|
114
|
+
if (formattedTags.length > 0) {
|
|
115
|
+
setValue('tags', formattedTags as [(typeof formattedTags)[0], ...typeof formattedTags], { shouldDirty: true });
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
setValue('tags', [], { shouldDirty: true });
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const handleClearSelection = () => {
|
|
123
|
+
setSelectedLocation(null);
|
|
124
|
+
reset();
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const renderSelectedLocationTile = () => {
|
|
128
|
+
if (!selectedLocation) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<div>
|
|
134
|
+
<ResultsTile location={selectedLocation} onClose={handleClearSelection} />
|
|
135
|
+
</div>
|
|
136
|
+
);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
promptBeforeClosing(() => isDirty);
|
|
141
|
+
}, [isDirty, promptBeforeClosing]);
|
|
142
|
+
|
|
143
|
+
const isFormReady = (selectedLocation || location) && isDirty;
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<Form onSubmit={handleSubmit(onSubmit)} className={styles.form}>
|
|
147
|
+
<div className={styles.formContainer}>
|
|
148
|
+
<Stack gap={3}>
|
|
149
|
+
<ResponsiveWrapper>
|
|
150
|
+
<FormGroup legendText="">
|
|
151
|
+
{!selectedLocation ? (
|
|
152
|
+
<LocationAutosuggest
|
|
153
|
+
onLocationSelected={handleLocationSelected}
|
|
154
|
+
labelText={t('searchForLocation', 'Search for location')}
|
|
155
|
+
placeholder={t('searchParentLocation', 'Search for location...')}
|
|
156
|
+
/>
|
|
157
|
+
) : (
|
|
158
|
+
renderSelectedLocationTile()
|
|
159
|
+
)}
|
|
160
|
+
</FormGroup>
|
|
161
|
+
</ResponsiveWrapper>
|
|
162
|
+
|
|
163
|
+
<ResponsiveWrapper>
|
|
164
|
+
<FormGroup legendText="">
|
|
165
|
+
<Controller
|
|
166
|
+
control={control}
|
|
167
|
+
name="tags"
|
|
168
|
+
render={({ field: { onChange, value, ref } }) => (
|
|
169
|
+
<FilterableMultiSelect
|
|
170
|
+
id="locationTags"
|
|
171
|
+
titleText={t('selectTags', 'Select tag(s)')}
|
|
172
|
+
placeholder={t('selectTagPlaceholder', 'Select a tag')}
|
|
173
|
+
items={Tags || []}
|
|
174
|
+
selectedItems={(value || []).map(
|
|
175
|
+
(selected) => Tags?.find((tag) => tag.uuid === selected.uuid) || selected,
|
|
176
|
+
)}
|
|
177
|
+
onChange={({ selectedItems }) => onChange(selectedItems || [])}
|
|
178
|
+
itemToString={(item) => (item && typeof item === 'object' ? item.display : '')}
|
|
179
|
+
selectionFeedback="top-after-reopen"
|
|
180
|
+
invalid={!!errors.tags?.message}
|
|
181
|
+
invalidText={errors.tags?.message}
|
|
182
|
+
disabled={!Tags?.length}
|
|
183
|
+
ref={ref}
|
|
184
|
+
/>
|
|
185
|
+
)}
|
|
186
|
+
/>
|
|
187
|
+
</FormGroup>
|
|
188
|
+
</ResponsiveWrapper>
|
|
189
|
+
</Stack>
|
|
190
|
+
</div>
|
|
191
|
+
|
|
192
|
+
<ButtonSet
|
|
193
|
+
className={classNames({
|
|
194
|
+
[styles.tablet]: isTablet,
|
|
195
|
+
[styles.desktop]: !isTablet,
|
|
196
|
+
})}>
|
|
197
|
+
<Button className={styles.buttonContainer} kind="secondary" onClick={() => closeWorkspace()}>
|
|
198
|
+
{t('cancel', 'Cancel')}
|
|
199
|
+
</Button>
|
|
200
|
+
<Button className={styles.buttonContainer} disabled={isSubmitting || !isFormReady} kind="primary" type="submit">
|
|
201
|
+
{isSubmitting ? (
|
|
202
|
+
<span className={styles.inlineLoading}>
|
|
203
|
+
{t('submitting', 'Submitting' + '...')}
|
|
204
|
+
<InlineLoading status="active" iconDescription="Loading" />
|
|
205
|
+
</span>
|
|
206
|
+
) : (
|
|
207
|
+
t('saveAndClose', 'Save & close')
|
|
208
|
+
)}
|
|
209
|
+
</Button>
|
|
210
|
+
</ButtonSet>
|
|
211
|
+
</Form>
|
|
212
|
+
);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
export default SearchLocationWorkspace;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Calendar, Location } from '@carbon/react/icons';
|
|
4
|
+
import {
|
|
5
|
+
ConfigurableLink,
|
|
6
|
+
formatDate,
|
|
7
|
+
InPatientPictogram,
|
|
8
|
+
PageHeader,
|
|
9
|
+
PageHeaderContent,
|
|
10
|
+
useSession,
|
|
11
|
+
} from '@openmrs/esm-framework';
|
|
12
|
+
import styles from './header.scss';
|
|
13
|
+
import { EuropeAfrica } from '@carbon/pictograms-react';
|
|
14
|
+
|
|
15
|
+
type HeaderProps = {
|
|
16
|
+
title: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const Header: React.FC<HeaderProps> = ({ title }) => {
|
|
20
|
+
const { t } = useTranslation();
|
|
21
|
+
const userSession = useSession();
|
|
22
|
+
const userLocation = userSession?.sessionLocation?.display;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<PageHeader className={styles.header}>
|
|
26
|
+
<PageHeaderContent
|
|
27
|
+
illustration={
|
|
28
|
+
<ConfigurableLink to={`${window.getOpenmrsSpaBase()}bed-management`}>
|
|
29
|
+
<EuropeAfrica className={styles.illustrationPictogram} />
|
|
30
|
+
</ConfigurableLink>
|
|
31
|
+
}
|
|
32
|
+
title={title}
|
|
33
|
+
className={styles.leftJustifiedItems}
|
|
34
|
+
/>
|
|
35
|
+
<div className={styles.rightJustifiedItems}>
|
|
36
|
+
<div className={styles.dateAndLocation}>
|
|
37
|
+
<Location size={16} />
|
|
38
|
+
<span className={styles.value}>{userLocation}</span>
|
|
39
|
+
<span className={styles.middot}>·</span>
|
|
40
|
+
<Calendar size={16} />
|
|
41
|
+
<span className={styles.value}>{formatDate(new Date(), { mode: 'standard' })}</span>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</PageHeader>
|
|
45
|
+
);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export default Header;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
@use '@carbon/colors';
|
|
2
|
+
@use '@carbon/layout';
|
|
3
|
+
@use '@carbon/type';
|
|
4
|
+
|
|
5
|
+
.header {
|
|
6
|
+
@include type.type-style('body-compact-02');
|
|
7
|
+
color: colors.$gray-70;
|
|
8
|
+
height: layout.$spacing-12;
|
|
9
|
+
background-color: colors.$white-0;
|
|
10
|
+
display: flex;
|
|
11
|
+
padding-left: layout.$spacing-03;
|
|
12
|
+
justify-content: space-between;
|
|
13
|
+
border-bottom: 1px solid colors.$gray-20;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.leftJustifiedItems {
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: row;
|
|
19
|
+
align-items: center;
|
|
20
|
+
cursor: pointer;
|
|
21
|
+
|
|
22
|
+
& > div:nth-child(2) {
|
|
23
|
+
margin: layout.$spacing-05;
|
|
24
|
+
|
|
25
|
+
& > p:last-child {
|
|
26
|
+
white-space: nowrap;
|
|
27
|
+
@include type.type-style('heading-04');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.illustrationPictogram {
|
|
33
|
+
width: 4.5rem;
|
|
34
|
+
height: 4.5rem;
|
|
35
|
+
fill: var(--brand-03);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.rightJustifiedItems {
|
|
39
|
+
@include type.type-style('body-compact-02');
|
|
40
|
+
color: colors.$gray-70;
|
|
41
|
+
margin: layout.$spacing-03;
|
|
42
|
+
padding-top: layout.$spacing-04;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.dateAndLocation {
|
|
46
|
+
display: flex;
|
|
47
|
+
justify-content: flex-end;
|
|
48
|
+
align-items: center;
|
|
49
|
+
margin-right: layout.$spacing-05;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.value {
|
|
53
|
+
margin-left: layout.$spacing-02;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.middot {
|
|
57
|
+
margin: 0 layout.$spacing-03;
|
|
58
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts a list of error messages from an error object returned from an API call.
|
|
3
|
+
* Checks if the error object contains a fieldErrors property, and if so, extracts all the error messages from the field errors.
|
|
4
|
+
* If the error object does not contain a fieldErrors property, it returns the error message from the error object.
|
|
5
|
+
* @param errorObject an object containing an error response from an API call.
|
|
6
|
+
* @returns a list of error messages.
|
|
7
|
+
*/
|
|
8
|
+
export function extractErrorMessagesFromResponse(errorObject) {
|
|
9
|
+
const fieldErrors = errorObject?.responseBody?.error?.fieldErrors;
|
|
10
|
+
|
|
11
|
+
if (!fieldErrors) {
|
|
12
|
+
return [errorObject?.responseBody?.error?.message ?? errorObject?.message];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return Object.values(fieldErrors).flatMap((errors: Array<Error>) => errors.map((error) => error.message));
|
|
16
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import Header from '../header/header.component';
|
|
4
|
+
import LocationsTable from '../tables/locations-table.component';
|
|
5
|
+
import styles from './home-locations.scss';
|
|
6
|
+
const HomeComponent: React.FC = () => {
|
|
7
|
+
const { t } = useTranslation();
|
|
8
|
+
return (
|
|
9
|
+
<div>
|
|
10
|
+
<Header title={t('locations', 'Locations')} />
|
|
11
|
+
<div className={styles.tableContainer}>
|
|
12
|
+
<LocationsTable />
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default HomeComponent;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
2
|
+
import { type LocationResponse } from '../types';
|
|
3
|
+
|
|
4
|
+
export const searchLocation = async (searchTerm: string) => {
|
|
5
|
+
const customPresentation =
|
|
6
|
+
'custom:(uuid,display,name,description,stateProvince,country,countyDistrict,address5,address6,tags,attributes:(uuid,attributeType:(uuid,display),value))';
|
|
7
|
+
|
|
8
|
+
const url = `${restBaseUrl}/location?v=${customPresentation}&q=${searchTerm}`;
|
|
9
|
+
|
|
10
|
+
const response = await openmrsFetch<{ results: Array<LocationResponse> }>(url);
|
|
11
|
+
return response?.data?.results ?? [];
|
|
12
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"results": [
|
|
3
|
+
{
|
|
4
|
+
"uuid": "45a10dd9-edaa-4e50-a2d7-343fef0415eb",
|
|
5
|
+
"display": "10 Engineer VCT",
|
|
6
|
+
"name": "10 Engineer VCT",
|
|
7
|
+
"description": "Dispensary",
|
|
8
|
+
"stateProvince": "Laikipia",
|
|
9
|
+
"country": "Kenya",
|
|
10
|
+
"countyDistrict": "Laikipia",
|
|
11
|
+
"address5": "Nanyuki",
|
|
12
|
+
"address6": "Laikipia East",
|
|
13
|
+
"tags": [],
|
|
14
|
+
|
|
15
|
+
"attributes": [
|
|
16
|
+
{
|
|
17
|
+
"display": "Master Facility Code: 14180",
|
|
18
|
+
"uuid": "21e6dc68-46e7-4f2e-916c-4281c62dad6b",
|
|
19
|
+
"attributeType": {
|
|
20
|
+
"uuid": "8a845a89-6aa5-4111-81d3-0af31c45c002",
|
|
21
|
+
"display": "Master Facility Code",
|
|
22
|
+
"links": [
|
|
23
|
+
{
|
|
24
|
+
"rel": "self",
|
|
25
|
+
"uri": "https://qa.kenyahmis.org/openmrs/ws/rest/v1/locationattributetype/8a845a89-6aa5-4111-81d3-0af31c45c002",
|
|
26
|
+
"resourceAlias": "locationattributetype"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
"value": "14180"
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|