@kenyaemr/esm-admin-app 5.4.2-pre.2271 → 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
package/dist/routes.json CHANGED
@@ -1 +1 @@
1
- {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"kenyaemrCharts":"^1.6.7"},"extensions":[{"component":"adminLeftPanelLink","name":"admin-left-panel-link","slot":"admin-left-panel-slot"},{"component":"userManagementLeftPannelLink","name":"user-management-left-panel-link","slot":"admin-left-panel-slot"},{"component":"etlAdministrationLeftPannelLink","name":"etl-administration-left-panel-link","slot":"admin-left-panel-slot"},{"component":"facilitySetupLeftPanelLink","name":"facility-setup-left-panel-link","slot":"admin-left-panel-slot"}],"workspaces":[{"name":"manage-user-workspace","component":"manageUserWorkspace","title":"Manage User Workspace","type":"other-form","canMaximize":true,"width":"extra-wide"},{"name":"user-role-scope-workspace","component":"userRoleScopeWorkspace","title":"User Rple Scope Workspace","type":"other-form","canMaximize":true,"width":"extra-wide"}],"modals":[{"component":"operationConfirmationModal","name":"operation-confirmation-modal"},{"component":"hwrConfirmationModal","name":"hwr-confirmation-modal"},{"component":"hwrEmptyModal","name":"hwr-empty-modal"},{"component":"hwrSyncModal","name":"hwr-syncing-modal"}],"pages":[{"component":"root","route":"admin"}],"version":"5.4.2-pre.2271"}
1
+ {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"kenyaemrCharts":"^1.6.7"},"extensions":[{"component":"adminLeftPanelLink","name":"admin-left-panel-link","slot":"admin-left-panel-slot"},{"component":"userManagementLeftPannelLink","name":"user-management-left-panel-link","slot":"admin-left-panel-slot"},{"component":"locationsLeftPanelLink","name":"locations-left-panel-link","slot":"admin-left-panel-slot"},{"component":"facilitySetupLeftPanelLink","name":"facility-setup-left-panel-link","slot":"admin-left-panel-slot"}],"workspaces":[{"name":"manage-user-workspace","component":"manageUserWorkspace","title":"Manage User Workspace","type":"other-form","canMaximize":true,"width":"extra-wide"},{"name":"user-role-scope-workspace","component":"userRoleScopeWorkspace","title":"User Rple Scope Workspace","type":"other-form","canMaximize":true,"width":"extra-wide"},{"name":"add-location-workspace","title":"Add Location","component":"addLocation","type":"workspace"},{"name":"search-location-workspace","title":"Search Location","component":"searchLocationWorkspace","type":"workspace"},{"name":"hwr-sync-workspace","title":"HWR Sync Workspace","component":"hwrSyncWorkspace","type":"other-form"},{"name":"hwr-sync-modal","title":"HWR Sync Modal","component":"hwrSyncModal","type":"modal"}],"modals":[{"component":"operationConfirmationModal","name":"operation-confirmation-modal"},{"component":"hwrConfirmationModal","name":"hwr-confirmation-modal"},{"component":"hwrEmptyModal","name":"hwr-empty-modal"},{"component":"hwrSyncModal","name":"hwr-syncing-modal"}],"pages":[{"component":"root","route":"admin"}],"version":"5.4.2-pre.2272"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kenyaemr/esm-admin-app",
3
- "version": "5.4.2-pre.2271",
3
+ "version": "5.4.2-pre.2272",
4
4
  "description": "Facilitates the management of ETL tables",
5
5
  "browser": "dist/kenyaemr-esm-admin-app.js",
6
6
  "main": "src/index.ts",
@@ -0,0 +1,149 @@
1
+ import { InlineLoading, Layer, Search } from '@carbon/react';
2
+ import classNames from 'classnames';
3
+ import React, { type HTMLAttributes, useEffect, useRef, useState } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
+ import styles from './autosuggest.scss';
6
+
7
+ type InputPropsBase = Omit<HTMLAttributes<HTMLInputElement>, 'onChange'>;
8
+
9
+ interface SearchProps extends InputPropsBase {
10
+ autoComplete?: string;
11
+ className?: string;
12
+ closeButtonLabelText?: string;
13
+ defaultValue?: string | number;
14
+ disabled?: boolean;
15
+ isExpanded?: boolean;
16
+ id?: string;
17
+ labelText: React.ReactNode;
18
+ onChange?(e: { target: HTMLInputElement; type: 'change' }): void;
19
+ onClear?(): void;
20
+ onExpand?(e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>): void;
21
+ placeholder?: string;
22
+ renderIcon?: React.ComponentType | React.FunctionComponent;
23
+ role?: string;
24
+ size?: 'sm' | 'md' | 'lg';
25
+ type?: string;
26
+ value?: string | number;
27
+ }
28
+
29
+ interface AutosuggestProps extends SearchProps {
30
+ getDisplayValue: (item: any) => string;
31
+ getFieldValue: (item: any) => string;
32
+ getSearchResults: (query: string) => Promise<any>;
33
+ onSuggestionSelected: (field: string, value: string) => void;
34
+ invalid?: boolean;
35
+ invalidText?: string;
36
+ renderSuggestionItem?: (item: any) => React.ReactNode;
37
+ renderEmptyState?: (value: any) => React.ReactNode;
38
+ }
39
+
40
+ export const Autosuggest: React.FC<AutosuggestProps> = ({
41
+ getDisplayValue,
42
+ getFieldValue,
43
+ getSearchResults,
44
+ onSuggestionSelected,
45
+ invalid,
46
+ invalidText,
47
+ renderSuggestionItem,
48
+ renderEmptyState,
49
+ ...searchProps
50
+ }) => {
51
+ const { t } = useTranslation();
52
+ const [suggestions, setSuggestions] = useState<any[]>([]);
53
+ const [isLoading, setIsLoading] = useState(false);
54
+ const searchBox = useRef<HTMLInputElement>(null);
55
+ const wrapper = useRef<HTMLDivElement>(null);
56
+ const { id: name, labelText } = searchProps;
57
+
58
+ useEffect(() => {
59
+ const handleClickOutsideComponent = (e: MouseEvent) => {
60
+ if (wrapper.current && !wrapper.current.contains(e.target as Node)) {
61
+ setSuggestions([]);
62
+ }
63
+ };
64
+
65
+ document.addEventListener('mousedown', handleClickOutsideComponent);
66
+ return () => {
67
+ document.removeEventListener('mousedown', handleClickOutsideComponent);
68
+ };
69
+ }, [wrapper]);
70
+
71
+ const handleChange = (e) => {
72
+ const query = e.target.value;
73
+ onSuggestionSelected(name, undefined);
74
+
75
+ if (query && query.trim()) {
76
+ setIsLoading(true);
77
+ getSearchResults(query.trim())
78
+ .then((results) => {
79
+ setSuggestions(results || []);
80
+ setIsLoading(false);
81
+ })
82
+ .catch((error) => {
83
+ setSuggestions([]);
84
+ setIsLoading(false);
85
+ });
86
+ } else {
87
+ setSuggestions([]);
88
+ setIsLoading(false);
89
+ }
90
+ };
91
+
92
+ const handleClear = () => {
93
+ onSuggestionSelected(name, undefined);
94
+ setSuggestions([]);
95
+ setIsLoading(false);
96
+ };
97
+
98
+ const handleClick = (index: number) => {
99
+ const selectedSuggestion = suggestions[index];
100
+ if (selectedSuggestion) {
101
+ const fieldValue = getFieldValue(selectedSuggestion);
102
+ const displayValue = getDisplayValue(selectedSuggestion);
103
+ if (searchBox.current) {
104
+ searchBox.current.value = displayValue;
105
+ }
106
+ onSuggestionSelected(name, fieldValue);
107
+ setSuggestions([]);
108
+ }
109
+ };
110
+
111
+ return (
112
+ <div className={styles.autocomplete} ref={wrapper}>
113
+ <label className="cds--label">{labelText}</label>
114
+ <Layer className={classNames({ [styles.invalid]: invalid })}>
115
+ <Search
116
+ id="autosuggest"
117
+ onChange={handleChange}
118
+ onClear={handleClear}
119
+ ref={searchBox}
120
+ className={styles.autocompleteSearch}
121
+ {...searchProps}
122
+ />
123
+ </Layer>
124
+ {isLoading && (
125
+ <div className={styles.loading}>
126
+ <InlineLoading status="active" description={t('loading', 'Loading' + '...')} />
127
+ </div>
128
+ )}
129
+ {suggestions.length > 0 ? (
130
+ <ul className={styles.suggestions}>
131
+ {suggestions.map((suggestion, index) => {
132
+ return (
133
+ <li
134
+ key={getFieldValue(suggestion) || index}
135
+ onClick={() => handleClick(index)}
136
+ role="presentation"
137
+ style={{ cursor: 'pointer' }}>
138
+ {renderSuggestionItem ? renderSuggestionItem(suggestion) : getDisplayValue(suggestion)}
139
+ </li>
140
+ );
141
+ })}
142
+ </ul>
143
+ ) : (
144
+ !isLoading && renderEmptyState && renderEmptyState(searchBox.current?.value || '')
145
+ )}
146
+ {invalid && <label className={classNames(styles.invalidMsg)}>{invalidText}</label>}
147
+ </div>
148
+ );
149
+ };
@@ -0,0 +1,61 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@carbon/colors';
4
+
5
+ .label01 {
6
+ @include type.type-style('label-01');
7
+ }
8
+
9
+ .suggestions {
10
+ position: relative;
11
+ border-top-width: 0;
12
+ list-style: none;
13
+ margin-top: 0;
14
+ max-height: 400px;
15
+ overflow-y: auto;
16
+ padding-left: 0;
17
+ width: 100%;
18
+ position: absolute;
19
+ left: 0;
20
+ background-color: colors.$white;
21
+ margin-bottom: 20px;
22
+ z-index: 99;
23
+ box-shadow: layout.$spacing-03 layout.$spacing-03 layout.$spacing-03 layout.$spacing-03 colors.$gray-30;
24
+ }
25
+
26
+ .suggestions li {
27
+ color: colors.$gray-70;
28
+ border-bottom: 1px solid colors.$gray-50;
29
+ }
30
+
31
+ .displayText {
32
+ padding: layout.$spacing-05;
33
+ }
34
+
35
+ .suggestions li:not(:last-of-type) {
36
+ border-bottom: 1px solid colors.$gray-40-hover;
37
+ }
38
+
39
+ .autocomplete {
40
+ position: relative;
41
+ }
42
+
43
+ .autocompleteSearch {
44
+ width: 100%;
45
+ }
46
+
47
+ .suggestions a {
48
+ color: inherit;
49
+ text-decoration: none;
50
+ }
51
+
52
+ .invalid input {
53
+ outline: 2px solid var(--cds-support-error, colors.$red-60);
54
+ outline-offset: -2px;
55
+ margin-bottom: layout.$spacing-02;
56
+ }
57
+
58
+ .invalidMsg {
59
+ color: var(--cds-text-error, colors.$red-60);
60
+ font-size: small;
61
+ }
@@ -0,0 +1,94 @@
1
+ import { Tile } from '@carbon/react';
2
+ import React, { useCallback, useState } from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import ResultsTile from '../common/results-tile.component';
5
+ import { searchLocation } from '../hooks/UseFacilityLocations';
6
+ import { LocationResponse } from '../types';
7
+ import { Autosuggest } from './autosuggest.component';
8
+ import styles from './location-autosuggest.scss';
9
+
10
+ interface LocationAutosuggestProps {
11
+ onLocationSelected: (locationUuid: string, locationData: LocationResponse) => void;
12
+ labelText?: string;
13
+ placeholder?: string;
14
+ invalid?: boolean;
15
+ invalidText?: string;
16
+ }
17
+
18
+ export const LocationAutosuggest: React.FC<LocationAutosuggestProps> = ({
19
+ onLocationSelected,
20
+ labelText = 'Select Location',
21
+ placeholder = 'Search for a location...',
22
+ invalid = false,
23
+ invalidText = 'Please select a valid location',
24
+ }) => {
25
+ const [searchResults, setSearchResults] = useState<LocationResponse[]>([]);
26
+ const { t } = useTranslation();
27
+
28
+ const getDisplayValue = useCallback((item: LocationResponse) => {
29
+ return item.display || item.name || '';
30
+ }, []);
31
+
32
+ const getFieldValue = useCallback((item: LocationResponse) => {
33
+ return item.uuid || '';
34
+ }, []);
35
+
36
+ const handleSuggestionSelected = useCallback(
37
+ (field: string, value: string) => {
38
+ if (value) {
39
+ const selected = searchResults.find((item) => item.uuid === value);
40
+ if (selected) {
41
+ onLocationSelected(value, selected);
42
+ }
43
+ }
44
+ },
45
+ [onLocationSelected, searchResults],
46
+ );
47
+
48
+ const handleSearchResults = useCallback(async (query: string) => {
49
+ const results = await searchLocation(query);
50
+ setSearchResults(results);
51
+ return results;
52
+ }, []);
53
+
54
+ const renderSuggestionItem = useCallback((item: LocationResponse) => {
55
+ return (
56
+ <div>
57
+ <ResultsTile location={item} />
58
+ </div>
59
+ );
60
+ }, []);
61
+
62
+ const renderEmptyState = useCallback((value: string) => {
63
+ if (!value) {
64
+ return null;
65
+ }
66
+
67
+ return (
68
+ <div className={styles.tileContainer}>
69
+ <Tile className={styles.tileNoContent}>
70
+ <div className={styles.tileContents}>
71
+ <p className={styles.content}>{t('searchNoResults', `Found no matching results`)}</p>
72
+ <p className={styles.helper}>{t('searchNoResultsHelper', 'Try searching for a different term')}</p>
73
+ </div>
74
+ </Tile>
75
+ </div>
76
+ );
77
+ }, []);
78
+
79
+ return (
80
+ <Autosuggest
81
+ id="location-autosuggest"
82
+ labelText={labelText}
83
+ placeholder={placeholder}
84
+ getDisplayValue={getDisplayValue}
85
+ getFieldValue={getFieldValue}
86
+ getSearchResults={handleSearchResults}
87
+ onSuggestionSelected={handleSuggestionSelected}
88
+ renderSuggestionItem={renderSuggestionItem}
89
+ renderEmptyState={renderEmptyState}
90
+ invalid={invalid}
91
+ invalidText={invalidText}
92
+ />
93
+ );
94
+ };
@@ -0,0 +1,48 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .tileContainer {
6
+ background-color: colors.$white-0;
7
+ border-top: 1px solid colors.$gray-20;
8
+ padding: layout.$spacing-09 0;
9
+ }
10
+
11
+ .tileNoContent {
12
+ margin: auto;
13
+ width: fit-content;
14
+ display: flex;
15
+ flex-direction: column;
16
+ align-items: center;
17
+ }
18
+
19
+ .tileContent {
20
+ display: flex;
21
+ flex-direction: row;
22
+ align-items: center;
23
+ gap: layout.$spacing-05;
24
+ }
25
+
26
+ .details {
27
+ display: flex;
28
+ flex-direction: column;
29
+ align-items: flex-start;
30
+ }
31
+
32
+ .content {
33
+ @include type.type-style('heading-compact-02');
34
+ color: colors.$gray-70;
35
+ margin-bottom: layout.$spacing-03;
36
+ }
37
+
38
+ .helper {
39
+ @include type.type-style('body-compact-01');
40
+ color: colors.$gray-70;
41
+ }
42
+
43
+ .illustrationPictogram {
44
+ width: layout.$spacing-07;
45
+ height: layout.$spacing-07;
46
+ fill: var(--brand-03);
47
+ flex-shrink: 0;
48
+ }
@@ -0,0 +1,45 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { type LocationResponse } from '../types';
4
+ import { Tag, Tile } from '@carbon/react';
5
+ import { Hospital } from '@carbon/pictograms-react';
6
+ import styles from './results-tile.scss';
7
+ import { Close } from '@carbon/react/icons';
8
+
9
+ interface ResultsTileProps {
10
+ location?: LocationResponse;
11
+ onClose?: () => void;
12
+ }
13
+
14
+ const ResultsTile: React.FC<ResultsTileProps> = ({ location, onClose }) => {
15
+ const { t } = useTranslation();
16
+
17
+ return (
18
+ <Tile className={styles.tile}>
19
+ <div className={styles.tileContent}>
20
+ {onClose && <Close size={16} className={styles.closeIcon} onClick={onClose} />}
21
+ <Hospital className={styles.illustrationPictogram} />
22
+ <div className={styles.details}>
23
+ <div style={{ fontWeight: 'bold' }}>{location.display || location.name}</div>
24
+ {location.description && <div style={{ fontSize: '0.875rem', color: '#666' }}>{location.description}</div>}
25
+ {(location.stateProvince || location.country) && (
26
+ <div style={{ fontSize: '0.75rem', color: '#888' }}>
27
+ {[location.stateProvince, location.country].filter(Boolean).join(', ')}
28
+ </div>
29
+ )}
30
+ {location.tags && (
31
+ <div style={{ fontSize: '0.75rem', color: '#888' }}>
32
+ {location.tags.map((tag, index) => (
33
+ <Tag key={index} className={styles.tag}>
34
+ {tag.display || tag.name}
35
+ </Tag>
36
+ ))}
37
+ </div>
38
+ )}
39
+ </div>
40
+ </div>
41
+ </Tile>
42
+ );
43
+ };
44
+
45
+ export default ResultsTile;
@@ -0,0 +1,64 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .tile {
6
+ position: relative;
7
+ }
8
+
9
+ .tileContainer {
10
+ background-color: colors.$white-0;
11
+ border-top: 1px solid colors.$gray-20;
12
+ padding: layout.$spacing-10 0;
13
+ }
14
+
15
+ .tileNoContent {
16
+ margin: auto;
17
+ width: fit-content;
18
+ display: flex;
19
+ flex-direction: column;
20
+ align-items: center;
21
+ }
22
+
23
+ .tileContent {
24
+ display: flex;
25
+ flex-direction: row;
26
+ align-items: center;
27
+ gap: layout.$spacing-05;
28
+ }
29
+
30
+ .details {
31
+ display: flex;
32
+ flex-direction: column;
33
+ align-items: flex-start;
34
+ }
35
+
36
+ .content {
37
+ @include type.type-style('heading-compact-02');
38
+ color: colors.$gray-70;
39
+ margin-bottom: layout.$spacing-03;
40
+ }
41
+
42
+ .helper {
43
+ @include type.type-style('body-compact-01');
44
+ color: colors.$gray-70;
45
+ }
46
+
47
+ .illustrationPictogram {
48
+ width: layout.$spacing-07;
49
+ height: layout.$spacing-07;
50
+ fill: var(--brand-03);
51
+ flex-shrink: 0;
52
+ }
53
+
54
+ .closeIcon {
55
+ position: absolute;
56
+ top: layout.$spacing-03;
57
+ right: layout.$spacing-03;
58
+ cursor: pointer;
59
+ color: colors.$gray-70;
60
+
61
+ &:hover {
62
+ color: colors.$gray-100;
63
+ }
64
+ }
@@ -0,0 +1,34 @@
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
+ }