@digihmis/esm-outpatient-app 1.0.4 → 1.0.8

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 (94) hide show
  1. package/dist/112.js +1 -0
  2. package/dist/112.js.map +1 -0
  3. package/dist/117.js +1 -1
  4. package/dist/117.js.map +1 -1
  5. package/dist/132.js +1 -1
  6. package/dist/132.js.map +1 -1
  7. package/dist/140.js +1 -1
  8. package/dist/140.js.map +1 -1
  9. package/dist/150.js +1 -0
  10. package/dist/150.js.map +1 -0
  11. package/dist/152.js +1 -1
  12. package/dist/152.js.map +1 -1
  13. package/dist/209.js +1 -1
  14. package/dist/209.js.map +1 -1
  15. package/dist/317.js +1 -1
  16. package/dist/317.js.map +1 -1
  17. package/dist/349.js +1 -1
  18. package/dist/349.js.map +1 -1
  19. package/dist/371.js +1 -1
  20. package/dist/371.js.map +1 -1
  21. package/dist/378.js +1 -1
  22. package/dist/378.js.map +1 -1
  23. package/dist/409.js +1 -0
  24. package/dist/409.js.map +1 -0
  25. package/dist/442.js +1 -1
  26. package/dist/442.js.map +1 -1
  27. package/dist/45.js +1 -1
  28. package/dist/45.js.map +1 -1
  29. package/dist/466.js +1 -1
  30. package/dist/466.js.map +1 -1
  31. package/dist/508.js +1 -1
  32. package/dist/508.js.map +1 -1
  33. package/dist/563.js +12 -0
  34. package/dist/563.js.map +1 -0
  35. package/dist/576.js +15 -0
  36. package/dist/576.js.map +1 -0
  37. package/dist/594.js +1 -1
  38. package/dist/594.js.map +1 -1
  39. package/dist/61.js +1 -1
  40. package/dist/61.js.map +1 -1
  41. package/dist/689.js +1 -1
  42. package/dist/689.js.map +1 -1
  43. package/dist/712.js +1 -1
  44. package/dist/712.js.map +1 -1
  45. package/dist/720.js +1 -1
  46. package/dist/720.js.map +1 -1
  47. package/dist/742.js +1 -1
  48. package/dist/742.js.map +1 -1
  49. package/dist/771.js +1 -1
  50. package/dist/771.js.map +1 -1
  51. package/dist/781.js +1 -0
  52. package/dist/781.js.map +1 -0
  53. package/dist/806.js +1 -1
  54. package/dist/806.js.map +1 -1
  55. package/dist/912.js +1 -1
  56. package/dist/912.js.map +1 -1
  57. package/dist/913.js +1 -1
  58. package/dist/913.js.map +1 -1
  59. package/dist/940.js +1 -1
  60. package/dist/940.js.map +1 -1
  61. package/dist/989.js +1 -0
  62. package/dist/989.js.map +1 -0
  63. package/dist/digihmis-esm-outpatient-app.js +3 -3
  64. package/dist/digihmis-esm-outpatient-app.js.buildmanifest.json +191 -115
  65. package/dist/digihmis-esm-outpatient-app.js.map +1 -1
  66. package/dist/main.js +3 -14
  67. package/dist/main.js.map +1 -1
  68. package/dist/routes.json +1 -1
  69. package/package.json +1 -1
  70. package/src/config-schema.ts +7 -0
  71. package/src/dashboard.meta.ts +3 -1
  72. package/src/patient-management/clinical-encounters.component.tsx +16 -9
  73. package/src/patient-management/patient-management-workspace/patient-management-schema/index.ts +2 -2
  74. package/src/patient-management/patient-management-workspace/patient-management-workspace.tsx +3 -0
  75. package/src/patient-management/patient-management.component.tsx +43 -26
  76. package/src/patient-management/patient-management.scss +33 -5
  77. package/src/patient-management/results/investigation-results.component.tsx +217 -0
  78. package/src/patient-management/results/investigation-results.scss +75 -0
  79. package/src/patient-management/results/results.resource.ts +149 -0
  80. package/src/queue-room-table/queue-room-table.component.tsx +18 -7
  81. package/src/queue-room-table/queue-room-table.resource.ts +26 -8
  82. package/src/queue-room-table/queue-room-table.scss +17 -0
  83. package/src/queue-room-table/type/index.ts +2 -0
  84. package/src/routes.json +1 -1
  85. package/dist/271.js +0 -15
  86. package/dist/271.js.map +0 -1
  87. package/dist/395.js +0 -12
  88. package/dist/395.js.map +0 -1
  89. package/dist/640.js +0 -1
  90. package/dist/640.js.map +0 -1
  91. package/dist/709.js +0 -1
  92. package/dist/709.js.map +0 -1
  93. package/dist/752.js +0 -1
  94. package/dist/752.js.map +0 -1
@@ -0,0 +1,149 @@
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
+ // The Procedure resource is registered at /ws/rest/v1/procedure (NOT under /digihmisorders).
41
+ const url = patientUuid ? `${restBaseUrl}/procedure?patient=${patientUuid}&v=${PROCEDURE_REP}` : null;
42
+ const { data, error, isLoading, mutate } = useSWR<{ data: { results: Array<ProcedureRecord> } }>(url, openmrsFetch);
43
+ const procedures = (data?.data?.results ?? [])
44
+ .slice()
45
+ .sort((a, b) => (b.startDatetime ?? '').localeCompare(a.startDatetime ?? ''));
46
+ return { procedures, error, isLoading, mutate };
47
+ }
48
+
49
+ /**
50
+ * The flattened set of concept uuids belonging to a concept set (recursing up to three levels of
51
+ * nested sets — enough for the curated radiology set). Used to decide Imaging vs Procedure.
52
+ */
53
+ export function useConceptSetMembers(setUuid: string): Set<string> {
54
+ const url = setUuid
55
+ ? `${restBaseUrl}/concept/${setUuid}?v=custom:(setMembers:(uuid,setMembers:(uuid,setMembers:(uuid))))`
56
+ : null;
57
+ const { data } = useSWR<{ data: { setMembers: Array<NestedConcept> } }>(url, openmrsFetch);
58
+ const members = new Set<string>();
59
+ collectMembers(data?.data?.setMembers, members);
60
+ return members;
61
+ }
62
+
63
+ interface NestedConcept {
64
+ uuid: string;
65
+ setMembers?: Array<NestedConcept>;
66
+ }
67
+
68
+ function collectMembers(nodes: Array<NestedConcept> | undefined, acc: Set<string>): void {
69
+ for (const node of nodes ?? []) {
70
+ if (node?.uuid) {
71
+ acc.add(node.uuid);
72
+ }
73
+ if (node?.setMembers?.length) {
74
+ collectMembers(node.setMembers, acc);
75
+ }
76
+ }
77
+ }
78
+
79
+ /**
80
+ * The patient's completed lab test orders with their result observations. Fetches the patient's
81
+ * orders, keeps the completed Test Orders, then pulls each order's results from the labresult
82
+ * endpoint. Newest first.
83
+ */
84
+ export function useLabResults(patientUuid: string) {
85
+ const ordersUrl = patientUuid
86
+ ? `${restBaseUrl}/order?patient=${patientUuid}&v=custom:(uuid,orderNumber,dateActivated,fulfillerStatus,action,concept:(display),orderType:(uuid,display))`
87
+ : null;
88
+
89
+ const { data, error, isLoading, mutate } = useSWR<Array<LabResultRecord>>(
90
+ ordersUrl,
91
+ async (url: string) => {
92
+ const response = await openmrsFetch<{ results: Array<LabOrder> }>(url);
93
+ const orders = response?.data?.results ?? [];
94
+ const testOrders = orders.filter((order) => {
95
+ const type = order?.orderType?.display?.toLowerCase() ?? '';
96
+ const isLabOrder = type.includes('test') || type.includes('lab');
97
+ // Show the patient's full lab history: any completed test order, across every visit.
98
+ return isLabOrder && order?.fulfillerStatus === 'COMPLETED' && order?.action !== 'DISCONTINUE';
99
+ });
100
+
101
+ const records = await Promise.all(
102
+ testOrders.map(async (order): Promise<LabResultRecord> => {
103
+ const base: LabResultRecord = {
104
+ orderUuid: order.uuid,
105
+ orderNumber: order.orderNumber,
106
+ testName: order?.concept?.display ?? order?.orderNumber ?? 'Test',
107
+ date: order.dateActivated,
108
+ results: [],
109
+ };
110
+ try {
111
+ const res = await openmrsFetch<{ results: Array<LabResultValue> }>(
112
+ `${restBaseUrl}/digihmisorders/labresult/${order.uuid}`,
113
+ );
114
+ return { ...base, results: res?.data?.results ?? [] };
115
+ } catch {
116
+ // A single order's results failing must not blank the whole tab.
117
+ return base;
118
+ }
119
+ }),
120
+ );
121
+
122
+ return records.sort((a, b) => (b.date ?? '').localeCompare(a.date ?? ''));
123
+ },
124
+ { revalidateOnFocus: false },
125
+ );
126
+
127
+ return { labResults: data ?? [], error, isLoading, mutate };
128
+ }
129
+
130
+ interface LabOrder {
131
+ uuid: string;
132
+ orderNumber?: string;
133
+ dateActivated?: string;
134
+ fulfillerStatus?: string;
135
+ action?: string;
136
+ concept?: { display: string };
137
+ orderType?: { uuid: string; display: string };
138
+ }
139
+
140
+ /** Render an obs value that may be a scalar or a coded {uuid,display}. */
141
+ export function formatLabValue(value: LabResultValue['value']): string {
142
+ if (value == null) {
143
+ return '—';
144
+ }
145
+ if (typeof value === 'object') {
146
+ return value.display ?? '—';
147
+ }
148
+ return String(value);
149
+ }
@@ -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';
@@ -204,8 +204,14 @@ export const QueueRoomTable: React.FC<QueueRoomTableProps> = ({ queueRoomUuid, q
204
204
  </TableRow>
205
205
  </TableHead>
206
206
  <TableBody>
207
- {rows.map((row) => (
208
- <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}>
209
215
  {row.cells.map((cell) => {
210
216
  if (
211
217
  ['statusUuid', 'patientUuid', 'entryUuid', 'priorityColor', 'entry'].includes(cell.info.header)
@@ -226,9 +232,13 @@ export const QueueRoomTable: React.FC<QueueRoomTableProps> = ({ queueRoomUuid, q
226
232
  if (cell.info.header === 'priority') {
227
233
  return (
228
234
  <TableCell key={cell.id}>
229
- <Tag type={row.cells.find((c) => c.info.header === 'priorityColor')?.value}>
230
- {cell.value}
231
- </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
+ )}
232
242
  </TableCell>
233
243
  );
234
244
  }
@@ -292,7 +302,8 @@ export const QueueRoomTable: React.FC<QueueRoomTableProps> = ({ queueRoomUuid, q
292
302
  return <TableCell key={cell.id}>{cell.value}</TableCell>;
293
303
  })}
294
304
  </TableRow>
295
- ))}
305
+ );
306
+ })}
296
307
  </TableBody>
297
308
  </Table>
298
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),queueComingFrom:(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;
@@ -35,4 +35,6 @@ export interface QueueEntry {
35
35
  };
36
36
  startedAt: string;
37
37
  sortWeight: number;
38
+ /** Free-text stamped by the backend; "…investigations complete…" marks a returned patient. */
39
+ priorityComment?: string;
38
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,