@openmrs/esm-patient-common-lib 11.3.1-patch.9310 → 11.3.1-pre.10001
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/package.json +2 -2
- package/src/form-entry/form-entry.ts +1 -0
- package/src/index.ts +1 -0
- package/src/orders/postOrders.ts +63 -50
- package/src/orders/showOrderSuccessToast.ts +78 -20
- package/src/orders/types/order.ts +28 -23
- package/src/orders/useMutatePatientOrders.ts +8 -4
- package/src/orders/useOrderTypes.ts +2 -2
- package/src/orders/useOrderableConceptSets.ts +1 -1
- package/src/orders/useOrders.ts +25 -4
- package/src/results/helpers.test.ts +123 -0
- package/src/results/helpers.ts +63 -0
- package/src/results/index.ts +3 -0
- package/src/results/reference-range-display.component.tsx +25 -0
- package/src/results/results.scss +11 -0
- package/src/results/useReferenceRanges.ts +72 -0
- package/src/store/patient-chart-store.ts +3 -3
- package/src/types/test-results.ts +11 -0
- package/src/visit/revalidation-utils.test.ts +27 -0
- package/src/visit/revalidation-utils.ts +18 -0
- package/src/visit/visit-mutations.ts +2 -2
- package/src/workspaces.ts +5 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openmrs/esm-patient-common-lib",
|
|
3
|
-
"version": "11.3.1-
|
|
3
|
+
"version": "11.3.1-pre.10001",
|
|
4
4
|
"license": "MPL-2.0",
|
|
5
5
|
"description": "Library for common patient chart components",
|
|
6
6
|
"browser": "dist/openmrs-esm-patient-common-lib.js",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"@carbon/react": "^1.83.0",
|
|
33
|
-
"lodash-es": "^4.17.
|
|
33
|
+
"lodash-es": "^4.17.23",
|
|
34
34
|
"uuid": "^8.3.2"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
@@ -28,5 +28,6 @@ export interface FormRendererProps {
|
|
|
28
28
|
preFilledQuestions?: Record<string, string>;
|
|
29
29
|
launchChildWorkspace?: Workspace2DefinitionProps['launchChildWorkspace'];
|
|
30
30
|
closeWorkspace?: Workspace2DefinitionProps['closeWorkspace'];
|
|
31
|
+
closeWorkspaceWithSavedChanges?: () => void;
|
|
31
32
|
setHasUnsavedChanges?(hasUnsavedChanges: boolean);
|
|
32
33
|
}
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ export * from './dashboards/createDashboardLink';
|
|
|
4
4
|
export * from './empty-state';
|
|
5
5
|
export * from './error-state';
|
|
6
6
|
export * from './form-entry/form-entry';
|
|
7
|
+
export * from './results';
|
|
7
8
|
export * from './launchStartVisitPrompt';
|
|
8
9
|
export * from './offline/visit';
|
|
9
10
|
export * from './orders';
|
package/src/orders/postOrders.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type Concept,
|
|
3
|
+
type Encounter,
|
|
3
4
|
openmrsFetch,
|
|
4
5
|
type OpenmrsResource,
|
|
5
6
|
parseDate,
|
|
@@ -7,20 +8,28 @@ import {
|
|
|
7
8
|
type Visit,
|
|
8
9
|
} from '@openmrs/esm-framework';
|
|
9
10
|
import { type OrderBasketStore, orderBasketStore } from './store';
|
|
10
|
-
import type {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
OrderPost
|
|
17
|
-
|
|
11
|
+
import type { ExtractedOrderErrorObject, Order, OrderBasketItem, OrderErrorObject, OrderPost } from './types';
|
|
12
|
+
|
|
13
|
+
function getOrdersPayloadFromOrderBasket(patientUuid: string, ordererUuid: string) {
|
|
14
|
+
const { items, postDataPrepFunctions }: OrderBasketStore = orderBasketStore.getState();
|
|
15
|
+
const patientItems = items[patientUuid];
|
|
16
|
+
|
|
17
|
+
const orders: Array<OrderPost> = [];
|
|
18
|
+
Object.entries(patientItems).forEach(([grouping, groupOrders]) => {
|
|
19
|
+
groupOrders.forEach((order) => {
|
|
20
|
+
orders.push(postDataPrepFunctions[grouping](order, patientUuid, null, ordererUuid));
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return orders;
|
|
25
|
+
}
|
|
18
26
|
|
|
19
27
|
export async function postOrdersOnNewEncounter(
|
|
20
28
|
patientUuid: string,
|
|
21
29
|
orderEncounterType: string,
|
|
22
30
|
currentVisit: Visit | null,
|
|
23
|
-
|
|
31
|
+
orderLocationUuid: string,
|
|
32
|
+
ordererUuid: string,
|
|
24
33
|
abortController?: AbortController,
|
|
25
34
|
|
|
26
35
|
/**
|
|
@@ -29,36 +38,23 @@ export async function postOrdersOnNewEncounter(
|
|
|
29
38
|
*/
|
|
30
39
|
encounterDate?: Date,
|
|
31
40
|
) {
|
|
32
|
-
const now = new Date();
|
|
33
|
-
const visitStartDate = parseDate(currentVisit?.startDatetime);
|
|
34
|
-
const visitEndDate = parseDate(currentVisit?.stopDatetime);
|
|
35
41
|
if (!encounterDate) {
|
|
36
|
-
if (
|
|
37
|
-
encounterDate =
|
|
42
|
+
if (currentVisit?.stopDatetime) {
|
|
43
|
+
encounterDate = parseDate(currentVisit.startDatetime);
|
|
38
44
|
} else {
|
|
39
|
-
|
|
40
|
-
'postOrdersOnNewEncounter received an active visit that is not currently active. This is a programming error. Attempting to place the order using the visit start date.',
|
|
41
|
-
);
|
|
42
|
-
encounterDate = visitStartDate;
|
|
45
|
+
encounterDate = null;
|
|
43
46
|
}
|
|
44
47
|
}
|
|
45
48
|
|
|
46
|
-
const
|
|
47
|
-
const patientItems = items[patientUuid];
|
|
48
|
-
|
|
49
|
-
const orders: Array<DrugOrderPost | TestOrderPost> = [];
|
|
50
|
-
|
|
51
|
-
Object.entries(patientItems).forEach(([grouping, groupOrders]) => {
|
|
52
|
-
groupOrders.forEach((order) => {
|
|
53
|
-
orders.push(postDataPrepFunctions[grouping](order, patientUuid, null));
|
|
54
|
-
});
|
|
55
|
-
});
|
|
49
|
+
const orders = getOrdersPayloadFromOrderBasket(patientUuid, ordererUuid);
|
|
56
50
|
|
|
57
51
|
const encounterPostData: EncounterPost = {
|
|
58
52
|
patient: patientUuid,
|
|
59
|
-
location:
|
|
53
|
+
location: orderLocationUuid,
|
|
60
54
|
encounterType: orderEncounterType,
|
|
61
|
-
encounterDatetime
|
|
55
|
+
// only specify the encounterDatetime if it's given, otherwise
|
|
56
|
+
// don't specify that let the server default it to `now`
|
|
57
|
+
...(encounterDate ? { encounterDatetime: encounterDate } : {}),
|
|
62
58
|
visit: currentVisit?.uuid,
|
|
63
59
|
obs: [],
|
|
64
60
|
orders,
|
|
@@ -75,53 +71,70 @@ export interface EncounterPost {
|
|
|
75
71
|
patient: string;
|
|
76
72
|
location: string;
|
|
77
73
|
encounterType: string;
|
|
78
|
-
encounterDatetime
|
|
74
|
+
encounterDatetime?: Date;
|
|
79
75
|
visit?: string;
|
|
80
76
|
obs: ObsPayload[];
|
|
81
77
|
orders: OrderPost[];
|
|
82
78
|
}
|
|
83
79
|
|
|
84
80
|
export async function postEncounter(encounterPostData: EncounterPost, abortController?: AbortController) {
|
|
85
|
-
return openmrsFetch<
|
|
81
|
+
return openmrsFetch<Encounter>(`${restBaseUrl}/encounter`, {
|
|
86
82
|
headers: {
|
|
87
83
|
'Content-Type': 'application/json',
|
|
88
84
|
},
|
|
89
85
|
method: 'POST',
|
|
90
86
|
body: encounterPostData,
|
|
91
87
|
signal: abortController?.signal,
|
|
92
|
-
}).then((res) => res?.data
|
|
88
|
+
}).then((res) => res?.data);
|
|
93
89
|
}
|
|
94
90
|
|
|
95
|
-
export async function postOrders(
|
|
91
|
+
export async function postOrders(
|
|
92
|
+
patientUuid: string,
|
|
93
|
+
encounterUuid: string,
|
|
94
|
+
abortController: AbortController,
|
|
95
|
+
ordererUuid: string,
|
|
96
|
+
) {
|
|
96
97
|
const { items, postDataPrepFunctions }: OrderBasketStore = orderBasketStore.getState();
|
|
97
98
|
const patientItems = items[patientUuid];
|
|
98
99
|
|
|
99
100
|
const erroredItems: Array<OrderBasketItem> = [];
|
|
100
|
-
|
|
101
|
+
const postedOrders: Array<Order> = [];
|
|
102
|
+
const promises: Array<Promise<void>> = [];
|
|
103
|
+
|
|
104
|
+
for (const grouping in patientItems) {
|
|
101
105
|
const orders = patientItems[grouping];
|
|
106
|
+
const dataPrepFn = postDataPrepFunctions[grouping];
|
|
107
|
+
|
|
108
|
+
if (typeof dataPrepFn !== 'function') {
|
|
109
|
+
console.warn(`The postDataPrep function registered for ${grouping} orders is not a function`);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
102
113
|
for (let i = 0; i < orders.length; i++) {
|
|
103
114
|
const order = orders[i];
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
extractedOrderError: extractErrorDetails(error),
|
|
115
|
+
|
|
116
|
+
const promise = postOrder(dataPrepFn(order, patientUuid, encounterUuid, ordererUuid), abortController)
|
|
117
|
+
.then((response) => {
|
|
118
|
+
postedOrders.push(response.data);
|
|
119
|
+
})
|
|
120
|
+
.catch((error) => {
|
|
121
|
+
erroredItems.push({
|
|
122
|
+
...order,
|
|
123
|
+
orderError: error,
|
|
124
|
+
extractedOrderError: extractErrorDetails(error),
|
|
125
|
+
});
|
|
116
126
|
});
|
|
117
|
-
|
|
127
|
+
|
|
128
|
+
promises.push(promise);
|
|
118
129
|
}
|
|
119
130
|
}
|
|
120
|
-
|
|
131
|
+
await Promise.allSettled(promises);
|
|
132
|
+
|
|
133
|
+
return { postedOrders, erroredItems };
|
|
121
134
|
}
|
|
122
135
|
|
|
123
136
|
export function postOrder(body: OrderPost, abortController?: AbortController) {
|
|
124
|
-
return openmrsFetch(`${restBaseUrl}/order`, {
|
|
137
|
+
return openmrsFetch<Order>(`${restBaseUrl}/order`, {
|
|
125
138
|
method: 'POST',
|
|
126
139
|
signal: abortController?.signal,
|
|
127
140
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -1,28 +1,86 @@
|
|
|
1
|
-
import { showSnackbar } from '@openmrs/esm-framework';
|
|
2
|
-
import { type TFunction } from 'i18next';
|
|
1
|
+
import { showSnackbar, translateFrom } from '@openmrs/esm-framework';
|
|
3
2
|
import { type OrderBasketItem } from './types';
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
4
|
+
type OrderAction = 'placed' | 'updated' | 'discontinued';
|
|
5
|
+
|
|
6
|
+
function getNotificationTitle(
|
|
7
|
+
moduleName: string,
|
|
8
|
+
placedOrders: OrderBasketItem[],
|
|
9
|
+
updatedOrders: OrderBasketItem[],
|
|
10
|
+
discontinuedOrders: OrderBasketItem[],
|
|
11
|
+
activeActions: OrderAction[],
|
|
12
|
+
): string {
|
|
13
|
+
if (activeActions.length > 1) {
|
|
14
|
+
return translateFrom(moduleName, 'ordersCompleted', 'Orders completed');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const action = activeActions[0];
|
|
18
|
+
|
|
19
|
+
if (action === 'placed') {
|
|
20
|
+
return placedOrders.length === 1
|
|
21
|
+
? translateFrom(moduleName, 'orderPlaced', 'Order placed')
|
|
22
|
+
: translateFrom(moduleName, 'ordersPlaced', 'Orders placed');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (action === 'updated') {
|
|
26
|
+
return updatedOrders.length === 1
|
|
27
|
+
? translateFrom(moduleName, 'orderUpdated', 'Order updated')
|
|
28
|
+
: translateFrom(moduleName, 'ordersUpdated', 'Orders updated');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// action === 'discontinued'
|
|
32
|
+
return discontinuedOrders.length === 1
|
|
33
|
+
? translateFrom(moduleName, 'orderDiscontinued', 'Order discontinued')
|
|
34
|
+
: translateFrom(moduleName, 'ordersDiscontinued', 'Orders discontinued');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Shows a success toast notification for order operations.
|
|
39
|
+
* The notification title dynamically reflects the action(s) taken (placed, updated, discontinued)
|
|
40
|
+
* and whether it's singular or plural.
|
|
41
|
+
*
|
|
42
|
+
* @param moduleName - The module name (e.g., '@openmrs/esm-patient-orders-app') to use for translations
|
|
43
|
+
* @param patientOrderItems - Array of order basket items that were processed
|
|
44
|
+
*/
|
|
45
|
+
export function showOrderSuccessToast(moduleName: string, patientOrderItems: OrderBasketItem[]) {
|
|
46
|
+
if (patientOrderItems.length === 0) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const placedOrders = patientOrderItems.filter((item) => ['NEW', 'RENEW'].includes(item.action));
|
|
51
|
+
const updatedOrders = patientOrderItems.filter((item) => item.action === 'REVISE');
|
|
52
|
+
const discontinuedOrders = patientOrderItems.filter((item) => item.action === 'DISCONTINUE');
|
|
53
|
+
|
|
54
|
+
const orderedString = placedOrders.map((item) => item.display).join(', ');
|
|
55
|
+
const updatedString = updatedOrders.map((item) => item.display).join(', ');
|
|
56
|
+
const discontinuedString = discontinuedOrders.map((item) => item.display).join(', ');
|
|
57
|
+
|
|
58
|
+
const activeActions: OrderAction[] = [
|
|
59
|
+
orderedString && 'placed',
|
|
60
|
+
updatedString && 'updated',
|
|
61
|
+
discontinuedString && 'discontinued',
|
|
62
|
+
].filter(Boolean) as OrderAction[];
|
|
63
|
+
|
|
64
|
+
const title = getNotificationTitle(moduleName, placedOrders, updatedOrders, discontinuedOrders, activeActions);
|
|
65
|
+
|
|
66
|
+
const subtitleParts: string[] = [];
|
|
67
|
+
|
|
68
|
+
if (orderedString) {
|
|
69
|
+
subtitleParts.push(`${translateFrom(moduleName, 'orderedFor', 'Placed order for')} ${orderedString}.`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (updatedString) {
|
|
73
|
+
subtitleParts.push(`${translateFrom(moduleName, 'updated', 'Updated')} ${updatedString}.`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (discontinuedString) {
|
|
77
|
+
subtitleParts.push(`${translateFrom(moduleName, 'discontinued', 'Discontinued')} ${discontinuedString}.`);
|
|
78
|
+
}
|
|
18
79
|
|
|
19
80
|
showSnackbar({
|
|
20
81
|
isLowContrast: true,
|
|
21
82
|
kind: 'success',
|
|
22
|
-
title
|
|
23
|
-
subtitle:
|
|
24
|
-
(orderedString && `${t('ordered', 'Placed order for')} ${orderedString}. `) +
|
|
25
|
-
(updatedString && `${t('updated', 'Updated')} ${updatedString}. `) +
|
|
26
|
-
(discontinuedString && `${t('discontinued', 'Discontinued')} ${discontinuedString}.`),
|
|
83
|
+
title,
|
|
84
|
+
subtitle: subtitleParts.join(' '),
|
|
27
85
|
});
|
|
28
86
|
}
|
|
@@ -47,8 +47,6 @@ export interface OrderBasketItem {
|
|
|
47
47
|
action: OrderAction;
|
|
48
48
|
display: string;
|
|
49
49
|
uuid?: string;
|
|
50
|
-
orderer?: string;
|
|
51
|
-
careSetting?: string;
|
|
52
50
|
orderError?: Error & {
|
|
53
51
|
responseBody?: {
|
|
54
52
|
error?: {
|
|
@@ -118,7 +116,7 @@ export interface DrugOrderPost extends OrderPost {
|
|
|
118
116
|
dosingInstructions?: string;
|
|
119
117
|
}
|
|
120
118
|
|
|
121
|
-
export
|
|
119
|
+
export type TestOrderPost = OrderPost;
|
|
122
120
|
|
|
123
121
|
export interface PatientOrderFetchResponse {
|
|
124
122
|
results: Array<Order>;
|
|
@@ -129,7 +127,7 @@ export interface Order {
|
|
|
129
127
|
action: OrderAction;
|
|
130
128
|
asNeeded: boolean;
|
|
131
129
|
asNeededCondition?: string;
|
|
132
|
-
autoExpireDate
|
|
130
|
+
autoExpireDate?: string | null;
|
|
133
131
|
brandName?: string;
|
|
134
132
|
careSetting: OpenmrsResource;
|
|
135
133
|
commentToFulfiller: string;
|
|
@@ -137,17 +135,17 @@ export interface Order {
|
|
|
137
135
|
dateActivated: string;
|
|
138
136
|
dateStopped?: string | null;
|
|
139
137
|
dispenseAsWritten: boolean;
|
|
140
|
-
dose: number;
|
|
141
|
-
doseUnits: OpenmrsResource;
|
|
138
|
+
dose: number | null;
|
|
139
|
+
doseUnits: OpenmrsResource | null;
|
|
142
140
|
dosingInstructions: string | null;
|
|
143
141
|
dosingType?: 'org.openmrs.FreeTextDosingInstructions' | 'org.openmrs.SimpleDosingInstructions';
|
|
144
|
-
drug: Drug;
|
|
145
|
-
duration: number;
|
|
146
|
-
durationUnits: OpenmrsResource;
|
|
142
|
+
drug: Drug | null;
|
|
143
|
+
duration: number | null;
|
|
144
|
+
durationUnits: OpenmrsResource | null;
|
|
147
145
|
encounter: Encounter;
|
|
148
|
-
frequency: OpenmrsResource;
|
|
146
|
+
frequency: OpenmrsResource | null;
|
|
149
147
|
instructions?: string | null;
|
|
150
|
-
numRefills: number;
|
|
148
|
+
numRefills: number | null;
|
|
151
149
|
orderNumber: string;
|
|
152
150
|
orderReason: string | null;
|
|
153
151
|
orderReasonNonCoded: string | null;
|
|
@@ -169,9 +167,9 @@ export interface Order {
|
|
|
169
167
|
};
|
|
170
168
|
patient: OpenmrsResource;
|
|
171
169
|
previousOrder: { uuid: string; type: string; display: string } | null;
|
|
172
|
-
quantity: number;
|
|
173
|
-
quantityUnits: OpenmrsResource;
|
|
174
|
-
route: OpenmrsResource;
|
|
170
|
+
quantity: number | null;
|
|
171
|
+
quantityUnits: OpenmrsResource | null;
|
|
172
|
+
route: OpenmrsResource | null;
|
|
175
173
|
scheduleDate: null;
|
|
176
174
|
urgency: OrderUrgency;
|
|
177
175
|
|
|
@@ -211,10 +209,15 @@ export interface OrderType {
|
|
|
211
209
|
|
|
212
210
|
export type FulfillerStatus = 'RECEIVED' | 'IN_PROGRESS' | 'EXCEPTION' | 'ON_HOLD' | 'DECLINED' | 'COMPLETED';
|
|
213
211
|
|
|
212
|
+
/**
|
|
213
|
+
* A function type that converts a OrderBasketItem into
|
|
214
|
+
* a POST order payload
|
|
215
|
+
*/
|
|
214
216
|
export type PostDataPrepFunction = (
|
|
215
217
|
order: OrderBasketItem,
|
|
216
218
|
patientUuid: string,
|
|
217
219
|
encounterUuid: string | null,
|
|
220
|
+
orderingProviderUuid: string,
|
|
218
221
|
) => OrderPost;
|
|
219
222
|
|
|
220
223
|
export interface OrderBasketExtensionProps {
|
|
@@ -226,21 +229,21 @@ export interface OrderBasketExtensionProps {
|
|
|
226
229
|
|
|
227
230
|
export interface DrugOrderBasketItem extends OrderBasketItem {
|
|
228
231
|
drug: Drug;
|
|
229
|
-
unit: DosingUnit;
|
|
232
|
+
unit: DosingUnit | null;
|
|
230
233
|
commonMedicationName: string;
|
|
231
|
-
dosage: number;
|
|
232
|
-
frequency: MedicationFrequency;
|
|
233
|
-
route: MedicationRoute;
|
|
234
|
-
quantityUnits: QuantityUnit;
|
|
235
|
-
patientInstructions: string;
|
|
234
|
+
dosage: number | null;
|
|
235
|
+
frequency: MedicationFrequency | null;
|
|
236
|
+
route: MedicationRoute | null;
|
|
237
|
+
quantityUnits: QuantityUnit | null;
|
|
238
|
+
patientInstructions: string | null;
|
|
236
239
|
asNeeded: boolean;
|
|
237
|
-
asNeededCondition: string;
|
|
240
|
+
asNeededCondition: string | null;
|
|
238
241
|
startDate: Date | string;
|
|
239
|
-
durationUnit: DurationUnit;
|
|
242
|
+
durationUnit: DurationUnit | null;
|
|
240
243
|
duration: number | null;
|
|
241
244
|
pillsDispensed: number | null;
|
|
242
245
|
numRefills: number | null;
|
|
243
|
-
indication: string;
|
|
246
|
+
indication: string | null;
|
|
244
247
|
isFreeTextDosage: boolean;
|
|
245
248
|
freeTextDosage: string;
|
|
246
249
|
previousOrder?: string;
|
|
@@ -308,6 +311,7 @@ export interface TestOrderBasketItem extends OrderBasketItem {
|
|
|
308
311
|
|
|
309
312
|
export interface OrderBasketWindowProps {
|
|
310
313
|
encounterUuid: string;
|
|
314
|
+
onOrderBasketSubmitted?: (encounterUuid: string, postedOrders: Array<Order>) => void;
|
|
311
315
|
}
|
|
312
316
|
|
|
313
317
|
export interface ExportedOrderBasketWindowProps {
|
|
@@ -319,4 +323,5 @@ export interface ExportedOrderBasketWindowProps {
|
|
|
319
323
|
patientUuid: string;
|
|
320
324
|
visitContext: Visit;
|
|
321
325
|
mutateVisitContext: () => void;
|
|
326
|
+
onOrderBasketSubmitted?: (encounterUuid: string, postedOrders: Array<Order>) => void;
|
|
322
327
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { restBaseUrl } from '@openmrs/esm-framework';
|
|
2
1
|
import { useCallback } from 'react';
|
|
3
2
|
import { useSWRConfig } from 'swr';
|
|
3
|
+
import { restBaseUrl } from '@openmrs/esm-framework';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Returns a function which refreshes the patient orders cache. Uses SWR's mutate function.
|
|
@@ -12,9 +12,13 @@ export function useMutatePatientOrders(patientUuid: string) {
|
|
|
12
12
|
const { mutate } = useSWRConfig();
|
|
13
13
|
const mutateOrders = useCallback(
|
|
14
14
|
() =>
|
|
15
|
-
mutate(
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
mutate(
|
|
16
|
+
(key) => {
|
|
17
|
+
return typeof key === 'string' && key.startsWith(`${restBaseUrl}/order?patient=${patientUuid}`);
|
|
18
|
+
},
|
|
19
|
+
undefined,
|
|
20
|
+
{ revalidate: true },
|
|
21
|
+
),
|
|
18
22
|
[patientUuid, mutate],
|
|
19
23
|
);
|
|
20
24
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
2
|
import useSWRImmutable from 'swr/immutable';
|
|
3
|
+
import { type FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
|
|
3
4
|
import type { OrderTypeFetchResponse } from './types';
|
|
4
|
-
import { useMemo } from 'react';
|
|
5
5
|
|
|
6
6
|
export function useOrderTypes() {
|
|
7
7
|
const orderTypesUrl = `${restBaseUrl}/ordertype`;
|
|
@@ -60,7 +60,7 @@ function useOrderableConceptSWR(searchTerm: string, orderableConceptSets?: Array
|
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
export
|
|
63
|
+
export type OrderableConcept = OpenmrsResource;
|
|
64
64
|
|
|
65
65
|
export function useOrderableConceptSets(searchTerm: string, orderableConcepts: Array<string>) {
|
|
66
66
|
const { data, isLoading, error } = useOrderableConceptSWR(
|
package/src/orders/useOrders.ts
CHANGED
|
@@ -15,6 +15,9 @@ export const drugCustomRepresentation =
|
|
|
15
15
|
'frequency:ref,asNeeded,asNeededCondition,quantity,quantityUnits:ref,numRefills,dosingInstructions,' +
|
|
16
16
|
'duration,durationUnits:ref,route:ref,brandName,dispenseAsWritten)';
|
|
17
17
|
|
|
18
|
+
export const orderCustomRepresentation =
|
|
19
|
+
'custom:(uuid,display,orderNumber,accessionNumber,patient,concept,action,careSetting,previousOrder,dateActivated,scheduledDate,dateStopped,autoExpireDate,encounter:(uuid,display,visit),orderer:ref,orderReason,orderReasonNonCoded,orderType,urgency,instructions,commentToFulfiller,fulfillerStatus)';
|
|
20
|
+
|
|
18
21
|
export function usePatientOrders(
|
|
19
22
|
patientUuid: string,
|
|
20
23
|
status?: Status,
|
|
@@ -23,10 +26,11 @@ export function usePatientOrders(
|
|
|
23
26
|
endDate?: string,
|
|
24
27
|
) {
|
|
25
28
|
const { mutate } = useSWRConfig();
|
|
29
|
+
const activeStatusParams = status === 'ACTIVE' ? '&excludeCanceledAndExpired=true' : '';
|
|
26
30
|
const baseOrdersUrl =
|
|
27
31
|
startDate && endDate
|
|
28
|
-
? `${restBaseUrl}/order?patient=${patientUuid}&careSetting=${careSettingUuid}&v
|
|
29
|
-
: `${restBaseUrl}/order?patient=${patientUuid}&careSetting=${careSettingUuid}&v
|
|
32
|
+
? `${restBaseUrl}/order?patient=${patientUuid}&careSetting=${careSettingUuid}&v=${orderCustomRepresentation}&activatedOnOrAfterDate=${startDate}&activatedOnOrBeforeDate=${endDate}&excludeDiscontinueOrders=true${activeStatusParams}`
|
|
33
|
+
: `${restBaseUrl}/order?patient=${patientUuid}&careSetting=${careSettingUuid}&v=${orderCustomRepresentation}&status=${status}&excludeDiscontinueOrders=true`;
|
|
30
34
|
const ordersUrl = orderType ? `${baseOrdersUrl}&orderTypes=${orderType}` : baseOrdersUrl;
|
|
31
35
|
|
|
32
36
|
const { data, error, isLoading, isValidating } = useSWR<FetchResponse<PatientOrderFetchResponse>, Error>(
|
|
@@ -36,8 +40,12 @@ export function usePatientOrders(
|
|
|
36
40
|
|
|
37
41
|
const mutateOrders = useCallback(
|
|
38
42
|
() =>
|
|
39
|
-
mutate(
|
|
40
|
-
|
|
43
|
+
mutate(
|
|
44
|
+
(key) => typeof key === 'string' && key.startsWith(`${restBaseUrl}/order?patient=${patientUuid}`),
|
|
45
|
+
undefined,
|
|
46
|
+
{ revalidate: true },
|
|
47
|
+
),
|
|
48
|
+
[mutate, patientUuid],
|
|
41
49
|
);
|
|
42
50
|
|
|
43
51
|
const orders = useMemo(
|
|
@@ -63,6 +71,19 @@ export function getDrugOrderByUuid(orderUuid: string) {
|
|
|
63
71
|
return openmrsFetch<Order>(`${restBaseUrl}/order/${orderUuid}?v=${drugCustomRepresentation}`);
|
|
64
72
|
}
|
|
65
73
|
|
|
74
|
+
export function useDrugOrderByUuid(orderUuid: string) {
|
|
75
|
+
const { data, error, isLoading } = useSWR<FetchResponse<Order>, Error>(
|
|
76
|
+
orderUuid ? `${restBaseUrl}/order/${orderUuid}?v=${drugCustomRepresentation}` : null,
|
|
77
|
+
openmrsFetch,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
data: data?.data ?? null,
|
|
82
|
+
error,
|
|
83
|
+
isLoading,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
66
87
|
// See the Urgency enum in https://github.com/openmrs/openmrs-core/blob/492dcd35b85d48730bd19da48f6db146cc882c22/api/src/main/java/org/openmrs/Order.java
|
|
67
88
|
export const priorityOptions: PriorityOption[] = [
|
|
68
89
|
{ value: 'ROUTINE', label: translateFrom(patientChartAppModuleName, 'Routine') },
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { type ReferenceRanges } from '../types';
|
|
2
|
+
import { exist, formatReferenceRange, assessValue } from './helpers';
|
|
3
|
+
|
|
4
|
+
describe('exist', () => {
|
|
5
|
+
it('returns true when all values are defined', () => {
|
|
6
|
+
expect(exist(1, 2, 3)).toBe(true);
|
|
7
|
+
expect(exist(0)).toBe(true);
|
|
8
|
+
expect(exist('')).toBe(true);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('returns false when any value is null or undefined', () => {
|
|
12
|
+
expect(exist(null)).toBe(false);
|
|
13
|
+
expect(exist(undefined)).toBe(false);
|
|
14
|
+
expect(exist(1, null, 3)).toBe(false);
|
|
15
|
+
expect(exist(1, undefined)).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('returns true for empty arguments', () => {
|
|
19
|
+
expect(exist()).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('formatReferenceRange', () => {
|
|
24
|
+
it('returns formatted range with en-dash', () => {
|
|
25
|
+
expect(formatReferenceRange({ lowNormal: 35, hiNormal: 147 })).toBe('35 – 147');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('appends units from ranges object', () => {
|
|
29
|
+
expect(formatReferenceRange({ lowNormal: 35, hiNormal: 147, units: 'U/L' })).toBe('35 – 147 U/L');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('appends units from parameter when ranges.units is absent', () => {
|
|
33
|
+
expect(formatReferenceRange({ lowNormal: 35, hiNormal: 147 }, 'mg/dL')).toBe('35 – 147 mg/dL');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('prefers ranges.units over the units parameter', () => {
|
|
37
|
+
expect(formatReferenceRange({ lowNormal: 35, hiNormal: 147, units: 'U/L' }, 'mg/dL')).toBe('35 – 147 U/L');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('returns -- when ranges is null', () => {
|
|
41
|
+
expect(formatReferenceRange(null)).toBe('--');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('returns -- when lowNormal or hiNormal is missing', () => {
|
|
45
|
+
expect(formatReferenceRange({ lowNormal: 35 })).toBe('--');
|
|
46
|
+
expect(formatReferenceRange({ hiNormal: 147 })).toBe('--');
|
|
47
|
+
expect(formatReferenceRange({})).toBe('--');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('handles zero values correctly', () => {
|
|
51
|
+
expect(formatReferenceRange({ lowNormal: 0, hiNormal: 10 })).toBe('0 – 10');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('assessValue', () => {
|
|
56
|
+
const fullRanges: ReferenceRanges = {
|
|
57
|
+
lowAbsolute: 0,
|
|
58
|
+
lowCritical: 10,
|
|
59
|
+
lowNormal: 35,
|
|
60
|
+
hiNormal: 100,
|
|
61
|
+
hiCritical: 150,
|
|
62
|
+
hiAbsolute: 200,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
it('returns NORMAL for values within range', () => {
|
|
66
|
+
expect(assessValue(50, fullRanges)).toBe('NORMAL');
|
|
67
|
+
expect(assessValue(70, fullRanges)).toBe('NORMAL');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('returns NORMAL for NaN', () => {
|
|
71
|
+
expect(assessValue(NaN, fullRanges)).toBe('NORMAL');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('returns HIGH when value exceeds hiNormal', () => {
|
|
75
|
+
expect(assessValue(101, fullRanges)).toBe('HIGH');
|
|
76
|
+
expect(assessValue(149, fullRanges)).toBe('HIGH');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('returns CRITICALLY_HIGH when value reaches hiCritical (inclusive)', () => {
|
|
80
|
+
expect(assessValue(150, fullRanges)).toBe('CRITICALLY_HIGH');
|
|
81
|
+
expect(assessValue(175, fullRanges)).toBe('CRITICALLY_HIGH');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('returns OFF_SCALE_HIGH when value reaches hiAbsolute (inclusive)', () => {
|
|
85
|
+
expect(assessValue(200, fullRanges)).toBe('OFF_SCALE_HIGH');
|
|
86
|
+
expect(assessValue(999, fullRanges)).toBe('OFF_SCALE_HIGH');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('returns LOW when value is below lowNormal', () => {
|
|
90
|
+
expect(assessValue(34, fullRanges)).toBe('LOW');
|
|
91
|
+
expect(assessValue(11, fullRanges)).toBe('LOW');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('returns CRITICALLY_LOW when value reaches lowCritical (inclusive)', () => {
|
|
95
|
+
expect(assessValue(10, fullRanges)).toBe('CRITICALLY_LOW');
|
|
96
|
+
expect(assessValue(5, fullRanges)).toBe('CRITICALLY_LOW');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('returns OFF_SCALE_LOW when value reaches lowAbsolute (inclusive)', () => {
|
|
100
|
+
expect(assessValue(0, fullRanges)).toBe('OFF_SCALE_LOW');
|
|
101
|
+
expect(assessValue(-5, fullRanges)).toBe('OFF_SCALE_LOW');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Boundary behavior: normal thresholds are exclusive, critical/absolute are inclusive
|
|
105
|
+
it('treats values at hiNormal as NORMAL (exclusive)', () => {
|
|
106
|
+
expect(assessValue(100, fullRanges)).toBe('NORMAL');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('treats values at lowNormal as NORMAL (exclusive)', () => {
|
|
110
|
+
expect(assessValue(35, fullRanges)).toBe('NORMAL');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('works with only normal thresholds', () => {
|
|
114
|
+
const normalOnly: ReferenceRanges = { lowNormal: 35, hiNormal: 100 };
|
|
115
|
+
expect(assessValue(50, normalOnly)).toBe('NORMAL');
|
|
116
|
+
expect(assessValue(101, normalOnly)).toBe('HIGH');
|
|
117
|
+
expect(assessValue(34, normalOnly)).toBe('LOW');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('returns NORMAL when no thresholds are provided', () => {
|
|
121
|
+
expect(assessValue(50, {})).toBe('NORMAL');
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { type OBSERVATION_INTERPRETATION, type ReferenceRanges } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Checks if all provided values exist (are not null or undefined).
|
|
5
|
+
*/
|
|
6
|
+
export function exist(...args: unknown[]): boolean {
|
|
7
|
+
return args.every((y) => y !== null && y !== undefined);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Formats reference range as a string with optional units.
|
|
12
|
+
* Uses en-dash (–) for range separator.
|
|
13
|
+
*
|
|
14
|
+
* @param ranges - The reference ranges object
|
|
15
|
+
* @param units - Optional units string to append
|
|
16
|
+
* @returns Formatted string like "35 – 147 U/L" or "--" if no valid range
|
|
17
|
+
*/
|
|
18
|
+
export function formatReferenceRange(ranges: ReferenceRanges | null, units?: string): string {
|
|
19
|
+
if (!ranges) return '--';
|
|
20
|
+
const { lowNormal, hiNormal } = ranges;
|
|
21
|
+
const displayUnits = ranges.units || units || '';
|
|
22
|
+
if (exist(lowNormal, hiNormal)) {
|
|
23
|
+
return `${lowNormal} – ${hiNormal}${displayUnits ? ` ${displayUnits}` : ''}`;
|
|
24
|
+
}
|
|
25
|
+
return '--';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Determines the interpretation of a lab value based on reference ranges.
|
|
30
|
+
* Returns the appropriate interpretation level based on the value's
|
|
31
|
+
* relationship to normal, critical, and absolute thresholds.
|
|
32
|
+
*
|
|
33
|
+
* @param value - The numeric lab result value
|
|
34
|
+
* @param ranges - The reference ranges to compare against
|
|
35
|
+
* @returns The interpretation category
|
|
36
|
+
*/
|
|
37
|
+
export function assessValue(value: number, ranges: ReferenceRanges): OBSERVATION_INTERPRETATION {
|
|
38
|
+
if (isNaN(value)) {
|
|
39
|
+
return 'NORMAL';
|
|
40
|
+
}
|
|
41
|
+
// Critical and absolute thresholds use inclusive (>=, <=) comparisons
|
|
42
|
+
// because values at those boundaries should be flagged for clinical safety.
|
|
43
|
+
// Normal thresholds use exclusive (>, <) so values at the boundary are still normal.
|
|
44
|
+
if (exist(ranges.hiAbsolute) && value >= ranges.hiAbsolute!) {
|
|
45
|
+
return 'OFF_SCALE_HIGH';
|
|
46
|
+
}
|
|
47
|
+
if (exist(ranges.hiCritical) && value >= ranges.hiCritical!) {
|
|
48
|
+
return 'CRITICALLY_HIGH';
|
|
49
|
+
}
|
|
50
|
+
if (exist(ranges.hiNormal) && value > ranges.hiNormal!) {
|
|
51
|
+
return 'HIGH';
|
|
52
|
+
}
|
|
53
|
+
if (exist(ranges.lowAbsolute) && value <= ranges.lowAbsolute!) {
|
|
54
|
+
return 'OFF_SCALE_LOW';
|
|
55
|
+
}
|
|
56
|
+
if (exist(ranges.lowCritical) && value <= ranges.lowCritical!) {
|
|
57
|
+
return 'CRITICALLY_LOW';
|
|
58
|
+
}
|
|
59
|
+
if (exist(ranges.lowNormal) && value < ranges.lowNormal!) {
|
|
60
|
+
return 'LOW';
|
|
61
|
+
}
|
|
62
|
+
return 'NORMAL';
|
|
63
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { type ReferenceRanges } from '../types';
|
|
4
|
+
import { formatReferenceRange } from './helpers';
|
|
5
|
+
import styles from './results.scss';
|
|
6
|
+
|
|
7
|
+
interface ReferenceRangeDisplayProps {
|
|
8
|
+
ranges: ReferenceRanges | null;
|
|
9
|
+
units?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Displays a formatted reference range with units.
|
|
14
|
+
* Shows "N/A" when no valid range is available.
|
|
15
|
+
*/
|
|
16
|
+
export const ReferenceRangeDisplay: React.FC<ReferenceRangeDisplayProps> = ({ ranges, units }) => {
|
|
17
|
+
const { t } = useTranslation();
|
|
18
|
+
const formatted = formatReferenceRange(ranges, units);
|
|
19
|
+
|
|
20
|
+
if (formatted === '--') {
|
|
21
|
+
return <span className={styles.noRange}>{t('notApplicable', 'N/A')}</span>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return <span className={styles.referenceRange}>{formatted}</span>;
|
|
25
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import useSWR from 'swr';
|
|
3
|
+
import { openmrsFetch, restBaseUrl, type FetchResponse } from '@openmrs/esm-framework';
|
|
4
|
+
import { type ReferenceRanges } from '../types';
|
|
5
|
+
|
|
6
|
+
interface ReferenceRangeResponse {
|
|
7
|
+
uuid: string;
|
|
8
|
+
concept: string;
|
|
9
|
+
lowNormal?: number;
|
|
10
|
+
hiNormal?: number;
|
|
11
|
+
lowAbsolute?: number;
|
|
12
|
+
hiAbsolute?: number;
|
|
13
|
+
lowCritical?: number;
|
|
14
|
+
hiCritical?: number;
|
|
15
|
+
units?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface UseReferenceRangesResult {
|
|
19
|
+
ranges: Map<string, ReferenceRanges>;
|
|
20
|
+
isLoading: boolean;
|
|
21
|
+
error: Error | undefined;
|
|
22
|
+
mutate: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Fetches patient-specific reference ranges for given concepts.
|
|
27
|
+
* Uses the /conceptreferencerange REST API endpoint.
|
|
28
|
+
*
|
|
29
|
+
* @param patientUuid - The UUID of the patient (optional; when absent, no request is made)
|
|
30
|
+
* @param conceptUuids - Array of concept UUIDs to fetch ranges for
|
|
31
|
+
* @returns Object containing ranges map, loading state, error, and mutate function
|
|
32
|
+
*/
|
|
33
|
+
export function useReferenceRanges(
|
|
34
|
+
patientUuid: string | undefined,
|
|
35
|
+
conceptUuids: Array<string>,
|
|
36
|
+
): UseReferenceRangesResult {
|
|
37
|
+
const conceptList = conceptUuids.filter(Boolean).join(',');
|
|
38
|
+
const apiUrl =
|
|
39
|
+
patientUuid && conceptList
|
|
40
|
+
? `${restBaseUrl}/conceptreferencerange?patient=${patientUuid}&concept=${conceptList}&v=full`
|
|
41
|
+
: null;
|
|
42
|
+
|
|
43
|
+
const { data, error, isLoading, mutate } = useSWR<FetchResponse<{ results: Array<ReferenceRangeResponse> }>, Error>(
|
|
44
|
+
apiUrl,
|
|
45
|
+
openmrsFetch,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const rangesMap = useMemo(() => {
|
|
49
|
+
const map = new Map<string, ReferenceRanges>();
|
|
50
|
+
data?.data?.results?.forEach((range) => {
|
|
51
|
+
if (range.concept) {
|
|
52
|
+
map.set(range.concept, {
|
|
53
|
+
lowNormal: range.lowNormal,
|
|
54
|
+
hiNormal: range.hiNormal,
|
|
55
|
+
lowAbsolute: range.lowAbsolute,
|
|
56
|
+
hiAbsolute: range.hiAbsolute,
|
|
57
|
+
lowCritical: range.lowCritical,
|
|
58
|
+
hiCritical: range.hiCritical,
|
|
59
|
+
units: range.units,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
return map;
|
|
64
|
+
}, [data]);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
ranges: rangesMap,
|
|
68
|
+
isLoading,
|
|
69
|
+
error,
|
|
70
|
+
mutate,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -16,7 +16,7 @@ const patientChartStore = createGlobalStore<PatientChartStore>(patientChartStore
|
|
|
16
16
|
mutateVisitContext: null,
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
const
|
|
19
|
+
const patientChartStoreActions = {
|
|
20
20
|
setPatient(_, patient: fhir.Patient) {
|
|
21
21
|
return { patient, patientUuid: patient?.id ?? null };
|
|
22
22
|
},
|
|
@@ -33,11 +33,11 @@ const patientCharStoreActions = {
|
|
|
33
33
|
* the clinical forms workspace in the ward app)
|
|
34
34
|
* should have the patient / visitContext explicitly passed in as props.
|
|
35
35
|
*
|
|
36
|
-
* As a safety feature, this hook requires the
|
|
36
|
+
* As a safety feature, this hook requires the patientUuid as the input, and only
|
|
37
37
|
* returns the actual store values if input patientUuid matches that in the store.
|
|
38
38
|
*/
|
|
39
39
|
export function usePatientChartStore(patientUuid: string) {
|
|
40
|
-
const store = useStoreWithActions(patientChartStore,
|
|
40
|
+
const store = useStoreWithActions(patientChartStore, patientChartStoreActions);
|
|
41
41
|
if (store.patientUuid === patientUuid) {
|
|
42
42
|
return store;
|
|
43
43
|
} else {
|
|
@@ -39,8 +39,19 @@ export type OBSERVATION_INTERPRETATION =
|
|
|
39
39
|
| 'LOW'
|
|
40
40
|
| 'CRITICALLY_LOW'
|
|
41
41
|
| 'OFF_SCALE_LOW'
|
|
42
|
+
// Legacy sentinel for "no data"; avoid new usage and prefer undefined instead.
|
|
42
43
|
| '--';
|
|
43
44
|
|
|
45
|
+
export interface ReferenceRanges {
|
|
46
|
+
hiAbsolute?: number;
|
|
47
|
+
hiCritical?: number;
|
|
48
|
+
hiNormal?: number;
|
|
49
|
+
lowAbsolute?: number;
|
|
50
|
+
lowCritical?: number;
|
|
51
|
+
lowNormal?: number;
|
|
52
|
+
units?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
44
55
|
export interface ExternalOverviewProps {
|
|
45
56
|
patientUuid: string;
|
|
46
57
|
filter: (filterProps: PanelFilterProps) => boolean;
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
invalidateVisitHistory,
|
|
3
3
|
invalidatePatientEncounters,
|
|
4
4
|
invalidateVisitAndEncounterData,
|
|
5
|
+
invalidateCurrentVisit,
|
|
5
6
|
} from './revalidation-utils';
|
|
6
7
|
|
|
7
8
|
const mockMutate = jest.fn();
|
|
@@ -95,4 +96,30 @@ describe('revalidation-utils', () => {
|
|
|
95
96
|
expect(encounterMatcherFn('/ws/rest/v1/encounter?patient=test-patient-123&v=custom')).toBe(true);
|
|
96
97
|
});
|
|
97
98
|
});
|
|
99
|
+
|
|
100
|
+
describe('invalidateCurrentVisit', () => {
|
|
101
|
+
it('should invalidate only current visit keys', () => {
|
|
102
|
+
const patientUuid = 'test-patient-123';
|
|
103
|
+
|
|
104
|
+
invalidateCurrentVisit(mockMutate, patientUuid);
|
|
105
|
+
|
|
106
|
+
expect(mockMutate).toHaveBeenCalledTimes(1);
|
|
107
|
+
expect(mockMutate).toHaveBeenCalledWith(expect.any(Function));
|
|
108
|
+
|
|
109
|
+
const matcherFn = mockMutate.mock.calls[0][0];
|
|
110
|
+
|
|
111
|
+
// Should match current visit key (includeInactive=false)
|
|
112
|
+
expect(matcherFn('/ws/rest/v1/visit?patient=test-patient-123&v=custom&includeInactive=false')).toBe(true);
|
|
113
|
+
|
|
114
|
+
// Should not match other visit keys
|
|
115
|
+
expect(matcherFn('/ws/rest/v1/visit?patient=test-patient-123&v=custom&includeInactive=true')).toBe(false);
|
|
116
|
+
expect(
|
|
117
|
+
matcherFn('/ws/rest/v1/visit?patient=test-patient-123&v=custom&limit=10&startIndex=0&totalCount=true'),
|
|
118
|
+
).toBe(false);
|
|
119
|
+
expect(matcherFn('/ws/rest/v1/visit/test-patient-123')).toBe(false);
|
|
120
|
+
|
|
121
|
+
// Should not match encounter keys
|
|
122
|
+
expect(matcherFn('/ws/rest/v1/encounter?patient=test-patient-123&v=custom')).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
98
125
|
});
|
|
@@ -81,6 +81,24 @@ export function invalidatePatientEncounters(mutate: KeyedMutator<unknown>, patie
|
|
|
81
81
|
});
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Invalidates only the current (active) visit cache for a specific patient.
|
|
86
|
+
*
|
|
87
|
+
* This refreshes components using useVisit without triggering the visit revalidation cascade.
|
|
88
|
+
*
|
|
89
|
+
* @param mutate - SWR mutate function from useSWRConfig()
|
|
90
|
+
* @param patientUuid - Patient UUID to target current visit data for
|
|
91
|
+
*/
|
|
92
|
+
export function invalidateCurrentVisit(mutate: KeyedMutator<unknown>, patientUuid: string): void {
|
|
93
|
+
mutate((key) => {
|
|
94
|
+
return (
|
|
95
|
+
typeof key === 'string' &&
|
|
96
|
+
key.includes(`${restBaseUrl}/visit?patient=${patientUuid}`) &&
|
|
97
|
+
key.includes('includeInactive=false')
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
84
102
|
/**
|
|
85
103
|
* Combination utility that invalidates both visit history and encounter data.
|
|
86
104
|
*
|
|
@@ -30,7 +30,7 @@ export function useOptimisticVisitMutations(patientUuid: string) {
|
|
|
30
30
|
(visitUuid: string, updates: Partial<Visit>) => {
|
|
31
31
|
// Update current visit SWR cache if it matches
|
|
32
32
|
if (visitContext?.uuid === visitUuid) {
|
|
33
|
-
mutateVisitContext();
|
|
33
|
+
mutateVisitContext?.();
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
// Update visit lists across all hooks using regex pattern matching
|
|
@@ -82,7 +82,7 @@ export function useOptimisticVisitMutations(patientUuid: string) {
|
|
|
82
82
|
|
|
83
83
|
// If deleted visit was current, revalidate current visit to get new state
|
|
84
84
|
if (visitContext?.uuid === visitUuid) {
|
|
85
|
-
mutateVisitContext();
|
|
85
|
+
mutateVisitContext?.();
|
|
86
86
|
}
|
|
87
87
|
},
|
|
88
88
|
[visitContext, mutateVisitContext, mutate, patientUuid],
|
package/src/workspaces.ts
CHANGED
|
@@ -21,8 +21,10 @@ export interface PatientChartWorkspaceActionButtonProps {
|
|
|
21
21
|
groupProps: PatientWorkspaceGroupProps;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export
|
|
25
|
-
extends
|
|
24
|
+
export type PatientWorkspace2DefinitionProps<
|
|
25
|
+
WorkspaceProps extends object,
|
|
26
|
+
WindowProps extends object,
|
|
27
|
+
> = Workspace2DefinitionProps<WorkspaceProps, WindowProps, PatientWorkspaceGroupProps>;
|
|
26
28
|
|
|
27
29
|
export function launchPatientChartWithWorkspaceOpen({
|
|
28
30
|
patientUuid,
|
|
@@ -45,7 +47,7 @@ export function useLaunchWorkspaceRequiringVisit<T extends object>(patientUuid:
|
|
|
45
47
|
(workspaceProps?: T, windowProps?: any, groupProps?: any) => {
|
|
46
48
|
startVisitIfNeeded().then((didStartVisit) => {
|
|
47
49
|
if (didStartVisit) {
|
|
48
|
-
launchWorkspace2(workspaceName, workspaceProps, windowProps);
|
|
50
|
+
launchWorkspace2(workspaceName, workspaceProps, windowProps, groupProps);
|
|
49
51
|
}
|
|
50
52
|
});
|
|
51
53
|
},
|