@openmrs/esm-patient-common-lib 11.3.1-patch.9508 → 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/empty-state/empty-state.test.tsx +4 -4
- package/src/form-entry/form-entry.ts +23 -2
- package/src/index.ts +1 -1
- package/src/launchStartVisitPrompt.tsx +2 -1
- package/src/orders/index.ts +4 -2
- package/src/orders/postOrders.ts +64 -51
- package/src/orders/showOrderSuccessToast.ts +86 -0
- package/src/orders/types/order.ts +128 -23
- package/src/orders/useMutatePatientOrders.ts +28 -0
- package/src/orders/useOrderTypes.ts +2 -2
- package/src/orders/useOrderableConceptSets.ts +1 -1
- package/src/orders/useOrders.ts +28 -7
- 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/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 +50 -24
- package/src/form-entry-interop.ts +0 -152
package/src/orders/useOrders.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback, useMemo } from 'react';
|
|
2
2
|
import useSWR, { useSWRConfig } from 'swr';
|
|
3
3
|
import { type FetchResponse, openmrsFetch, restBaseUrl, translateFrom } from '@openmrs/esm-framework';
|
|
4
|
-
import type { PatientOrderFetchResponse, PriorityOption } from './types';
|
|
4
|
+
import type { Order, PatientOrderFetchResponse, PriorityOption } from './types';
|
|
5
5
|
|
|
6
6
|
export type Status = 'ACTIVE' | 'any';
|
|
7
7
|
export const careSettingUuid = '6f0c9a92-6f24-11e3-af88-005056821db0';
|
|
@@ -10,11 +10,14 @@ const patientChartAppModuleName = '@openmrs/esm-patient-chart-app';
|
|
|
10
10
|
export const drugCustomRepresentation =
|
|
11
11
|
'custom:(uuid,dosingType,orderNumber,accessionNumber,' +
|
|
12
12
|
'patient:ref,action,careSetting:ref,previousOrder:ref,dateActivated,scheduledDate,dateStopped,autoExpireDate,' +
|
|
13
|
-
'orderType:ref,encounter:
|
|
13
|
+
'orderType:ref,encounter:(uuid,display,visit),orderer:(uuid,display,person:(display)),orderReason,orderReasonNonCoded,orderType,urgency,instructions,' +
|
|
14
14
|
'commentToFulfiller,drug:(uuid,display,strength,dosageForm:(display,uuid),concept),dose,doseUnits:ref,' +
|
|
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(
|
|
@@ -60,7 +68,20 @@ export function usePatientOrders(
|
|
|
60
68
|
}
|
|
61
69
|
|
|
62
70
|
export function getDrugOrderByUuid(orderUuid: string) {
|
|
63
|
-
return openmrsFetch(`${restBaseUrl}/order/${orderUuid}?v=${drugCustomRepresentation}`);
|
|
71
|
+
return openmrsFetch<Order>(`${restBaseUrl}/order/${orderUuid}?v=${drugCustomRepresentation}`);
|
|
72
|
+
}
|
|
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
|
+
};
|
|
64
85
|
}
|
|
65
86
|
|
|
66
87
|
// See the Urgency enum in https://github.com/openmrs/openmrs-core/blob/492dcd35b85d48730bd19da48f6db146cc882c22/api/src/main/java/org/openmrs/Order.java
|
|
@@ -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
|
+
}
|
|
@@ -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
|
@@ -1,23 +1,31 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
navigateAndLaunchWorkspace,
|
|
3
|
+
launchWorkspace2,
|
|
4
|
+
navigate,
|
|
6
5
|
showModal,
|
|
7
6
|
useFeatureFlag,
|
|
8
7
|
type Visit,
|
|
8
|
+
type Workspace2DefinitionProps,
|
|
9
9
|
} from '@openmrs/esm-framework';
|
|
10
|
-
import { launchStartVisitPrompt } from './launchStartVisitPrompt';
|
|
11
10
|
import { usePatientChartStore } from './store/patient-chart-store';
|
|
12
11
|
import { useSystemVisitSetting } from './useSystemVisitSetting';
|
|
13
12
|
|
|
14
|
-
export interface
|
|
13
|
+
export interface PatientWorkspaceGroupProps {
|
|
15
14
|
patient: fhir.Patient;
|
|
16
15
|
patientUuid: string;
|
|
17
16
|
visitContext: Visit;
|
|
18
17
|
mutateVisitContext: () => void;
|
|
19
18
|
}
|
|
20
19
|
|
|
20
|
+
export interface PatientChartWorkspaceActionButtonProps {
|
|
21
|
+
groupProps: PatientWorkspaceGroupProps;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type PatientWorkspace2DefinitionProps<
|
|
25
|
+
WorkspaceProps extends object,
|
|
26
|
+
WindowProps extends object,
|
|
27
|
+
> = Workspace2DefinitionProps<WorkspaceProps, WindowProps, PatientWorkspaceGroupProps>;
|
|
28
|
+
|
|
21
29
|
export function launchPatientChartWithWorkspaceOpen({
|
|
22
30
|
patientUuid,
|
|
23
31
|
workspaceName,
|
|
@@ -29,37 +37,55 @@ export function launchPatientChartWithWorkspaceOpen({
|
|
|
29
37
|
dashboardName?: string;
|
|
30
38
|
additionalProps?: object;
|
|
31
39
|
}) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
workspaceName: workspaceName,
|
|
35
|
-
contextKey: `patient/${patientUuid}`,
|
|
36
|
-
additionalProps,
|
|
37
|
-
});
|
|
40
|
+
launchWorkspace2(workspaceName, additionalProps);
|
|
41
|
+
navigate({ to: '${openmrsSpaBase}/patient/' + `${patientUuid}/chart` + (dashboardName ? `/${dashboardName}` : '') });
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
export function useLaunchWorkspaceRequiringVisit<T extends object>(patientUuid: string, workspaceName: string) {
|
|
45
|
+
const startVisitIfNeeded = useStartVisitIfNeeded(patientUuid);
|
|
46
|
+
const launchPatientWorkspaceCb = useCallback(
|
|
47
|
+
(workspaceProps?: T, windowProps?: any, groupProps?: any) => {
|
|
48
|
+
startVisitIfNeeded().then((didStartVisit) => {
|
|
49
|
+
if (didStartVisit) {
|
|
50
|
+
launchWorkspace2(workspaceName, workspaceProps, windowProps, groupProps);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
[startVisitIfNeeded, workspaceName],
|
|
55
|
+
);
|
|
56
|
+
return launchPatientWorkspaceCb;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function useStartVisitIfNeeded(patientUuid: string) {
|
|
41
60
|
const { visitContext } = usePatientChartStore(patientUuid);
|
|
42
61
|
const { systemVisitEnabled } = useSystemVisitSetting();
|
|
43
62
|
const isRdeEnabled = useFeatureFlag('rde');
|
|
44
63
|
|
|
45
|
-
const
|
|
46
|
-
(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
64
|
+
const startVisitIfNeeded = useCallback(async (): Promise<boolean> => {
|
|
65
|
+
if (!systemVisitEnabled || visitContext) {
|
|
66
|
+
return true;
|
|
67
|
+
} else {
|
|
68
|
+
return new Promise<boolean>((resolve) => {
|
|
50
69
|
if (isRdeEnabled) {
|
|
51
70
|
const dispose = showModal('visit-context-switcher', {
|
|
52
71
|
patientUuid,
|
|
53
|
-
closeModal: () =>
|
|
54
|
-
|
|
72
|
+
closeModal: () => {
|
|
73
|
+
dispose();
|
|
74
|
+
resolve(false);
|
|
75
|
+
},
|
|
76
|
+
onAfterVisitSelected: () => {
|
|
77
|
+
resolve(true);
|
|
78
|
+
},
|
|
55
79
|
size: 'sm',
|
|
56
80
|
});
|
|
57
81
|
} else {
|
|
58
|
-
|
|
82
|
+
const dispose = showModal('start-visit-dialog', {
|
|
83
|
+
closeModal: () => dispose(),
|
|
84
|
+
onVisitStarted: () => resolve(true),
|
|
85
|
+
});
|
|
59
86
|
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return launchPatientWorkspaceCb;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}, [visitContext, systemVisitEnabled, isRdeEnabled, patientUuid]);
|
|
90
|
+
return startVisitIfNeeded;
|
|
65
91
|
}
|