@openmrs/esm-patient-flags-app 11.3.1-pre.9537 → 11.3.1-pre.9541
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 +10 -10
- package/dist/4300.js +1 -1
- package/dist/4480.js +1 -0
- package/dist/4480.js.map +1 -0
- package/dist/597.js +1 -0
- package/dist/597.js.map +1 -0
- package/dist/7231.js +1 -0
- package/dist/7231.js.map +1 -0
- package/dist/8519.js +1 -1
- package/dist/8519.js.map +1 -1
- package/dist/8572.js +1 -0
- package/dist/8572.js.map +1 -0
- package/dist/9388.js +1 -0
- package/dist/9388.js.map +1 -0
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/openmrs-esm-patient-flags-app.js +1 -1
- package/dist/openmrs-esm-patient-flags-app.js.buildmanifest.json +129 -105
- package/dist/routes.json +1 -1
- package/package.json +2 -2
- package/src/config-schema.ts +13 -2
- package/src/flags/flags-list-extension/flags-list.extension.tsx +12 -0
- package/src/flags/flags-list.component.tsx +68 -0
- package/src/flags/flags-list.scss +43 -147
- package/src/flags/flags-list.test.tsx +50 -68
- package/src/flags/flags-risk-count-extension/extension-config-schema.ts +17 -0
- package/src/flags/flags-risk-count-extension/flags-risk-count.extension.tsx +84 -0
- package/src/flags/{flags-highlight-bar.scss → flags-risk-count-extension/flags-risk-count.scss} +14 -5
- package/src/flags/flags-risk-count-extension/flags-risk-count.test.tsx +99 -0
- package/src/flags/flags-workspace/flags-workspace.scss +167 -0
- package/src/flags/flags-workspace/flags-workspace.test.tsx +97 -0
- package/src/flags/flags-workspace/flags.workspace.tsx +255 -0
- package/src/flags/hooks/usePatientFlags.ts +19 -11
- package/src/index.ts +12 -7
- package/src/routes.json +18 -9
- package/translations/en.json +7 -15
- package/dist/1837.js +0 -1
- package/dist/1837.js.map +0 -1
- package/dist/5628.js +0 -1
- package/dist/5628.js.map +0 -1
- package/dist/6138.js +0 -1
- package/dist/6138.js.map +0 -1
- package/dist/6173.js +0 -1
- package/dist/6173.js.map +0 -1
- package/src/flags/flags-highlight-bar.component.tsx +0 -81
- package/src/flags/flags-highlight-bar.test.tsx +0 -85
- package/src/flags/flags.component.tsx +0 -82
- package/src/flags/flags.scss +0 -71
- package/src/flags/flags.test.tsx +0 -49
- package/src/flags/patient-flags.workspace.tsx +0 -234
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
@use '@carbon/colors';
|
|
2
|
+
@use '@carbon/layout';
|
|
3
|
+
@use '@carbon/type';
|
|
4
|
+
|
|
5
|
+
.listWrapper {
|
|
6
|
+
margin: layout.$spacing-05;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.flagTile {
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
justify-content: space-between;
|
|
13
|
+
border: 1px solid #ededed;
|
|
14
|
+
background-color: colors.$gray-10;
|
|
15
|
+
padding: layout.$spacing-03;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
:global(.omrs-breakpoint-lt-desktop) {
|
|
19
|
+
.flagTile {
|
|
20
|
+
margin-bottom: layout.$spacing-03;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
:global(.omrs-breakpoint-gt-tablet) {
|
|
25
|
+
.flagTile {
|
|
26
|
+
margin-bottom: layout.$spacing-02;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.flagHeader {
|
|
31
|
+
display: flex;
|
|
32
|
+
justify-content: space-between;
|
|
33
|
+
align-items: center;
|
|
34
|
+
margin-bottom: layout.$spacing-03;
|
|
35
|
+
font-size: 0.875rem;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.titleAndType {
|
|
39
|
+
display: flex;
|
|
40
|
+
align-items: baseline;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.flagTitle {
|
|
44
|
+
@include type.type-style('body-01');
|
|
45
|
+
color: colors.$gray-100;
|
|
46
|
+
margin-right: layout.$spacing-02;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.type {
|
|
50
|
+
@include type.type-style('label-01');
|
|
51
|
+
color: colors.$gray-70;
|
|
52
|
+
margin-left: layout.$spacing-02;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.secondRow {
|
|
56
|
+
font-size: 0.875rem;
|
|
57
|
+
display: flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.metadata {
|
|
62
|
+
display: flex;
|
|
63
|
+
flex-direction: column;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.label {
|
|
67
|
+
@include type.type-style('label-01');
|
|
68
|
+
color: colors.$gray-70;
|
|
69
|
+
margin-bottom: layout.$spacing-02;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.value {
|
|
73
|
+
@include type.type-style('body-01');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.flagToggle {
|
|
77
|
+
:global(.cds--toggle__switch--checked) {
|
|
78
|
+
background-color: colors.$green-50;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.flagsHeaderInfo {
|
|
83
|
+
display: flex;
|
|
84
|
+
justify-content: space-between;
|
|
85
|
+
align-items: center;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.resultsCount {
|
|
89
|
+
@include type.type-style('label-01');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.formWrapper {
|
|
93
|
+
display: flex;
|
|
94
|
+
flex-direction: column;
|
|
95
|
+
justify-content: space-between;
|
|
96
|
+
height: 100%;
|
|
97
|
+
background-color: colors.$white;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.button {
|
|
101
|
+
height: layout.$spacing-10;
|
|
102
|
+
display: flex;
|
|
103
|
+
align-content: flex-start;
|
|
104
|
+
align-items: baseline;
|
|
105
|
+
min-width: 50%;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.tabletButtonSet {
|
|
109
|
+
padding: layout.$spacing-06 layout.$spacing-05;
|
|
110
|
+
background-color: colors.$white;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.desktopButtonSet {
|
|
114
|
+
padding: 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.emptyText {
|
|
118
|
+
color: colors.$gray-70;
|
|
119
|
+
font-size: 0.875rem;
|
|
120
|
+
text-align: center;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.sortDropdown {
|
|
124
|
+
@include type.type-style('body-compact-01');
|
|
125
|
+
color: colors.$gray-100;
|
|
126
|
+
gap: 0;
|
|
127
|
+
|
|
128
|
+
:global(.cds--dropdown--inline .cds--list-box__menu) {
|
|
129
|
+
min-width: layout.$spacing-13;
|
|
130
|
+
max-width: 12rem;
|
|
131
|
+
left: -4.375rem;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.emptyState {
|
|
136
|
+
display: flex;
|
|
137
|
+
justify-content: center;
|
|
138
|
+
align-items: center;
|
|
139
|
+
padding: layout.$spacing-05;
|
|
140
|
+
text-align: center;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.tile {
|
|
144
|
+
margin: auto;
|
|
145
|
+
width: 18.75rem;
|
|
146
|
+
display: flex;
|
|
147
|
+
flex-direction: column;
|
|
148
|
+
align-items: center;
|
|
149
|
+
justify-content: center;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.content {
|
|
153
|
+
max-width: layout.$spacing-13;
|
|
154
|
+
@include type.type-style('heading-compact-02');
|
|
155
|
+
margin-bottom: layout.$spacing-05;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.helper {
|
|
159
|
+
@include type.type-style('body-compact-01');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.loader {
|
|
163
|
+
display: flex;
|
|
164
|
+
justify-content: center;
|
|
165
|
+
background-color: colors.$gray-10;
|
|
166
|
+
min-height: layout.$spacing-09;
|
|
167
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { mockPatient } from 'tools';
|
|
5
|
+
import { mockPatientFlags } from '__mocks__';
|
|
6
|
+
import { usePatientFlags } from '../hooks/usePatientFlags';
|
|
7
|
+
import FlagsWorkspace from './flags.workspace';
|
|
8
|
+
|
|
9
|
+
const mockUsePatientFlags = usePatientFlags as jest.Mock;
|
|
10
|
+
|
|
11
|
+
jest.mock('../hooks/usePatientFlags', () => {
|
|
12
|
+
const originalModule = jest.requireActual('../hooks/usePatientFlags');
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
...originalModule,
|
|
16
|
+
usePatientFlags: jest.fn(),
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('renders an Edit form that enables users to toggle flags on or off', async () => {
|
|
21
|
+
mockUsePatientFlags.mockReturnValue({
|
|
22
|
+
flags: mockPatientFlags,
|
|
23
|
+
isLoading: false,
|
|
24
|
+
error: null,
|
|
25
|
+
isValidating: false,
|
|
26
|
+
mutate: jest.fn(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
render(
|
|
30
|
+
<FlagsWorkspace
|
|
31
|
+
closeWorkspace={jest.fn()}
|
|
32
|
+
groupProps={{
|
|
33
|
+
patientUuid: mockPatient.id,
|
|
34
|
+
patient: mockPatient,
|
|
35
|
+
visitContext: null,
|
|
36
|
+
mutateVisitContext: null,
|
|
37
|
+
}}
|
|
38
|
+
workspaceProps={{}}
|
|
39
|
+
windowProps={{}}
|
|
40
|
+
workspaceName=""
|
|
41
|
+
launchChildWorkspace={null}
|
|
42
|
+
windowName={''}
|
|
43
|
+
isRootWorkspace={false}
|
|
44
|
+
/>,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const searchbox = screen.getByRole('searchbox', { name: /search for a flag/i });
|
|
48
|
+
const clearSearchInputButton = screen.getByRole('button', { name: /clear search input/i });
|
|
49
|
+
const discardButton = screen.getByRole('button', { name: /discard/i });
|
|
50
|
+
const saveButton = screen.getByRole('button', { name: /save & close/i });
|
|
51
|
+
|
|
52
|
+
expect(searchbox).toBeInTheDocument();
|
|
53
|
+
expect(clearSearchInputButton).toBeInTheDocument();
|
|
54
|
+
expect(discardButton).toBeInTheDocument();
|
|
55
|
+
expect(saveButton).toBeInTheDocument();
|
|
56
|
+
expect(screen.getByText(/patient has a future appointment scheduled/i)).toBeInTheDocument();
|
|
57
|
+
expect(screen.getByText(/patient needs to be followed up/i)).toBeInTheDocument();
|
|
58
|
+
expect(screen.getByText(/social/i)).toBeInTheDocument();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('sorts by active and retired correctly via controlled dropdown', async () => {
|
|
62
|
+
mockUsePatientFlags.mockReturnValue({
|
|
63
|
+
flags: mockPatientFlags,
|
|
64
|
+
isLoading: false,
|
|
65
|
+
error: null,
|
|
66
|
+
isValidating: false,
|
|
67
|
+
mutate: jest.fn(),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
render(
|
|
71
|
+
<FlagsWorkspace
|
|
72
|
+
closeWorkspace={jest.fn()}
|
|
73
|
+
groupProps={{
|
|
74
|
+
patientUuid: mockPatient.id,
|
|
75
|
+
patient: mockPatient,
|
|
76
|
+
visitContext: null,
|
|
77
|
+
mutateVisitContext: null,
|
|
78
|
+
}}
|
|
79
|
+
workspaceProps={{}}
|
|
80
|
+
windowProps={{}}
|
|
81
|
+
workspaceName=""
|
|
82
|
+
launchChildWorkspace={null}
|
|
83
|
+
windowName={''}
|
|
84
|
+
isRootWorkspace={false}
|
|
85
|
+
/>,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const user = userEvent.setup();
|
|
89
|
+
const sortDropdown = screen.getByRole('combobox');
|
|
90
|
+
expect(sortDropdown).toBeInTheDocument();
|
|
91
|
+
|
|
92
|
+
// select "Retired first" then "Active first" to exercise both flows
|
|
93
|
+
await user.click(sortDropdown);
|
|
94
|
+
await user.click(screen.getByText(/Retired first/i));
|
|
95
|
+
await user.click(sortDropdown);
|
|
96
|
+
await user.click(screen.getByText(/Active first/i));
|
|
97
|
+
});
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
import { orderBy } from 'lodash-es';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { Button, ButtonSet, Dropdown, Form, InlineLoading, Search, Tile, Toggle, Stack } from '@carbon/react';
|
|
5
|
+
import { type PatientWorkspace2DefinitionProps } from '@openmrs/esm-patient-common-lib';
|
|
6
|
+
import {
|
|
7
|
+
useLayoutType,
|
|
8
|
+
showSnackbar,
|
|
9
|
+
parseDate,
|
|
10
|
+
formatDate,
|
|
11
|
+
ResponsiveWrapper,
|
|
12
|
+
Workspace2,
|
|
13
|
+
} from '@openmrs/esm-framework';
|
|
14
|
+
import { usePatientFlags, enablePatientFlag, disablePatientFlag } from '../hooks/usePatientFlags';
|
|
15
|
+
import { getFlagType } from '../utils';
|
|
16
|
+
import styles from './flags-workspace.scss';
|
|
17
|
+
|
|
18
|
+
type SortKey = 'alpha' | 'active' | 'retired';
|
|
19
|
+
|
|
20
|
+
const FlagsWorkspace: React.FC<PatientWorkspace2DefinitionProps<{}, {}>> = ({
|
|
21
|
+
closeWorkspace,
|
|
22
|
+
groupProps: { patientUuid },
|
|
23
|
+
}) => {
|
|
24
|
+
const { t } = useTranslation();
|
|
25
|
+
const { flags, isLoading, error, mutate } = usePatientFlags(patientUuid);
|
|
26
|
+
const isTablet = useLayoutType() === 'tablet';
|
|
27
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
28
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
29
|
+
const [sortBy, setSortBy] = useState<SortKey>('alpha');
|
|
30
|
+
// Track pending changes: Map of flagUuid -> desired active state
|
|
31
|
+
const [pendingActiveStates, setPendingActiveStates] = useState<Map<string, boolean>>(new Map());
|
|
32
|
+
|
|
33
|
+
const hasUnsavedChanges = pendingActiveStates.size > 0;
|
|
34
|
+
|
|
35
|
+
const sortItems = useMemo(
|
|
36
|
+
() => [
|
|
37
|
+
{ id: 'alpha' as const, label: t('alphabetically', 'A - Z') },
|
|
38
|
+
{ id: 'active' as const, label: t('activeFirst', 'Active first') },
|
|
39
|
+
{ id: 'retired' as const, label: t('retiredFirst', 'Retired first') },
|
|
40
|
+
],
|
|
41
|
+
[t],
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const sortedRows = useMemo(() => {
|
|
45
|
+
if (!sortBy) {
|
|
46
|
+
return flags;
|
|
47
|
+
}
|
|
48
|
+
if (sortBy === 'active') {
|
|
49
|
+
return orderBy(flags, [(item) => Number(item.voided)], ['asc']);
|
|
50
|
+
}
|
|
51
|
+
if (sortBy === 'retired') {
|
|
52
|
+
return orderBy(flags, [(item) => Number(item.voided)], ['desc']);
|
|
53
|
+
}
|
|
54
|
+
return orderBy(flags, [(f) => f.message], ['asc']);
|
|
55
|
+
}, [sortBy, flags]);
|
|
56
|
+
|
|
57
|
+
const searchResults = useMemo(() => {
|
|
58
|
+
const query = searchTerm.trim().toLowerCase();
|
|
59
|
+
if (query) {
|
|
60
|
+
return sortedRows.filter((f) => f.message.toLowerCase().includes(query));
|
|
61
|
+
}
|
|
62
|
+
return sortedRows;
|
|
63
|
+
}, [searchTerm, sortedRows]);
|
|
64
|
+
|
|
65
|
+
const handleSortByChange = ({ selectedItem }) => setSortBy(selectedItem?.id as SortKey);
|
|
66
|
+
|
|
67
|
+
// Get the effective active state for a flag (considering pending changes)
|
|
68
|
+
const isActive = useCallback(
|
|
69
|
+
(flagUuid: string, originallyActive: boolean) => {
|
|
70
|
+
return pendingActiveStates.has(flagUuid) ? pendingActiveStates.get(flagUuid) : originallyActive;
|
|
71
|
+
},
|
|
72
|
+
[pendingActiveStates],
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const handleToggle = useCallback((flagUuid: string, originallyActive: boolean, isNowActive: boolean) => {
|
|
76
|
+
setPendingActiveStates((prev) => {
|
|
77
|
+
const next = new Map(prev);
|
|
78
|
+
if (isNowActive === originallyActive) {
|
|
79
|
+
next.delete(flagUuid);
|
|
80
|
+
} else {
|
|
81
|
+
next.set(flagUuid, isNowActive);
|
|
82
|
+
}
|
|
83
|
+
return next;
|
|
84
|
+
});
|
|
85
|
+
}, []);
|
|
86
|
+
|
|
87
|
+
const handleSave = useCallback(async () => {
|
|
88
|
+
if (pendingActiveStates.size === 0) {
|
|
89
|
+
closeWorkspace({ discardUnsavedChanges: true });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
setIsSaving(true);
|
|
94
|
+
const promises: Promise<{ flagUuid: string; success: boolean }>[] = [];
|
|
95
|
+
|
|
96
|
+
pendingActiveStates.forEach((active, flagUuid) => {
|
|
97
|
+
const promise = (active ? enablePatientFlag(flagUuid) : disablePatientFlag(flagUuid))
|
|
98
|
+
.then((res) => ({ flagUuid, success: res.ok }))
|
|
99
|
+
.catch(() => ({ flagUuid, success: false }));
|
|
100
|
+
promises.push(promise);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const results = await Promise.all(promises);
|
|
104
|
+
const failures = results.filter((r) => !r.success);
|
|
105
|
+
|
|
106
|
+
if (failures.length === 0) {
|
|
107
|
+
showSnackbar({
|
|
108
|
+
isLowContrast: true,
|
|
109
|
+
kind: 'success',
|
|
110
|
+
subtitle: t('flagsUpdatedSuccessfully', 'Flags updated successfully'),
|
|
111
|
+
title: t('flagsUpdated', 'Flags updated'),
|
|
112
|
+
});
|
|
113
|
+
mutate();
|
|
114
|
+
closeWorkspace({ discardUnsavedChanges: true });
|
|
115
|
+
} else {
|
|
116
|
+
showSnackbar({
|
|
117
|
+
isLowContrast: false,
|
|
118
|
+
kind: 'error',
|
|
119
|
+
subtitle: t('flagsUpdateError', 'Some flags failed to update'),
|
|
120
|
+
title: t('updateError', 'Update error'),
|
|
121
|
+
});
|
|
122
|
+
// Clear successful changes from pending, keep failures
|
|
123
|
+
setPendingActiveStates((prev) => {
|
|
124
|
+
const next = new Map(prev);
|
|
125
|
+
results.filter((r) => r.success).forEach((r) => next.delete(r.flagUuid));
|
|
126
|
+
return next;
|
|
127
|
+
});
|
|
128
|
+
mutate();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
setIsSaving(false);
|
|
132
|
+
}, [pendingActiveStates, closeWorkspace, mutate, t]);
|
|
133
|
+
|
|
134
|
+
if (isLoading) {
|
|
135
|
+
return <InlineLoading className={styles.loading} description={`${t('loading', 'Loading')} ...`} />;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (error) {
|
|
139
|
+
return <div>{error.message}</div>;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<Workspace2 title={t('editPatientFlags', 'Edit patient flags')} hasUnsavedChanges={hasUnsavedChanges}>
|
|
144
|
+
<Form className={styles.formWrapper}>
|
|
145
|
+
{/* The <div> below is required to maintain the page layout styling */}
|
|
146
|
+
<div>
|
|
147
|
+
<ResponsiveWrapper>
|
|
148
|
+
<Search
|
|
149
|
+
labelText={t('searchForAFlag', 'Search for a flag')}
|
|
150
|
+
placeholder={t('searchForAFlag', 'Search for a flag')}
|
|
151
|
+
value={searchTerm}
|
|
152
|
+
size={isTablet ? 'lg' : 'md'}
|
|
153
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
154
|
+
/>
|
|
155
|
+
</ResponsiveWrapper>
|
|
156
|
+
<Stack gap={4}>
|
|
157
|
+
<div className={styles.listWrapper}>
|
|
158
|
+
<div className={styles.flagsHeaderInfo}>
|
|
159
|
+
{searchResults.length > 0 ? (
|
|
160
|
+
<>
|
|
161
|
+
<span className={styles.resultsCount}>
|
|
162
|
+
{t('matchesForSearchTerm', '{{count}} flags', {
|
|
163
|
+
count: searchResults.length,
|
|
164
|
+
})}
|
|
165
|
+
</span>
|
|
166
|
+
<Dropdown
|
|
167
|
+
className={styles.sortDropdown}
|
|
168
|
+
id="sortBy"
|
|
169
|
+
label=""
|
|
170
|
+
type="inline"
|
|
171
|
+
items={sortItems}
|
|
172
|
+
itemToString={(item) => item?.label ?? ''}
|
|
173
|
+
selectedItem={sortItems.find((item) => item.id === sortBy)}
|
|
174
|
+
onChange={handleSortByChange}
|
|
175
|
+
titleText={t('sortBy', 'Sort by')}
|
|
176
|
+
/>
|
|
177
|
+
</>
|
|
178
|
+
) : null}
|
|
179
|
+
</div>
|
|
180
|
+
{searchResults.length > 0
|
|
181
|
+
? searchResults.map((result) => {
|
|
182
|
+
const originallyActive = !result.voided;
|
|
183
|
+
const active = isActive(result.uuid, originallyActive);
|
|
184
|
+
return (
|
|
185
|
+
<div className={styles.flagTile} key={result.uuid}>
|
|
186
|
+
<div className={styles.flagHeader}>
|
|
187
|
+
<div className={styles.titleAndType}>
|
|
188
|
+
<div className={styles.flagTitle}>{result.message}</div>·
|
|
189
|
+
<span className={styles.type}>
|
|
190
|
+
{(() => {
|
|
191
|
+
const typeLabel = getFlagType(result.tags);
|
|
192
|
+
return typeLabel ? t(typeLabel, typeLabel) : t('clinical', 'Clinical');
|
|
193
|
+
})()}
|
|
194
|
+
</span>
|
|
195
|
+
</div>
|
|
196
|
+
<Toggle
|
|
197
|
+
className={styles.flagToggle}
|
|
198
|
+
toggled={active}
|
|
199
|
+
id={result.uuid}
|
|
200
|
+
labelA=""
|
|
201
|
+
labelB=""
|
|
202
|
+
onToggle={(isNowActive) => handleToggle(result.uuid, originallyActive, isNowActive)}
|
|
203
|
+
size="sm"
|
|
204
|
+
/>
|
|
205
|
+
</div>
|
|
206
|
+
{active && (
|
|
207
|
+
<div className={styles.secondRow}>
|
|
208
|
+
<div className={styles.metadata}>
|
|
209
|
+
<span className={styles.label}>{t('assigned', 'Assigned')}</span>
|
|
210
|
+
<span className={styles.value}>
|
|
211
|
+
{formatDate(parseDate(result.auditInfo?.dateCreated), { time: false })}
|
|
212
|
+
</span>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
)}
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
})
|
|
219
|
+
: null}
|
|
220
|
+
|
|
221
|
+
{searchResults.length === 0 ? (
|
|
222
|
+
<div className={styles.emptyState}>
|
|
223
|
+
<Tile className={styles.tile}>
|
|
224
|
+
<p className={styles.content}>{t('noFlagsFound', 'Sorry, no flags found matching your search')}</p>
|
|
225
|
+
<p className={styles.helper}>
|
|
226
|
+
<Button
|
|
227
|
+
kind="ghost"
|
|
228
|
+
size="sm"
|
|
229
|
+
onClick={() => {
|
|
230
|
+
setSearchTerm('');
|
|
231
|
+
}}
|
|
232
|
+
>
|
|
233
|
+
{t('clearSearch', 'Clear search')}
|
|
234
|
+
</Button>
|
|
235
|
+
</p>
|
|
236
|
+
</Tile>
|
|
237
|
+
</div>
|
|
238
|
+
) : null}
|
|
239
|
+
</div>
|
|
240
|
+
</Stack>
|
|
241
|
+
</div>
|
|
242
|
+
<ButtonSet className={isTablet ? styles.tabletButtonSet : styles.desktopButtonSet}>
|
|
243
|
+
<Button className={styles.button} kind="secondary" onClick={() => closeWorkspace()}>
|
|
244
|
+
{t('discard', 'Discard')}
|
|
245
|
+
</Button>
|
|
246
|
+
<Button className={styles.button} disabled={isSaving} kind="primary" type="button" onClick={handleSave}>
|
|
247
|
+
{isSaving ? t('saving', 'Saving...') : t('saveAndClose', 'Save & close')}
|
|
248
|
+
</Button>
|
|
249
|
+
</ButtonSet>
|
|
250
|
+
</Form>
|
|
251
|
+
</Workspace2>
|
|
252
|
+
);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
export default FlagsWorkspace;
|
|
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
|
|
|
2
2
|
import useSWR from 'swr';
|
|
3
3
|
import { openmrsFetch, restBaseUrl, type FetchResponse } from '@openmrs/esm-framework';
|
|
4
4
|
|
|
5
|
-
interface FlagFetchResponse {
|
|
5
|
+
export interface FlagFetchResponse {
|
|
6
6
|
uuid: string;
|
|
7
7
|
message: string;
|
|
8
8
|
voided: boolean;
|
|
@@ -12,23 +12,23 @@ interface FlagFetchResponse {
|
|
|
12
12
|
auditInfo: { dateCreated: string };
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
interface FlagDefinition {
|
|
15
|
+
export interface FlagDefinition {
|
|
16
16
|
uuid: string;
|
|
17
17
|
display: string;
|
|
18
18
|
priority: { uuid: string; name: string };
|
|
19
19
|
tags: Array<{ uuid: string; display: string }>;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
interface FlagsFetchResponse {
|
|
22
|
+
export interface FlagsFetchResponse {
|
|
23
23
|
results: Array<FlagFetchResponse>;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
interface FlagDefinitionsFetchResponse {
|
|
26
|
+
export interface FlagDefinitionsFetchResponse {
|
|
27
27
|
results: Array<FlagDefinition>;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
interface FlagWithPriority extends FlagFetchResponse {
|
|
31
|
-
|
|
30
|
+
export interface FlagWithPriority extends FlagFetchResponse {
|
|
31
|
+
flagDefinition?: FlagDefinition;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
|
@@ -44,6 +44,7 @@ export function usePatientFlags(patientUuid: string) {
|
|
|
44
44
|
data: patientFlagsData,
|
|
45
45
|
error: patientFlagsError,
|
|
46
46
|
isLoading: isLoadingPatientFlags,
|
|
47
|
+
mutate,
|
|
47
48
|
} = useSWR<FetchResponse<FlagsFetchResponse>, Error>(patientUuid ? patientFlagsUrl : null, openmrsFetch);
|
|
48
49
|
|
|
49
50
|
const patientFlags = patientFlagsData?.data?.results ?? [];
|
|
@@ -60,17 +61,24 @@ export function usePatientFlags(patientUuid: string) {
|
|
|
60
61
|
const flagDefinitions = flagDefinitionsData?.data?.results ?? [];
|
|
61
62
|
|
|
62
63
|
// Merge patient flags with flag definitions to get priority information
|
|
63
|
-
const flagsWithPriority: FlagWithPriority[] = patientFlags.map((pf) =>
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
const flagsWithPriority: FlagWithPriority[] = patientFlags.map((pf) => {
|
|
65
|
+
const flagDefinition = flagDefinitions.find((f) => f.uuid === pf.flag.uuid);
|
|
66
|
+
if (!flagDefinition) {
|
|
67
|
+
console.warn(`Flag definition not found for flag ${pf.flag.display}. Tag will be missing priority information.`);
|
|
68
|
+
return pf;
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
...pf,
|
|
72
|
+
flagDefinition,
|
|
73
|
+
};
|
|
74
|
+
});
|
|
67
75
|
|
|
68
76
|
const result = {
|
|
69
77
|
flags: flagsWithPriority,
|
|
70
78
|
error: patientFlagsError || flagDefinitionsError,
|
|
71
79
|
isLoading: isLoadingPatientFlags || isLoadingFlagDefinitions,
|
|
72
80
|
isValidating: false,
|
|
73
|
-
mutate
|
|
81
|
+
mutate,
|
|
74
82
|
};
|
|
75
83
|
|
|
76
84
|
return result;
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { defineConfigSchema, getAsyncLifecycle } from '@openmrs/esm-framework';
|
|
1
|
+
import { defineConfigSchema, defineExtensionConfigSchema, getAsyncLifecycle } from '@openmrs/esm-framework';
|
|
2
2
|
import { configSchema } from './config-schema';
|
|
3
|
+
import { riskCountExtensionConfigSchema } from './flags/flags-risk-count-extension/extension-config-schema';
|
|
3
4
|
|
|
4
5
|
const moduleName = '@openmrs/esm-patient-flags-app';
|
|
5
6
|
|
|
@@ -7,19 +8,23 @@ export const importTranslation = require.context('../translations', false, /.jso
|
|
|
7
8
|
|
|
8
9
|
export function startupApp() {
|
|
9
10
|
defineConfigSchema(moduleName, configSchema);
|
|
11
|
+
defineExtensionConfigSchema('patient-flags-risk-count', riskCountExtensionConfigSchema);
|
|
10
12
|
}
|
|
11
13
|
|
|
12
|
-
export const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
export const flagsRiskCountExtension = getAsyncLifecycle(
|
|
15
|
+
() => import('./flags/flags-risk-count-extension/flags-risk-count.extension'),
|
|
16
|
+
{
|
|
17
|
+
featureName: 'patient-flags-risk-count',
|
|
18
|
+
moduleName,
|
|
19
|
+
},
|
|
20
|
+
);
|
|
16
21
|
|
|
17
|
-
export const
|
|
22
|
+
export const flagsListExtension = getAsyncLifecycle(() => import('./flags/flags-list-extension/flags-list.extension'), {
|
|
18
23
|
featureName: 'patient-flags-overview',
|
|
19
24
|
moduleName,
|
|
20
25
|
});
|
|
21
26
|
|
|
22
|
-
export const patientFlagsWorkspace = getAsyncLifecycle(() => import('./flags/
|
|
27
|
+
export const patientFlagsWorkspace = getAsyncLifecycle(() => import('./flags/flags-workspace/flags.workspace'), {
|
|
23
28
|
featureName: 'patient-flags-workspace',
|
|
24
29
|
moduleName,
|
|
25
30
|
});
|
package/src/routes.json
CHANGED
|
@@ -5,16 +5,19 @@
|
|
|
5
5
|
},
|
|
6
6
|
"extensions": [
|
|
7
7
|
{
|
|
8
|
-
"name": "patient-
|
|
9
|
-
"slot": "patient-
|
|
10
|
-
"component": "
|
|
8
|
+
"name": "patient-flags-risk-count",
|
|
9
|
+
"slot": "patient-info-slot",
|
|
10
|
+
"component": "flagsRiskCountExtension",
|
|
11
11
|
"order": 0,
|
|
12
|
-
"featureFlag": "patient-flags"
|
|
12
|
+
"featureFlag": "patient-flags",
|
|
13
|
+
"meta": {
|
|
14
|
+
"fullWidth": true
|
|
15
|
+
}
|
|
13
16
|
},
|
|
14
17
|
{
|
|
15
|
-
"name": "patient-flags-
|
|
18
|
+
"name": "patient-flags-list",
|
|
16
19
|
"slot": "patient-chart-summary-dashboard-slot",
|
|
17
|
-
"component": "
|
|
20
|
+
"component": "flagsListExtension",
|
|
18
21
|
"order": 0,
|
|
19
22
|
"featureFlag": "patient-flags",
|
|
20
23
|
"meta": {
|
|
@@ -29,11 +32,17 @@
|
|
|
29
32
|
"description": "Visual components that enable healthcare providers to see relevant patient information with a glance in the Patient chart. Flags are displayed in the Patient Summary, just below the patient banner, and can link users to other areas of the chart to perform relevant actions during a visit."
|
|
30
33
|
}
|
|
31
34
|
],
|
|
32
|
-
"
|
|
35
|
+
"workspaces2": [
|
|
33
36
|
{
|
|
34
37
|
"name": "patient-flags-workspace",
|
|
35
|
-
"
|
|
36
|
-
"
|
|
38
|
+
"component": "patientFlagsWorkspace",
|
|
39
|
+
"window": "patient-flags"
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"workspaceWindows2": [
|
|
43
|
+
{
|
|
44
|
+
"name": "patient-flags",
|
|
45
|
+
"group": "patient-chart"
|
|
37
46
|
}
|
|
38
47
|
]
|
|
39
48
|
}
|