@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.
@@ -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:ref,orderer:(uuid,display,person:(display)),orderReason,orderReasonNonCoded,orderType,urgency,instructions,' +
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=default&activatedOnOrAfterDate=${startDate}&activatedOnOrBeforeDate=${endDate}&excludeDiscontinueOrders=true`
29
- : `${restBaseUrl}/order?patient=${patientUuid}&careSetting=${careSettingUuid}&v=default&status=${status}&excludeDiscontinueOrders=true`;
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((key) => typeof key === 'string' && key.startsWith(`${restBaseUrl}/order?patient=${patientUuid}`), data),
40
- [data, mutate, patientUuid],
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,3 @@
1
+ export * from './helpers';
2
+ export * from './useReferenceRanges';
3
+ export { ReferenceRangeDisplay } from './reference-range-display.component';
@@ -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,11 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/type';
3
+ @use '@openmrs/esm-styleguide/src/vars' as *;
4
+
5
+ .referenceRange {
6
+ @include type.type-style('body-compact-01');
7
+ }
8
+
9
+ .noRange {
10
+ color: colors.$gray-50;
11
+ }
@@ -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
- type DefaultWorkspaceProps,
4
- launchWorkspace,
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 DefaultPatientWorkspaceProps extends DefaultWorkspaceProps {
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
- navigateAndLaunchWorkspace({
33
- targetUrl: '${openmrsSpaBase}/patient/' + `${patientUuid}/chart` + (dashboardName ? `/${dashboardName}` : ''),
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 launchPatientWorkspaceCb = useCallback(
46
- (additionalProps?: T) => {
47
- if (!systemVisitEnabled || visitContext) {
48
- launchWorkspace(workspaceName, additionalProps);
49
- } else {
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: () => dispose(),
54
- onAfterVisitSelected: () => launchWorkspace(workspaceName, additionalProps),
72
+ closeModal: () => {
73
+ dispose();
74
+ resolve(false);
75
+ },
76
+ onAfterVisitSelected: () => {
77
+ resolve(true);
78
+ },
55
79
  size: 'sm',
56
80
  });
57
81
  } else {
58
- launchStartVisitPrompt();
82
+ const dispose = showModal('start-visit-dialog', {
83
+ closeModal: () => dispose(),
84
+ onVisitStarted: () => resolve(true),
85
+ });
59
86
  }
60
- }
61
- },
62
- [visitContext, systemVisitEnabled, workspaceName, isRdeEnabled, patientUuid],
63
- );
64
- return launchPatientWorkspaceCb;
87
+ });
88
+ }
89
+ }, [visitContext, systemVisitEnabled, isRdeEnabled, patientUuid]);
90
+ return startVisitIfNeeded;
65
91
  }