@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.
Files changed (43) hide show
  1. package/.turbo/turbo-build.log +15 -15
  2. package/dist/342.js +1 -0
  3. package/dist/342.js.map +1 -0
  4. package/dist/965.js +1 -0
  5. package/dist/965.js.map +1 -0
  6. package/dist/kenyaemr-esm-admin-app.js +1 -1
  7. package/dist/kenyaemr-esm-admin-app.js.buildmanifest.json +54 -54
  8. package/dist/kenyaemr-esm-admin-app.js.map +1 -1
  9. package/dist/main.js +1 -1
  10. package/dist/main.js.map +1 -1
  11. package/dist/routes.json +1 -1
  12. package/package.json +1 -1
  13. package/src/components/locations/auto-suggest/autosuggest.component.tsx +149 -0
  14. package/src/components/locations/auto-suggest/autosuggest.scss +61 -0
  15. package/src/components/locations/auto-suggest/location-autosuggest.component.tsx +94 -0
  16. package/src/components/locations/auto-suggest/location-autosuggest.scss +48 -0
  17. package/src/components/locations/common/results-tile.component.tsx +45 -0
  18. package/src/components/locations/common/results-tile.scss +64 -0
  19. package/src/components/locations/forms/add-location/add-location.workspace.scss +34 -0
  20. package/src/components/locations/forms/add-location/add-location.workspace.tsx +202 -0
  21. package/src/components/locations/forms/search-location/search-location.workspace.scss +79 -0
  22. package/src/components/locations/forms/search-location/search-location.workspace.tsx +215 -0
  23. package/src/components/locations/header/header.component.tsx +48 -0
  24. package/src/components/locations/header/header.scss +58 -0
  25. package/src/components/locations/helpers/index.ts +16 -0
  26. package/src/components/locations/home/home-locations.component.tsx +18 -0
  27. package/src/components/locations/home/home-locations.scss +8 -0
  28. package/src/components/locations/hooks/UseFacilityLocations.ts +12 -0
  29. package/src/components/locations/hooks/index.json +35 -0
  30. package/src/components/locations/hooks/useLocation.ts +24 -0
  31. package/src/components/locations/hooks/useLocationTags.ts +15 -0
  32. package/src/components/locations/tables/locations-table.component.tsx +243 -0
  33. package/src/components/locations/tables/locations-table.resource.ts +41 -0
  34. package/src/components/locations/tables/locations-table.scss +115 -0
  35. package/src/components/locations/types/index.ts +120 -0
  36. package/src/components/locations/utils/index.ts +5 -0
  37. package/src/index.ts +9 -0
  38. package/src/root.component.tsx +2 -0
  39. package/src/routes.json +27 -3
  40. package/dist/479.js +0 -1
  41. package/dist/479.js.map +0 -1
  42. package/dist/512.js +0 -1
  43. 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}>&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,8 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .tableContainer {
6
+ margin-top: layout.$spacing-05;
7
+ margin-right: layout.$spacing-04;
8
+ }
@@ -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
+ }