@kenyaemr/esm-pcs-app 5.4.5-pre.1092
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +6 -0
- package/dist/1072.js +1 -0
- package/dist/1072.js.map +1 -0
- package/dist/1102.js +1 -0
- package/dist/1102.js.map +1 -0
- package/dist/1105.js +1 -0
- package/dist/1105.js.map +1 -0
- package/dist/1148.js +1 -0
- package/dist/1148.js.map +1 -0
- package/dist/1278.js +1 -0
- package/dist/1278.js.map +1 -0
- package/dist/1442.js +1 -0
- package/dist/1442.js.map +1 -0
- package/dist/15.js +1 -0
- package/dist/15.js.map +1 -0
- package/dist/1620.js +1 -0
- package/dist/1620.js.map +1 -0
- package/dist/1789.js +1 -0
- package/dist/1789.js.map +1 -0
- package/dist/1818.js +1 -0
- package/dist/1818.js.map +1 -0
- package/dist/1831.js +1 -0
- package/dist/1831.js.map +1 -0
- package/dist/1844.js +1 -0
- package/dist/1844.js.map +1 -0
- package/dist/1854.js +1 -0
- package/dist/1854.js.map +1 -0
- package/dist/1946.js +1 -0
- package/dist/1946.js.map +1 -0
- package/dist/2007.js +1 -0
- package/dist/2007.js.map +1 -0
- package/dist/2115.js +1 -0
- package/dist/2115.js.map +1 -0
- package/dist/2152.js +1 -0
- package/dist/2152.js.map +1 -0
- package/dist/2202.js +1 -0
- package/dist/2202.js.map +1 -0
- package/dist/2394.js +1 -0
- package/dist/2394.js.map +1 -0
- package/dist/2530.js +1 -0
- package/dist/2530.js.map +1 -0
- package/dist/2534.js +1 -0
- package/dist/2534.js.map +1 -0
- package/dist/2537.js +1 -0
- package/dist/2537.js.map +1 -0
- package/dist/2556.js +1 -0
- package/dist/2556.js.map +1 -0
- package/dist/2648.js +1 -0
- package/dist/2648.js.map +1 -0
- package/dist/2795.js +1 -0
- package/dist/2795.js.map +1 -0
- package/dist/2827.js +1 -0
- package/dist/2827.js.map +1 -0
- package/dist/2916.js +1 -0
- package/dist/2916.js.map +1 -0
- package/dist/2924.js +1 -0
- package/dist/2924.js.map +1 -0
- package/dist/2947.js +1 -0
- package/dist/2947.js.map +1 -0
- package/dist/3022.js +1 -0
- package/dist/3022.js.map +1 -0
- package/dist/3285.js +1 -0
- package/dist/3285.js.map +1 -0
- package/dist/3335.js +1 -0
- package/dist/3335.js.map +1 -0
- package/dist/3378.js +1 -0
- package/dist/3378.js.map +1 -0
- package/dist/3381.js +1 -0
- package/dist/3381.js.map +1 -0
- package/dist/3504.js +1 -0
- package/dist/3504.js.map +1 -0
- package/dist/3530.js +1 -0
- package/dist/3530.js.map +1 -0
- package/dist/356.js +1 -0
- package/dist/356.js.map +1 -0
- package/dist/3569.js +1 -0
- package/dist/3569.js.map +1 -0
- package/dist/3723.js +1 -0
- package/dist/3723.js.map +1 -0
- package/dist/3745.js +1 -0
- package/dist/3745.js.map +1 -0
- package/dist/3813.js +1 -0
- package/dist/3813.js.map +1 -0
- package/dist/393.js +6 -0
- package/dist/393.js.map +1 -0
- package/dist/3953.js +1 -0
- package/dist/3953.js.map +1 -0
- package/dist/3989.js +1 -0
- package/dist/3989.js.map +1 -0
- package/dist/412.js +1 -0
- package/dist/412.js.map +1 -0
- package/dist/4178.js +1 -0
- package/dist/4178.js.map +1 -0
- package/dist/4413.js +1 -0
- package/dist/4413.js.map +1 -0
- package/dist/4487.js +11 -0
- package/dist/4487.js.map +1 -0
- package/dist/4537.js +1 -0
- package/dist/4537.js.map +1 -0
- package/dist/4588.js +1 -0
- package/dist/4588.js.map +1 -0
- package/dist/4637.js +1 -0
- package/dist/4637.js.map +1 -0
- package/dist/4650.js +1 -0
- package/dist/4650.js.map +1 -0
- package/dist/466.js +1 -0
- package/dist/466.js.map +1 -0
- package/dist/4817.js +1 -0
- package/dist/4817.js.map +1 -0
- package/dist/4818.js +1 -0
- package/dist/4818.js.map +1 -0
- package/dist/500.js +1 -0
- package/dist/500.js.map +1 -0
- package/dist/5086.js +1 -0
- package/dist/5086.js.map +1 -0
- package/dist/5117.js +1 -0
- package/dist/5117.js.map +1 -0
- package/dist/5279.js +1 -0
- package/dist/5279.js.map +1 -0
- package/dist/5366.js +1 -0
- package/dist/5366.js.map +1 -0
- package/dist/5598.js +1 -0
- package/dist/5598.js.map +1 -0
- package/dist/5676.js +1 -0
- package/dist/5676.js.map +1 -0
- package/dist/5731.js +5 -0
- package/dist/5731.js.map +1 -0
- package/dist/5792.js +1 -0
- package/dist/5792.js.map +1 -0
- package/dist/5825.js +1 -0
- package/dist/5825.js.map +1 -0
- package/dist/5932.js +1 -0
- package/dist/5932.js.map +1 -0
- package/dist/5941.js +1 -0
- package/dist/5941.js.map +1 -0
- package/dist/5967.js +1 -0
- package/dist/5967.js.map +1 -0
- package/dist/6104.js +1 -0
- package/dist/6104.js.map +1 -0
- package/dist/624.js +1 -0
- package/dist/624.js.map +1 -0
- package/dist/6259.js +1 -0
- package/dist/6259.js.map +1 -0
- package/dist/6273.js +1 -0
- package/dist/6273.js.map +1 -0
- package/dist/6337.js +1 -0
- package/dist/6337.js.map +1 -0
- package/dist/6427.js +1 -0
- package/dist/6427.js.map +1 -0
- package/dist/6478.js +7 -0
- package/dist/6478.js.map +1 -0
- package/dist/6508.js +1 -0
- package/dist/6508.js.map +1 -0
- package/dist/6561.js +1 -0
- package/dist/6561.js.map +1 -0
- package/dist/6566.js +1 -0
- package/dist/6566.js.map +1 -0
- package/dist/6567.js +1 -0
- package/dist/6567.js.map +1 -0
- package/dist/6634.js +1 -0
- package/dist/6634.js.map +1 -0
- package/dist/7022.js +1 -0
- package/dist/7022.js.map +1 -0
- package/dist/7085.js +1 -0
- package/dist/7085.js.map +1 -0
- package/dist/7209.js +1 -0
- package/dist/7209.js.map +1 -0
- package/dist/7291.js +1 -0
- package/dist/7291.js.map +1 -0
- package/dist/7360.js +1 -0
- package/dist/7360.js.map +1 -0
- package/dist/7453.js +1 -0
- package/dist/7453.js.map +1 -0
- package/dist/7454.js +1 -0
- package/dist/7454.js.map +1 -0
- package/dist/7478.js +1 -0
- package/dist/7478.js.map +1 -0
- package/dist/7778.js +1 -0
- package/dist/7778.js.map +1 -0
- package/dist/7782.js +1 -0
- package/dist/7782.js.map +1 -0
- package/dist/8028.js +1 -0
- package/dist/8028.js.map +1 -0
- package/dist/8031.js +1 -0
- package/dist/8031.js.map +1 -0
- package/dist/8196.js +1 -0
- package/dist/8196.js.map +1 -0
- package/dist/820.js +1 -0
- package/dist/820.js.map +1 -0
- package/dist/8240.js +1 -0
- package/dist/8240.js.map +1 -0
- package/dist/8472.js +1 -0
- package/dist/8472.js.map +1 -0
- package/dist/8607.js +1 -0
- package/dist/8607.js.map +1 -0
- package/dist/8793.js +1 -0
- package/dist/8793.js.map +1 -0
- package/dist/8924.js +1 -0
- package/dist/8924.js.map +1 -0
- package/dist/8990.js +1 -0
- package/dist/8990.js.map +1 -0
- package/dist/9060.js +1 -0
- package/dist/9060.js.map +1 -0
- package/dist/9061.js +1 -0
- package/dist/9061.js.map +1 -0
- package/dist/9087.js +1 -0
- package/dist/9087.js.map +1 -0
- package/dist/9165.js +1 -0
- package/dist/9165.js.map +1 -0
- package/dist/9274.js +1 -0
- package/dist/9274.js.map +1 -0
- package/dist/9425.js +1 -0
- package/dist/9520.js +1 -0
- package/dist/9520.js.map +1 -0
- package/dist/9547.js +1 -0
- package/dist/9547.js.map +1 -0
- package/dist/9612.js +1 -0
- package/dist/9612.js.map +1 -0
- package/dist/9771.js +1 -0
- package/dist/9771.js.map +1 -0
- package/dist/9774.js +1 -0
- package/dist/9774.js.map +1 -0
- package/dist/9806.js +1 -0
- package/dist/9806.js.map +1 -0
- package/dist/9885.js +1 -0
- package/dist/9885.js.map +1 -0
- package/dist/992.js +1 -0
- package/dist/992.js.map +1 -0
- package/dist/9941.js +1 -0
- package/dist/9941.js.map +1 -0
- package/dist/997.js +1 -0
- package/dist/997.js.map +1 -0
- package/dist/kenyaemr-esm-pcs-app.js +3 -0
- package/dist/kenyaemr-esm-pcs-app.js.buildmanifest.json +3173 -0
- package/dist/kenyaemr-esm-pcs-app.js.map +1 -0
- package/dist/main.js +3 -0
- package/dist/main.js.map +1 -0
- package/dist/routes.json +1 -0
- package/package.json +50 -0
- package/rspack.config.js +1 -0
- package/src/add-dependant/add-dependant.scss +29 -0
- package/src/add-dependant/add-dependant.workspace.test.ts +65 -0
- package/src/add-dependant/add-dependant.workspace.tsx +249 -0
- package/src/config-schema.ts +67 -0
- package/src/declarations.d.ts +2 -0
- package/src/empty-svg.component.tsx +565 -0
- package/src/index.ts +34 -0
- package/src/link-hie-dependant/link-hie-dependant.scss +55 -0
- package/src/link-hie-dependant/link-hie-dependant.workspace.tsx +207 -0
- package/src/linked/dependant-row.component.test.tsx +134 -0
- package/src/linked/dependant-row.component.tsx +122 -0
- package/src/linked/dependants-list.component.tsx +60 -0
- package/src/linked/linked-participant-body.component.tsx +82 -0
- package/src/linked/linked-participant.component.test.tsx +134 -0
- package/src/linked/linked-participant.component.tsx +142 -0
- package/src/linked/participant-details.component.tsx +89 -0
- package/src/linked/use-study-sync-snackbars.ts +41 -0
- package/src/modals/delink-participant.modal.tsx +96 -0
- package/src/modals/link-dependant.modal.tsx +244 -0
- package/src/modals/link-participant.modal.tsx +143 -0
- package/src/modals/link-participant.scss +48 -0
- package/src/pcs.scss +241 -0
- package/src/resources/hie-contacts.ts +61 -0
- package/src/resources/identifiers.ts +52 -0
- package/src/resources/link-dependant.resource.test.ts +445 -0
- package/src/resources/link-dependant.resource.ts +268 -0
- package/src/resources/link-participant.resource.test.ts +336 -0
- package/src/resources/link-participant.resource.ts +452 -0
- package/src/resources/patient-create.resource.ts +296 -0
- package/src/resources/patient-search.resource.ts +106 -0
- package/src/resources/pcs.resource.test.ts +36 -0
- package/src/resources/pcs.resource.ts +182 -0
- package/src/resources/use-identifier-type-uuids.ts +38 -0
- package/src/routes.json +54 -0
- package/src/search/participant-results.component.tsx +96 -0
- package/src/search/participant-tile.component.tsx +133 -0
- package/src/search/pcs-empty-state.component.tsx +31 -0
- package/src/search/pcs-results-skeleton.component.tsx +21 -0
- package/src/search/pcs-search.component.test.tsx +169 -0
- package/src/search/pcs-search.component.tsx +228 -0
- package/src/types/index.ts +140 -0
- package/translations/am.json +120 -0
- package/translations/en.json +120 -0
- package/translations/fr.json +122 -0
- package/translations/sw.json +120 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getSessionLocation,
|
|
3
|
+
launchWorkspace2,
|
|
4
|
+
launchWorkspaceGroup2,
|
|
5
|
+
openmrsFetch,
|
|
6
|
+
restBaseUrl,
|
|
7
|
+
type Visit,
|
|
8
|
+
} from '@openmrs/esm-framework';
|
|
9
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
10
|
+
import useSWR from 'swr';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Express-workflow's visit form props, redeclared rather than imported: the workspace registry is
|
|
14
|
+
* global so the form can be launched by name across the app boundary, but its module cannot be.
|
|
15
|
+
*/
|
|
16
|
+
interface VisitFormProps {
|
|
17
|
+
openedFrom: string;
|
|
18
|
+
showPatientHeader?: boolean;
|
|
19
|
+
}
|
|
20
|
+
import { getLocalIdentifierValue } from './identifiers';
|
|
21
|
+
import { findExistingLocalPatient, searchLocalPatientByIdentifier } from './patient-search.resource';
|
|
22
|
+
import { createPatientFromHiePatient } from './patient-create.resource';
|
|
23
|
+
import { formatParticipantName } from './pcs.resource';
|
|
24
|
+
import { type IdentifierTypeUuids, type PcsParticipant, type PcsSearchSubject } from '../types';
|
|
25
|
+
|
|
26
|
+
const PATIENT_REPRESENTATION =
|
|
27
|
+
'custom:(patientId,uuid,identifiers,display,patientIdentifier:(uuid,identifier),person:(gender,age,birthdate,birthdateEstimated,personName,addresses,display,dead,deathDate),attributes:(value,attributeType:(uuid,display)))';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Both attribute types are `java.lang.Boolean` format. OpenMRS stores `person_attribute.value`
|
|
31
|
+
* as a string column and hydrates them with `Boolean.valueOf(value)`, so "true"/"false"
|
|
32
|
+
* round-trip exactly — and the unchanged-value check in `writePersonAttribute` stays a plain
|
|
33
|
+
* string compare.
|
|
34
|
+
*/
|
|
35
|
+
const toAttributeValue = (flag: boolean) => String(Boolean(flag));
|
|
36
|
+
|
|
37
|
+
interface LinkParticipantOptions {
|
|
38
|
+
subject: PcsSearchSubject;
|
|
39
|
+
participant: PcsParticipant;
|
|
40
|
+
studyParticipantIdentifierType: string;
|
|
41
|
+
pbidsEnrollmentAttributeType: string;
|
|
42
|
+
cardseEnrollmentAttributeType: string;
|
|
43
|
+
/** The identifier types a created patient's HIE identifiers are written against. */
|
|
44
|
+
uuids: IdentifierTypeUuids;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Reads a patient by uuid. Used to pick up writes the caller just made — and by uuid rather
|
|
49
|
+
* than by identifier search, because a patient created moments ago may have nothing
|
|
50
|
+
* searchable on them yet.
|
|
51
|
+
*/
|
|
52
|
+
export async function readLocalPatient(patientUuid: string) {
|
|
53
|
+
const { data } = await openmrsFetch(`${restBaseUrl}/patient/${patientUuid}?v=${PATIENT_REPRESENTATION}`);
|
|
54
|
+
return data;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Read-only: the local record behind this subject, or null when they are not registered here. */
|
|
58
|
+
export async function findLocalPatientForSubject(subject: PcsSearchSubject) {
|
|
59
|
+
if (subject.source === 'local') {
|
|
60
|
+
return readLocalPatient(subject.id);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return findExistingLocalPatient(subject.patient, false);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolves the authorized patient to a local OpenMRS record, creating them when the search
|
|
68
|
+
* came from the HIE and they are not registered here yet.
|
|
69
|
+
*
|
|
70
|
+
* The create is kept separate from the check-in deliberately: express-workflow's own
|
|
71
|
+
* `registerOrLaunchHIEPatient` launches the visit workspace inside its create branch, which
|
|
72
|
+
* would fire before the study data is written.
|
|
73
|
+
*/
|
|
74
|
+
async function resolveLocalPatient(subject: PcsSearchSubject, uuids: IdentifierTypeUuids) {
|
|
75
|
+
const existing = await findLocalPatientForSubject(subject);
|
|
76
|
+
if (existing) {
|
|
77
|
+
return existing;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return createPatientFromHiePatient(subject.patient, uuids);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Whether this patient already carries a study participant ID. Having one means we hold a
|
|
85
|
+
* unique key into PCS, so the pane reports on that participant instead of searching.
|
|
86
|
+
*/
|
|
87
|
+
export function usePatientStudyLink(subject: PcsSearchSubject, studyParticipantIdentifierType: string) {
|
|
88
|
+
const key = `pcs-study-link/${subject.id}/${studyParticipantIdentifierType}`;
|
|
89
|
+
|
|
90
|
+
const { data, isLoading, error, mutate } = useSWR(key, () => findLocalPatientForSubject(subject), {
|
|
91
|
+
revalidateOnFocus: false,
|
|
92
|
+
revalidateOnReconnect: false,
|
|
93
|
+
shouldRetryOnError: false,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
localPatient: data ?? null,
|
|
98
|
+
studyParticipantId: getLocalIdentifierValue(data, studyParticipantIdentifierType),
|
|
99
|
+
isLoading,
|
|
100
|
+
error,
|
|
101
|
+
mutate,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Updates the study identifier in place when it already exists, rather than duplicating it. */
|
|
106
|
+
async function writeStudyIdentifier(localPatient: any, identifierType: string, value: string) {
|
|
107
|
+
const existing = localPatient?.identifiers?.find(
|
|
108
|
+
(identifier: any) => identifier.identifierType?.uuid === identifierType && !identifier.voided,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
if (existing) {
|
|
112
|
+
// Overwriting here would silently re-point the patient from one participant to another and
|
|
113
|
+
// destroy the previous link. Callers check before they get this far; this makes the case
|
|
114
|
+
// unreachable for the ones that don't, and for two registrars racing.
|
|
115
|
+
if (existing.identifier !== value) {
|
|
116
|
+
throw new Error(`This patient is already linked to PCS participant ${existing.identifier}`);
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const sessionLocation = await getSessionLocation();
|
|
122
|
+
const locationUuid =
|
|
123
|
+
sessionLocation?.uuid ??
|
|
124
|
+
localPatient?.identifiers?.find((identifier: any) => identifier.location?.uuid)?.location?.uuid;
|
|
125
|
+
|
|
126
|
+
await openmrsFetch(`${restBaseUrl}/patient/${localPatient.uuid}/identifier`, {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
headers: { 'Content-Type': 'application/json' },
|
|
129
|
+
body: {
|
|
130
|
+
identifier: value,
|
|
131
|
+
identifierType,
|
|
132
|
+
location: locationUuid,
|
|
133
|
+
preferred: false,
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Reads the person's attributes first so an existing one is updated rather than stacked.
|
|
140
|
+
* Returns whether anything was actually written — the background sync uses that to report
|
|
141
|
+
* what it corrected.
|
|
142
|
+
*/
|
|
143
|
+
async function writePersonAttribute(personUuid: string, attributeType: string, value: string): Promise<boolean> {
|
|
144
|
+
const response = await openmrsFetch(`${restBaseUrl}/person/${personUuid}/attribute?v=default`);
|
|
145
|
+
const attributes: Array<any> = response?.data?.results ?? [];
|
|
146
|
+
const existing = attributes.find((attribute) => attribute.attributeType?.uuid === attributeType && !attribute.voided);
|
|
147
|
+
|
|
148
|
+
if (existing) {
|
|
149
|
+
if (existing.value === value) {
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
await openmrsFetch(`${restBaseUrl}/person/${personUuid}/attribute/${existing.uuid}`, {
|
|
153
|
+
method: 'POST',
|
|
154
|
+
headers: { 'Content-Type': 'application/json' },
|
|
155
|
+
body: { value },
|
|
156
|
+
});
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
await openmrsFetch(`${restBaseUrl}/person/${personUuid}/attribute`, {
|
|
161
|
+
method: 'POST',
|
|
162
|
+
headers: { 'Content-Type': 'application/json' },
|
|
163
|
+
body: { attributeType, value },
|
|
164
|
+
});
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Exported for the add-dependant flow, which checks in without going through the stamping. */
|
|
169
|
+
export async function launchCheckInWorkspace(patient: any, patientUuid: string) {
|
|
170
|
+
await launchWorkspaceGroup2('ewf-patient-chart', {
|
|
171
|
+
patient,
|
|
172
|
+
patientUuid,
|
|
173
|
+
visitContext: null as unknown as Visit,
|
|
174
|
+
mutateVisitContext: () => {},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
launchWorkspace2<VisitFormProps, {}, {}>(
|
|
178
|
+
'custom-start-visit-workspace-form',
|
|
179
|
+
{
|
|
180
|
+
openedFrom: 'registration-check-in',
|
|
181
|
+
showPatientHeader: false,
|
|
182
|
+
},
|
|
183
|
+
{},
|
|
184
|
+
null,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Links the authorized patient to a PCS participant: resolve (creating if needed), stamp the
|
|
190
|
+
* study identifier and attributes, then check in. The workspace launches last and only once,
|
|
191
|
+
* so a failed write leaves the clinician on the modal to retry rather than dropping them into
|
|
192
|
+
* a visit form for a half-written record.
|
|
193
|
+
*/
|
|
194
|
+
export async function linkParticipantToPatient({
|
|
195
|
+
subject,
|
|
196
|
+
participant,
|
|
197
|
+
studyParticipantIdentifierType,
|
|
198
|
+
pbidsEnrollmentAttributeType,
|
|
199
|
+
cardseEnrollmentAttributeType,
|
|
200
|
+
uuids,
|
|
201
|
+
}: LinkParticipantOptions) {
|
|
202
|
+
const localPatient = await resolveLocalPatient(subject, uuids);
|
|
203
|
+
|
|
204
|
+
return stampAndCheckIn({
|
|
205
|
+
localPatient,
|
|
206
|
+
participant,
|
|
207
|
+
studyParticipantIdentifierType,
|
|
208
|
+
pbidsEnrollmentAttributeType,
|
|
209
|
+
cardseEnrollmentAttributeType,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
interface StampAndCheckInOptions {
|
|
214
|
+
localPatient: any;
|
|
215
|
+
participant: PcsParticipant;
|
|
216
|
+
studyParticipantIdentifierType: string;
|
|
217
|
+
pbidsEnrollmentAttributeType: string;
|
|
218
|
+
cardseEnrollmentAttributeType: string;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Stamps the study data onto an already-resolved patient and checks them in. Shared by the
|
|
223
|
+
* main link flow and the dependant one, which differ only in how they resolve the patient.
|
|
224
|
+
*
|
|
225
|
+
* The workspace launches last and exactly once, so a failed write leaves the caller able to
|
|
226
|
+
* report it rather than dropping the user into a visit form for a half-written record.
|
|
227
|
+
*/
|
|
228
|
+
export async function stampAndCheckIn({
|
|
229
|
+
localPatient,
|
|
230
|
+
participant,
|
|
231
|
+
studyParticipantIdentifierType,
|
|
232
|
+
pbidsEnrollmentAttributeType,
|
|
233
|
+
cardseEnrollmentAttributeType,
|
|
234
|
+
}: StampAndCheckInOptions) {
|
|
235
|
+
if (!localPatient?.uuid) {
|
|
236
|
+
throw new Error(`Could not resolve a local patient record for ${formatParticipantName(participant)}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
await writeStudyIdentifier(localPatient, studyParticipantIdentifierType, participant.individualId);
|
|
240
|
+
await syncStudyAttributes({
|
|
241
|
+
personUuid: localPatient.uuid,
|
|
242
|
+
participant,
|
|
243
|
+
pbidsEnrollmentAttributeType,
|
|
244
|
+
cardseEnrollmentAttributeType,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Re-read so the caller gets a record that actually carries what we just wrote. Without
|
|
248
|
+
// this the dependants row caches a patient whose identifiers predate the link and goes on
|
|
249
|
+
// offering to link them.
|
|
250
|
+
//
|
|
251
|
+
// Tolerating a failed read is deliberate: the writes have already succeeded by this point,
|
|
252
|
+
// so reporting failure would be a lie. The row stays stale until a refresh instead.
|
|
253
|
+
const updated = (await readLocalPatient(localPatient.uuid).catch(() => null)) ?? localPatient;
|
|
254
|
+
|
|
255
|
+
await launchCheckInWorkspace(updated, updated.uuid);
|
|
256
|
+
|
|
257
|
+
return updated;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The local patient carrying a participant's `individualId`, or null when nobody does.
|
|
262
|
+
*
|
|
263
|
+
* The value search is verified rather than trusted: called without an identifier type,
|
|
264
|
+
* `searchLocalPatientByIdentifier` falls back to a `q=` search that matches on
|
|
265
|
+
* `identifier.display.includes(value)`, so it can return a patient who merely contains the
|
|
266
|
+
* string. Confirming the exact value under one of the study types is what makes a match mean
|
|
267
|
+
* "linked".
|
|
268
|
+
*
|
|
269
|
+
* `mutate` matters here — the row links and unlinks against this, and would otherwise keep
|
|
270
|
+
* reporting the state it read before the write.
|
|
271
|
+
*/
|
|
272
|
+
export function useLinkedPatientForParticipant(individualId: string | null, identifierTypes: Array<string>) {
|
|
273
|
+
const {
|
|
274
|
+
data: localPatient,
|
|
275
|
+
isLoading,
|
|
276
|
+
error,
|
|
277
|
+
mutate,
|
|
278
|
+
} = useSWR(individualId ? `pcs-linked-patient/${individualId}` : null, () =>
|
|
279
|
+
searchLocalPatientByIdentifier(individualId!),
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const isLinked = identifierTypes.some(
|
|
283
|
+
(identifierType) => identifierType && getLocalIdentifierValue(localPatient, identifierType) === individualId,
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
return { linkedPatient: isLinked ? localPatient : null, isLoading, error, mutate };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
interface DelinkParticipantOptions {
|
|
290
|
+
localPatient: any;
|
|
291
|
+
studyParticipantIdentifierType: string;
|
|
292
|
+
pbidsEnrollmentAttributeType: string;
|
|
293
|
+
cardseEnrollmentAttributeType: string;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Removes the study data from a patient: the participant identifier and both attributes.
|
|
298
|
+
* OpenMRS voids rather than hard-deletes these, so the history survives.
|
|
299
|
+
*
|
|
300
|
+
* Anything already absent is skipped rather than treated as an error — a patient left
|
|
301
|
+
* half-written by a failed link must still be delinkable.
|
|
302
|
+
*/
|
|
303
|
+
export async function delinkParticipant({
|
|
304
|
+
localPatient,
|
|
305
|
+
studyParticipantIdentifierType,
|
|
306
|
+
pbidsEnrollmentAttributeType,
|
|
307
|
+
cardseEnrollmentAttributeType,
|
|
308
|
+
}: DelinkParticipantOptions) {
|
|
309
|
+
const personUuid = localPatient?.uuid;
|
|
310
|
+
|
|
311
|
+
if (!personUuid) {
|
|
312
|
+
throw new Error('Could not resolve a local patient record to delink');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const identifier = localPatient?.identifiers?.find(
|
|
316
|
+
(candidate: any) => candidate.identifierType?.uuid === studyParticipantIdentifierType && !candidate.voided,
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
if (identifier) {
|
|
320
|
+
await openmrsFetch(`${restBaseUrl}/patient/${personUuid}/identifier/${identifier.uuid}`, { method: 'DELETE' });
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const response = await openmrsFetch(`${restBaseUrl}/person/${personUuid}/attribute?v=default`);
|
|
324
|
+
const attributes: Array<any> = response?.data?.results ?? [];
|
|
325
|
+
|
|
326
|
+
for (const attributeType of [pbidsEnrollmentAttributeType, cardseEnrollmentAttributeType]) {
|
|
327
|
+
const attribute = attributes.find(
|
|
328
|
+
(candidate) => candidate.attributeType?.uuid === attributeType && !candidate.voided,
|
|
329
|
+
);
|
|
330
|
+
if (attribute) {
|
|
331
|
+
await openmrsFetch(`${restBaseUrl}/person/${personUuid}/attribute/${attribute.uuid}`, { method: 'DELETE' });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Which of the two enrolment flags a sync actually corrected. */
|
|
337
|
+
export type StudyAttributeFlag = 'pbids' | 'cardse';
|
|
338
|
+
|
|
339
|
+
interface SyncStudyAttributesOptions {
|
|
340
|
+
personUuid: string;
|
|
341
|
+
participant: PcsParticipant;
|
|
342
|
+
pbidsEnrollmentAttributeType: string;
|
|
343
|
+
cardseEnrollmentAttributeType: string;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Brings the patient's two enrolment attributes in line with what PCS currently reports.
|
|
348
|
+
* PCS owns these flags, so its values win over whatever is on the patient.
|
|
349
|
+
*
|
|
350
|
+
* Returns the flags that were actually written; an already-in-sync patient costs one read
|
|
351
|
+
* and no writes, because `writePersonAttribute` short-circuits on an unchanged value.
|
|
352
|
+
*/
|
|
353
|
+
export async function syncStudyAttributes({
|
|
354
|
+
personUuid,
|
|
355
|
+
participant,
|
|
356
|
+
pbidsEnrollmentAttributeType,
|
|
357
|
+
cardseEnrollmentAttributeType,
|
|
358
|
+
}: SyncStudyAttributesOptions): Promise<Array<StudyAttributeFlag>> {
|
|
359
|
+
const changed: Array<StudyAttributeFlag> = [];
|
|
360
|
+
|
|
361
|
+
if (
|
|
362
|
+
await writePersonAttribute(personUuid, pbidsEnrollmentAttributeType, toAttributeValue(participant.pbidsEnrolled))
|
|
363
|
+
) {
|
|
364
|
+
changed.push('pbids');
|
|
365
|
+
}
|
|
366
|
+
if (await writePersonAttribute(personUuid, cardseEnrollmentAttributeType, toAttributeValue(participant.cardse))) {
|
|
367
|
+
changed.push('cardse');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return changed;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
interface UseSyncStudyAttributesOptions {
|
|
374
|
+
participant: PcsParticipant | null;
|
|
375
|
+
localPatient: any;
|
|
376
|
+
pbidsEnrollmentAttributeType: string;
|
|
377
|
+
cardseEnrollmentAttributeType: string;
|
|
378
|
+
onSynced?: (changed: Array<StudyAttributeFlag>) => void;
|
|
379
|
+
onSyncError?: (error: unknown) => void;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Syncs the enrolment attributes in the background whenever a participant is pulled by id, so
|
|
384
|
+
* the patient record tracks PCS without anyone pressing anything.
|
|
385
|
+
*
|
|
386
|
+
* The signature ref is what keeps this to one sync per participant rather than one per render
|
|
387
|
+
* — without it React 18's StrictMode double-invoke alone would double-write.
|
|
388
|
+
*/
|
|
389
|
+
export function useSyncStudyAttributes({
|
|
390
|
+
participant,
|
|
391
|
+
localPatient,
|
|
392
|
+
pbidsEnrollmentAttributeType,
|
|
393
|
+
cardseEnrollmentAttributeType,
|
|
394
|
+
onSynced,
|
|
395
|
+
onSyncError,
|
|
396
|
+
}: UseSyncStudyAttributesOptions) {
|
|
397
|
+
const personUuid = localPatient?.uuid;
|
|
398
|
+
const signature = participant
|
|
399
|
+
? `${personUuid}|${participant.individualId}|${participant.pbidsEnrolled}|${participant.cardse}`
|
|
400
|
+
: null;
|
|
401
|
+
|
|
402
|
+
const syncedSignature = useRef<string | null>(null);
|
|
403
|
+
const [forcedAt, setForcedAt] = useState(0);
|
|
404
|
+
|
|
405
|
+
const callbacks = useRef({ onSynced, onSyncError });
|
|
406
|
+
callbacks.current = { onSynced, onSyncError };
|
|
407
|
+
|
|
408
|
+
useEffect(() => {
|
|
409
|
+
if (!participant || !personUuid || !signature) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (syncedSignature.current === signature) {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
syncedSignature.current = signature;
|
|
416
|
+
|
|
417
|
+
let cancelled = false;
|
|
418
|
+
syncStudyAttributes({
|
|
419
|
+
personUuid,
|
|
420
|
+
participant,
|
|
421
|
+
pbidsEnrollmentAttributeType,
|
|
422
|
+
cardseEnrollmentAttributeType,
|
|
423
|
+
})
|
|
424
|
+
.then((changed) => {
|
|
425
|
+
if (!cancelled && changed.length > 0) {
|
|
426
|
+
callbacks.current.onSynced?.(changed);
|
|
427
|
+
}
|
|
428
|
+
})
|
|
429
|
+
.catch((error) => {
|
|
430
|
+
// Let the next pull retry rather than pinning the failed signature.
|
|
431
|
+
syncedSignature.current = null;
|
|
432
|
+
if (!cancelled) {
|
|
433
|
+
callbacks.current.onSyncError?.(error);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
return () => {
|
|
438
|
+
cancelled = true;
|
|
439
|
+
};
|
|
440
|
+
}, [participant, personUuid, signature, pbidsEnrollmentAttributeType, cardseEnrollmentAttributeType, forcedAt]);
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Reconciles even when the PCS values are unchanged — the guard would otherwise suppress a
|
|
444
|
+
* sync that is needed because the *patient's* attributes drifted.
|
|
445
|
+
*/
|
|
446
|
+
const syncNow = useCallback(() => {
|
|
447
|
+
syncedSignature.current = null;
|
|
448
|
+
setForcedAt(Date.now());
|
|
449
|
+
}, []);
|
|
450
|
+
|
|
451
|
+
return { syncNow };
|
|
452
|
+
}
|