@digihmis/esm-outpatient-app 1.0.3 → 1.0.5

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.
Files changed (30) hide show
  1. package/dist/{564.js → 140.js} +1 -1
  2. package/dist/140.js.map +1 -0
  3. package/dist/409.js +1 -0
  4. package/dist/409.js.map +1 -0
  5. package/dist/594.js +1 -1
  6. package/dist/594.js.map +1 -1
  7. package/dist/digihmis-esm-outpatient-app.js +2 -2
  8. package/dist/digihmis-esm-outpatient-app.js.buildmanifest.json +54 -54
  9. package/dist/main.js +5 -5
  10. package/dist/main.js.map +1 -1
  11. package/dist/routes.json +1 -1
  12. package/package.json +1 -1
  13. package/src/config-schema.ts +7 -0
  14. package/src/dashboard.meta.ts +3 -1
  15. package/src/patient-management/clinical-encounters.component.tsx +16 -9
  16. package/src/patient-management/patient-management-workspace/patient-management-schema/index.ts +2 -2
  17. package/src/patient-management/patient-management-workspace/patient-management-workspace.tsx +3 -0
  18. package/src/patient-management/patient-management.component.tsx +43 -26
  19. package/src/patient-management/patient-management.scss +33 -5
  20. package/src/patient-management/results/investigation-results.component.tsx +210 -0
  21. package/src/patient-management/results/investigation-results.scss +75 -0
  22. package/src/patient-management/results/results.resource.ts +150 -0
  23. package/src/queue-room-table/queue-room-table.component.tsx +21 -9
  24. package/src/queue-room-table/queue-room-table.resource.ts +26 -8
  25. package/src/queue-room-table/queue-room-table.scss +17 -0
  26. package/src/queue-room-table/type/index.ts +6 -0
  27. package/src/routes.json +1 -1
  28. package/dist/304.js +0 -1
  29. package/dist/304.js.map +0 -1
  30. package/dist/564.js.map +0 -1
@@ -0,0 +1,150 @@
1
+ import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
+ import useSWR from 'swr';
3
+
4
+ /** A single named observation captured against a lab order (concept → value). */
5
+ export interface LabResultValue {
6
+ concept?: { uuid: string; display: string };
7
+ value?: string | number | { uuid: string; display: string };
8
+ obsDatetime?: string;
9
+ }
10
+
11
+ /** One completed lab test order together with its result observations. */
12
+ export interface LabResultRecord {
13
+ orderUuid: string;
14
+ orderNumber?: string;
15
+ testName: string;
16
+ date?: string;
17
+ results: Array<LabResultValue>;
18
+ }
19
+
20
+ /** A radiology/procedure study record (shared Procedure entity in the orders module). */
21
+ export interface ProcedureRecord {
22
+ uuid: string;
23
+ status?: string;
24
+ startDatetime?: string;
25
+ orderUuid?: string;
26
+ concept?: { uuid: string; display: string };
27
+ preliminaryReport?: string;
28
+ procedureReport?: string;
29
+ impressions?: string;
30
+ }
31
+
32
+ const PROCEDURE_REP =
33
+ 'custom:(uuid,status,startDatetime,orderUuid,concept:(uuid,display),preliminaryReport,procedureReport,impressions)';
34
+
35
+ /**
36
+ * All radiology/procedure studies for a patient (both Imaging and Procedure — the caller splits them
37
+ * by concept-set membership). Newest first.
38
+ */
39
+ export function useProcedureRecords(patientUuid: string) {
40
+ const url = patientUuid
41
+ ? `${restBaseUrl}/digihmisorders/procedure?patient=${patientUuid}&v=${PROCEDURE_REP}`
42
+ : null;
43
+ const { data, error, isLoading, mutate } = useSWR<{ data: { results: Array<ProcedureRecord> } }>(url, openmrsFetch);
44
+ const procedures = (data?.data?.results ?? [])
45
+ .slice()
46
+ .sort((a, b) => (b.startDatetime ?? '').localeCompare(a.startDatetime ?? ''));
47
+ return { procedures, error, isLoading, mutate };
48
+ }
49
+
50
+ /**
51
+ * The flattened set of concept uuids belonging to a concept set (recursing up to three levels of
52
+ * nested sets — enough for the curated radiology set). Used to decide Imaging vs Procedure.
53
+ */
54
+ export function useConceptSetMembers(setUuid: string): Set<string> {
55
+ const url = setUuid
56
+ ? `${restBaseUrl}/concept/${setUuid}?v=custom:(setMembers:(uuid,setMembers:(uuid,setMembers:(uuid))))`
57
+ : null;
58
+ const { data } = useSWR<{ data: { setMembers: Array<NestedConcept> } }>(url, openmrsFetch);
59
+ const members = new Set<string>();
60
+ collectMembers(data?.data?.setMembers, members);
61
+ return members;
62
+ }
63
+
64
+ interface NestedConcept {
65
+ uuid: string;
66
+ setMembers?: Array<NestedConcept>;
67
+ }
68
+
69
+ function collectMembers(nodes: Array<NestedConcept> | undefined, acc: Set<string>): void {
70
+ for (const node of nodes ?? []) {
71
+ if (node?.uuid) {
72
+ acc.add(node.uuid);
73
+ }
74
+ if (node?.setMembers?.length) {
75
+ collectMembers(node.setMembers, acc);
76
+ }
77
+ }
78
+ }
79
+
80
+ /**
81
+ * The patient's completed lab test orders with their result observations. Fetches the patient's
82
+ * orders, keeps the completed Test Orders, then pulls each order's results from the labresult
83
+ * endpoint. Newest first.
84
+ */
85
+ export function useLabResults(patientUuid: string) {
86
+ const ordersUrl = patientUuid
87
+ ? `${restBaseUrl}/order?patient=${patientUuid}&v=custom:(uuid,orderNumber,dateActivated,fulfillerStatus,action,concept:(display),orderType:(uuid,display))`
88
+ : null;
89
+
90
+ const { data, error, isLoading, mutate } = useSWR<Array<LabResultRecord>>(
91
+ ordersUrl,
92
+ async (url: string) => {
93
+ const response = await openmrsFetch<{ results: Array<LabOrder> }>(url);
94
+ const orders = response?.data?.results ?? [];
95
+ const testOrders = orders.filter(
96
+ (order) =>
97
+ order?.orderType?.display?.toLowerCase().includes('test') &&
98
+ order?.fulfillerStatus === 'COMPLETED' &&
99
+ order?.action !== 'DISCONTINUE',
100
+ );
101
+
102
+ const records = await Promise.all(
103
+ testOrders.map(async (order): Promise<LabResultRecord> => {
104
+ const base: LabResultRecord = {
105
+ orderUuid: order.uuid,
106
+ orderNumber: order.orderNumber,
107
+ testName: order?.concept?.display ?? order?.orderNumber ?? 'Test',
108
+ date: order.dateActivated,
109
+ results: [],
110
+ };
111
+ try {
112
+ const res = await openmrsFetch<{ results: Array<LabResultValue> }>(
113
+ `${restBaseUrl}/digihmisorders/labresult/${order.uuid}`,
114
+ );
115
+ return { ...base, results: res?.data?.results ?? [] };
116
+ } catch {
117
+ // A single order's results failing must not blank the whole tab.
118
+ return base;
119
+ }
120
+ }),
121
+ );
122
+
123
+ return records.sort((a, b) => (b.date ?? '').localeCompare(a.date ?? ''));
124
+ },
125
+ { revalidateOnFocus: false },
126
+ );
127
+
128
+ return { labResults: data ?? [], error, isLoading, mutate };
129
+ }
130
+
131
+ interface LabOrder {
132
+ uuid: string;
133
+ orderNumber?: string;
134
+ dateActivated?: string;
135
+ fulfillerStatus?: string;
136
+ action?: string;
137
+ concept?: { display: string };
138
+ orderType?: { uuid: string; display: string };
139
+ }
140
+
141
+ /** Render an obs value that may be a scalar or a coded {uuid,display}. */
142
+ export function formatLabValue(value: LabResultValue['value']): string {
143
+ if (value == null) {
144
+ return '—';
145
+ }
146
+ if (typeof value === 'object') {
147
+ return value.display ?? '—';
148
+ }
149
+ return String(value);
150
+ }
@@ -18,7 +18,7 @@ import {
18
18
  import { CheckmarkFilled, Time } from '@carbon/react/icons';
19
19
  import { useTranslation } from 'react-i18next';
20
20
  import { isDesktop, launchWorkspace, navigate, showSnackbar, useConfig, useLayoutType } from '@openmrs/esm-framework';
21
- import { useQueueEntries } from './queue-room-table.resource';
21
+ import { useQueueEntries, isReturnedEntry } from './queue-room-table.resource';
22
22
  import { useQueueActions } from './useQueueActions';
23
23
  import styles from './queue-room-table.scss';
24
24
  import { type QueueEntry } from './type';
@@ -55,7 +55,8 @@ export const QueueRoomTable: React.FC<QueueRoomTableProps> = ({ queueRoomUuid, q
55
55
  (entry) =>
56
56
  entry?.patient?.display.toLowerCase().includes(search) ||
57
57
  entry?.display.toLowerCase().includes(search) ||
58
- entry?.queueRoomComingFrom?.display?.toLowerCase().includes(search),
58
+ entry?.queueRoomComingFrom?.display?.toLowerCase().includes(search) ||
59
+ entry?.queueComingFrom?.display?.toLowerCase().includes(search),
59
60
  );
60
61
  }, [queueEntries, searchTerm]);
61
62
 
@@ -144,7 +145,7 @@ export const QueueRoomTable: React.FC<QueueRoomTableProps> = ({ queueRoomUuid, q
144
145
  patientUuid: patientUuid,
145
146
  age: entry?.patient?.person?.age,
146
147
  gender: entry?.patient?.person?.gender === 'M' ? t('male', 'Male') : t('female', 'Female'),
147
- comingFrom: entry?.queueRoomComingFrom?.display || t('direct', 'Direct Entry'),
148
+ comingFrom: entry?.queueRoomComingFrom?.display || entry?.queueComingFrom?.display || t('direct', 'Direct Entry'),
148
149
  priority: entry?.priority?.display || t('normal', 'Normal'),
149
150
  priorityColor: getPriorityColor(entry?.priority?.display || ''),
150
151
  waitTime: calculateWaitTime(entry?.startedAt),
@@ -203,8 +204,14 @@ export const QueueRoomTable: React.FC<QueueRoomTableProps> = ({ queueRoomUuid, q
203
204
  </TableRow>
204
205
  </TableHead>
205
206
  <TableBody>
206
- {rows.map((row) => (
207
- <TableRow key={row.id} {...getRowProps({ row })}>
207
+ {rows.map((row) => {
208
+ const rowEntry = row.cells.find((c) => c.info.header === 'entry')?.value as QueueEntry;
209
+ const returned = isReturnedEntry(rowEntry);
210
+ return (
211
+ <TableRow
212
+ key={row.id}
213
+ {...getRowProps({ row })}
214
+ className={returned ? styles.returnedRow : undefined}>
208
215
  {row.cells.map((cell) => {
209
216
  if (
210
217
  ['statusUuid', 'patientUuid', 'entryUuid', 'priorityColor', 'entry'].includes(cell.info.header)
@@ -225,9 +232,13 @@ export const QueueRoomTable: React.FC<QueueRoomTableProps> = ({ queueRoomUuid, q
225
232
  if (cell.info.header === 'priority') {
226
233
  return (
227
234
  <TableCell key={cell.id}>
228
- <Tag type={row.cells.find((c) => c.info.header === 'priorityColor')?.value}>
229
- {cell.value}
230
- </Tag>
235
+ {returned ? (
236
+ <Tag type="teal">{t('returned', 'Returned')}</Tag>
237
+ ) : (
238
+ <Tag type={row.cells.find((c) => c.info.header === 'priorityColor')?.value}>
239
+ {cell.value}
240
+ </Tag>
241
+ )}
231
242
  </TableCell>
232
243
  );
233
244
  }
@@ -291,7 +302,8 @@ export const QueueRoomTable: React.FC<QueueRoomTableProps> = ({ queueRoomUuid, q
291
302
  return <TableCell key={cell.id}>{cell.value}</TableCell>;
292
303
  })}
293
304
  </TableRow>
294
- ))}
305
+ );
306
+ })}
295
307
  </TableBody>
296
308
  </Table>
297
309
  </TableContainer>
@@ -2,9 +2,19 @@ import { FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework
2
2
  import useSWR from 'swr';
3
3
  import { type QueueEntry } from './type';
4
4
 
5
+ /**
6
+ * A "returned" entry is a patient sent back to the consultation queue after all their ordered
7
+ * investigations completed. The backend stamps such entries with a priorityComment of the form
8
+ * "All N investigations complete - Results ready for review" and a queueComingFrom pointing at the
9
+ * department they just finished. We surface these distinctly (own colour, ranked right after
10
+ * Emergency) so the clinician picks up results-ready patients first.
11
+ */
12
+ export const isReturnedEntry = (entry: Pick<QueueEntry, 'priorityComment'>): boolean =>
13
+ !!entry?.priorityComment?.toLowerCase().includes('investigations complete');
14
+
5
15
  export const useQueueEntries = (queueRoomUuid: string, queueUuid: string) => {
6
16
  const customRepresentation =
7
- 'custom:(uuid,display,patient:(uuid,display,person:(display,age,gender)),priority:(uuid,display),status:(uuid,display),visit:(uuid),startedAt,sortWeight,queueRoom:(uuid,display),queueRoomComingFrom:(uuid,display))';
17
+ 'custom:(uuid,display,patient:(uuid,display,person:(display,age,gender)),priority:(uuid,display),status:(uuid,display),visit:(uuid),startedAt,sortWeight,priorityComment,queueRoom:(uuid,display),queueComingFrom:(uuid,display),queueRoomComingFrom:(uuid,display))';
8
18
 
9
19
  const params = new URLSearchParams();
10
20
  params.append('v', customRepresentation);
@@ -24,18 +34,26 @@ export const useQueueEntries = (queueRoomUuid: string, queueUuid: string) => {
24
34
  );
25
35
 
26
36
  const sortedEntries = data?.data?.results?.sort((a, b) => {
37
+ // Emergency first, then results-ready "Returned" patients, then the usual priorities.
27
38
  const priorityOrder: Record<string, number> = {
28
39
  emergency: 0,
29
- urgent: 1,
30
- priority: 2,
31
- normal: 3,
40
+ returned: 1,
41
+ urgent: 2,
42
+ priority: 3,
43
+ normal: 4,
32
44
  };
33
45
 
34
- const aPriority = a.priority?.display?.toLowerCase() || 'normal';
35
- const bPriority = b.priority?.display?.toLowerCase() || 'normal';
46
+ const rank = (entry: QueueEntry) => {
47
+ const p = entry.priority?.display?.toLowerCase() || 'normal';
48
+ // A returned patient outranks everything except a true emergency.
49
+ if (isReturnedEntry(entry) && p !== 'emergency') {
50
+ return priorityOrder.returned;
51
+ }
52
+ return priorityOrder[p] ?? priorityOrder.normal;
53
+ };
36
54
 
37
- const aOrder = priorityOrder[aPriority] ?? 3;
38
- const bOrder = priorityOrder[bPriority] ?? 3;
55
+ const aOrder = rank(a);
56
+ const bOrder = rank(b);
39
57
 
40
58
  if (aOrder !== bOrder) {
41
59
  return aOrder - bOrder;
@@ -139,6 +139,23 @@
139
139
  }
140
140
  }
141
141
 
142
+ /* Results-ready "Returned" patients: a distinct teal wash + left accent so they stand out from
143
+ ordinary waiting rows, even under Carbon zebra striping. Ranked right after Emergency. */
144
+ .returnedRow {
145
+ &,
146
+ td {
147
+ background-color: colors.$teal-10 !important;
148
+ }
149
+
150
+ td:first-child {
151
+ box-shadow: inset 3px 0 0 0 colors.$teal-50;
152
+ }
153
+
154
+ &:hover td {
155
+ background-color: colors.$teal-20 !important;
156
+ }
157
+ }
158
+
142
159
  .longWait {
143
160
  display: flex;
144
161
  align-items: center;
@@ -29,6 +29,12 @@ export interface QueueEntry {
29
29
  uuid: string;
30
30
  display: string;
31
31
  };
32
+ queueComingFrom?: {
33
+ uuid: string;
34
+ display: string;
35
+ };
32
36
  startedAt: string;
33
37
  sortWeight: number;
38
+ /** Free-text stamped by the backend; "…investigations complete…" marks a returned patient. */
39
+ priorityComment?: string;
34
40
  }
package/src/routes.json CHANGED
@@ -39,7 +39,7 @@
39
39
  "name": "outpatient/patient/:patientUuid/file",
40
40
  "slot": "outpatient-file-dashboard-slot",
41
41
  "title": "Patient file",
42
- "showOnlyOnRoute": "/file"
42
+ "showOnlyOnRoute": "/patient/"
43
43
  },
44
44
  "order": 2,
45
45
  "online": true,