@sasindi/radiologytestscheduling 0.0.0-stage → 1.0.1
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 +875 -0
- package/src/radiology-test-scheduling.resource.ts +321 -0
- package/src/radiology-test-scheduling.scss +478 -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,875 @@
|
|
|
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
|
+
fetchLocationDisplay,
|
|
11
|
+
fetchUserDisplay,
|
|
12
|
+
fetchExamRooms,
|
|
13
|
+
fetchCalendarData,
|
|
14
|
+
saveScheduling,
|
|
15
|
+
SessionInfo,
|
|
16
|
+
} from './radiology-test-scheduling.resource';
|
|
17
|
+
import {
|
|
18
|
+
SchedulingOrderDTO,
|
|
19
|
+
PatientDemographics,
|
|
20
|
+
InstituteOption,
|
|
21
|
+
ExamRoom,
|
|
22
|
+
RadiologyOrder,
|
|
23
|
+
} from './types';
|
|
24
|
+
import styles from './radiology-test-scheduling.scss';
|
|
25
|
+
|
|
26
|
+
const DEFAULT_AVATAR =
|
|
27
|
+
'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>';
|
|
28
|
+
|
|
29
|
+
const RadiologyTestScheduling: React.FC = () => {
|
|
30
|
+
const { t } = useTranslation();
|
|
31
|
+
|
|
32
|
+
// Session & Institute states
|
|
33
|
+
const [sessionInfo, setSessionInfo] = useState<SessionInfo>({
|
|
34
|
+
userUuid: '',
|
|
35
|
+
personUuid: '',
|
|
36
|
+
locationUuid: '',
|
|
37
|
+
locationName: '',
|
|
38
|
+
});
|
|
39
|
+
const [institutes, setInstitutes] = useState<InstituteOption[]>([]);
|
|
40
|
+
const [selectedInstitute, setSelectedInstitute] = useState<string>('all');
|
|
41
|
+
|
|
42
|
+
// Pending Tests & Table states
|
|
43
|
+
const [pendingOrders, setPendingOrders] = useState<SchedulingOrderDTO[]>([]);
|
|
44
|
+
const [loadingOrders, setLoadingOrders] = useState<boolean>(true);
|
|
45
|
+
const [searchTerm, setSearchTerm] = useState<string>('');
|
|
46
|
+
|
|
47
|
+
// Selected Order & Patient state
|
|
48
|
+
const [selectedOrder, setSelectedOrder] = useState<SchedulingOrderDTO | null>(null);
|
|
49
|
+
const [patientDetails, setPatientDetails] = useState<PatientDemographics | null>(null);
|
|
50
|
+
const [loadingPatient, setLoadingPatient] = useState<boolean>(false);
|
|
51
|
+
|
|
52
|
+
// Scheduling inputs
|
|
53
|
+
const [scheduledDateTime, setScheduledDateTime] = useState<string>('');
|
|
54
|
+
const [remarks, setRemarks] = useState<string>('');
|
|
55
|
+
const [isSaving, setIsSaving] = useState<boolean>(false);
|
|
56
|
+
|
|
57
|
+
// Calendar filter states
|
|
58
|
+
const [selectedModality, setSelectedModality] = useState<string>('all');
|
|
59
|
+
const [examRooms, setExamRooms] = useState<ExamRoom[]>([]);
|
|
60
|
+
const [selectedExamRoom, setSelectedExamRoom] = useState<string>('all');
|
|
61
|
+
const [calendarOrders, setCalendarOrders] = useState<RadiologyOrder[]>([]);
|
|
62
|
+
const [calendarDate, setCalendarDate] = useState<Date>(new Date());
|
|
63
|
+
const [loadingCalendar, setLoadingCalendar] = useState<boolean>(false);
|
|
64
|
+
|
|
65
|
+
// Initialize Session and Institutes
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
let isMounted = true;
|
|
68
|
+
(async () => {
|
|
69
|
+
try {
|
|
70
|
+
const [session, instList] = await Promise.all([
|
|
71
|
+
fetchCurrentSessionAndInstitute(),
|
|
72
|
+
fetchInstitutes(),
|
|
73
|
+
]);
|
|
74
|
+
if (!isMounted) return;
|
|
75
|
+
setSessionInfo(session);
|
|
76
|
+
setInstitutes(instList);
|
|
77
|
+
if (session.locationUuid) {
|
|
78
|
+
setSelectedInstitute(session.locationUuid);
|
|
79
|
+
}
|
|
80
|
+
} catch (err) {
|
|
81
|
+
console.error('Initialization failed', err);
|
|
82
|
+
}
|
|
83
|
+
})();
|
|
84
|
+
return () => {
|
|
85
|
+
isMounted = false;
|
|
86
|
+
};
|
|
87
|
+
}, []);
|
|
88
|
+
|
|
89
|
+
// Load Pending Orders whenever selected institute changes
|
|
90
|
+
const loadPendingOrders = useCallback(async (instUuid: string) => {
|
|
91
|
+
setLoadingOrders(true);
|
|
92
|
+
try {
|
|
93
|
+
const orders = await fetchPendingSchedulingOrders(instUuid);
|
|
94
|
+
// Enhance with async location / user names if available
|
|
95
|
+
const enhanced = await Promise.all(
|
|
96
|
+
orders.map(async (dto) => {
|
|
97
|
+
const loc = dto.radiology.location ? await fetchLocationDisplay(dto.radiology.location) : '';
|
|
98
|
+
const ordBy = dto.radiology.requesterReference
|
|
99
|
+
? await fetchUserDisplay(dto.radiology.requesterReference)
|
|
100
|
+
: '';
|
|
101
|
+
return {
|
|
102
|
+
...dto,
|
|
103
|
+
locationName: loc,
|
|
104
|
+
orderedByName: ordBy,
|
|
105
|
+
};
|
|
106
|
+
}),
|
|
107
|
+
);
|
|
108
|
+
setPendingOrders(enhanced);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
console.error('Failed to load pending scheduling orders', err);
|
|
111
|
+
Swal.fire({
|
|
112
|
+
icon: 'error',
|
|
113
|
+
title: 'Error Loading Orders',
|
|
114
|
+
text: 'Unable to fetch pending radiology test orders.',
|
|
115
|
+
});
|
|
116
|
+
} finally {
|
|
117
|
+
setLoadingOrders(false);
|
|
118
|
+
}
|
|
119
|
+
}, []);
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
loadPendingOrders(selectedInstitute);
|
|
123
|
+
}, [selectedInstitute, loadPendingOrders]);
|
|
124
|
+
|
|
125
|
+
// Load Exam Rooms when modality changes
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
(async () => {
|
|
128
|
+
if (selectedModality === 'all' || !selectedModality) {
|
|
129
|
+
setExamRooms([]);
|
|
130
|
+
setSelectedExamRoom('all');
|
|
131
|
+
} else {
|
|
132
|
+
const rooms = await fetchExamRooms(selectedModality);
|
|
133
|
+
setExamRooms(rooms);
|
|
134
|
+
setSelectedExamRoom('all');
|
|
135
|
+
}
|
|
136
|
+
})();
|
|
137
|
+
}, [selectedModality]);
|
|
138
|
+
|
|
139
|
+
// Load Calendar Data
|
|
140
|
+
const loadCalendar = useCallback(async () => {
|
|
141
|
+
setLoadingCalendar(true);
|
|
142
|
+
try {
|
|
143
|
+
const data = await fetchCalendarData(selectedModality, selectedExamRoom, selectedInstitute);
|
|
144
|
+
setCalendarOrders(data);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.error('Failed to load calendar data', err);
|
|
147
|
+
} finally {
|
|
148
|
+
setLoadingCalendar(false);
|
|
149
|
+
}
|
|
150
|
+
}, [selectedModality, selectedExamRoom, selectedInstitute]);
|
|
151
|
+
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
loadCalendar();
|
|
154
|
+
}, [loadCalendar]);
|
|
155
|
+
|
|
156
|
+
// Handle row selection
|
|
157
|
+
const handleSelectRow = async (dto: SchedulingOrderDTO) => {
|
|
158
|
+
setSelectedOrder(dto);
|
|
159
|
+
setScheduledDateTime('');
|
|
160
|
+
setRemarks('');
|
|
161
|
+
setLoadingPatient(true);
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const details = await fetchPatientDetails(dto.radiology.patientPhn);
|
|
165
|
+
setPatientDetails(details);
|
|
166
|
+
} catch (e) {
|
|
167
|
+
console.error('Failed to load patient details', e);
|
|
168
|
+
} finally {
|
|
169
|
+
setLoadingPatient(false);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// Reset selection
|
|
174
|
+
const handleClear = () => {
|
|
175
|
+
setSelectedOrder(null);
|
|
176
|
+
setPatientDetails(null);
|
|
177
|
+
setScheduledDateTime('');
|
|
178
|
+
setRemarks('');
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// Save Schedule Handler
|
|
182
|
+
const handleSave = async () => {
|
|
183
|
+
if (!selectedOrder) {
|
|
184
|
+
Swal.fire({
|
|
185
|
+
icon: 'warning',
|
|
186
|
+
title: 'No Order Selected',
|
|
187
|
+
text: 'Please select a pending test order from the table to schedule.',
|
|
188
|
+
confirmButtonColor: '#0f62fe',
|
|
189
|
+
});
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!scheduledDateTime) {
|
|
194
|
+
Swal.fire({
|
|
195
|
+
icon: 'warning',
|
|
196
|
+
title: 'Date & Time Required',
|
|
197
|
+
text: 'Please choose the Scheduled Date and Time before saving.',
|
|
198
|
+
confirmButtonColor: '#0f62fe',
|
|
199
|
+
});
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Format datetime string: "YYYY-MM-DD HH:mm:ss"
|
|
204
|
+
const formattedDateTime = scheduledDateTime.replace('T', ' ') + ':00';
|
|
205
|
+
|
|
206
|
+
const result = await Swal.fire({
|
|
207
|
+
title: 'Confirm Scheduling',
|
|
208
|
+
text: `Schedule ${selectedOrder.radiology.codeDisplay || 'test'} on ${formattedDateTime}?`,
|
|
209
|
+
icon: 'question',
|
|
210
|
+
showCancelButton: true,
|
|
211
|
+
confirmButtonColor: '#0f62fe',
|
|
212
|
+
cancelButtonColor: '#6c757d',
|
|
213
|
+
confirmButtonText: 'Yes, Schedule',
|
|
214
|
+
cancelButtonText: 'Cancel',
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
if (!result.isConfirmed) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
setIsSaving(true);
|
|
222
|
+
try {
|
|
223
|
+
await saveScheduling(selectedOrder.type, {
|
|
224
|
+
id: selectedOrder.radiology.id,
|
|
225
|
+
scheduledDateTime: formattedDateTime,
|
|
226
|
+
remarks: remarks || '',
|
|
227
|
+
cby: sessionInfo.userUuid || '1',
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
await Swal.fire({
|
|
231
|
+
icon: 'success',
|
|
232
|
+
title: 'Saved Successfully!',
|
|
233
|
+
text: 'The radiology test has been scheduled.',
|
|
234
|
+
timer: 2000,
|
|
235
|
+
showConfirmButton: false,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
handleClear();
|
|
239
|
+
loadPendingOrders(selectedInstitute);
|
|
240
|
+
loadCalendar();
|
|
241
|
+
} catch (err: any) {
|
|
242
|
+
console.error('Failed to save schedule', err);
|
|
243
|
+
Swal.fire({
|
|
244
|
+
icon: 'error',
|
|
245
|
+
title: 'Save Failed',
|
|
246
|
+
text: err?.message || 'Could not schedule radiology test. Please check server logs.',
|
|
247
|
+
confirmButtonColor: '#da1e28',
|
|
248
|
+
});
|
|
249
|
+
} finally {
|
|
250
|
+
setIsSaving(false);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// Filtered Orders for search input
|
|
255
|
+
const filteredOrders = useMemo(() => {
|
|
256
|
+
if (!searchTerm.trim()) return pendingOrders;
|
|
257
|
+
const term = searchTerm.toLowerCase();
|
|
258
|
+
return pendingOrders.filter((dto) => {
|
|
259
|
+
const phn = dto.radiology.patientPhn?.toLowerCase() || '';
|
|
260
|
+
const test = dto.radiology.codeDisplay?.toLowerCase() || '';
|
|
261
|
+
const section = dto.type?.toLowerCase() || '';
|
|
262
|
+
const priority = dto.radiology.priority?.toLowerCase() || '';
|
|
263
|
+
const location = dto.locationName?.toLowerCase() || '';
|
|
264
|
+
return (
|
|
265
|
+
phn.includes(term) ||
|
|
266
|
+
test.includes(term) ||
|
|
267
|
+
section.includes(term) ||
|
|
268
|
+
priority.includes(term) ||
|
|
269
|
+
location.includes(term)
|
|
270
|
+
);
|
|
271
|
+
});
|
|
272
|
+
}, [pendingOrders, searchTerm]);
|
|
273
|
+
|
|
274
|
+
// Calendar grouping helper
|
|
275
|
+
const calendarDays = useMemo(() => {
|
|
276
|
+
const year = calendarDate.getFullYear();
|
|
277
|
+
const month = calendarDate.getMonth();
|
|
278
|
+
const firstDayIndex = new Date(year, month, 1).getDay();
|
|
279
|
+
const totalDays = new Date(year, month + 1, 0).getDate();
|
|
280
|
+
const prevMonthDays = new Date(year, month, 0).getDate();
|
|
281
|
+
|
|
282
|
+
// Map scheduled tests by date string "YYYY-MM-DD"
|
|
283
|
+
const dateMap = new Map<string, RadiologyOrder[]>();
|
|
284
|
+
for (const ord of calendarOrders) {
|
|
285
|
+
if (ord.scheduledDateTime) {
|
|
286
|
+
const dStr = ord.scheduledDateTime.split(' ')[0];
|
|
287
|
+
if (!dateMap.has(dStr)) {
|
|
288
|
+
dateMap.set(dStr, []);
|
|
289
|
+
}
|
|
290
|
+
dateMap.get(dStr)!.push(ord);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const cells = [];
|
|
295
|
+
|
|
296
|
+
// Previous month filler days
|
|
297
|
+
for (let i = firstDayIndex - 1; i >= 0; i--) {
|
|
298
|
+
const dayNum = prevMonthDays - i;
|
|
299
|
+
cells.push({
|
|
300
|
+
dayNum,
|
|
301
|
+
isCurrentMonth: false,
|
|
302
|
+
dateStr: '',
|
|
303
|
+
orders: [],
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Current month days
|
|
308
|
+
const todayStr = new Date().toISOString().split('T')[0];
|
|
309
|
+
for (let day = 1; day <= totalDays; day++) {
|
|
310
|
+
const monthStr = String(month + 1).padStart(2, '0');
|
|
311
|
+
const dayStr = String(day).padStart(2, '0');
|
|
312
|
+
const dateStr = `${year}-${monthStr}-${dayStr}`;
|
|
313
|
+
cells.push({
|
|
314
|
+
dayNum: day,
|
|
315
|
+
isCurrentMonth: true,
|
|
316
|
+
isToday: dateStr === todayStr,
|
|
317
|
+
dateStr,
|
|
318
|
+
orders: dateMap.get(dateStr) || [],
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Next month filler days to complete grid (multiples of 7)
|
|
323
|
+
const remaining = 7 - (cells.length % 7);
|
|
324
|
+
if (remaining < 7) {
|
|
325
|
+
for (let i = 1; i <= remaining; i++) {
|
|
326
|
+
cells.push({
|
|
327
|
+
dayNum: i,
|
|
328
|
+
isCurrentMonth: false,
|
|
329
|
+
dateStr: '',
|
|
330
|
+
orders: [],
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return cells;
|
|
336
|
+
}, [calendarDate, calendarOrders]);
|
|
337
|
+
|
|
338
|
+
// Handle clicking calendar event or day
|
|
339
|
+
const handleCalendarEventClick = (dateStr: string, orders: RadiologyOrder[]) => {
|
|
340
|
+
if (!orders || orders.length === 0) return;
|
|
341
|
+
|
|
342
|
+
// Group orders by modality and exam room
|
|
343
|
+
const breakdown: Record<string, Record<string, number>> = {};
|
|
344
|
+
for (const ord of orders) {
|
|
345
|
+
const mod = ord.categoryDisplay || 'Other';
|
|
346
|
+
const room = ord.supportingInfo || 'Default Room';
|
|
347
|
+
if (!breakdown[mod]) {
|
|
348
|
+
breakdown[mod] = {};
|
|
349
|
+
}
|
|
350
|
+
breakdown[mod][room] = (breakdown[mod][room] || 0) + 1;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
let html = `<div style="text-align: left; font-size: 0.92rem;">`;
|
|
354
|
+
for (const [mod, rooms] of Object.entries(breakdown)) {
|
|
355
|
+
html += `<div style="margin-bottom: 0.75rem;"><strong style="color: #009384;">${mod}</strong><ul style="margin: 0.25rem 0 0 1.25rem; padding: 0;">`;
|
|
356
|
+
for (const [room, count] of Object.entries(rooms)) {
|
|
357
|
+
html += `<li><strong>${room}:</strong> ${count} scheduled</li>`;
|
|
358
|
+
}
|
|
359
|
+
html += `</ul></div>`;
|
|
360
|
+
}
|
|
361
|
+
html += `</div>`;
|
|
362
|
+
|
|
363
|
+
Swal.fire({
|
|
364
|
+
title: `Scheduled Tests (${dateStr})`,
|
|
365
|
+
html,
|
|
366
|
+
icon: 'info',
|
|
367
|
+
confirmButtonColor: '#0f62fe',
|
|
368
|
+
confirmButtonText: 'Close',
|
|
369
|
+
});
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const monthNames = [
|
|
373
|
+
'January', 'February', 'March', 'April', 'May', 'June',
|
|
374
|
+
'July', 'August', 'September', 'October', 'November', 'December',
|
|
375
|
+
];
|
|
376
|
+
|
|
377
|
+
return (
|
|
378
|
+
<div className={styles.bodyWrapper}>
|
|
379
|
+
{/* Main Unified Card: Heading inside the form container as required */}
|
|
380
|
+
<div className={styles.card}>
|
|
381
|
+
<div className={styles.cardHeader}>
|
|
382
|
+
<h2 className={styles.formHeading}>
|
|
383
|
+
{t('radiologyTestSchedulingHeading', 'Radiology Test Scheduling')}
|
|
384
|
+
</h2>
|
|
385
|
+
</div>
|
|
386
|
+
|
|
387
|
+
{/* Institute and Search Filter Box */}
|
|
388
|
+
<div className={styles.filterBox}>
|
|
389
|
+
<div className={styles.row}>
|
|
390
|
+
<div className={`${styles.col4} ${styles.formGroup}`}>
|
|
391
|
+
<label className={styles.label}>
|
|
392
|
+
<span>{t('institute', 'Institute')}</span>
|
|
393
|
+
</label>
|
|
394
|
+
<select
|
|
395
|
+
className={styles.formControl}
|
|
396
|
+
value={selectedInstitute}
|
|
397
|
+
onChange={(e) => setSelectedInstitute(e.target.value)}
|
|
398
|
+
>
|
|
399
|
+
<option value="all">{t('allInstitutes', '-- All Institutes --')}</option>
|
|
400
|
+
{institutes.map((inst) => (
|
|
401
|
+
<option key={inst.uuid} value={inst.uuid}>
|
|
402
|
+
{inst.name}
|
|
403
|
+
</option>
|
|
404
|
+
))}
|
|
405
|
+
</select>
|
|
406
|
+
</div>
|
|
407
|
+
|
|
408
|
+
<div className={`${styles.col8} ${styles.formGroup}`}>
|
|
409
|
+
<label className={styles.label}>
|
|
410
|
+
<span>{t('searchPendingTests', 'Search Pending Tests')}</span>
|
|
411
|
+
</label>
|
|
412
|
+
<input
|
|
413
|
+
type="text"
|
|
414
|
+
className={styles.formControl}
|
|
415
|
+
placeholder={t('searchPlaceholder', 'Type PHN, Test Name, Section, or Location...')}
|
|
416
|
+
value={searchTerm}
|
|
417
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
418
|
+
/>
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
|
|
423
|
+
{/* Pending Tests Table */}
|
|
424
|
+
<div className={styles.sectionHeader}>
|
|
425
|
+
<span>{t('pendingSchedulingRequests', 'Pending Scheduling Requests')}</span>
|
|
426
|
+
<span className={`${styles.badge} ${styles.badgeType}`} style={{ marginLeft: 'auto' }}>
|
|
427
|
+
{filteredOrders.length} {t('requests', 'Requests')}
|
|
428
|
+
</span>
|
|
429
|
+
</div>
|
|
430
|
+
|
|
431
|
+
<div className={styles.tableContainer}>
|
|
432
|
+
{loadingOrders ? (
|
|
433
|
+
<div style={{ padding: '2rem' }}>
|
|
434
|
+
<Loading description="Loading pending tests..." withOverlay={false} small />
|
|
435
|
+
</div>
|
|
436
|
+
) : filteredOrders.length === 0 ? (
|
|
437
|
+
<div className={styles.emptyState}>
|
|
438
|
+
{t('noPendingOrders', 'No pending radiology test scheduling requests found.')}
|
|
439
|
+
</div>
|
|
440
|
+
) : (
|
|
441
|
+
<table className={styles.table}>
|
|
442
|
+
<thead>
|
|
443
|
+
<tr>
|
|
444
|
+
<th>#</th>
|
|
445
|
+
<th>{t('institute', 'Institute')}</th>
|
|
446
|
+
<th>{t('section', 'Section')}</th>
|
|
447
|
+
<th>{t('phn', 'PHN')}</th>
|
|
448
|
+
<th>{t('admittedDate', 'Admitted Date')}</th>
|
|
449
|
+
<th>{t('orderedBy', 'Ordered By')}</th>
|
|
450
|
+
<th>{t('orderedDatetime', 'Ordered Datetime')}</th>
|
|
451
|
+
<th>{t('testName', 'Test Name')}</th>
|
|
452
|
+
<th>{t('region', 'Region')}</th>
|
|
453
|
+
<th>{t('examRoom', 'Exam Room')}</th>
|
|
454
|
+
<th>{t('comment', 'Comment')}</th>
|
|
455
|
+
<th>{t('priority', 'Priority')}</th>
|
|
456
|
+
<th>{t('consultant', 'Consultant')}</th>
|
|
457
|
+
<th style={{ textAlign: 'center' }}>{t('action', 'Action')}</th>
|
|
458
|
+
</tr>
|
|
459
|
+
</thead>
|
|
460
|
+
<tbody>
|
|
461
|
+
{filteredOrders.map((dto, idx) => {
|
|
462
|
+
const isUrgent = dto.radiology.priority === 'Urgent';
|
|
463
|
+
const isSelected = selectedOrder?.radiology.id === dto.radiology.id;
|
|
464
|
+
const rowClass = `${isUrgent ? styles.urgentRow : ''} ${
|
|
465
|
+
isSelected ? styles.selectedRow : ''
|
|
466
|
+
}`.trim();
|
|
467
|
+
|
|
468
|
+
return (
|
|
469
|
+
<tr key={`${dto.type}-${dto.radiology.id}`} className={rowClass}>
|
|
470
|
+
<td>{idx + 1}</td>
|
|
471
|
+
<td>{dto.locationName || dto.radiology.location}</td>
|
|
472
|
+
<td>
|
|
473
|
+
<span className={`${styles.badge} ${styles.badgeType}`}>
|
|
474
|
+
{dto.type}
|
|
475
|
+
</span>
|
|
476
|
+
</td>
|
|
477
|
+
<td>
|
|
478
|
+
<strong>{dto.radiology.patientPhn}</strong>
|
|
479
|
+
</td>
|
|
480
|
+
<td>
|
|
481
|
+
{dto.radiology.occurrenceDateTime
|
|
482
|
+
? dto.radiology.occurrenceDateTime.split(' ')[0]
|
|
483
|
+
: '-'}
|
|
484
|
+
</td>
|
|
485
|
+
<td>{dto.orderedByName || dto.radiology.requesterReference || '-'}</td>
|
|
486
|
+
<td>{dto.radiology.occurrenceDateTime || '-'}</td>
|
|
487
|
+
<td>
|
|
488
|
+
<strong>{dto.radiology.codeDisplay || '-'}</strong>
|
|
489
|
+
</td>
|
|
490
|
+
<td>{dto.radiology.bodySiteText || '-'}</td>
|
|
491
|
+
<td>
|
|
492
|
+
{dto.radiology.supportingInfo === '--Select--'
|
|
493
|
+
? '-'
|
|
494
|
+
: dto.radiology.supportingInfo || '-'}
|
|
495
|
+
</td>
|
|
496
|
+
<td>{dto.radiology.note || '-'}</td>
|
|
497
|
+
<td>
|
|
498
|
+
<span
|
|
499
|
+
className={`${styles.badge} ${
|
|
500
|
+
isUrgent ? styles.badgeUrgent : styles.badgeRoutine
|
|
501
|
+
}`}
|
|
502
|
+
>
|
|
503
|
+
{dto.radiology.priority || 'Routine'}
|
|
504
|
+
</span>
|
|
505
|
+
</td>
|
|
506
|
+
<td>
|
|
507
|
+
{dto.radiology.requesterDisplay === '--Select--'
|
|
508
|
+
? '-'
|
|
509
|
+
: dto.radiology.requesterDisplay || '-'}
|
|
510
|
+
</td>
|
|
511
|
+
<td style={{ textAlign: 'center' }}>
|
|
512
|
+
<button
|
|
513
|
+
type="button"
|
|
514
|
+
className={styles.btnSelect}
|
|
515
|
+
onClick={() => handleSelectRow(dto)}
|
|
516
|
+
>
|
|
517
|
+
{isSelected ? t('selected', 'Selected') : t('select', 'Select')}
|
|
518
|
+
</button>
|
|
519
|
+
</td>
|
|
520
|
+
</tr>
|
|
521
|
+
);
|
|
522
|
+
})}
|
|
523
|
+
</tbody>
|
|
524
|
+
</table>
|
|
525
|
+
)}
|
|
526
|
+
</div>
|
|
527
|
+
|
|
528
|
+
{/* Details and Scheduling Form */}
|
|
529
|
+
{selectedOrder && (
|
|
530
|
+
<div className={styles.detailsPanel}>
|
|
531
|
+
<div className={styles.row}>
|
|
532
|
+
{/* Patient Demographics */}
|
|
533
|
+
<div className={`${styles.col6} ${styles.formGroup}`}>
|
|
534
|
+
<div className={styles.sectionHeader}>
|
|
535
|
+
<span>{t('patientDemographics', 'Patient Demographics')}</span>
|
|
536
|
+
{selectedOrder.radiology.patientPhn && (
|
|
537
|
+
<span style={{ fontSize: '0.85rem', color: '#6c757d', marginLeft: 'auto' }}>
|
|
538
|
+
PHN: ({selectedOrder.radiology.patientPhn})
|
|
539
|
+
</span>
|
|
540
|
+
)}
|
|
541
|
+
</div>
|
|
542
|
+
|
|
543
|
+
{loadingPatient ? (
|
|
544
|
+
<Loading description="Loading patient demographics..." withOverlay={false} small />
|
|
545
|
+
) : (
|
|
546
|
+
<div className={styles.row}>
|
|
547
|
+
<div className={styles.col8}>
|
|
548
|
+
<div className={styles.infoField}>
|
|
549
|
+
<span className={styles.infoLabel}>{t('fullName', 'Full Name')}:</span>
|
|
550
|
+
<span className={styles.infoValue}>
|
|
551
|
+
{patientDetails?.fullName || '-'}
|
|
552
|
+
</span>
|
|
553
|
+
</div>
|
|
554
|
+
<div className={styles.infoField}>
|
|
555
|
+
<span className={styles.infoLabel}>{t('gender', 'Gender')}:</span>
|
|
556
|
+
<span className={styles.infoValue}>{patientDetails?.gender || '-'}</span>
|
|
557
|
+
</div>
|
|
558
|
+
<div className={styles.infoField}>
|
|
559
|
+
<span className={styles.infoLabel}>{t('nicNo', 'NIC No')}:</span>
|
|
560
|
+
<span className={styles.infoValue}>{patientDetails?.nicNo || '-'}</span>
|
|
561
|
+
</div>
|
|
562
|
+
<div className={styles.infoField}>
|
|
563
|
+
<span className={styles.infoLabel}>{t('guardianInfo', 'Guardian Info')}:</span>
|
|
564
|
+
<span className={styles.infoValue}>
|
|
565
|
+
{patientDetails?.guardianInfo || '-'}
|
|
566
|
+
</span>
|
|
567
|
+
</div>
|
|
568
|
+
<div className={styles.infoField}>
|
|
569
|
+
<span className={styles.infoLabel}>{t('address', 'Address')}:</span>
|
|
570
|
+
<span className={styles.infoValue}>{patientDetails?.address || '-'}</span>
|
|
571
|
+
</div>
|
|
572
|
+
<div className={styles.infoField}>
|
|
573
|
+
<span className={styles.infoLabel}>{t('age', 'Age')}:</span>
|
|
574
|
+
<span className={styles.infoValue}>
|
|
575
|
+
Y: {patientDetails?.years || 0} | M: {patientDetails?.months || 0} | D:{' '}
|
|
576
|
+
{patientDetails?.days || 0}
|
|
577
|
+
</span>
|
|
578
|
+
</div>
|
|
579
|
+
<div className={styles.infoField}>
|
|
580
|
+
<span className={styles.infoLabel}>{t('dob', 'Date Of Birth')}:</span>
|
|
581
|
+
<span className={styles.infoValue}>
|
|
582
|
+
{patientDetails?.dateOfBirth || '-'}
|
|
583
|
+
</span>
|
|
584
|
+
</div>
|
|
585
|
+
<div className={styles.infoField}>
|
|
586
|
+
<span className={styles.infoLabel}>{t('telephone', 'Telephone')}:</span>
|
|
587
|
+
<span className={styles.infoValue}>
|
|
588
|
+
{[patientDetails?.telephoneHome, patientDetails?.telephoneMobile]
|
|
589
|
+
.filter(Boolean)
|
|
590
|
+
.join(', ') || '-'}
|
|
591
|
+
</span>
|
|
592
|
+
</div>
|
|
593
|
+
</div>
|
|
594
|
+
|
|
595
|
+
<div className={`${styles.col4} ${styles.avatarContainer}`}>
|
|
596
|
+
<img
|
|
597
|
+
className={styles.avatarImg}
|
|
598
|
+
src={patientDetails?.profileImage || DEFAULT_AVATAR}
|
|
599
|
+
alt="Patient"
|
|
600
|
+
onError={(e) => {
|
|
601
|
+
(e.currentTarget as HTMLImageElement).src = DEFAULT_AVATAR;
|
|
602
|
+
}}
|
|
603
|
+
/>
|
|
604
|
+
</div>
|
|
605
|
+
</div>
|
|
606
|
+
)}
|
|
607
|
+
</div>
|
|
608
|
+
|
|
609
|
+
{/* Investigation Details */}
|
|
610
|
+
<div className={`${styles.col6} ${styles.formGroup}`}>
|
|
611
|
+
<div className={styles.sectionHeader}>
|
|
612
|
+
<span>{t('investigationDetails', 'Investigation Details')}</span>
|
|
613
|
+
</div>
|
|
614
|
+
|
|
615
|
+
<div className={styles.infoField}>
|
|
616
|
+
<span className={styles.infoLabel}>{t('testName', 'Test Name')}:</span>
|
|
617
|
+
<span className={styles.infoValue}>
|
|
618
|
+
{selectedOrder.radiology.codeDisplay || '-'}
|
|
619
|
+
</span>
|
|
620
|
+
</div>
|
|
621
|
+
<div className={styles.infoField}>
|
|
622
|
+
<span className={styles.infoLabel}>{t('region', 'Region')}:</span>
|
|
623
|
+
<span className={styles.infoValue}>
|
|
624
|
+
{selectedOrder.radiology.bodySiteText || '-'}
|
|
625
|
+
</span>
|
|
626
|
+
</div>
|
|
627
|
+
<div className={styles.infoField}>
|
|
628
|
+
<span className={styles.infoLabel}>{t('localization', 'Localization')}:</span>
|
|
629
|
+
<span className={styles.infoValue}>
|
|
630
|
+
{selectedOrder.radiology.specimen || '-'}
|
|
631
|
+
</span>
|
|
632
|
+
</div>
|
|
633
|
+
<div className={styles.infoField}>
|
|
634
|
+
<span className={styles.infoLabel}>{t('position', 'Position')}:</span>
|
|
635
|
+
<span className={styles.infoValue}>
|
|
636
|
+
{selectedOrder.radiology.bodySiteDisplay || '-'}
|
|
637
|
+
</span>
|
|
638
|
+
</div>
|
|
639
|
+
<div className={styles.infoField}>
|
|
640
|
+
<span className={styles.infoLabel}>{t('comment', 'Comment')}:</span>
|
|
641
|
+
<span className={styles.infoValue}>{selectedOrder.radiology.note || '-'}</span>
|
|
642
|
+
</div>
|
|
643
|
+
<div className={styles.infoField}>
|
|
644
|
+
<span className={styles.infoLabel}>{t('priority', 'Priority')}:</span>
|
|
645
|
+
<span
|
|
646
|
+
className={`${styles.badge} ${
|
|
647
|
+
selectedOrder.radiology.priority === 'Urgent'
|
|
648
|
+
? styles.badgeUrgent
|
|
649
|
+
: styles.badgeRoutine
|
|
650
|
+
}`}
|
|
651
|
+
>
|
|
652
|
+
{selectedOrder.radiology.priority || 'Routine'}
|
|
653
|
+
</span>
|
|
654
|
+
</div>
|
|
655
|
+
<div className={styles.infoField}>
|
|
656
|
+
<span className={styles.infoLabel}>{t('consultant', 'Consultant')}:</span>
|
|
657
|
+
<span className={styles.infoValue}>
|
|
658
|
+
{selectedOrder.radiology.requesterDisplay === '--Select--'
|
|
659
|
+
? '-'
|
|
660
|
+
: selectedOrder.radiology.requesterDisplay || '-'}
|
|
661
|
+
</span>
|
|
662
|
+
</div>
|
|
663
|
+
<div className={styles.infoField}>
|
|
664
|
+
<span className={styles.infoLabel}>{t('orderedDateTime', 'Ordered Date & Time')}:</span>
|
|
665
|
+
<span className={styles.infoValue}>
|
|
666
|
+
{selectedOrder.radiology.occurrenceDateTime || '-'}
|
|
667
|
+
</span>
|
|
668
|
+
</div>
|
|
669
|
+
</div>
|
|
670
|
+
</div>
|
|
671
|
+
|
|
672
|
+
{/* Scheduling Form Inputs */}
|
|
673
|
+
<div className={styles.row} style={{ marginTop: '1.25rem' }}>
|
|
674
|
+
<div className={`${styles.col6} ${styles.formGroup}`}>
|
|
675
|
+
<label className={styles.label}>
|
|
676
|
+
<span>{t('scheduledDateTime', 'Scheduled Date Time')} *</span>
|
|
677
|
+
</label>
|
|
678
|
+
<input
|
|
679
|
+
type="datetime-local"
|
|
680
|
+
className={styles.formControl}
|
|
681
|
+
value={scheduledDateTime}
|
|
682
|
+
onChange={(e) => setScheduledDateTime(e.target.value)}
|
|
683
|
+
required
|
|
684
|
+
/>
|
|
685
|
+
</div>
|
|
686
|
+
|
|
687
|
+
<div className={`${styles.col6} ${styles.formGroup}`}>
|
|
688
|
+
<label className={styles.label}>
|
|
689
|
+
<span>{t('remarks', 'Remarks')}</span>
|
|
690
|
+
</label>
|
|
691
|
+
<textarea
|
|
692
|
+
className={styles.formControl}
|
|
693
|
+
rows={2}
|
|
694
|
+
placeholder={t('enterRemarks', 'Add scheduling remarks or instructions...')}
|
|
695
|
+
value={remarks}
|
|
696
|
+
onChange={(e) => setRemarks(e.target.value)}
|
|
697
|
+
/>
|
|
698
|
+
</div>
|
|
699
|
+
</div>
|
|
700
|
+
|
|
701
|
+
{/* Action Buttons: Save button is prominent blue as requested */}
|
|
702
|
+
<div className={styles.buttonGroup}>
|
|
703
|
+
<button
|
|
704
|
+
type="button"
|
|
705
|
+
className={styles.btnPrimary}
|
|
706
|
+
onClick={handleSave}
|
|
707
|
+
disabled={isSaving}
|
|
708
|
+
>
|
|
709
|
+
{isSaving ? t('saving', 'Saving...') : t('saveSchedule', 'Save Schedule')}
|
|
710
|
+
</button>
|
|
711
|
+
<button
|
|
712
|
+
type="button"
|
|
713
|
+
className={styles.btnCancel}
|
|
714
|
+
onClick={handleClear}
|
|
715
|
+
disabled={isSaving}
|
|
716
|
+
>
|
|
717
|
+
{t('cancel', 'Cancel')}
|
|
718
|
+
</button>
|
|
719
|
+
</div>
|
|
720
|
+
</div>
|
|
721
|
+
)}
|
|
722
|
+
|
|
723
|
+
{/* Calendar Overview Section */}
|
|
724
|
+
<div style={{ marginTop: '2.5rem' }}>
|
|
725
|
+
<div className={styles.sectionHeader}>
|
|
726
|
+
<span>{t('scheduledCalendarOverview', 'Scheduled Calendar Overview')}</span>
|
|
727
|
+
</div>
|
|
728
|
+
|
|
729
|
+
<div className={styles.row} style={{ marginBottom: '1rem' }}>
|
|
730
|
+
<div className={`${styles.col4} ${styles.formGroup}`}>
|
|
731
|
+
<label className={styles.label}>
|
|
732
|
+
<span>{t('modality', 'Modality')}</span>
|
|
733
|
+
</label>
|
|
734
|
+
<select
|
|
735
|
+
className={styles.formControl}
|
|
736
|
+
value={selectedModality}
|
|
737
|
+
onChange={(e) => setSelectedModality(e.target.value)}
|
|
738
|
+
>
|
|
739
|
+
<option value="all">{t('allModalities', '-- All Modalities --')}</option>
|
|
740
|
+
<option value="Xray">Xray</option>
|
|
741
|
+
<option value="CT">CT</option>
|
|
742
|
+
<option value="MRI">MRI</option>
|
|
743
|
+
</select>
|
|
744
|
+
</div>
|
|
745
|
+
|
|
746
|
+
<div className={`${styles.col4} ${styles.formGroup}`}>
|
|
747
|
+
<label className={styles.label}>
|
|
748
|
+
<span>{t('examRoom', 'Exam Room')}</span>
|
|
749
|
+
</label>
|
|
750
|
+
<select
|
|
751
|
+
className={styles.formControl}
|
|
752
|
+
value={selectedExamRoom}
|
|
753
|
+
onChange={(e) => setSelectedExamRoom(e.target.value)}
|
|
754
|
+
disabled={selectedModality === 'all'}
|
|
755
|
+
>
|
|
756
|
+
<option value="all">{t('allExamRooms', '-- All Exam Rooms --')}</option>
|
|
757
|
+
{examRooms.map((room, i) => (
|
|
758
|
+
<option key={`${room.name}-${i}`} value={room.name}>
|
|
759
|
+
{room.name}
|
|
760
|
+
</option>
|
|
761
|
+
))}
|
|
762
|
+
</select>
|
|
763
|
+
</div>
|
|
764
|
+
</div>
|
|
765
|
+
|
|
766
|
+
{/* Interactive Calendar Grid */}
|
|
767
|
+
<div className={styles.calendarWrapper}>
|
|
768
|
+
<div className={styles.calendarNav}>
|
|
769
|
+
<button
|
|
770
|
+
type="button"
|
|
771
|
+
className={styles.calendarNavBtn}
|
|
772
|
+
onClick={() =>
|
|
773
|
+
setCalendarDate(
|
|
774
|
+
new Date(calendarDate.getFullYear(), calendarDate.getMonth() - 1, 1),
|
|
775
|
+
)
|
|
776
|
+
}
|
|
777
|
+
>
|
|
778
|
+
← {t('previous', 'Previous')}
|
|
779
|
+
</button>
|
|
780
|
+
<span className={styles.calendarMonthTitle}>
|
|
781
|
+
{monthNames[calendarDate.getMonth()]} {calendarDate.getFullYear()}
|
|
782
|
+
</span>
|
|
783
|
+
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
|
784
|
+
<button
|
|
785
|
+
type="button"
|
|
786
|
+
className={styles.calendarNavBtn}
|
|
787
|
+
onClick={() => setCalendarDate(new Date())}
|
|
788
|
+
>
|
|
789
|
+
{t('today', 'Today')}
|
|
790
|
+
</button>
|
|
791
|
+
<button
|
|
792
|
+
type="button"
|
|
793
|
+
className={styles.calendarNavBtn}
|
|
794
|
+
onClick={() =>
|
|
795
|
+
setCalendarDate(
|
|
796
|
+
new Date(calendarDate.getFullYear(), calendarDate.getMonth() + 1, 1),
|
|
797
|
+
)
|
|
798
|
+
}
|
|
799
|
+
>
|
|
800
|
+
{t('next', 'Next')} →
|
|
801
|
+
</button>
|
|
802
|
+
</div>
|
|
803
|
+
</div>
|
|
804
|
+
|
|
805
|
+
{loadingCalendar ? (
|
|
806
|
+
<div style={{ padding: '2rem' }}>
|
|
807
|
+
<Loading description="Loading calendar appointments..." withOverlay={false} small />
|
|
808
|
+
</div>
|
|
809
|
+
) : (
|
|
810
|
+
<div className={styles.calendarGrid}>
|
|
811
|
+
{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((d) => (
|
|
812
|
+
<div key={d} className={styles.calendarDayHeader}>
|
|
813
|
+
{d}
|
|
814
|
+
</div>
|
|
815
|
+
))}
|
|
816
|
+
|
|
817
|
+
{calendarDays.map((cell, idx) => {
|
|
818
|
+
const cellClass = `${styles.calendarCell} ${
|
|
819
|
+
!cell.isCurrentMonth ? styles.calendarCellInactive : ''
|
|
820
|
+
}`.trim();
|
|
821
|
+
|
|
822
|
+
// Group cell orders by category
|
|
823
|
+
const categoryCounts: Record<string, number> = {};
|
|
824
|
+
for (const ord of cell.orders) {
|
|
825
|
+
const cat = ord.categoryDisplay || 'Other';
|
|
826
|
+
categoryCounts[cat] = (categoryCounts[cat] || 0) + 1;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
return (
|
|
830
|
+
<div
|
|
831
|
+
key={idx}
|
|
832
|
+
className={cellClass}
|
|
833
|
+
onClick={() =>
|
|
834
|
+
cell.orders.length > 0 &&
|
|
835
|
+
handleCalendarEventClick(cell.dateStr, cell.orders)
|
|
836
|
+
}
|
|
837
|
+
style={{ cursor: cell.orders.length > 0 ? 'pointer' : 'default' }}
|
|
838
|
+
>
|
|
839
|
+
<span
|
|
840
|
+
className={`${styles.calendarDayNumber} ${
|
|
841
|
+
cell.isToday ? styles.calendarDayToday : ''
|
|
842
|
+
}`}
|
|
843
|
+
>
|
|
844
|
+
{cell.dayNum}
|
|
845
|
+
</span>
|
|
846
|
+
|
|
847
|
+
{Object.entries(categoryCounts).map(([cat, count]) => {
|
|
848
|
+
let badgeCls = styles.badgeOther;
|
|
849
|
+
if (cat === 'Xray') badgeCls = styles.badgeXray;
|
|
850
|
+
else if (cat === 'CT') badgeCls = styles.badgeCT;
|
|
851
|
+
else if (cat === 'MRI') badgeCls = styles.badgeMRI;
|
|
852
|
+
|
|
853
|
+
return (
|
|
854
|
+
<span
|
|
855
|
+
key={cat}
|
|
856
|
+
className={`${styles.calendarBadge} ${badgeCls}`}
|
|
857
|
+
title={`${cat}: ${count} scheduled. Click to see details.`}
|
|
858
|
+
>
|
|
859
|
+
{cat}: {count}
|
|
860
|
+
</span>
|
|
861
|
+
);
|
|
862
|
+
})}
|
|
863
|
+
</div>
|
|
864
|
+
);
|
|
865
|
+
})}
|
|
866
|
+
</div>
|
|
867
|
+
)}
|
|
868
|
+
</div>
|
|
869
|
+
</div>
|
|
870
|
+
</div>
|
|
871
|
+
</div>
|
|
872
|
+
);
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
export default RadiologyTestScheduling;
|