@kenyaemr/esm-shr-app 5.4.2-pre.2364 → 5.4.2-pre.2368

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/dist/routes.json CHANGED
@@ -1 +1 @@
1
- {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"kenyaemr":"^19.0.0"},"pages":[{"component":"shrRoot","route":"referrals"}],"extensions":[{"component":"shrAuthorizationForm","name":"shr-authorization-form"},{"name":"patient-chart-shr-summary-dashboard-link","component":"shrSummaryDashboardLink","slot":"patient-chart-dashboard-slot","meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-shr-summary-dashboard-slot","path":"SHR Portal"},"featureFlag":"healthInformationExchange"},{"name":"SHR Portal","slot":"patient-chart-shr-summary-dashboard-slot","component":"patientSHRSummary","meta":{"columnSpan":4}},{"name":"referrals","slot":"referrals-slot","component":"shrRoot"},{"name":"referrals-link","slot":"homepage-dashboard-slot","component":"ReferralsDashboardLink","meta":{"name":"referrals","title":"referrals","slot":"referrals-slot"}},{"name":"referral-reasons-dialog","component":"referralReasonsDialogPopup"},{"name":"facility-referral-form","component":"facilityRefferalForm"},{"name":"referral-view-link","component":"referralLink","slot":"patient-chart-dashboard-slot","order":13,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-referral-slot","path":"referral","layoutMode":"anchored"}},{"name":"referral-view","slot":"patient-chart-referral-slot","component":"referralWidget","order":0,"online":true,"offline":false}],"version":"5.4.2-pre.2364"}
1
+ {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"kenyaemr":"^19.0.0"},"pages":[{"component":"shrRoot","route":"referrals"}],"extensions":[{"component":"shrAuthorizationForm","name":"shr-authorization-form"},{"name":"patient-chart-shr-summary-dashboard-link","component":"shrSummaryDashboardLink","slot":"patient-chart-dashboard-slot","meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-shr-summary-dashboard-slot","path":"SHR Portal"},"featureFlag":"healthInformationExchange"},{"name":"SHR Portal","slot":"patient-chart-shr-summary-dashboard-slot","component":"patientSHRSummary","meta":{"columnSpan":4}},{"name":"referrals","slot":"referrals-slot","component":"shrRoot"},{"name":"referrals-link","slot":"homepage-dashboard-slot","component":"ReferralsDashboardLink","meta":{"name":"referrals","title":"referrals","slot":"referrals-slot"}},{"name":"referral-reasons-dialog","component":"referralReasonsDialogPopup"},{"name":"facility-referral-form","component":"facilityRefferalForm"},{"name":"referral-view-link","component":"referralLink","slot":"patient-chart-dashboard-slot","order":13,"meta":{"columns":1,"columnSpan":1,"slot":"patient-chart-referral-slot","path":"referral","layoutMode":"anchored"}},{"name":"referral-view","slot":"patient-chart-referral-slot","component":"referralWidget","order":0,"online":true,"offline":false}],"version":"5.4.2-pre.2368"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kenyaemr/esm-shr-app",
3
- "version": "5.4.2-pre.2364",
3
+ "version": "5.4.2-pre.2368",
4
4
  "description": "Patient care panels microfrontend for the OpenMRS SPA",
5
5
  "browser": "dist/kenyaemr-esm-shr-app.js",
6
6
  "main": "src/index.ts",
@@ -16,10 +16,16 @@ export const configSchema = {
16
16
  _description: 'Social Health Authority Unique Identification Number',
17
17
  _default: '24aedd37-b5be-4e08-8311-3721b8d5100d',
18
18
  },
19
+ artDirectoryUrl: {
20
+ _type: Type.String,
21
+ _description: 'URL for the ART Directory',
22
+ _default: 'https://art.kenyahmis.org/api/patients',
23
+ },
19
24
  };
20
25
 
21
26
  export type ReferralConfigObject = {
22
27
  nationalPatientUniqueIdentifier: string;
23
28
  phoneNumberAttributeType: string;
24
29
  socialHealthAuthorityIdentifierType: string;
30
+ artDirectoryUrl: string;
25
31
  };
@@ -1,4 +1,4 @@
1
- import { FetchResponse, openmrsFetch, Patient, restBaseUrl, useConfig } from '@openmrs/esm-framework';
1
+ import { type FetchResponse, openmrsFetch, type Patient, restBaseUrl, useConfig } from '@openmrs/esm-framework';
2
2
  import useSWR from 'swr';
3
3
  import { ReferralConfigObject } from '../config-schema';
4
4
 
@@ -0,0 +1,30 @@
1
+ import { z } from 'zod';
2
+
3
+ export const facilityReferralSchema = z.object({
4
+ referralType: z.string().min(1, 'Referral type is required'),
5
+ patientUuid: z.string().min(1, 'Patient selection is required'),
6
+ selectedFacility: z
7
+ .object({
8
+ uuid: z.string(),
9
+ name: z.string(),
10
+ attributes: z.array(z.any()),
11
+ })
12
+ .nullable()
13
+ .refine((facility) => facility !== null, {
14
+ message: 'Destination facility is required',
15
+ }),
16
+ selectedReasons: z
17
+ .array(
18
+ z.object({
19
+ uuid: z.string(),
20
+ name: z.object({
21
+ name: z.string(),
22
+ }),
23
+ }),
24
+ )
25
+ .min(1, 'At least one referral reason is required'),
26
+ clinicalNotes: z.string().min(10, 'Clinical notes must be at least 10 characters long'),
27
+ });
28
+
29
+ export type FacilityReferralFormData = z.infer<typeof facilityReferralSchema>;
30
+ export type ValidationErrors = Partial<Record<keyof FacilityReferralFormData, string>>;
@@ -209,3 +209,115 @@ export interface PatientIdentifier {
209
209
  display: string;
210
210
  };
211
211
  }
212
+
213
+ export interface Concept {
214
+ uuid: string;
215
+ name: {
216
+ name: string;
217
+ };
218
+ conceptClass: {
219
+ name: string;
220
+ };
221
+ }
222
+
223
+ export interface ReasonResponse {
224
+ results: Concept[];
225
+ }
226
+ export interface Facility {
227
+ uuid: string;
228
+ name: string;
229
+ attributes: [
230
+ {
231
+ value: string;
232
+ },
233
+ ];
234
+ }
235
+
236
+ export interface FacilityResponse {
237
+ results: Facility[];
238
+ }
239
+
240
+ export interface ReferralPayload {
241
+ MESSAGE_HEADER: {
242
+ SENDING_APPLICATION: string;
243
+ SENDING_FACILITY: string;
244
+ RECEIVING_APPLICATION: string;
245
+ RECEIVING_FACILITY: string;
246
+ MESSAGE_DATETIME: string;
247
+ SECURITY: null;
248
+ MESSAGE_TYPE: string;
249
+ PROCESSING_ID: string;
250
+ };
251
+ PATIENT_IDENTIFICATION: {
252
+ EXTERNAL_PATIENT_ID: {
253
+ ID: string | null;
254
+ IDENTIFIER_TYPE: string;
255
+ ASSIGNING_AUTHORITY: string;
256
+ };
257
+ INTERNAL_PATIENT_ID: Array<{
258
+ ID: string;
259
+ IDENTIFIER_TYPE: string;
260
+ ASSIGNING_AUTHORITY: string;
261
+ }>;
262
+ PATIENT_NAME: {
263
+ FIRST_NAME: string;
264
+ MIDDLE_NAME: string;
265
+ LAST_NAME: string;
266
+ };
267
+ MOTHER_NAME: {
268
+ FIRST_NAME: string | null;
269
+ MIDDLE_NAME: string | null;
270
+ LAST_NAME: string | null;
271
+ };
272
+ DATE_OF_BIRTH: string;
273
+ SEX: string;
274
+ PATIENT_ADDRESS: {
275
+ PHYSICAL_ADDRESS: {
276
+ VILLAGE: string;
277
+ WARD: string;
278
+ SUB_COUNTY: string;
279
+ COUNTY: string;
280
+ GPS_LOCATION: string | null;
281
+ NEAREST_LANDMARK: string | null;
282
+ };
283
+ POSTAL_ADDRESS: string | null;
284
+ };
285
+ PHONE_NUMBER: string;
286
+ MARITAL_STATUS: string | null;
287
+ DEATH_DATE: string | null;
288
+ DEATH_INDICATOR: string | null;
289
+ DATE_OF_BIRTH_PRECISION: string;
290
+ };
291
+ DISCONTINUATION_MESSAGE: {
292
+ DISCONTINUATION_REASON: string;
293
+ EFFECTIVE_DISCONTINUATION_DATE: string;
294
+ TARGET_PROGRAM: string;
295
+ SERVICE_REQUEST: {
296
+ TRANSFER_STATUS: string;
297
+ TRANSFER_INTENT: string;
298
+ TRANSFER_PRIORITY: string;
299
+ TRANSFER_OUT_DATE: string;
300
+ TO_ACCEPTANCE_DATE: string | null;
301
+ SENDING_FACILITY_MFLCODE: string;
302
+ RECEIVING_FACILITY_MFLCODE: string;
303
+ SUPPORTING_INFO: string | null;
304
+ };
305
+ };
306
+ }
307
+
308
+ interface ApiSuccessResponse {
309
+ status: 'success';
310
+ message: ReferralPayload;
311
+ }
312
+
313
+ export interface ReferralResponse {
314
+ success: boolean;
315
+ data: ApiSuccessResponse;
316
+ payload: ReferralPayload;
317
+ }
318
+
319
+ export interface ReferralError extends Error {
320
+ status?: number;
321
+ statusText?: string;
322
+ responseBody?: string;
323
+ }
@@ -1,5 +1,6 @@
1
1
  import mergeWith from 'lodash/mergeWith';
2
2
  import isArray from 'lodash/isArray';
3
+ import { formatDate, Patient } from '@openmrs/esm-framework';
3
4
 
4
5
  export function extractNameString(formattedString) {
5
6
  if (!formattedString) {
@@ -32,3 +33,187 @@ function customizer(objValue: Record<string, any>, srcValue: Record<string, any>
32
33
  export function deepMerge(obj1: Record<string, any>, obj2: Record<string, any>) {
33
34
  return mergeWith({}, obj1, obj2, customizer);
34
35
  }
36
+
37
+ export const getPatientName = (patient: Patient) => {
38
+ let displayName = '';
39
+
40
+ if (patient?.person?.preferredName?.display) {
41
+ displayName = patient.person.preferredName.display;
42
+ } else if (patient?.person?.display || patient?.display) {
43
+ displayName = patient.person?.display || patient.display;
44
+ }
45
+
46
+ if (displayName) {
47
+ const nameParts = displayName.trim().split(/\s+/).filter(Boolean);
48
+
49
+ if (nameParts.length === 1) {
50
+ return {
51
+ FIRST_NAME: nameParts[0],
52
+ MIDDLE_NAME: '',
53
+ LAST_NAME: '',
54
+ };
55
+ }
56
+
57
+ if (nameParts.length === 2) {
58
+ return {
59
+ FIRST_NAME: nameParts[0],
60
+ MIDDLE_NAME: '',
61
+ LAST_NAME: nameParts[1],
62
+ };
63
+ }
64
+
65
+ return {
66
+ FIRST_NAME: nameParts[0],
67
+ MIDDLE_NAME: nameParts.slice(1, -1).join(' '),
68
+ LAST_NAME: nameParts[nameParts.length - 1],
69
+ };
70
+ }
71
+
72
+ return {
73
+ FIRST_NAME: '',
74
+ MIDDLE_NAME: '',
75
+ LAST_NAME: '',
76
+ };
77
+ };
78
+
79
+ export const getPatientGender = (patient: Patient) => {
80
+ const gender = patient?.person?.gender;
81
+ if (gender) {
82
+ const genderUpper = gender.toUpperCase();
83
+ if (genderUpper === 'MALE' || genderUpper === 'M') {
84
+ return 'M';
85
+ }
86
+ if (genderUpper === 'FEMALE' || genderUpper === 'F') {
87
+ return 'F';
88
+ }
89
+ return genderUpper.charAt(0);
90
+ }
91
+
92
+ return 'M';
93
+ };
94
+ export const getPatientAddress = (patient: Patient) => {
95
+ if (patient?.person?.preferredAddress?.display) {
96
+ const addressParts = patient.person.preferredAddress.display.split(',').map((part) => part.trim());
97
+ return {
98
+ VILLAGE: addressParts[0] || 'VILLAGE',
99
+ WARD: addressParts[1] || 'VILLAGE',
100
+ SUB_COUNTY: addressParts[2] || 'VILLAGE',
101
+ COUNTY: addressParts[3] || 'COUNTY',
102
+ GPS_LOCATION: null,
103
+ NEAREST_LANDMARK: null,
104
+ };
105
+ }
106
+
107
+ if (patient?.person?.attributes?.length) {
108
+ const addressAttrs = patient.person.attributes.filter(
109
+ (attr) =>
110
+ attr.attributeType?.display?.toLowerCase().includes('address') ||
111
+ attr.attributeType?.display?.toLowerCase().includes('location') ||
112
+ attr.attributeType?.display?.toLowerCase().includes('county') ||
113
+ attr.attributeType?.display?.toLowerCase().includes('village'),
114
+ );
115
+
116
+ if (addressAttrs.length > 0) {
117
+ return {
118
+ VILLAGE:
119
+ addressAttrs.find((a) => a.attributeType?.display?.toLowerCase().includes('village'))?.value || 'VILLAGE',
120
+ WARD: addressAttrs.find((a) => a.attributeType?.display?.toLowerCase().includes('ward'))?.value || 'VILLAGE',
121
+ SUB_COUNTY:
122
+ addressAttrs.find((a) => a.attributeType?.display?.toLowerCase().includes('sub'))?.value || 'VILLAGE',
123
+ COUNTY: addressAttrs.find((a) => a.attributeType?.display?.toLowerCase().includes('county'))?.value || 'COUNTY',
124
+ GPS_LOCATION: null,
125
+ NEAREST_LANDMARK: null,
126
+ };
127
+ }
128
+ }
129
+
130
+ return {
131
+ VILLAGE: 'VILLAGE',
132
+ WARD: 'VILLAGE',
133
+ SUB_COUNTY: 'VILLAGE',
134
+ COUNTY: 'COUNTY',
135
+ GPS_LOCATION: null,
136
+ NEAREST_LANDMARK: null,
137
+ };
138
+ };
139
+
140
+ export const getPatientIdentifiers = (patient: Patient) => {
141
+ const identifiers = [];
142
+
143
+ if (patient?.identifiers?.length) {
144
+ patient.identifiers.forEach((id) => {
145
+ if (id.identifier && id.identifierType?.name) {
146
+ const idTypeName = id.identifierType.name.toUpperCase();
147
+
148
+ if (
149
+ idTypeName.includes('CCC') ||
150
+ idTypeName.includes('COMPREHENSIVE') ||
151
+ idTypeName.includes('CARE') ||
152
+ idTypeName.includes('CLINIC')
153
+ ) {
154
+ identifiers.push({
155
+ ID: id.identifier,
156
+ IDENTIFIER_TYPE: 'CCC_NUMBER',
157
+ ASSIGNING_AUTHORITY: 'CCC',
158
+ });
159
+ } else if (idTypeName.includes('NATIONAL') || idTypeName.includes('ID')) {
160
+ identifiers.push({
161
+ ID: id.identifier,
162
+ IDENTIFIER_TYPE: 'NATIONAL_ID',
163
+ ASSIGNING_AUTHORITY: 'GOK',
164
+ });
165
+ } else if (idTypeName.includes('FACILITY') || idTypeName.includes('MRN')) {
166
+ identifiers.push({
167
+ ID: id.identifier,
168
+ IDENTIFIER_TYPE: 'FACILITY_ID',
169
+ ASSIGNING_AUTHORITY: 'FACILITY',
170
+ });
171
+ }
172
+ }
173
+ });
174
+ }
175
+
176
+ if (identifiers.length === 0) {
177
+ const defaultCccNumber = `1234567${String(Math.floor(Math.random() * 100)).padStart(2, '0')}${Math.floor(
178
+ Math.random() * 10,
179
+ )}`;
180
+ identifiers.push({
181
+ ID: defaultCccNumber,
182
+ IDENTIFIER_TYPE: 'CCC_NUMBER',
183
+ ASSIGNING_AUTHORITY: 'CCC',
184
+ });
185
+ }
186
+
187
+ return identifiers;
188
+ };
189
+
190
+ export const getPhoneNumber = (patient: Patient) => {
191
+ if (patient?.person?.attributes?.length) {
192
+ const phoneAttr = patient.person.attributes.find(
193
+ (attr) =>
194
+ attr.attributeType?.display?.toLowerCase().includes('phone') ||
195
+ attr.attributeType?.display?.toLowerCase().includes('contact') ||
196
+ attr.attributeType?.display?.toLowerCase().includes('telephone'),
197
+ );
198
+ return phoneAttr?.value || '';
199
+ }
200
+ return '';
201
+ };
202
+
203
+ export const formatBirthDate = (patient: Patient) => {
204
+ const birthDate = patient?.person?.birthdate;
205
+ if (birthDate) {
206
+ return formatDate(new Date(birthDate), { mode: 'standard' });
207
+ }
208
+ return '';
209
+ };
210
+
211
+ export const getPatientDeathInfo = (patient: Patient) => {
212
+ const dead = patient?.person?.dead;
213
+ const deathDate = patient?.person?.deathDate;
214
+
215
+ return {
216
+ DEATH_DATE: dead && deathDate ? formatDate(new Date(deathDate), { mode: 'standard' }) : null,
217
+ DEATH_INDICATOR: dead ? 'YES' : null,
218
+ };
219
+ };
@@ -1,45 +1,81 @@
1
- import { openmrsFetch } from '@openmrs/esm-framework';
1
+ import { openmrsFetch, OpenmrsResource, restBaseUrl, useConfig } from '@openmrs/esm-framework';
2
2
  import useSWRImmutable from 'swr/immutable';
3
+ import { FacilityResponse, ReasonResponse, ReferralError, ReferralPayload, ReferralResponse } from '../types';
4
+ import { ReferralConfigObject } from '../config-schema';
3
5
 
4
- export interface Concept {
5
- uuid: string;
6
- name: {
7
- name: string;
8
- };
9
- conceptClass: {
10
- name: string;
11
- };
12
- }
13
-
14
- export interface ReasonResponse {
15
- results: Concept[];
16
- }
17
- export interface Facility {
18
- uuid: string;
19
- name: string;
20
- attributes: [
21
- {
22
- value: string;
23
- },
24
- ];
25
- }
26
-
27
- export interface FacilityResponse {
28
- results: Facility[];
29
- }
30
-
31
- export const useReason = (searchTerm: string) => {
32
- const customRepresentation = 'custom:(uuid,name)';
6
+ export const useReasons = (searchTerm: string) => {
7
+ const customRepresentation = 'custom:(uuid,name:(name))';
33
8
  const url = `/ws/rest/v1/concept?v=${customRepresentation}&q=${searchTerm}&limit=15`;
34
9
  const { data, error } = useSWRImmutable<{ data: ReasonResponse }>(searchTerm ? url : null, openmrsFetch);
35
10
 
36
11
  return { data: data?.data?.results, error };
37
12
  };
38
13
 
39
- export const useFacility = (searchTerm: string) => {
14
+ export const useFacilities = (searchTerm: string) => {
40
15
  const customRepresentation = 'custom:(uuid,name,attributes:(value))';
41
16
  const url = `/ws/rest/v1/location?v=${customRepresentation}&q=${searchTerm}&limit=15`;
42
17
  const { data, error } = useSWRImmutable<{ data: FacilityResponse }>(searchTerm ? url : null, openmrsFetch);
43
18
 
44
19
  return { data: data?.data?.results, error };
45
20
  };
21
+
22
+ export const useSendReferralToArtDirectory = () => {
23
+ const { artDirectoryUrl } = useConfig<ReferralConfigObject>();
24
+
25
+ const sendReferral = async (apiPayload: ReferralPayload): Promise<ReferralResponse> => {
26
+ try {
27
+ const response = await openmrsFetch(artDirectoryUrl, {
28
+ method: 'POST',
29
+ headers: {
30
+ 'Content-Type': 'application/json',
31
+ Accept: 'application/json',
32
+ },
33
+ body: JSON.stringify(apiPayload),
34
+ });
35
+
36
+ if (!response.ok) {
37
+ const errorText = await response.text();
38
+ const error: ReferralError = new Error(
39
+ `Failed to send referral: ${response.status} ${response.statusText}. ${errorText}`,
40
+ );
41
+ error.status = response.status;
42
+ error.statusText = response.statusText;
43
+ error.responseBody = errorText;
44
+ throw error;
45
+ }
46
+
47
+ return {
48
+ success: true,
49
+ data: await response.json(),
50
+ payload: apiPayload,
51
+ };
52
+ } catch (error) {
53
+ if (error instanceof Error && 'status' in error) {
54
+ throw error;
55
+ }
56
+
57
+ const networkError: ReferralError = new Error(
58
+ `Network error while sending referral: ${error instanceof Error ? error.message : 'Unknown error'}`,
59
+ );
60
+ throw networkError;
61
+ }
62
+ };
63
+
64
+ return { mutate: sendReferral };
65
+ };
66
+
67
+ export function useSystemSetting(key: string) {
68
+ const { data, isLoading } = useSWRImmutable<{ data: { results: Array<OpenmrsResource> } }>(
69
+ `${restBaseUrl}systemsetting?q=${key}&v=full`,
70
+ openmrsFetch,
71
+ {
72
+ revalidateOnFocus: false,
73
+ revalidateOnReconnect: false,
74
+ },
75
+ );
76
+
77
+ const mflCodeResource = data?.data?.results?.find((resource) => resource.property === 'facility.mflcode');
78
+ const mflCodeValue = mflCodeResource?.value;
79
+
80
+ return { mflCodeValue, isLoading };
81
+ }