@sasindi/radiologytestscheduling 0.0.0-stage → 1.0.2
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/LICENSE +354 -0
- package/README.md +166 -2
- package/dist/117.js +1 -0
- package/dist/117.js.map +1 -0
- package/dist/197.js +1 -0
- package/dist/197.js.map +1 -0
- package/dist/206.css +1 -0
- package/dist/206.css.map +1 -0
- package/dist/206.js +39 -0
- package/dist/206.js.map +1 -0
- package/dist/237.js +1 -0
- package/dist/237.js.map +1 -0
- package/dist/29.js +1 -0
- package/dist/29.js.map +1 -0
- package/dist/362.js +1 -0
- package/dist/362.js.map +1 -0
- package/dist/410.js +1 -0
- package/dist/410.js.map +1 -0
- package/dist/480.js +1 -0
- package/dist/480.js.map +1 -0
- package/dist/517.js +1 -0
- package/dist/517.js.map +1 -0
- package/dist/538.js +1 -0
- package/dist/538.js.map +1 -0
- package/dist/540.css +1 -0
- package/dist/540.css.map +1 -0
- package/dist/559.js +1 -0
- package/dist/559.js.map +1 -0
- package/dist/570.js +60 -0
- package/dist/570.js.map +1 -0
- package/dist/578.js +1 -0
- package/dist/578.js.map +1 -0
- package/dist/647.js +1 -0
- package/dist/647.js.map +1 -0
- package/dist/652.js +1 -0
- package/dist/652.js.map +1 -0
- package/dist/668.js +1 -0
- package/dist/668.js.map +1 -0
- package/dist/749.js +1 -0
- package/dist/749.js.map +1 -0
- package/dist/809.js +22 -0
- package/dist/809.js.map +1 -0
- package/dist/813.js +1 -0
- package/dist/813.js.map +1 -0
- package/dist/898.js +1 -0
- package/dist/898.js.map +1 -0
- package/dist/974.css +1 -0
- package/dist/974.css.map +1 -0
- package/dist/974.js +1 -0
- package/dist/974.js.map +1 -0
- package/dist/976.js +1 -0
- package/dist/976.js.map +1 -0
- package/dist/977.js +1 -0
- package/dist/977.js.map +1 -0
- package/dist/98.js +1 -0
- package/dist/98.js.map +1 -0
- package/dist/openmrs-esm-radiologytestscheduling.js +3 -0
- package/dist/openmrs-esm-radiologytestscheduling.js.map +1 -0
- package/dist/routes.json +1 -0
- package/package.json +101 -4
- package/src/config-schema.ts +21 -0
- package/src/constants.ts +1 -0
- package/src/declarations.d.ts +23 -0
- package/src/index.ts +28 -0
- package/src/radiology-test-scheduling.component.tsx +711 -0
- package/src/radiology-test-scheduling.resource.ts +339 -0
- package/src/radiology-test-scheduling.scss +396 -0
- package/src/root.component.test.tsx +12 -0
- package/src/root.component.tsx +13 -0
- package/src/root.scss +5 -0
- package/src/routes.json +12 -0
- package/src/types.ts +100 -0
|
@@ -0,0 +1,711 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Loading } from '@carbon/react';
|
|
4
|
+
import Swal from 'sweetalert2';
|
|
5
|
+
import {
|
|
6
|
+
fetchCurrentSessionAndInstitute,
|
|
7
|
+
fetchInstitutes,
|
|
8
|
+
fetchPendingSchedulingOrders,
|
|
9
|
+
fetchPatientDetails,
|
|
10
|
+
fetchPatientBasicInfo,
|
|
11
|
+
fetchLocationDisplay,
|
|
12
|
+
fetchUserDisplay,
|
|
13
|
+
fetchExamRooms,
|
|
14
|
+
saveScheduling,
|
|
15
|
+
SessionInfo,
|
|
16
|
+
} from './radiology-test-scheduling.resource';
|
|
17
|
+
import {
|
|
18
|
+
SchedulingOrderDTO,
|
|
19
|
+
PatientDemographics,
|
|
20
|
+
InstituteOption,
|
|
21
|
+
ExamRoom,
|
|
22
|
+
} from './types';
|
|
23
|
+
import styles from './radiology-test-scheduling.scss';
|
|
24
|
+
|
|
25
|
+
const DEFAULT_AVATAR =
|
|
26
|
+
'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 24 24" fill="%23b0bec5"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>';
|
|
27
|
+
|
|
28
|
+
const RadiologyTestScheduling: React.FC = () => {
|
|
29
|
+
const { t } = useTranslation();
|
|
30
|
+
|
|
31
|
+
// Session & Institute states
|
|
32
|
+
const [sessionInfo, setSessionInfo] = useState<SessionInfo>({
|
|
33
|
+
userUuid: '',
|
|
34
|
+
personUuid: '',
|
|
35
|
+
locationUuid: '',
|
|
36
|
+
locationName: '',
|
|
37
|
+
});
|
|
38
|
+
const [institutes, setInstitutes] = useState<InstituteOption[]>([]);
|
|
39
|
+
const [selectedInstitute, setSelectedInstitute] = useState<string>('all');
|
|
40
|
+
|
|
41
|
+
// Pending Tests & Table states
|
|
42
|
+
const [pendingOrders, setPendingOrders] = useState<SchedulingOrderDTO[]>([]);
|
|
43
|
+
const [loadingOrders, setLoadingOrders] = useState<boolean>(true);
|
|
44
|
+
const [searchTerm, setSearchTerm] = useState<string>('');
|
|
45
|
+
|
|
46
|
+
// Selected Order & Patient state
|
|
47
|
+
const [selectedOrder, setSelectedOrder] = useState<SchedulingOrderDTO | null>(null);
|
|
48
|
+
const [patientDetails, setPatientDetails] = useState<PatientDemographics | null>(null);
|
|
49
|
+
const [loadingPatient, setLoadingPatient] = useState<boolean>(false);
|
|
50
|
+
|
|
51
|
+
// Scheduling Form inputs: Scheduled Date Time, Remarks, Modality, Exam Room
|
|
52
|
+
const [scheduledDateTime, setScheduledDateTime] = useState<string>('');
|
|
53
|
+
const [remarks, setRemarks] = useState<string>('');
|
|
54
|
+
const [selectedModality, setSelectedModality] = useState<string>('all');
|
|
55
|
+
const [examRooms, setExamRooms] = useState<ExamRoom[]>([]);
|
|
56
|
+
const [selectedExamRoom, setSelectedExamRoom] = useState<string>('all');
|
|
57
|
+
const [isSaving, setIsSaving] = useState<boolean>(false);
|
|
58
|
+
|
|
59
|
+
// Initialize Session and Institutes
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
let isMounted = true;
|
|
62
|
+
(async () => {
|
|
63
|
+
try {
|
|
64
|
+
const [session, instList] = await Promise.all([
|
|
65
|
+
fetchCurrentSessionAndInstitute(),
|
|
66
|
+
fetchInstitutes(),
|
|
67
|
+
]);
|
|
68
|
+
if (!isMounted) return;
|
|
69
|
+
setSessionInfo(session);
|
|
70
|
+
setInstitutes(instList);
|
|
71
|
+
if (session.locationUuid) {
|
|
72
|
+
setSelectedInstitute(session.locationUuid);
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.error('Initialization failed', err);
|
|
76
|
+
}
|
|
77
|
+
})();
|
|
78
|
+
return () => {
|
|
79
|
+
isMounted = false;
|
|
80
|
+
};
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
// Load Pending Orders whenever selected institute changes
|
|
84
|
+
const loadPendingOrders = useCallback(async (instUuid: string) => {
|
|
85
|
+
setLoadingOrders(true);
|
|
86
|
+
try {
|
|
87
|
+
const orders = await fetchPendingSchedulingOrders(instUuid);
|
|
88
|
+
|
|
89
|
+
// Enhance with location, clinician, and patient basic demographics
|
|
90
|
+
const enhanced = await Promise.all(
|
|
91
|
+
orders.map(async (dto) => {
|
|
92
|
+
const loc = dto.radiology.location ? await fetchLocationDisplay(dto.radiology.location) : '';
|
|
93
|
+
const ordBy = dto.radiology.requesterReference
|
|
94
|
+
? await fetchUserDisplay(dto.radiology.requesterReference)
|
|
95
|
+
: '';
|
|
96
|
+
const patientInfo = dto.radiology.patientPhn
|
|
97
|
+
? await fetchPatientBasicInfo(dto.radiology.patientPhn)
|
|
98
|
+
: { name: '', gender: '', age: '' };
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
...dto,
|
|
102
|
+
locationName: loc,
|
|
103
|
+
orderedByName: ordBy,
|
|
104
|
+
patientName: patientInfo.name,
|
|
105
|
+
patientGender: patientInfo.gender,
|
|
106
|
+
patientAge: patientInfo.age,
|
|
107
|
+
};
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
setPendingOrders(enhanced);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
console.error('Failed to load pending scheduling orders', err);
|
|
113
|
+
Swal.fire({
|
|
114
|
+
icon: 'error',
|
|
115
|
+
title: 'Error Loading Orders',
|
|
116
|
+
text: 'Unable to fetch pending radiology test orders.',
|
|
117
|
+
confirmButtonColor: '#da1e28',
|
|
118
|
+
});
|
|
119
|
+
} finally {
|
|
120
|
+
setLoadingOrders(false);
|
|
121
|
+
}
|
|
122
|
+
}, []);
|
|
123
|
+
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
loadPendingOrders(selectedInstitute);
|
|
126
|
+
}, [selectedInstitute, loadPendingOrders]);
|
|
127
|
+
|
|
128
|
+
// Load Exam Rooms when modality changes
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
if (selectedModality === 'all' || !selectedModality) {
|
|
131
|
+
setExamRooms([]);
|
|
132
|
+
setSelectedExamRoom('all');
|
|
133
|
+
} else {
|
|
134
|
+
(async () => {
|
|
135
|
+
const rooms = await fetchExamRooms(selectedModality);
|
|
136
|
+
setExamRooms(rooms);
|
|
137
|
+
})();
|
|
138
|
+
}
|
|
139
|
+
}, [selectedModality]);
|
|
140
|
+
|
|
141
|
+
// Handle row selection
|
|
142
|
+
const handleSelectRow = async (dto: SchedulingOrderDTO) => {
|
|
143
|
+
setSelectedOrder(dto);
|
|
144
|
+
setScheduledDateTime('');
|
|
145
|
+
setRemarks('');
|
|
146
|
+
setLoadingPatient(true);
|
|
147
|
+
|
|
148
|
+
// Auto-populate Modality and Exam Room from selected test order if available
|
|
149
|
+
const modality = dto.radiology.categoryDisplay || 'all';
|
|
150
|
+
setSelectedModality(modality);
|
|
151
|
+
|
|
152
|
+
if (modality && modality !== 'all') {
|
|
153
|
+
try {
|
|
154
|
+
const rooms = await fetchExamRooms(modality);
|
|
155
|
+
setExamRooms(rooms);
|
|
156
|
+
const roomName = dto.radiology.supportingInfo;
|
|
157
|
+
if (roomName && roomName !== '--Select--') {
|
|
158
|
+
setSelectedExamRoom(roomName);
|
|
159
|
+
} else {
|
|
160
|
+
setSelectedExamRoom('all');
|
|
161
|
+
}
|
|
162
|
+
} catch (e) {
|
|
163
|
+
console.warn('Failed to load exam rooms on select', e);
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
setSelectedExamRoom('all');
|
|
167
|
+
setExamRooms([]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const details = await fetchPatientDetails(dto.radiology.patientPhn);
|
|
172
|
+
setPatientDetails(details);
|
|
173
|
+
} catch (e) {
|
|
174
|
+
console.error('Failed to load patient details', e);
|
|
175
|
+
} finally {
|
|
176
|
+
setLoadingPatient(false);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Reset selection
|
|
181
|
+
const handleClear = () => {
|
|
182
|
+
setSelectedOrder(null);
|
|
183
|
+
setPatientDetails(null);
|
|
184
|
+
setScheduledDateTime('');
|
|
185
|
+
setRemarks('');
|
|
186
|
+
setSelectedModality('all');
|
|
187
|
+
setSelectedExamRoom('all');
|
|
188
|
+
setExamRooms([]);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Save Schedule Handler
|
|
192
|
+
const handleSave = async () => {
|
|
193
|
+
if (!selectedOrder) {
|
|
194
|
+
Swal.fire({
|
|
195
|
+
icon: 'warning',
|
|
196
|
+
title: 'No Order Selected',
|
|
197
|
+
text: 'Please select a pending test order from the table to schedule.',
|
|
198
|
+
confirmButtonColor: '#0f62fe',
|
|
199
|
+
});
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!scheduledDateTime) {
|
|
204
|
+
Swal.fire({
|
|
205
|
+
icon: 'warning',
|
|
206
|
+
title: 'Date & Time Required',
|
|
207
|
+
text: 'Please choose the Scheduled Date and Time before saving.',
|
|
208
|
+
confirmButtonColor: '#0f62fe',
|
|
209
|
+
});
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Format datetime string: "YYYY-MM-DD HH:mm:ss"
|
|
214
|
+
const formattedDateTime = scheduledDateTime.replace('T', ' ') + (scheduledDateTime.length === 16 ? ':00' : '');
|
|
215
|
+
|
|
216
|
+
const result = await Swal.fire({
|
|
217
|
+
title: 'Confirm Scheduling',
|
|
218
|
+
text: `Schedule ${selectedOrder.radiology.codeDisplay || 'test'} on ${formattedDateTime}?`,
|
|
219
|
+
icon: 'question',
|
|
220
|
+
showCancelButton: true,
|
|
221
|
+
confirmButtonColor: '#0f62fe',
|
|
222
|
+
cancelButtonColor: '#6c757d',
|
|
223
|
+
confirmButtonText: 'Yes, Schedule',
|
|
224
|
+
cancelButtonText: 'Cancel',
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
if (!result.isConfirmed) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
setIsSaving(true);
|
|
232
|
+
try {
|
|
233
|
+
await saveScheduling(selectedOrder.type, {
|
|
234
|
+
id: selectedOrder.radiology.id,
|
|
235
|
+
scheduledDateTime: formattedDateTime,
|
|
236
|
+
remarks: remarks || '',
|
|
237
|
+
cby: sessionInfo.userUuid || '1',
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
await Swal.fire({
|
|
241
|
+
icon: 'success',
|
|
242
|
+
title: 'Saved Successfully!',
|
|
243
|
+
text: 'The radiology test has been scheduled.',
|
|
244
|
+
timer: 2000,
|
|
245
|
+
showConfirmButton: false,
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
handleClear();
|
|
249
|
+
loadPendingOrders(selectedInstitute);
|
|
250
|
+
} catch (err: any) {
|
|
251
|
+
console.error('Failed to save schedule', err);
|
|
252
|
+
Swal.fire({
|
|
253
|
+
icon: 'error',
|
|
254
|
+
title: 'Save Failed',
|
|
255
|
+
text: err?.message || 'Could not schedule radiology test. Please check server logs.',
|
|
256
|
+
confirmButtonColor: '#da1e28',
|
|
257
|
+
});
|
|
258
|
+
} finally {
|
|
259
|
+
setIsSaving(false);
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// Filtered Orders for search input
|
|
264
|
+
const filteredOrders = useMemo(() => {
|
|
265
|
+
if (!searchTerm.trim()) return pendingOrders;
|
|
266
|
+
const term = searchTerm.toLowerCase();
|
|
267
|
+
return pendingOrders.filter((dto) => {
|
|
268
|
+
const phn = dto.radiology.patientPhn?.toLowerCase() || '';
|
|
269
|
+
const name = dto.patientName?.toLowerCase() || '';
|
|
270
|
+
const test = dto.radiology.codeDisplay?.toLowerCase() || '';
|
|
271
|
+
const section = dto.type?.toLowerCase() || '';
|
|
272
|
+
const priority = dto.radiology.priority?.toLowerCase() || '';
|
|
273
|
+
const location = dto.locationName?.toLowerCase() || '';
|
|
274
|
+
return (
|
|
275
|
+
phn.includes(term) ||
|
|
276
|
+
name.includes(term) ||
|
|
277
|
+
test.includes(term) ||
|
|
278
|
+
section.includes(term) ||
|
|
279
|
+
priority.includes(term) ||
|
|
280
|
+
location.includes(term)
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
}, [pendingOrders, searchTerm]);
|
|
284
|
+
|
|
285
|
+
return (
|
|
286
|
+
<div className={styles.bodyWrapper}>
|
|
287
|
+
{/* Main Unified Card: Heading inside the form container as required */}
|
|
288
|
+
<div className={styles.card}>
|
|
289
|
+
<div className={styles.cardHeader}>
|
|
290
|
+
<h2 className={styles.formHeading}>
|
|
291
|
+
{t('radiologyTestSchedulingHeading', 'Radiology Test Scheduling')}
|
|
292
|
+
</h2>
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
<div className={styles.serviceBox}>
|
|
296
|
+
{/* Top Filter Row: Institute Dropdown & Search Bar */}
|
|
297
|
+
<div className={styles.filterRow}>
|
|
298
|
+
<div className={styles.instituteSelectGroup}>
|
|
299
|
+
<h4 className={styles.instituteLabel}>{t('institute', 'Institute')}</h4>
|
|
300
|
+
<select
|
|
301
|
+
id="drpInstitute"
|
|
302
|
+
className={styles.formControl}
|
|
303
|
+
style={{ width: '260px' }}
|
|
304
|
+
value={selectedInstitute}
|
|
305
|
+
onChange={(e) => setSelectedInstitute(e.target.value)}
|
|
306
|
+
>
|
|
307
|
+
<option value="all">{t('allInstitutes', '-- All Institutes --')}</option>
|
|
308
|
+
{institutes.map((inst) => (
|
|
309
|
+
<option key={inst.uuid} value={inst.uuid}>
|
|
310
|
+
{inst.name}
|
|
311
|
+
</option>
|
|
312
|
+
))}
|
|
313
|
+
</select>
|
|
314
|
+
</div>
|
|
315
|
+
|
|
316
|
+
<div className={styles.searchGroup}>
|
|
317
|
+
<input
|
|
318
|
+
type="text"
|
|
319
|
+
className={styles.formControl}
|
|
320
|
+
placeholder={t('searchPlaceholder', 'Search by PHN, Patient Name, Test Name, Section, or Location...')}
|
|
321
|
+
value={searchTerm}
|
|
322
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
323
|
+
/>
|
|
324
|
+
</div>
|
|
325
|
+
</div>
|
|
326
|
+
|
|
327
|
+
{/* Table Container: Exact 17 columns as in team's module */}
|
|
328
|
+
<div className={styles.tableContainer}>
|
|
329
|
+
{loadingOrders ? (
|
|
330
|
+
<div style={{ padding: '2rem' }}>
|
|
331
|
+
<Loading description="Loading pending tests..." withOverlay={false} small />
|
|
332
|
+
</div>
|
|
333
|
+
) : filteredOrders.length === 0 ? (
|
|
334
|
+
<div className={styles.emptyState}>
|
|
335
|
+
{t('noPendingOrders', 'No pending radiology test scheduling requests found.')}
|
|
336
|
+
</div>
|
|
337
|
+
) : (
|
|
338
|
+
<table id="tblRadiologyTestScheduling" className={styles.table}>
|
|
339
|
+
<thead>
|
|
340
|
+
<tr>
|
|
341
|
+
<th>#</th>
|
|
342
|
+
<th>{t('institute', 'Institute')}</th>
|
|
343
|
+
<th>{t('section', 'Section')}</th>
|
|
344
|
+
<th>{t('phn', 'PHN')}</th>
|
|
345
|
+
<th>{t('name', 'Name')}</th>
|
|
346
|
+
<th>{t('gender', 'Gender')}</th>
|
|
347
|
+
<th>{t('age', 'Age')}</th>
|
|
348
|
+
<th>{t('admittedDate', 'Admitted Date')}</th>
|
|
349
|
+
<th>{t('orderedBy', 'Ordered By')}</th>
|
|
350
|
+
<th>{t('orderedDatetime', 'Ordered Datetime')}</th>
|
|
351
|
+
<th>{t('testName', 'Test Name')}</th>
|
|
352
|
+
<th>{t('region', 'Region')}</th>
|
|
353
|
+
<th>{t('examRoom', 'Exam Room')}</th>
|
|
354
|
+
<th>{t('comment', 'Comment')}</th>
|
|
355
|
+
<th>{t('priority', 'Priority')}</th>
|
|
356
|
+
<th>{t('consultant', 'Consultant')}</th>
|
|
357
|
+
<th style={{ textAlign: 'center' }}>{t('action', 'Action')}</th>
|
|
358
|
+
</tr>
|
|
359
|
+
</thead>
|
|
360
|
+
<tbody id="tbodyRadTestsScheduling">
|
|
361
|
+
{filteredOrders.map((dto, idx) => {
|
|
362
|
+
const isUrgent = dto.radiology.priority === 'Urgent';
|
|
363
|
+
const isSelected = selectedOrder?.radiology.id === dto.radiology.id;
|
|
364
|
+
const rowClass = `${isUrgent ? styles.urgentRow : ''} ${
|
|
365
|
+
isSelected ? styles.selectedRow : ''
|
|
366
|
+
}`.trim();
|
|
367
|
+
|
|
368
|
+
return (
|
|
369
|
+
<tr key={`${dto.type}-${dto.radiology.id}`} className={rowClass}>
|
|
370
|
+
<td>{idx + 1}</td>
|
|
371
|
+
<td>{dto.locationName || dto.radiology.location}</td>
|
|
372
|
+
<td>
|
|
373
|
+
<span className={`${styles.badge} ${styles.badgeType}`}>
|
|
374
|
+
{dto.type}
|
|
375
|
+
</span>
|
|
376
|
+
</td>
|
|
377
|
+
<td>
|
|
378
|
+
<strong>{dto.radiology.patientPhn}</strong>
|
|
379
|
+
</td>
|
|
380
|
+
<td>{dto.patientName || '-'}</td>
|
|
381
|
+
<td>{dto.patientGender || '-'}</td>
|
|
382
|
+
<td>{dto.patientAge !== undefined && dto.patientAge !== '' ? dto.patientAge : '-'}</td>
|
|
383
|
+
<td>
|
|
384
|
+
{dto.radiology.occurrenceDateTime
|
|
385
|
+
? dto.radiology.occurrenceDateTime.split(' ')[0]
|
|
386
|
+
: '-'}
|
|
387
|
+
</td>
|
|
388
|
+
<td>{dto.orderedByName || dto.radiology.requesterReference || '-'}</td>
|
|
389
|
+
<td>{dto.radiology.occurrenceDateTime || '-'}</td>
|
|
390
|
+
<td>
|
|
391
|
+
<strong>{dto.radiology.codeDisplay || '-'}</strong>
|
|
392
|
+
</td>
|
|
393
|
+
<td>{dto.radiology.bodySiteText || '-'}</td>
|
|
394
|
+
<td>
|
|
395
|
+
{dto.radiology.supportingInfo === '--Select--'
|
|
396
|
+
? '-'
|
|
397
|
+
: dto.radiology.supportingInfo || '-'}
|
|
398
|
+
</td>
|
|
399
|
+
<td>{dto.radiology.note || '-'}</td>
|
|
400
|
+
<td>
|
|
401
|
+
<span
|
|
402
|
+
className={`${styles.badge} ${
|
|
403
|
+
isUrgent ? styles.badgeUrgent : styles.badgeRoutine
|
|
404
|
+
}`}
|
|
405
|
+
>
|
|
406
|
+
{dto.radiology.priority || 'Routine'}
|
|
407
|
+
</span>
|
|
408
|
+
</td>
|
|
409
|
+
<td>
|
|
410
|
+
{dto.radiology.requesterDisplay === '--Select--'
|
|
411
|
+
? '-'
|
|
412
|
+
: dto.radiology.requesterDisplay || '-'}
|
|
413
|
+
</td>
|
|
414
|
+
<td style={{ textAlign: 'center' }}>
|
|
415
|
+
<button
|
|
416
|
+
type="button"
|
|
417
|
+
className={`${styles.btnSelect} ${isSelected ? styles.btnSelected : ''}`}
|
|
418
|
+
onClick={() => handleSelectRow(dto)}
|
|
419
|
+
>
|
|
420
|
+
{isSelected ? t('selected', 'Selected') : t('select', 'Select')}
|
|
421
|
+
</button>
|
|
422
|
+
</td>
|
|
423
|
+
</tr>
|
|
424
|
+
);
|
|
425
|
+
})}
|
|
426
|
+
</tbody>
|
|
427
|
+
</table>
|
|
428
|
+
)}
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
{/* Demographics, Investigation Details & Scheduling Form Section */}
|
|
432
|
+
<div className={styles.demographicsSection}>
|
|
433
|
+
<div className={styles.row}>
|
|
434
|
+
{/* Patient Info (col10) */}
|
|
435
|
+
<div className={`${styles.col10} ${styles.formGroup}`}>
|
|
436
|
+
{loadingPatient ? (
|
|
437
|
+
<div style={{ padding: '1rem' }}>
|
|
438
|
+
<Loading description="Loading patient details..." withOverlay={false} small />
|
|
439
|
+
</div>
|
|
440
|
+
) : (
|
|
441
|
+
<div className={styles.row}>
|
|
442
|
+
{/* Left Column */}
|
|
443
|
+
<div className={styles.col6}>
|
|
444
|
+
<div className={styles.fieldItem}>
|
|
445
|
+
<span className={styles.fieldIcon}>👉</span>
|
|
446
|
+
<span className={styles.fieldTitle}>{t('fullName', 'Full Name')}:</span>
|
|
447
|
+
<span id="txtFullName" className={styles.fieldVal}>
|
|
448
|
+
{patientDetails?.fullName || (selectedOrder?.patientName || '-')}
|
|
449
|
+
</span>
|
|
450
|
+
</div>
|
|
451
|
+
<div className={styles.fieldItem}>
|
|
452
|
+
<span className={styles.fieldIcon}>👉</span>
|
|
453
|
+
<span className={styles.fieldTitle}>{t('gender', 'Gender')}:</span>
|
|
454
|
+
<span id="txtGender" className={styles.fieldVal}>
|
|
455
|
+
{patientDetails?.gender || (selectedOrder?.patientGender || '-')}
|
|
456
|
+
</span>
|
|
457
|
+
</div>
|
|
458
|
+
<div className={styles.fieldItem}>
|
|
459
|
+
<span className={styles.fieldIcon}>👉</span>
|
|
460
|
+
<span className={styles.fieldTitle}>{t('nicNo', 'NIC No')}:</span>
|
|
461
|
+
<span id="txtNICNo" className={styles.fieldVal}>{patientDetails?.nicNo || '-'}</span>
|
|
462
|
+
</div>
|
|
463
|
+
<div className={styles.fieldItem}>
|
|
464
|
+
<span className={styles.fieldIcon}>👉</span>
|
|
465
|
+
<span className={styles.fieldTitle}>{t('guardianInfo', 'Guardian Info')}:</span>
|
|
466
|
+
<span id="txtGurdianInfo" className={styles.fieldVal}>{patientDetails?.guardianInfo || '-'}</span>
|
|
467
|
+
</div>
|
|
468
|
+
</div>
|
|
469
|
+
|
|
470
|
+
{/* Right Column */}
|
|
471
|
+
<div className={styles.col6}>
|
|
472
|
+
<div className={styles.fieldItem}>
|
|
473
|
+
<span className={styles.fieldIcon}>👉</span>
|
|
474
|
+
<span className={styles.fieldTitle}>{t('address', 'Address')}:</span>
|
|
475
|
+
<span id="txtAddress" className={styles.fieldVal}>{patientDetails?.address || '-'}</span>
|
|
476
|
+
</div>
|
|
477
|
+
<div className={styles.fieldItem}>
|
|
478
|
+
<span className={styles.fieldIcon}>👉</span>
|
|
479
|
+
<span className={styles.fieldTitle}>{t('age', 'Age')}:</span>
|
|
480
|
+
<span className={styles.fieldVal}>
|
|
481
|
+
Y: <span id="txtYears">{patientDetails?.years !== undefined && patientDetails?.years !== '' ? patientDetails.years : '-'}</span> |{' '}
|
|
482
|
+
M: <span id="txtMonths">{patientDetails?.months !== undefined && patientDetails?.months !== '' ? patientDetails.months : '-'}</span> |{' '}
|
|
483
|
+
D: <span id="txtDays">{patientDetails?.days !== undefined && patientDetails?.days !== '' ? patientDetails.days : '-'}</span>
|
|
484
|
+
</span>
|
|
485
|
+
</div>
|
|
486
|
+
<div className={styles.fieldItem}>
|
|
487
|
+
<span className={styles.fieldIcon}>👉</span>
|
|
488
|
+
<span className={styles.fieldTitle}>{t('dob', 'Date Of Birth')}:</span>
|
|
489
|
+
<span id="txtDateOfBirth" className={styles.fieldVal}>{patientDetails?.dateOfBirth || '-'}</span>
|
|
490
|
+
</div>
|
|
491
|
+
</div>
|
|
492
|
+
</div>
|
|
493
|
+
)}
|
|
494
|
+
</div>
|
|
495
|
+
|
|
496
|
+
{/* Patient Image (col2) */}
|
|
497
|
+
<div className={`${styles.col2} ${styles.uploadImgWrapper}`}>
|
|
498
|
+
<img
|
|
499
|
+
id="file_upload"
|
|
500
|
+
className={styles.uploadImg}
|
|
501
|
+
src={patientDetails?.profileImage || DEFAULT_AVATAR}
|
|
502
|
+
alt="Patient"
|
|
503
|
+
onError={(e) => {
|
|
504
|
+
(e.currentTarget as HTMLImageElement).src = DEFAULT_AVATAR;
|
|
505
|
+
}}
|
|
506
|
+
/>
|
|
507
|
+
</div>
|
|
508
|
+
|
|
509
|
+
{/* Investigation Details Row (col12 with border-bottom) */}
|
|
510
|
+
<div className={`${styles.col12} ${styles.investigationRow}`}>
|
|
511
|
+
<div className={styles.row}>
|
|
512
|
+
<div className={styles.col3}>
|
|
513
|
+
<div className={styles.fieldItem}>
|
|
514
|
+
<span className={styles.fieldIcon}>👉</span>
|
|
515
|
+
<span className={styles.fieldTitle}>{t('testName', 'Test Name')}:</span>
|
|
516
|
+
<span id="lblTestName" className={styles.fieldVal}>
|
|
517
|
+
{selectedOrder?.radiology.codeDisplay || '-'}
|
|
518
|
+
</span>
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
521
|
+
<div className={styles.col3}>
|
|
522
|
+
<div className={styles.fieldItem}>
|
|
523
|
+
<span className={styles.fieldIcon}>👉</span>
|
|
524
|
+
<span className={styles.fieldTitle}>{t('region', 'Region')}:</span>
|
|
525
|
+
<span id="lblRegion" className={styles.fieldVal}>
|
|
526
|
+
{selectedOrder?.radiology.bodySiteText || '-'}
|
|
527
|
+
</span>
|
|
528
|
+
</div>
|
|
529
|
+
</div>
|
|
530
|
+
<div className={styles.col3}>
|
|
531
|
+
<div className={styles.fieldItem}>
|
|
532
|
+
<span className={styles.fieldIcon}>👉</span>
|
|
533
|
+
<span className={styles.fieldTitle}>{t('localization', 'Localization')}:</span>
|
|
534
|
+
<span id="lblLocalization" className={styles.fieldVal}>
|
|
535
|
+
{selectedOrder?.radiology.specimen || '-'}
|
|
536
|
+
</span>
|
|
537
|
+
</div>
|
|
538
|
+
</div>
|
|
539
|
+
<div className={styles.col3}>
|
|
540
|
+
<div className={styles.fieldItem}>
|
|
541
|
+
<span className={styles.fieldIcon}>👉</span>
|
|
542
|
+
<span className={styles.fieldTitle}>{t('position', 'Position')}:</span>
|
|
543
|
+
<span id="lblPosition" className={styles.fieldVal}>
|
|
544
|
+
{selectedOrder?.radiology.bodySiteDisplay || '-'}
|
|
545
|
+
</span>
|
|
546
|
+
</div>
|
|
547
|
+
</div>
|
|
548
|
+
|
|
549
|
+
<div className={styles.col3}>
|
|
550
|
+
<div className={styles.fieldItem}>
|
|
551
|
+
<span className={styles.fieldIcon}>👉</span>
|
|
552
|
+
<span className={styles.fieldTitle}>{t('comment', 'Comment')}:</span>
|
|
553
|
+
<span id="lblComment" className={styles.fieldVal}>
|
|
554
|
+
{selectedOrder?.radiology.note || '-'}
|
|
555
|
+
</span>
|
|
556
|
+
</div>
|
|
557
|
+
</div>
|
|
558
|
+
<div className={styles.col3}>
|
|
559
|
+
<div className={styles.fieldItem}>
|
|
560
|
+
<span className={styles.fieldIcon}>👉</span>
|
|
561
|
+
<span className={styles.fieldTitle}>{t('priority', 'Priority')}:</span>
|
|
562
|
+
<span id="lblPriority" className={styles.fieldVal}>
|
|
563
|
+
{selectedOrder?.radiology.priority || '-'}
|
|
564
|
+
</span>
|
|
565
|
+
</div>
|
|
566
|
+
</div>
|
|
567
|
+
<div className={styles.col3}>
|
|
568
|
+
<div className={styles.fieldItem}>
|
|
569
|
+
<span className={styles.fieldIcon}>👉</span>
|
|
570
|
+
<span className={styles.fieldTitle}>{t('consultant', 'Consultant')}:</span>
|
|
571
|
+
<span id="lblConsultant" className={styles.fieldVal}>
|
|
572
|
+
{selectedOrder?.radiology.requesterDisplay === '--Select--'
|
|
573
|
+
? '-'
|
|
574
|
+
: selectedOrder?.radiology.requesterDisplay || '-'}
|
|
575
|
+
</span>
|
|
576
|
+
</div>
|
|
577
|
+
</div>
|
|
578
|
+
<div className={styles.col3}>
|
|
579
|
+
<div className={styles.fieldItem}>
|
|
580
|
+
<span className={styles.fieldIcon}>👉</span>
|
|
581
|
+
<span className={styles.fieldTitle}>{t('orderedDateTime', 'Ordered Date & Time')}:</span>
|
|
582
|
+
<span id="lblOdrDateTime" className={styles.fieldVal}>
|
|
583
|
+
{selectedOrder?.radiology.occurrenceDateTime || '-'}
|
|
584
|
+
</span>
|
|
585
|
+
</div>
|
|
586
|
+
</div>
|
|
587
|
+
</div>
|
|
588
|
+
</div>
|
|
589
|
+
|
|
590
|
+
{/* Scheduling Form Section: Scheduled Date Time, Remarks, Modality, Exam Room */}
|
|
591
|
+
<div className={`${styles.col12} ${styles.schedulingFormArea}`}>
|
|
592
|
+
<div className={styles.row}>
|
|
593
|
+
{/* Left Column: Scheduled Date Time & Remarks */}
|
|
594
|
+
<div className={styles.col6}>
|
|
595
|
+
<div className={`${styles.row} ${styles.formGroup}`}>
|
|
596
|
+
<div className={styles.col4}>
|
|
597
|
+
<label className={styles.label}>
|
|
598
|
+
{t('scheduledDateTime', 'Scheduled Date Time')}
|
|
599
|
+
</label>
|
|
600
|
+
</div>
|
|
601
|
+
<div className={styles.col8}>
|
|
602
|
+
<input
|
|
603
|
+
id="txtScheduledDateTime"
|
|
604
|
+
type="datetime-local"
|
|
605
|
+
className={styles.formControl}
|
|
606
|
+
value={scheduledDateTime}
|
|
607
|
+
onChange={(e) => setScheduledDateTime(e.target.value)}
|
|
608
|
+
/>
|
|
609
|
+
</div>
|
|
610
|
+
</div>
|
|
611
|
+
|
|
612
|
+
<div className={`${styles.row} ${styles.formGroup}`}>
|
|
613
|
+
<div className={styles.col4}>
|
|
614
|
+
<label className={styles.label}>
|
|
615
|
+
{t('remarks', 'Remarks')}
|
|
616
|
+
</label>
|
|
617
|
+
</div>
|
|
618
|
+
<div className={styles.col8}>
|
|
619
|
+
<textarea
|
|
620
|
+
id="txtRemarks"
|
|
621
|
+
className={styles.formControl}
|
|
622
|
+
rows={2}
|
|
623
|
+
placeholder={t('enterRemarks', 'Remarks...')}
|
|
624
|
+
value={remarks}
|
|
625
|
+
onChange={(e) => setRemarks(e.target.value)}
|
|
626
|
+
/>
|
|
627
|
+
</div>
|
|
628
|
+
</div>
|
|
629
|
+
|
|
630
|
+
{/* Action Buttons: Big blue Save button + Cancel */}
|
|
631
|
+
<div className={styles.buttonGroup}>
|
|
632
|
+
<button
|
|
633
|
+
id="btnSave"
|
|
634
|
+
type="button"
|
|
635
|
+
className={styles.btnPrimary}
|
|
636
|
+
onClick={handleSave}
|
|
637
|
+
disabled={isSaving}
|
|
638
|
+
>
|
|
639
|
+
{isSaving ? t('saving', 'Saving...') : t('save', 'Save')}
|
|
640
|
+
</button>
|
|
641
|
+
<button
|
|
642
|
+
id="btnClear"
|
|
643
|
+
type="button"
|
|
644
|
+
className={styles.btnCancel}
|
|
645
|
+
onClick={handleClear}
|
|
646
|
+
disabled={isSaving}
|
|
647
|
+
>
|
|
648
|
+
{t('cancel', 'Cancel')}
|
|
649
|
+
</button>
|
|
650
|
+
</div>
|
|
651
|
+
</div>
|
|
652
|
+
|
|
653
|
+
{/* Right Column: Modality & Exam Room */}
|
|
654
|
+
<div className={styles.col6}>
|
|
655
|
+
<div className={`${styles.row} ${styles.formGroup}`}>
|
|
656
|
+
<div className={styles.col4}>
|
|
657
|
+
<label className={styles.label}>
|
|
658
|
+
{t('modality', 'Modality')}
|
|
659
|
+
</label>
|
|
660
|
+
</div>
|
|
661
|
+
<div className={styles.col8}>
|
|
662
|
+
<select
|
|
663
|
+
id="drpSelectModality"
|
|
664
|
+
className={styles.formControl}
|
|
665
|
+
value={selectedModality}
|
|
666
|
+
onChange={(e) => setSelectedModality(e.target.value)}
|
|
667
|
+
>
|
|
668
|
+
<option value="all">--Select--</option>
|
|
669
|
+
<option value="Xray">Xray</option>
|
|
670
|
+
<option value="CT">CT</option>
|
|
671
|
+
<option value="MRI">MRI</option>
|
|
672
|
+
</select>
|
|
673
|
+
</div>
|
|
674
|
+
</div>
|
|
675
|
+
|
|
676
|
+
<div className={`${styles.row} ${styles.formGroup}`}>
|
|
677
|
+
<div className={styles.col4}>
|
|
678
|
+
<label className={styles.label}>
|
|
679
|
+
{t('examRoom', 'Exam Room')}
|
|
680
|
+
</label>
|
|
681
|
+
</div>
|
|
682
|
+
<div className={styles.col8}>
|
|
683
|
+
<select
|
|
684
|
+
id="drpSelectExamRoom"
|
|
685
|
+
className={styles.formControl}
|
|
686
|
+
value={selectedExamRoom}
|
|
687
|
+
onChange={(e) => setSelectedExamRoom(e.target.value)}
|
|
688
|
+
disabled={selectedModality === 'all'}
|
|
689
|
+
>
|
|
690
|
+
<option value="all">--Select--</option>
|
|
691
|
+
{examRooms.map((room, i) => (
|
|
692
|
+
<option key={`${room.name}-${i}`} value={room.name}>
|
|
693
|
+
{room.name}
|
|
694
|
+
</option>
|
|
695
|
+
))}
|
|
696
|
+
</select>
|
|
697
|
+
</div>
|
|
698
|
+
</div>
|
|
699
|
+
</div>
|
|
700
|
+
</div>
|
|
701
|
+
</div>
|
|
702
|
+
|
|
703
|
+
</div>
|
|
704
|
+
</div>
|
|
705
|
+
</div>
|
|
706
|
+
</div>
|
|
707
|
+
</div>
|
|
708
|
+
);
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
export default RadiologyTestScheduling;
|