@ampath/esm-dha-workflow-app 4.0.0-next.340 → 4.0.0-next.344
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/1149.js +1 -1
- package/dist/1171.js +1 -0
- package/dist/1171.js.map +1 -0
- package/dist/1177.js +1 -0
- package/dist/1177.js.map +1 -0
- package/dist/1239.js +1 -0
- package/dist/1239.js.map +1 -0
- package/dist/1551.js +1 -1
- package/dist/1560.js +1 -0
- package/dist/1560.js.map +1 -0
- package/dist/1928.js +1 -0
- package/dist/1928.js.map +1 -0
- package/dist/2093.js +1 -1
- package/dist/2230.js +1 -1
- package/dist/2493.js +1 -1
- package/dist/2914.js +1 -1
- package/dist/3078.js +1 -0
- package/dist/3078.js.map +1 -0
- package/dist/327.js +1 -0
- package/dist/327.js.map +1 -0
- package/dist/4605.js +1 -1
- package/dist/4611.js +1 -1
- package/dist/4674.js +1 -1
- package/dist/4674.js.map +1 -1
- package/dist/4986.js +1 -1
- package/dist/5167.js +1 -1
- package/dist/5317.js +1 -0
- package/dist/5317.js.map +1 -0
- package/dist/5344.js +1 -0
- package/dist/{2936.js.map → 5344.js.map} +1 -1
- package/dist/5765.js +1 -0
- package/dist/5765.js.map +1 -0
- package/dist/5851.js +1 -1
- package/dist/5851.js.map +1 -1
- package/dist/5961.js +1 -1
- package/dist/6264.js +1 -1
- package/dist/6264.js.map +1 -1
- package/dist/6720.js +1 -1
- package/dist/6942.js +1 -1
- package/dist/6942.js.map +1 -1
- package/dist/7521.js +1 -1
- package/dist/7521.js.map +1 -1
- package/dist/7549.js +1 -0
- package/dist/7549.js.map +1 -0
- package/dist/7717.js +1 -1
- package/dist/7857.js +1 -0
- package/dist/7857.js.map +1 -0
- package/dist/850.js +1 -0
- package/dist/850.js.map +1 -0
- package/dist/8733.js +1 -1
- package/dist/9070.js +1 -1
- package/dist/9093.js +1 -1
- package/dist/9093.js.map +1 -1
- package/dist/9338.js +1 -1
- package/dist/9338.js.map +1 -1
- package/dist/9349.js +1 -0
- package/dist/9349.js.map +1 -0
- package/dist/9428.js +1 -1
- package/dist/9428.js.map +1 -1
- package/dist/9789.js +1 -0
- package/dist/9789.js.map +1 -0
- package/dist/9871.js +1 -0
- package/dist/9871.js.map +1 -0
- package/dist/esm-dha-workflow-app.js +1 -1
- package/dist/esm-dha-workflow-app.js.buildmanifest.json +362 -239
- package/dist/esm-dha-workflow-app.js.map +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +1 -1
- package/src/billing/billing-claims.resource.ts +8 -7
- package/src/billing/dashboard/v2/clearance/cash.component.tsx +9 -9
- package/src/billing/dashboard/v2/clearance/pay-cash.component.tsx +24 -22
- package/src/billing/dashboard/v2/facility-bills/facility-bills.component.tsx +5 -4
- package/src/billing/dashboard/v2/patient-bill-details/attachments/final-bill.component.tsx +2 -2
- package/src/billing/dashboard/v2/patient-bill-details/attachments/generate-attachments.component.tsx +15 -4
- package/src/billing/dashboard/v2/patient-bill-details/attachments/invoice.component.tsx +2 -2
- package/src/billing/dashboard/v2/types/index.ts +6 -0
- package/src/billing/dashboard/v3/claim-visits/claim-intervention-details/claim-intervention-details.component.tsx +6 -1
- package/src/billing/dashboard/v3/claim-visits/claim-visit-details/claim-visit-details.component.tsx +5 -8
- package/src/billing/dashboard/v3/facility-bills/facility-bills.component.tsx +65 -62
- package/src/billing/dashboard/v3/patient-bill-details/attachments/case-summary/case-summary.tsx +30 -16
- package/src/billing/dashboard/v3/patient-bill-details/attachments/generate-attachments.component.tsx +3 -1
- package/src/billing/dashboard/v3/patient-bill-details/bill-details/bill-details.scss +4 -0
- package/src/billing/dashboard/v3/patient-bill-details/bill-details/bill-details.tsx +154 -156
- package/src/billing/dashboard/v3/patient-bill-details/claim-details/patient-claim-details.component.tsx +22 -23
- package/src/billing/dashboard/v3/patient-bill-details/patient-bill-details.tsx +5 -4
- package/src/dashboard/dashboard.component.tsx +3 -2
- package/src/dashboard/overview/overview.component.tsx +3 -1
- package/src/dashboard-meta/emt-dashboard.meta.ts +6 -0
- package/src/emt/cr-lookup.resource.test.ts +88 -0
- package/src/emt/cr-lookup.resource.ts +42 -0
- package/src/emt/emt-queue.component.test.tsx +324 -0
- package/src/emt/emt-queue.component.tsx +449 -0
- package/src/emt/emt-queue.scss +83 -0
- package/src/emt/emt.resource.test.ts +247 -0
- package/src/emt/emt.resource.ts +150 -0
- package/src/emt/handover-modal/handover-modal.component.test.tsx +273 -0
- package/src/emt/handover-modal/handover-modal.component.tsx +524 -0
- package/src/emt/handover-modal/handover-modal.scss +88 -0
- package/src/emt/types/emt.types.ts +137 -0
- package/src/index.ts +10 -0
- package/src/registry/registry.component.tsx +49 -1
- package/src/registry/utils/hie-client-adapter.ts +1 -1
- package/src/root.component.tsx +2 -0
- package/src/routes.json +25 -0
- package/src/side-nav-menu/nav-link-config.ts +4 -0
- package/dist/1362.js +0 -1
- package/dist/1362.js.map +0 -1
- package/dist/203.js +0 -1
- package/dist/203.js.map +0 -1
- package/dist/2200.js +0 -1
- package/dist/2200.js.map +0 -1
- package/dist/2936.js +0 -1
- package/dist/2988.js +0 -1
- package/dist/2988.js.map +0 -1
- package/dist/307.js +0 -1
- package/dist/307.js.map +0 -1
- package/dist/4008.js +0 -1
- package/dist/4008.js.map +0 -1
- package/dist/446.js +0 -1
- package/dist/446.js.map +0 -1
- package/dist/5097.js +0 -1
- package/dist/5097.js.map +0 -1
- package/dist/7240.js +0 -1
- package/dist/7240.js.map +0 -1
- package/dist/874.js +0 -1
- package/dist/874.js.map +0 -1
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Button,
|
|
4
|
+
DataTable,
|
|
5
|
+
type DataTableRow,
|
|
6
|
+
DataTableSkeleton,
|
|
7
|
+
InlineLoading,
|
|
8
|
+
Pagination,
|
|
9
|
+
Table,
|
|
10
|
+
TableBody,
|
|
11
|
+
TableCell,
|
|
12
|
+
TableHead,
|
|
13
|
+
TableHeader,
|
|
14
|
+
TableRow,
|
|
15
|
+
Tag,
|
|
16
|
+
} from '@carbon/react';
|
|
17
|
+
import { launchWorkspace, navigate, showSnackbar, useSession } from '@openmrs/esm-framework';
|
|
18
|
+
import { useTranslation } from 'react-i18next';
|
|
19
|
+
import dayjs from 'dayjs';
|
|
20
|
+
|
|
21
|
+
import styles from './emt-queue.scss';
|
|
22
|
+
import EmptyState from '../billing/dashboard/v3/shared/empty-state.component';
|
|
23
|
+
import TableToolbar from '../billing/dashboard/v3/shared/table-toolbar.component';
|
|
24
|
+
import FacilityAndWorkerSlot from '../shared/ui/facility-worker-slot/facility-worker.component-slot.component';
|
|
25
|
+
import { EMT_HANDOVER_WORKSPACE } from './handover-modal/handover-modal.component';
|
|
26
|
+
import {
|
|
27
|
+
usePendingReferrals,
|
|
28
|
+
EMT_PENDING_KEY,
|
|
29
|
+
} from './emt.resource';
|
|
30
|
+
import { fetchClientByCrId, clientDisplayName } from './cr-lookup.resource';
|
|
31
|
+
import { EmtApiError, type EmtReferralRow } from './types/emt.types';
|
|
32
|
+
import { searchPatientByCrNumber } from '../resources/patient-search.resource';
|
|
33
|
+
import { IdentifierTypesUuids } from '../resources/identifier-types';
|
|
34
|
+
|
|
35
|
+
const EmtQueue: React.FC = () => {
|
|
36
|
+
const { t } = useTranslation();
|
|
37
|
+
const session = useSession();
|
|
38
|
+
const locationUuid = session?.sessionLocation?.uuid ?? '';
|
|
39
|
+
const [searchString, setSearchString] = useState('');
|
|
40
|
+
const [pageSize, setPageSize] = useState(10);
|
|
41
|
+
const [pageIndex, setPageIndex] = useState(0); // 0-based page for server offset
|
|
42
|
+
const [rows, setRows] = useState<EmtReferralRow[]>([]);
|
|
43
|
+
const [globalError, setGlobalError] = useState<string>('');
|
|
44
|
+
|
|
45
|
+
const limit = pageSize;
|
|
46
|
+
const offset = pageIndex * pageSize;
|
|
47
|
+
|
|
48
|
+
const {
|
|
49
|
+
referrals,
|
|
50
|
+
count,
|
|
51
|
+
isLoading,
|
|
52
|
+
error,
|
|
53
|
+
mutate,
|
|
54
|
+
} = usePendingReferrals(limit, offset, locationUuid);
|
|
55
|
+
|
|
56
|
+
// Surface upstream errors visibly (network / auth / 5xx) with a retry path.
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (!error) {
|
|
59
|
+
setGlobalError('');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (error instanceof EmtApiError) {
|
|
63
|
+
const { status } = error;
|
|
64
|
+
if (status === 401 || status === 403) {
|
|
65
|
+
setGlobalError('Your session may have expired. Please re-authenticate and retry.');
|
|
66
|
+
} else if (status >= 500 || status === 0) {
|
|
67
|
+
setGlobalError('The EMT service is temporarily unavailable. Please retry.');
|
|
68
|
+
} else if (status === 404) {
|
|
69
|
+
setGlobalError('No pending referrals found for this facility.');
|
|
70
|
+
} else {
|
|
71
|
+
setGlobalError(error.message || 'Failed to load referrals.');
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
setGlobalError('Unable to reach the EMT service. Please check your connection and retry.');
|
|
75
|
+
}
|
|
76
|
+
}, [error]);
|
|
77
|
+
|
|
78
|
+
// Map the fetched page into display rows.
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
const mapped: EmtReferralRow[] = (referrals ?? []).map((r) => ({
|
|
81
|
+
...r,
|
|
82
|
+
patientName: r.cr_id, // placeholder until CR resolves
|
|
83
|
+
crLoading: true,
|
|
84
|
+
}));
|
|
85
|
+
setRows(mapped);
|
|
86
|
+
|
|
87
|
+
// Lazy CR enrichment: fetch each row's CR record in parallel. A failed
|
|
88
|
+
// lookup degrades the row (keeps cr_id, marks "details unavailable")
|
|
89
|
+
// rather than failing the whole queue.
|
|
90
|
+
let cancelled = false;
|
|
91
|
+
mapped.forEach(async (row, idx) => {
|
|
92
|
+
try {
|
|
93
|
+
const client = await fetchClientByCrId(row.cr_id, locationUuid);
|
|
94
|
+
if (cancelled) return;
|
|
95
|
+
setRows((prev) => {
|
|
96
|
+
const next = [...prev];
|
|
97
|
+
if (!next[idx]) return prev;
|
|
98
|
+
next[idx] = {
|
|
99
|
+
...next[idx],
|
|
100
|
+
client: client ?? undefined,
|
|
101
|
+
patientName: clientDisplayName(client, row.cr_id),
|
|
102
|
+
crLoading: false,
|
|
103
|
+
crError: client ? undefined : 'Patient details unavailable',
|
|
104
|
+
};
|
|
105
|
+
return next;
|
|
106
|
+
});
|
|
107
|
+
} catch {
|
|
108
|
+
if (cancelled) return;
|
|
109
|
+
setRows((prev) => {
|
|
110
|
+
const next = [...prev];
|
|
111
|
+
if (!next[idx]) return prev;
|
|
112
|
+
next[idx] = {
|
|
113
|
+
...next[idx],
|
|
114
|
+
crLoading: false,
|
|
115
|
+
crError: 'Patient details unavailable',
|
|
116
|
+
};
|
|
117
|
+
return next;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return () => {
|
|
123
|
+
cancelled = true;
|
|
124
|
+
};
|
|
125
|
+
}, [referrals, locationUuid]);
|
|
126
|
+
|
|
127
|
+
const columns = [
|
|
128
|
+
{ id: 'patientName', header: 'Patient', key: 'patientName' },
|
|
129
|
+
{ id: 'crId', header: 'CR ID', key: 'crId' },
|
|
130
|
+
{ id: 'caseNumber', header: 'Case number', key: 'caseNumber' },
|
|
131
|
+
{ id: 'ambulance', header: 'Ambulance', key: 'ambulance' },
|
|
132
|
+
{ id: 'requestedAt', header: 'Requested', key: 'requestedAt' },
|
|
133
|
+
{ id: 'interventions', header: 'Interventions', key: 'interventions' },
|
|
134
|
+
{ id: 'notes', header: 'Referral notes', key: 'notes' },
|
|
135
|
+
{ id: 'status', header: 'Status', key: 'status' },
|
|
136
|
+
{ id: 'action', header: 'Action', key: 'action' },
|
|
137
|
+
{ id: 'submissionId', header: '', key: 'submissionId' }, // hidden payload carrier
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
// Map each referral into a DataTable row whose `id` + keys match the columns.
|
|
141
|
+
const tableRows = useMemo(
|
|
142
|
+
() =>
|
|
143
|
+
rows.map((r) => ({
|
|
144
|
+
id: String(r.submission_id),
|
|
145
|
+
patientName: r.patientName,
|
|
146
|
+
crId: r.cr_id,
|
|
147
|
+
caseNumber: r.case_number,
|
|
148
|
+
ambulance: r.ambulance_fr_code,
|
|
149
|
+
requestedAt: r.requested_at,
|
|
150
|
+
interventions: (r.interventions ?? []).join(', '),
|
|
151
|
+
notes: r.referral_notes,
|
|
152
|
+
status: r.status,
|
|
153
|
+
action: String(r.submission_id),
|
|
154
|
+
submissionId: String(r.submission_id),
|
|
155
|
+
})),
|
|
156
|
+
[rows],
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// Sort newest first, then apply the client-side search filter.
|
|
160
|
+
const visibleRows = useMemo(() => {
|
|
161
|
+
const sorted = [...tableRows].sort(
|
|
162
|
+
(a, b) => dayjs(b.requestedAt).valueOf() - dayjs(a.requestedAt).valueOf(),
|
|
163
|
+
);
|
|
164
|
+
if (!searchString.trim()) return sorted;
|
|
165
|
+
const q = searchString.toLowerCase();
|
|
166
|
+
return sorted.filter(
|
|
167
|
+
(r) =>
|
|
168
|
+
r.patientName?.toLowerCase().includes(q) ||
|
|
169
|
+
r.crId?.toLowerCase().includes(q) ||
|
|
170
|
+
r.caseNumber?.toLowerCase().includes(q) ||
|
|
171
|
+
r.ambulance?.toLowerCase().includes(q),
|
|
172
|
+
);
|
|
173
|
+
}, [tableRows, searchString]);
|
|
174
|
+
|
|
175
|
+
// Lookup of raw referral rows by submission id, so cell renderers can read
|
|
176
|
+
// enriched fields (interventions, notes, crLoading, crError) directly.
|
|
177
|
+
const rowsById = useMemo(
|
|
178
|
+
() => new Map(rows.map((r) => [String(r.submission_id), r])),
|
|
179
|
+
[rows],
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const showSkeleton = isLoading && rows.length === 0;
|
|
183
|
+
|
|
184
|
+
const relativeTime = (iso: string) => {
|
|
185
|
+
const mins = dayjs().diff(dayjs(iso), 'minute');
|
|
186
|
+
if (mins < 1) return 'just now';
|
|
187
|
+
if (mins < 60) return `${mins}m ago`;
|
|
188
|
+
const hrs = Math.floor(mins / 60);
|
|
189
|
+
const rem = mins % 60;
|
|
190
|
+
return rem ? `${hrs}h ${rem}m ago` : `${hrs}h ago`;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const handleRefresh = async () => {
|
|
194
|
+
try {
|
|
195
|
+
await mutate();
|
|
196
|
+
} catch {
|
|
197
|
+
showSnackbar({ kind: 'error', title: 'Refresh failed', subtitle: '' });
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const handleHandoverComplete = async (referral: EmtReferralRow) => {
|
|
202
|
+
// Refresh from the source of truth — don't just splice locally.
|
|
203
|
+
mutate();
|
|
204
|
+
showSnackbar({
|
|
205
|
+
kind: 'success',
|
|
206
|
+
title: 'Handover complete',
|
|
207
|
+
subtitle: `Starting visit for ${referral.patientName} (${referral.case_number}).`,
|
|
208
|
+
});
|
|
209
|
+
// EmtQueue is mounted as a bare extension (no React Router ancestor), so
|
|
210
|
+
// cross-app navigation has to go through the shell's navigate() rather
|
|
211
|
+
// than react-router's useNavigate — and there's no router `state` channel
|
|
212
|
+
// either.
|
|
213
|
+
//
|
|
214
|
+
// If the patient already exists in AMRS (registered on an earlier visit),
|
|
215
|
+
// skip straight to their chart. Otherwise fall back to the registry
|
|
216
|
+
// screen so staff complete registration manually, same as today — the CR
|
|
217
|
+
// id travels via a query param and the Registration screen re-resolves
|
|
218
|
+
// the CR record itself.
|
|
219
|
+
let amrsPatientUuid: string | undefined;
|
|
220
|
+
try {
|
|
221
|
+
const resp = await searchPatientByCrNumber(referral.cr_id);
|
|
222
|
+
amrsPatientUuid = (resp.results ?? []).find((p) =>
|
|
223
|
+
p.identifiers.some(
|
|
224
|
+
(id) =>
|
|
225
|
+
id.identifier === referral.cr_id &&
|
|
226
|
+
id.identifierType.uuid === IdentifierTypesUuids.CLIENT_REGISTRY_NO_UUID,
|
|
227
|
+
),
|
|
228
|
+
)?.uuid;
|
|
229
|
+
} catch {
|
|
230
|
+
// Registration-status check failed — fall back to the manual registry path below.
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
navigate({
|
|
234
|
+
to: amrsPatientUuid
|
|
235
|
+
? `\${openmrsSpaBase}/patient/${amrsPatientUuid}/chart`
|
|
236
|
+
: `\${openmrsSpaBase}/home/registry?emtCrId=${encodeURIComponent(referral.cr_id)}`,
|
|
237
|
+
});
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// The backend returned 404 during initiate/verify — another facility/user
|
|
241
|
+
// already handled this referral, or it no longer exists.
|
|
242
|
+
const handleReferralUnavailable = (referral: EmtReferralRow, reason: string) => {
|
|
243
|
+
mutate();
|
|
244
|
+
showSnackbar({
|
|
245
|
+
kind: 'warning',
|
|
246
|
+
title: 'Referral no longer available',
|
|
247
|
+
subtitle:
|
|
248
|
+
reason ||
|
|
249
|
+
`${referral.patientName || referral.cr_id} (${referral.case_number}) could not be found — it may have already been handled.`,
|
|
250
|
+
});
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const openHandover = (row: DataTableRow<any[]>) => {
|
|
254
|
+
const target = rowsById.get(String(row.id));
|
|
255
|
+
if (!target) return;
|
|
256
|
+
if (!locationUuid) {
|
|
257
|
+
showSnackbar({ kind: 'error', title: 'No default location selected', subtitle: '' });
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
launchWorkspace(EMT_HANDOVER_WORKSPACE, {
|
|
261
|
+
workspaceTitle: 'EMT Handover',
|
|
262
|
+
referral: target,
|
|
263
|
+
locationUuid,
|
|
264
|
+
onHandoverComplete: handleHandoverComplete,
|
|
265
|
+
onReferralUnavailable: handleReferralUnavailable,
|
|
266
|
+
});
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
return (
|
|
270
|
+
<div className={styles.container}>
|
|
271
|
+
<div className={styles.header}>
|
|
272
|
+
<FacilityAndWorkerSlot />
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
<div className={styles.toolbarRow}>
|
|
276
|
+
<div>
|
|
277
|
+
<h3 className={styles.title}>EMT / Referral</h3>
|
|
278
|
+
<p className={styles.subtitle}>
|
|
279
|
+
Incoming ambulance and EMT referrals awaiting handover.
|
|
280
|
+
</p>
|
|
281
|
+
</div>
|
|
282
|
+
<Button kind="tertiary" onClick={handleRefresh} disabled={isLoading}>
|
|
283
|
+
{isLoading ? <InlineLoading description="Refreshing…" /> : 'Refresh'}
|
|
284
|
+
</Button>
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
{globalError && (
|
|
288
|
+
<div className={styles.errorBanner} role="alert">
|
|
289
|
+
<span>{globalError}</span>
|
|
290
|
+
<Button size="sm" kind="ghost" onClick={handleRefresh}>
|
|
291
|
+
Retry
|
|
292
|
+
</Button>
|
|
293
|
+
</div>
|
|
294
|
+
)}
|
|
295
|
+
|
|
296
|
+
{showSkeleton ? (
|
|
297
|
+
<div className={styles.tableCard}>
|
|
298
|
+
<DataTableSkeleton role="progressbar" showHeader={false} showToolbar={false} />
|
|
299
|
+
</div>
|
|
300
|
+
) : rows.length === 0 && !globalError ? (
|
|
301
|
+
<div className={styles.tableCard}>
|
|
302
|
+
<EmptyState message="No pending EMT referrals for your facility." />
|
|
303
|
+
</div>
|
|
304
|
+
) : (
|
|
305
|
+
<>
|
|
306
|
+
<TableToolbar
|
|
307
|
+
id="emt-queue"
|
|
308
|
+
search={searchString}
|
|
309
|
+
onSearch={setSearchString}
|
|
310
|
+
searchPlaceholder={t('searchThisList', 'Search this list')}
|
|
311
|
+
/>
|
|
312
|
+
{visibleRows.length === 0 ? (
|
|
313
|
+
<div className={styles.tableCard}>
|
|
314
|
+
<EmptyState message="No referrals match your search." />
|
|
315
|
+
</div>
|
|
316
|
+
) : (
|
|
317
|
+
<DataTable rows={visibleRows} headers={columns}>
|
|
318
|
+
{({ rows: tableRows, headers, getTableProps, getHeaderProps, getRowProps, getCellProps }) => (
|
|
319
|
+
<div className={styles.tableCard}>
|
|
320
|
+
<Table size="sm" useZebraStyles aria-label="EMT referrals" {...getTableProps()}>
|
|
321
|
+
<TableHead>
|
|
322
|
+
<TableRow>
|
|
323
|
+
{headers
|
|
324
|
+
.filter((h) => h.key !== 'submissionId')
|
|
325
|
+
.map((header) => (
|
|
326
|
+
<TableHeader {...getHeaderProps({ header })}>{header.header}</TableHeader>
|
|
327
|
+
))}
|
|
328
|
+
</TableRow>
|
|
329
|
+
</TableHead>
|
|
330
|
+
<TableBody>
|
|
331
|
+
{tableRows.map((row) => {
|
|
332
|
+
const dataRow = rowsById.get(String(row.id));
|
|
333
|
+
return (
|
|
334
|
+
<TableRow {...getRowProps({ row })}>
|
|
335
|
+
{row.cells.map((cell) => {
|
|
336
|
+
if (cell.info.header === 'submissionId') return null;
|
|
337
|
+
|
|
338
|
+
if (cell.info.header === 'patientName') {
|
|
339
|
+
return (
|
|
340
|
+
<TableCell key={cell.id}>
|
|
341
|
+
<div className={styles.patientCell}>
|
|
342
|
+
<span>{cell.value}</span>
|
|
343
|
+
{dataRow?.crLoading && (
|
|
344
|
+
<InlineLoading description="…" />
|
|
345
|
+
)}
|
|
346
|
+
{dataRow?.crError && (
|
|
347
|
+
<span className={styles.muted}> ({dataRow.crError})</span>
|
|
348
|
+
)}
|
|
349
|
+
</div>
|
|
350
|
+
</TableCell>
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
if (cell.info.header === 'interventions') {
|
|
354
|
+
const list = dataRow?.interventions ?? [];
|
|
355
|
+
return (
|
|
356
|
+
<TableCell key={cell.id}>
|
|
357
|
+
{list.length ? (
|
|
358
|
+
list.map((iv) => (
|
|
359
|
+
<Tag key={iv} size="sm" type="gray">
|
|
360
|
+
{iv}
|
|
361
|
+
</Tag>
|
|
362
|
+
))
|
|
363
|
+
) : (
|
|
364
|
+
'—'
|
|
365
|
+
)}
|
|
366
|
+
</TableCell>
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
if (cell.info.header === 'notes') {
|
|
370
|
+
const notes = dataRow?.referral_notes ?? '';
|
|
371
|
+
return (
|
|
372
|
+
<TableCell key={cell.id} className={styles.notesCell} title={notes}>
|
|
373
|
+
{notes ? `${notes.slice(0, 60)}${notes.length > 60 ? '…' : ''}` : '—'}
|
|
374
|
+
</TableCell>
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
if (cell.info.header === 'requestedAt') {
|
|
378
|
+
return (
|
|
379
|
+
<TableCell key={cell.id}>
|
|
380
|
+
<div className={styles.timeCell}>
|
|
381
|
+
<span>{relativeTime(dataRow?.requested_at)}</span>
|
|
382
|
+
<span className={styles.muted}>
|
|
383
|
+
{dataRow?.requested_at
|
|
384
|
+
? dayjs(dataRow.requested_at).format('DD MMM, HH:mm')
|
|
385
|
+
: ''}
|
|
386
|
+
</span>
|
|
387
|
+
</div>
|
|
388
|
+
</TableCell>
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
if (cell.info.header === 'status') {
|
|
392
|
+
return (
|
|
393
|
+
<TableCell key={cell.id}>
|
|
394
|
+
<Tag size="sm" type="warm-gray">
|
|
395
|
+
{dataRow?.status ?? 'pending'}
|
|
396
|
+
</Tag>
|
|
397
|
+
</TableCell>
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
if (cell.info.header === 'action') {
|
|
401
|
+
return (
|
|
402
|
+
<TableCell key={cell.id} className={styles.actionCell}>
|
|
403
|
+
<Button kind="tertiary" size="sm" onClick={() => openHandover(row)}>
|
|
404
|
+
Handover
|
|
405
|
+
</Button>
|
|
406
|
+
</TableCell>
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
return (
|
|
410
|
+
<TableCell key={cell.id} {...getCellProps({ cell })}>
|
|
411
|
+
{cell.value}
|
|
412
|
+
</TableCell>
|
|
413
|
+
);
|
|
414
|
+
})}
|
|
415
|
+
</TableRow>
|
|
416
|
+
);
|
|
417
|
+
})}
|
|
418
|
+
</TableBody>
|
|
419
|
+
</Table>
|
|
420
|
+
</div>
|
|
421
|
+
)}
|
|
422
|
+
</DataTable>
|
|
423
|
+
)}
|
|
424
|
+
{visibleRows.length > 0 && (
|
|
425
|
+
<Pagination
|
|
426
|
+
forwardText={t('nextPage', 'Next page')}
|
|
427
|
+
backwardText={t('previousPage', 'Previous page')}
|
|
428
|
+
page={pageIndex + 1}
|
|
429
|
+
pageSize={pageSize}
|
|
430
|
+
pageSizes={[10, 20, 30, 40, 50]}
|
|
431
|
+
totalItems={count}
|
|
432
|
+
onChange={({ pageSize: ps, page }) => {
|
|
433
|
+
if (ps !== pageSize) {
|
|
434
|
+
setPageSize(ps);
|
|
435
|
+
setPageIndex(0);
|
|
436
|
+
} else {
|
|
437
|
+
setPageIndex(page - 1);
|
|
438
|
+
}
|
|
439
|
+
}}
|
|
440
|
+
/>
|
|
441
|
+
)}
|
|
442
|
+
</>
|
|
443
|
+
)}
|
|
444
|
+
</div>
|
|
445
|
+
);
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
export default EmtQueue;
|
|
449
|
+
export { EMT_PENDING_KEY };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
padding: 1rem;
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
gap: 0.75rem;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.header {
|
|
9
|
+
/* Matches FacilityAndWorkerSlot layout in other queue pages. */
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.toolbarRow {
|
|
13
|
+
display: flex;
|
|
14
|
+
align-items: flex-start;
|
|
15
|
+
justify-content: space-between;
|
|
16
|
+
gap: 1rem;
|
|
17
|
+
flex-wrap: wrap;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.title {
|
|
21
|
+
margin: 0;
|
|
22
|
+
font-size: 1rem;
|
|
23
|
+
font-weight: 600;
|
|
24
|
+
color: #161616;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.subtitle {
|
|
28
|
+
margin: 0.125rem 0 0;
|
|
29
|
+
font-size: 0.8125rem;
|
|
30
|
+
color: #6f6f6f;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.errorBanner {
|
|
34
|
+
display: flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
gap: 0.75rem;
|
|
37
|
+
padding: 0.75rem 1rem;
|
|
38
|
+
background-color: #fff1f1;
|
|
39
|
+
border: 1px solid #e0737a;
|
|
40
|
+
border-radius: 0.25rem;
|
|
41
|
+
color: #da1e28;
|
|
42
|
+
font-size: 0.8125rem;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.tableCard {
|
|
46
|
+
overflow-x: auto;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.actionCell {
|
|
50
|
+
white-space: nowrap;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.actionCell :global(.cds--btn) {
|
|
54
|
+
min-height: 2rem;
|
|
55
|
+
width: max-content;
|
|
56
|
+
white-space: nowrap;
|
|
57
|
+
padding-inline: 0.75rem;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.patientCell {
|
|
61
|
+
display: flex;
|
|
62
|
+
align-items: center;
|
|
63
|
+
gap: 0.375rem;
|
|
64
|
+
flex-wrap: wrap;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.notesCell {
|
|
68
|
+
max-width: 12rem;
|
|
69
|
+
overflow: hidden;
|
|
70
|
+
text-overflow: ellipsis;
|
|
71
|
+
white-space: nowrap;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.timeCell {
|
|
75
|
+
display: flex;
|
|
76
|
+
flex-direction: column;
|
|
77
|
+
line-height: 1.3;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.muted {
|
|
81
|
+
color: #a8a8a8;
|
|
82
|
+
font-size: 0.75rem;
|
|
83
|
+
}
|