@digihmis/esm-outpatient-app 1.0.4 → 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.
@@ -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';
@@ -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,
package/dist/752.js DELETED
@@ -1 +0,0 @@
1
- "use strict";(globalThis.webpackChunk_digihmis_esm_outpatient_app=globalThis.webpackChunk_digihmis_esm_outpatient_app||[]).push([["752"],{79214(e){e.exports=JSON.parse('{"actions":"Actions","age":"Age","avgWaitTime":"Avg. Wait Time","carePanel":"Care panel","changeStation":"Change Station","clockedInTo":"Clocked in to","clockedOutFromCurrent":"Clocked out from current station","clockIn":"Clock In","clockingIn":"Clocking in","clockingOut":"Clocking out","clockInPrompt":"Click the \\"Clock In\\" button above to start managing patients","clockOut":"Clock Out","clockOutError":"Clock Out Failed","clockOutFailed":"Failed to clock out","clockOutSuccess":"Clock Out Successful","clockOutSuccessMessage":"You have been clocked out successfully","comingFrom":"Coming From","completedToday":"Completed Today","direct":"Direct Entry","error":"Error","female":"Female","filterPatients":"Filter patients","gender":"Gender","loading":"Loading...","loadingQueue":"Loading queue...","loadingServices":"Loading services...","male":"Male","myQueueInservice":"My Queue - In Service","myQueueWaiting":"My Queue - Waiting","noActiveSession":"No active session found","noBillableServices":"No billable services","noBillableServicesDescription":"Please configure billable services to continue.","noDiagnosisFound":"No diagnosis codes found. Try different keywords.","noPatients":"No patients in queue","noPatientsDescription":"There are currently no patients waiting in this queue room.","normal":"Normal","outpatient":"Outpatient","outpatientQueue":"Outpatient Queue","patient":"Patient","patientServed":"Patient Served","patientServedMessage":"","pleaseClockIn":"Please clock in to view the queue","priority":"Priority","searchingDiagnosis":"Searching...","servePatient":"Serve Patient","servePatientError":"Failed to serve patient. Please try again.","servePatientLoading":"Serving Patient...","since":"since","startConsultation":"Start Consultation","status":"Status","switchingStation":"Switching Station","switchStationError":"Switch Station Failed","switchStationFailed":"Failed to switch station","typeMoreCharacters":"Type at least 3 characters to search","waitTime":"Wait Time"}')}}]);
package/dist/752.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"752.js","sources":["webpack://@digihmis/esm-outpatient-app/json|/home/kios09/Desktop/DigiHMIS/backend/DigiHMIS-apps/packages/esm-outpatient-app/translations/vi.json"],"names":["JSON"],"mappings":"mJAAA,EAAO,OAAO,CAAGA,KAAK,KAAK,CAAC,s9D"}